import type { EditorState } from "./editorState"; import { StrandPath, strandPathsEqual, strandPathIsAncestor, TokenPath, } from "./path"; export type RawCursor = { strandPath: StrandPath; pos: number; }; export class Cursor { strandPath: StrandPath; pos: number; constructor(strandPath: StrandPath = [], pos: number = 0) { this.strandPath = strandPath; this.pos = pos; } equals(other: Cursor): boolean { return ( this.pos === other.pos && strandPathsEqual(this.strandPath, other.strandPath) ); } toRaw(): RawCursor { return { strandPath: this.strandPath, pos: this.pos, }; } static fromRaw(raw: RawCursor): Cursor { return new Cursor(raw.strandPath, raw.pos); } clone(): Cursor { return new Cursor( this.strandPath.map((level) => ({ ...level })), this.pos ); } moveRight(editorState: EditorState) { let currentStrand = editorState.content; for (const level of this.strandPath) { const token = currentStrand.tokens[level.tokenIndex]; currentStrand = token.children[level.childIndex]; } // If the next token has children, move into its first child strand const nextToken = currentStrand.tokens[this.pos]; if (nextToken && nextToken.children.length > 0) { this.strandPath.push({ tokenIndex: this.pos, childIndex: 0, }); this.pos = 0; return; } if (this.pos < currentStrand.tokens.length) { this.pos += 1; return; } // We are at the end of the current strand. Try to move into a sibling strand or up to the parent. let parentStrand = editorState.content; for (let i = 0; i < this.strandPath.length - 1; i++) { const level = this.strandPath[i]; const token = parentStrand.tokens[level.tokenIndex]; parentStrand = token.children[level.childIndex]; } const lastStrandPathPart = this.strandPath[this.strandPath.length - 1]; if (!lastStrandPathPart) { // Already at the top-level strand; cannot move right return; } const { tokenIndex, childIndex } = lastStrandPathPart; const parentToken = parentStrand.tokens[tokenIndex]; const parentChildIndex = childIndex; if (parentChildIndex + 1 < parentToken.children.length) { // Move to the next sibling child strand this.strandPath[this.strandPath.length - 1].childIndex += 1; this.pos = 0; return; } else { // Move up to the parent strand this.strandPath.pop(); this.pos = tokenIndex + 1; // Move to the position after the parent token return; } } moveLeft(editorState: EditorState) { let currentStrand = editorState.content; for (const level of this.strandPath) { const token = currentStrand.tokens[level.tokenIndex]; currentStrand = token.children[level.childIndex]; } // If the previous token has children, move into its last child strand if (this.pos > 0) { const prevToken = currentStrand.tokens[this.pos - 1]; if (prevToken && prevToken.children.length > 0) { this.strandPath.push({ tokenIndex: this.pos - 1, childIndex: prevToken.children.length - 1, }); const lastChildStrand = prevToken.children[prevToken.children.length - 1]; this.pos = lastChildStrand.tokens.length; return; } } if (this.pos > 0) { this.pos -= 1; return; } // We are at the start of the current strand. Try to move into a sibling strand or up to the parent. let parentStrand = editorState.content; for (let i = 0; i < this.strandPath.length - 1; i++) { const level = this.strandPath[i]; const token = parentStrand.tokens[level.tokenIndex]; parentStrand = token.children[level.childIndex]; } const lastStrandPathPart = this.strandPath[this.strandPath.length - 1]; if (!lastStrandPathPart) { // Already at the top-level strand; cannot move left return; } const { tokenIndex, childIndex } = lastStrandPathPart; const parentToken = parentStrand.tokens[tokenIndex]; const parentChildIndex = childIndex; if (parentChildIndex > 0) { // Move to the previous sibling child strand this.strandPath[this.strandPath.length - 1].childIndex -= 1; const siblingStrand = parentToken.children[ this.strandPath[this.strandPath.length - 1].childIndex ]; this.pos = siblingStrand.tokens.length; return; } else { // Move up to the parent strand this.strandPath.pop(); this.pos = tokenIndex; // Move to the position before the parent token return; } } /** * Adjust the cursor to account for an insertion at a given location. * @param insertCursor The location where the insertion occurred (referencing a cursor in the old document before insertion). * @param insertCount The number of tokens inserted. * @returns void The cursor is updated in place. */ updateAfterInsertion(insertCursor: Cursor, insertCount: number) { if (strandPathsEqual(this.strandPath, insertCursor.strandPath)) { if (insertCursor.pos <= this.pos) { this.pos += insertCount; } return; } if (strandPathIsAncestor(insertCursor.strandPath, this.strandPath)) { const relativeLevel = this.strandPath[insertCursor.strandPath.length]; if (relativeLevel.tokenIndex >= insertCursor.pos) { relativeLevel.tokenIndex += insertCount; } return; } } /** * Adjust the cursor to account for a token deletion at a given path. * @param deletedTokenPath The path of the token that was deleted. * @returns void The cursor is updated in place. */ updateAfterTokenDeletion(deletedTokenPath: TokenPath) { if (strandPathsEqual(this.strandPath, deletedTokenPath.strandPath)) { if (deletedTokenPath.tokenIndex < this.pos) { this.pos -= 1; } return; } if (strandPathIsAncestor(deletedTokenPath.strandPath, this.strandPath)) { const relativeLevel = this.strandPath[deletedTokenPath.strandPath.length]; if (relativeLevel.tokenIndex > deletedTokenPath.tokenIndex) { relativeLevel.tokenIndex -= 1; } else if (relativeLevel.tokenIndex === deletedTokenPath.tokenIndex) { this.strandPath = this.strandPath.slice( 0, deletedTokenPath.strandPath.length ); this.pos = deletedTokenPath.tokenIndex; } return; } } }