Switch to a discriminated unions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-07-20 16:33:53 +01:00
parent 2bf5e4b142
commit ed0d9973b7
9 changed files with 130 additions and 74 deletions

View file

@ -17,13 +17,13 @@ limitations under the License.
import {KeyboardEvent} from "react";
import {BasePart, CommandPartCreator, PartCreator} from "./parts";
import {Part, CommandPartCreator, PartCreator} from "./parts";
import DocumentPosition from "./position";
import {ICompletion} from "../autocomplete/Autocompleter";
import Autocomplete from "../components/views/rooms/Autocomplete";
export interface ICallback {
replaceParts?: BasePart[];
replaceParts?: Part[];
close?: boolean;
}
@ -32,7 +32,7 @@ export type GetAutocompleterComponent = () => Autocomplete;
export type UpdateQuery = (test: string) => Promise<void>;
export default class AutocompleteWrapperModel {
private queryPart: BasePart;
private queryPart: Part;
private partIndex: number;
constructor(
@ -89,7 +89,7 @@ export default class AutocompleteWrapperModel {
this.getAutocompleterComponent().moveSelection(+1);
}
onPartUpdate(part: BasePart, pos: DocumentPosition) {
onPartUpdate(part: Part, pos: DocumentPosition) {
// cache the typed value and caret here
// so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text)
this.queryPart = part;

View file

@ -19,7 +19,7 @@ import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render";
import Range from "./range";
import EditorModel from "./model";
import DocumentPosition, {IPosition} from "./position";
import {BasePart} from "./parts";
import {Part} from "./parts";
export type Caret = Range | DocumentPosition;
@ -104,7 +104,7 @@ export function getLineAndNodePosition(model: EditorModel, caretPosition: IPosit
return {lineIndex, nodeIndex, offset};
}
function findNodeInLineForPart(parts: BasePart[], partIndex: number) {
function findNodeInLineForPart(parts: Part[], partIndex: number) {
let lineIndex = 0;
let nodeIndex = -1;
@ -140,7 +140,7 @@ function findNodeInLineForPart(parts: BasePart[], partIndex: number) {
return {lineIndex, nodeIndex};
}
function moveOutOfUneditablePart(parts: BasePart[], partIndex: number, nodeIndex: number, offset: number) {
function moveOutOfUneditablePart(parts: Part[], partIndex: number, nodeIndex: number, offset: number) {
// move caret before or after uneditable part
const part = parts[partIndex];
if (part && !part.canEdit) {

View file

@ -16,12 +16,11 @@ limitations under the License.
import EditorModel from "./model";
import {IDiff} from "./diff";
import {ISerializedPart} from "./parts";
import Range from "./range";
import {SerializedPart} from "./parts";
import {Caret} from "./caret";
interface IHistory {
parts: ISerializedPart[];
parts: SerializedPart[];
caret: Caret;
}

View file

@ -18,7 +18,7 @@ limitations under the License.
import {diffAtCaret, diffDeletion, IDiff} from "./diff";
import DocumentPosition, {IPosition} from "./position";
import Range from "./range";
import {BasePart, ISerializedPart, PartCreator} from "./parts";
import {SerializedPart, Part, PartCreator} from "./parts";
import AutocompleteWrapperModel, {ICallback} from "./autocomplete";
import DocumentOffset from "./offset";
import {Caret} from "./caret";
@ -49,7 +49,7 @@ type UpdateCallback = (caret: Caret, inputType?: string, diff?: IDiff) => void;
type ManualTransformCallback = () => Caret;
export default class EditorModel {
private _parts: BasePart[];
private _parts: Part[];
private readonly _partCreator: PartCreator;
private activePartIdx: number = null;
private _autoComplete: AutocompleteWrapperModel = null;
@ -57,7 +57,7 @@ export default class EditorModel {
private autoCompletePartCount = 0;
private transformCallback: TransformCallback = null;
constructor(parts: BasePart[], partCreator: PartCreator, private updateCallback: UpdateCallback = null) {
constructor(parts: Part[], partCreator: PartCreator, private updateCallback: UpdateCallback = null) {
this._parts = parts;
this._partCreator = partCreator;
this.transformCallback = null;
@ -94,7 +94,7 @@ export default class EditorModel {
return new EditorModel(this._parts, this._partCreator, this.updateCallback);
}
private insertPart(index: number, part: BasePart) {
private insertPart(index: number, part: Part) {
this._parts.splice(index, 0, part);
if (this.activePartIdx >= index) {
++this.activePartIdx;
@ -118,7 +118,7 @@ export default class EditorModel {
}
}
private replacePart(index: number, part: BasePart) {
private replacePart(index: number, part: Part) {
this._parts.splice(index, 1, part);
}
@ -158,7 +158,7 @@ export default class EditorModel {
}
}
reset(serializedParts: ISerializedPart[], caret: Caret, inputType: string) {
reset(serializedParts: SerializedPart[], caret: Caret, inputType: string) {
this._parts = serializedParts.map(p => this._partCreator.deserializePart(p));
if (!caret) {
caret = this.getPositionAtEnd();
@ -180,7 +180,7 @@ export default class EditorModel {
* @param {DocumentPosition} position the position to start inserting at
* @return {Number} the amount of characters added
*/
insert(parts: BasePart[], position: IPosition) {
insert(parts: Part[], position: IPosition) {
const insertIndex = this.splitAt(position);
let newTextLength = 0;
for (let i = 0; i < parts.length; ++i) {
@ -420,7 +420,7 @@ export default class EditorModel {
return new Range(this, positionA, positionB);
}
replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: BasePart[]) {
replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]) {
// convert end position to offset, so it is independent of how the document is split into parts
// which we'll change when splitting up at the start position
const endOffset = endPosition.asOffset(this);

View file

@ -15,13 +15,13 @@ limitations under the License.
*/
import Range from "./range";
import {BasePart} from "./parts";
import {Part} from "./parts";
/**
* Some common queries and transformations on the editor model
*/
export function replaceRangeAndExpandSelection(range: Range, newParts: BasePart[]) {
export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) {
const {model} = range;
model.transform(() => {
const oldLen = range.length;
@ -32,7 +32,7 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: BasePart[
});
}
export function replaceRangeAndMoveCaret(range: Range, newParts: BasePart[]) {
export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) {
const {model} = range;
model.transform(() => {
const oldLen = range.length;

View file

@ -19,15 +19,66 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {Room} from "matrix-js-sdk/src/models/room";
import AutocompleteWrapperModel, {GetAutocompleterComponent, UpdateCallback, UpdateQuery} from "./autocomplete";
import AutocompleteWrapperModel, {
GetAutocompleterComponent,
UpdateCallback,
UpdateQuery
} from "./autocomplete";
import * as Avatar from "../Avatar";
export interface ISerializedPart {
type: string;
interface ISerializedPart {
type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate;
text: string;
}
export abstract class BasePart {
interface ISerializedPillPart {
type: Type.AtRoomPill | Type.RoomPill | Type.UserPill;
text: string;
resourceId: string;
}
export type SerializedPart = ISerializedPart | ISerializedPillPart;
enum Type {
Plain = "plain",
Newline = "newline",
Command = "command",
UserPill = "user-pill",
RoomPill = "room-pill",
AtRoomPill = "at-room-pill",
PillCandidate = "pill-candidate",
}
interface IBasePart {
text: string;
type: Type.Plain | Type.Newline;
canEdit: boolean;
createAutoComplete(updateCallback: UpdateCallback): void;
serialize(): SerializedPart;
remove(offset: number, len: number): string;
split(offset: number): IBasePart;
validateAndInsert(offset: number, str: string, inputType: string): boolean;
appendUntilRejected(str: string, inputType: string): string;
updateDOMNode(node: Node);
canUpdateDOMNode(node: Node);
toDOMNode(): Node;
}
interface IPillCandidatePart extends Omit<IBasePart, "type" | "createAutoComplete"> {
type: Type.PillCandidate | Type.Command;
createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel;
}
interface IPillPart extends Omit<IBasePart, "type" | "resourceId"> {
type: Type.AtRoomPill | Type.RoomPill | Type.UserPill;
resourceId: string;
}
export type Part = IBasePart | IPillCandidatePart | IPillPart;
abstract class BasePart {
protected _text: string;
constructor(text = "") {
@ -42,7 +93,7 @@ export abstract class BasePart {
return true;
}
merge(part: BasePart) {
merge(part: Part) {
return false;
}
@ -94,7 +145,7 @@ export abstract class BasePart {
return true;
}
createAutoComplete(updateCallback): AutocompleteWrapperModel | void {}
createAutoComplete(updateCallback: UpdateCallback): void {}
trim(len: number) {
const remaining = this._text.substr(len);
@ -106,7 +157,7 @@ export abstract class BasePart {
return this._text;
}
abstract get type(): string;
abstract get type(): Type;
get canEdit() {
return true;
@ -116,8 +167,11 @@ export abstract class BasePart {
return `${this.type}(${this.text})`;
}
serialize(): ISerializedPart {
return {type: this.type, text: this.text};
serialize(): SerializedPart {
return {
type: this.type as ISerializedPart["type"],
text: this.text,
};
}
abstract updateDOMNode(node: Node);
@ -125,8 +179,7 @@ export abstract class BasePart {
abstract toDOMNode(): Node;
}
// exported for unit tests, should otherwise only be used through PartCreator
export class PlainPart extends BasePart {
abstract class PlainBasePart extends BasePart {
acceptsInsertion(chr: string, offset: number, inputType: string) {
if (chr === "\n") {
return false;
@ -150,10 +203,6 @@ export class PlainPart extends BasePart {
return false;
}
get type() {
return "plain";
}
updateDOMNode(node: Node) {
if (node.textContent !== this.text) {
node.textContent = this.text;
@ -165,7 +214,14 @@ export class PlainPart extends BasePart {
}
}
export abstract class PillPart extends BasePart {
// exported for unit tests, should otherwise only be used through PartCreator
export class PlainPart extends PlainBasePart implements IBasePart {
get type(): IBasePart["type"] {
return Type.Plain;
}
}
abstract class PillPart extends BasePart implements IPillPart {
constructor(public resourceId: string, label) {
super(label);
}
@ -223,12 +279,14 @@ export abstract class PillPart extends BasePart {
return false;
}
abstract get type(): IPillPart["type"];
abstract get className(): string;
abstract setAvatar(node: HTMLElement): void;
}
class NewlinePart extends BasePart {
class NewlinePart extends BasePart implements IBasePart {
acceptsInsertion(chr: string, offset: number) {
return offset === 0 && chr === "\n";
}
@ -251,8 +309,8 @@ class NewlinePart extends BasePart {
return node.tagName === "BR";
}
get type() {
return "newline";
get type(): IBasePart["type"] {
return Type.Newline;
}
// this makes the cursor skip this part when it is inserted
@ -283,8 +341,8 @@ class RoomPillPart extends PillPart {
this._setAvatarVars(node, avatarUrl, initialLetter);
}
get type() {
return "room-pill";
get type(): IPillPart["type"] {
return Type.RoomPill;
}
get className() {
@ -293,8 +351,8 @@ class RoomPillPart extends PillPart {
}
class AtRoomPillPart extends RoomPillPart {
get type() {
return "at-room-pill";
get type(): IPillPart["type"] {
return Type.AtRoomPill;
}
}
@ -321,28 +379,29 @@ class UserPillPart extends PillPart {
this._setAvatarVars(node, avatarUrl, initialLetter);
}
get type() {
return "user-pill";
get type(): IPillPart["type"] {
return Type.UserPill;
}
get className() {
return "mx_UserPill mx_Pill";
}
serialize() {
serialize(): ISerializedPillPart {
return {
...super.serialize(),
type: this.type,
text: this.text,
resourceId: this.resourceId,
};
}
}
class PillCandidatePart extends PlainPart {
class PillCandidatePart extends PlainBasePart implements IPillCandidatePart {
constructor(text: string, private autoCompleteCreator: IAutocompleteCreator) {
super(text);
}
createAutoComplete(updateCallback): AutocompleteWrapperModel {
createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel {
return this.autoCompleteCreator.create(updateCallback);
}
@ -362,8 +421,8 @@ class PillCandidatePart extends PlainPart {
return true;
}
get type() {
return "pill-candidate";
get type(): IPillCandidatePart["type"] {
return Type.PillCandidate;
}
}
@ -399,7 +458,7 @@ export class PartCreator {
this.autoCompleteCreator.create = autoCompleteCreator(this);
}
createPartForInput(input: string, partIndex: number, inputType?: string): BasePart {
createPartForInput(input: string, partIndex: number, inputType?: string): Part {
switch (input[0]) {
case "#":
case "@":
@ -416,20 +475,20 @@ export class PartCreator {
return this.plain(text);
}
deserializePart(part: ISerializedPart) {
deserializePart(part: SerializedPart): Part {
switch (part.type) {
case "plain":
case Type.Plain:
return this.plain(part.text);
case "newline":
case Type.Newline:
return this.newline();
case "at-room-pill":
case Type.AtRoomPill:
return this.atRoomPill(part.text);
case "pill-candidate":
case Type.PillCandidate:
return this.pillCandidate(part.text);
case "room-pill":
case Type.RoomPill:
return this.roomPill(part.text);
case "user-pill":
return this.userPill(part.text, (part as PillPart).resourceId);
case Type.UserPill:
return this.userPill(part.text, part.resourceId);
}
}
@ -491,7 +550,7 @@ export class CommandPartCreator extends PartCreator {
return new CommandPart(text, this.autoCompleteCreator);
}
deserializePart(part: BasePart) {
deserializePart(part: Part): Part {
if (part.type === "command") {
return this.command(part.text);
} else {
@ -501,7 +560,7 @@ export class CommandPartCreator extends PartCreator {
}
class CommandPart extends PillCandidatePart {
get type() {
return "command";
get type(): IPillCandidatePart["type"] {
return Type.Command;
}
}

View file

@ -16,15 +16,15 @@ limitations under the License.
import DocumentOffset from "./offset";
import EditorModel from "./model";
import {BasePart} from "./parts";
import {Part} from "./parts";
export interface IPosition {
index: number;
offset: number;
}
type Callback = (part: BasePart, startIdx: number, endIdx: number) => void;
type Predicate = (index: number, offset: number, part: BasePart) => boolean;
type Callback = (part: Part, startIdx: number, endIdx: number) => void;
type Predicate = (index: number, offset: number, part: Part) => boolean;
export default class DocumentPosition implements IPosition {
constructor(public readonly index: number, public readonly offset: number) {

View file

@ -15,16 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BasePart} from "./parts";
import {Part} from "./parts";
import EditorModel from "./model";
import {instanceOf} from "prop-types";
export function needsCaretNodeBefore(part: BasePart, prevPart: BasePart) {
export function needsCaretNodeBefore(part: Part, prevPart: Part) {
const isFirst = !prevPart || prevPart.type === "newline";
return !part.canEdit && (isFirst || !prevPart.canEdit);
}
export function needsCaretNodeAfter(part: BasePart, isLastOfLine: boolean) {
export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean) {
return !part.canEdit && isLastOfLine;
}
@ -83,7 +82,7 @@ function removeChildren(parent: HTMLElement) {
}
}
function reconcileLine(lineContainer: ChildNode, parts: BasePart[]) {
function reconcileLine(lineContainer: ChildNode, parts: Part[]) {
let currentNode;
let prevPart;
const lastPart = parts[parts.length - 1];

View file

@ -18,7 +18,6 @@ limitations under the License.
import Markdown from '../Markdown';
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
import EditorModel from "./model";
import {PillPart} from "./parts";
export function mdSerialize(model: EditorModel) {
return model.parts.reduce((html, part) => {
@ -32,7 +31,7 @@ export function mdSerialize(model: EditorModel) {
return html + part.text;
case "room-pill":
case "user-pill":
return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink((part as PillPart).resourceId)})`;
return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
}
}, "");
}