A world-class math input for the web
at main 214 lines 6.6 kB view raw
1import type { EditorState } from "./editorState"; 2import { 3 StrandPath, 4 strandPathsEqual, 5 strandPathIsAncestor, 6 TokenPath, 7} from "./path"; 8 9export type RawCursor = { 10 strandPath: StrandPath; 11 pos: number; 12}; 13 14export class Cursor { 15 strandPath: StrandPath; 16 pos: number; 17 18 constructor(strandPath: StrandPath = [], pos: number = 0) { 19 this.strandPath = strandPath; 20 this.pos = pos; 21 } 22 23 equals(other: Cursor): boolean { 24 return ( 25 this.pos === other.pos && 26 strandPathsEqual(this.strandPath, other.strandPath) 27 ); 28 } 29 30 toRaw(): RawCursor { 31 return { 32 strandPath: this.strandPath, 33 pos: this.pos, 34 }; 35 } 36 37 static fromRaw(raw: RawCursor): Cursor { 38 return new Cursor(raw.strandPath, raw.pos); 39 } 40 41 clone(): Cursor { 42 return new Cursor( 43 this.strandPath.map((level) => ({ ...level })), 44 this.pos 45 ); 46 } 47 48 moveRight(editorState: EditorState) { 49 let currentStrand = editorState.content; 50 for (const level of this.strandPath) { 51 const token = currentStrand.tokens[level.tokenIndex]; 52 currentStrand = token.children[level.childIndex]; 53 } 54 55 // If the next token has children, move into its first child strand 56 const nextToken = currentStrand.tokens[this.pos]; 57 if (nextToken && nextToken.children.length > 0) { 58 this.strandPath.push({ 59 tokenIndex: this.pos, 60 childIndex: 0, 61 }); 62 this.pos = 0; 63 return; 64 } 65 66 if (this.pos < currentStrand.tokens.length) { 67 this.pos += 1; 68 return; 69 } 70 71 // We are at the end of the current strand. Try to move into a sibling strand or up to the parent. 72 let parentStrand = editorState.content; 73 for (let i = 0; i < this.strandPath.length - 1; i++) { 74 const level = this.strandPath[i]; 75 const token = parentStrand.tokens[level.tokenIndex]; 76 parentStrand = token.children[level.childIndex]; 77 } 78 79 const lastStrandPathPart = this.strandPath[this.strandPath.length - 1]; 80 if (!lastStrandPathPart) { 81 // Already at the top-level strand; cannot move right 82 return; 83 } 84 85 const { tokenIndex, childIndex } = lastStrandPathPart; 86 const parentToken = parentStrand.tokens[tokenIndex]; 87 const parentChildIndex = childIndex; 88 89 if (parentChildIndex + 1 < parentToken.children.length) { 90 // Move to the next sibling child strand 91 this.strandPath[this.strandPath.length - 1].childIndex += 1; 92 this.pos = 0; 93 return; 94 } else { 95 // Move up to the parent strand 96 this.strandPath.pop(); 97 this.pos = tokenIndex + 1; // Move to the position after the parent token 98 return; 99 } 100 } 101 102 moveLeft(editorState: EditorState) { 103 let currentStrand = editorState.content; 104 for (const level of this.strandPath) { 105 const token = currentStrand.tokens[level.tokenIndex]; 106 currentStrand = token.children[level.childIndex]; 107 } 108 109 // If the previous token has children, move into its last child strand 110 if (this.pos > 0) { 111 const prevToken = currentStrand.tokens[this.pos - 1]; 112 if (prevToken && prevToken.children.length > 0) { 113 this.strandPath.push({ 114 tokenIndex: this.pos - 1, 115 childIndex: prevToken.children.length - 1, 116 }); 117 const lastChildStrand = 118 prevToken.children[prevToken.children.length - 1]; 119 this.pos = lastChildStrand.tokens.length; 120 return; 121 } 122 } 123 124 if (this.pos > 0) { 125 this.pos -= 1; 126 return; 127 } 128 129 // We are at the start of the current strand. Try to move into a sibling strand or up to the parent. 130 let parentStrand = editorState.content; 131 for (let i = 0; i < this.strandPath.length - 1; i++) { 132 const level = this.strandPath[i]; 133 const token = parentStrand.tokens[level.tokenIndex]; 134 parentStrand = token.children[level.childIndex]; 135 } 136 137 const lastStrandPathPart = this.strandPath[this.strandPath.length - 1]; 138 if (!lastStrandPathPart) { 139 // Already at the top-level strand; cannot move left 140 return; 141 } 142 143 const { tokenIndex, childIndex } = lastStrandPathPart; 144 const parentToken = parentStrand.tokens[tokenIndex]; 145 const parentChildIndex = childIndex; 146 147 if (parentChildIndex > 0) { 148 // Move to the previous sibling child strand 149 this.strandPath[this.strandPath.length - 1].childIndex -= 1; 150 const siblingStrand = 151 parentToken.children[ 152 this.strandPath[this.strandPath.length - 1].childIndex 153 ]; 154 this.pos = siblingStrand.tokens.length; 155 return; 156 } else { 157 // Move up to the parent strand 158 this.strandPath.pop(); 159 this.pos = tokenIndex; // Move to the position before the parent token 160 return; 161 } 162 } 163 164 /** 165 * Adjust the cursor to account for an insertion at a given location. 166 * @param insertCursor The location where the insertion occurred (referencing a cursor in the old document before insertion). 167 * @param insertCount The number of tokens inserted. 168 * @returns void The cursor is updated in place. 169 */ 170 updateAfterInsertion(insertCursor: Cursor, insertCount: number) { 171 if (strandPathsEqual(this.strandPath, insertCursor.strandPath)) { 172 if (insertCursor.pos <= this.pos) { 173 this.pos += insertCount; 174 } 175 return; 176 } 177 178 if (strandPathIsAncestor(insertCursor.strandPath, this.strandPath)) { 179 const relativeLevel = this.strandPath[insertCursor.strandPath.length]; 180 if (relativeLevel.tokenIndex >= insertCursor.pos) { 181 relativeLevel.tokenIndex += insertCount; 182 } 183 return; 184 } 185 } 186 187 /** 188 * Adjust the cursor to account for a token deletion at a given path. 189 * @param deletedTokenPath The path of the token that was deleted. 190 * @returns void The cursor is updated in place. 191 */ 192 updateAfterTokenDeletion(deletedTokenPath: TokenPath) { 193 if (strandPathsEqual(this.strandPath, deletedTokenPath.strandPath)) { 194 if (deletedTokenPath.tokenIndex < this.pos) { 195 this.pos -= 1; 196 } 197 return; 198 } 199 200 if (strandPathIsAncestor(deletedTokenPath.strandPath, this.strandPath)) { 201 const relativeLevel = this.strandPath[deletedTokenPath.strandPath.length]; 202 if (relativeLevel.tokenIndex > deletedTokenPath.tokenIndex) { 203 relativeLevel.tokenIndex -= 1; 204 } else if (relativeLevel.tokenIndex === deletedTokenPath.tokenIndex) { 205 this.strandPath = this.strandPath.slice( 206 0, 207 deletedTokenPath.strandPath.length 208 ); 209 this.pos = deletedTokenPath.tokenIndex; 210 } 211 return; 212 } 213 } 214}