1/*@internal*/ 2namespace ts.server { 3 const lineCollectionCapacity = 4; 4 5 interface LineCollection { 6 charCount(): number; 7 lineCount(): number; 8 isLeaf(): this is LineLeaf; 9 walk(rangeStart: number, rangeLength: number, walkFns: LineIndexWalker): void; 10 } 11 12 export interface AbsolutePositionAndLineText { 13 absolutePosition: number; 14 lineText: string | undefined; 15 } 16 17 const enum CharRangeSection { 18 PreStart, 19 Start, 20 Entire, 21 Mid, 22 End, 23 PostEnd 24 } 25 26 interface LineIndexWalker { 27 goSubtree: boolean; 28 done: boolean; 29 leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void; 30 pre?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, 31 parent: LineNode, nodeType: CharRangeSection): void; 32 post?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, 33 parent: LineNode, nodeType: CharRangeSection): void; 34 } 35 36 class EditWalker implements LineIndexWalker { 37 goSubtree = true; 38 get done() { return false; } 39 40 readonly lineIndex = new LineIndex(); 41 // path to start of range 42 private readonly startPath: LineCollection[]; 43 private readonly endBranch: LineCollection[] = []; 44 private branchNode: LineNode | undefined; 45 // path to current node 46 private readonly stack: LineNode[]; 47 private state = CharRangeSection.Entire; 48 private lineCollectionAtBranch: LineCollection | undefined; 49 private initialText = ""; 50 private trailingText = ""; 51 52 constructor() { 53 this.lineIndex.root = new LineNode(); 54 this.startPath = [this.lineIndex.root]; 55 this.stack = [this.lineIndex.root]; 56 } 57 58 insertLines(insertedText: string | undefined, suppressTrailingText: boolean) { 59 if (suppressTrailingText) { 60 this.trailingText = ""; 61 } 62 if (insertedText) { 63 insertedText = this.initialText + insertedText + this.trailingText; 64 } 65 else { 66 insertedText = this.initialText + this.trailingText; 67 } 68 const lm = LineIndex.linesFromText(insertedText); 69 const lines = lm.lines; 70 if (lines.length > 1 && lines[lines.length - 1] === "") { 71 lines.pop(); 72 } 73 let branchParent: LineNode | undefined; 74 let lastZeroCount: LineCollection | undefined; 75 76 for (let k = this.endBranch.length - 1; k >= 0; k--) { 77 (this.endBranch[k] as LineNode).updateCounts(); 78 if (this.endBranch[k].charCount() === 0) { 79 lastZeroCount = this.endBranch[k]; 80 if (k > 0) { 81 branchParent = this.endBranch[k - 1] as LineNode; 82 } 83 else { 84 branchParent = this.branchNode; 85 } 86 } 87 } 88 if (lastZeroCount) { 89 branchParent!.remove(lastZeroCount); 90 } 91 92 // path at least length two (root and leaf) 93 const leafNode = this.startPath[this.startPath.length - 1] as LineLeaf; 94 95 if (lines.length > 0) { 96 leafNode.text = lines[0]; 97 98 if (lines.length > 1) { 99 let insertedNodes = new Array(lines.length - 1) as LineCollection[]; 100 let startNode = leafNode as LineCollection; 101 for (let i = 1; i < lines.length; i++) { 102 insertedNodes[i - 1] = new LineLeaf(lines[i]); 103 } 104 let pathIndex = this.startPath.length - 2; 105 while (pathIndex >= 0) { 106 const insertionNode = this.startPath[pathIndex] as LineNode; 107 insertedNodes = insertionNode.insertAt(startNode, insertedNodes); 108 pathIndex--; 109 startNode = insertionNode; 110 } 111 let insertedNodesLen = insertedNodes.length; 112 while (insertedNodesLen > 0) { 113 const newRoot = new LineNode(); 114 newRoot.add(this.lineIndex.root); 115 insertedNodes = newRoot.insertAt(this.lineIndex.root, insertedNodes); 116 insertedNodesLen = insertedNodes.length; 117 this.lineIndex.root = newRoot; 118 } 119 this.lineIndex.root.updateCounts(); 120 } 121 else { 122 for (let j = this.startPath.length - 2; j >= 0; j--) { 123 (this.startPath[j] as LineNode).updateCounts(); 124 } 125 } 126 } 127 else { 128 const insertionNode = this.startPath[this.startPath.length - 2] as LineNode; 129 // no content for leaf node, so delete it 130 insertionNode.remove(leafNode); 131 for (let j = this.startPath.length - 2; j >= 0; j--) { 132 (this.startPath[j] as LineNode).updateCounts(); 133 } 134 } 135 136 return this.lineIndex; 137 } 138 139 post(_relativeStart: number, _relativeLength: number, lineCollection: LineCollection): void { 140 // have visited the path for start of range, now looking for end 141 // if range is on single line, we will never make this state transition 142 if (lineCollection === this.lineCollectionAtBranch) { 143 this.state = CharRangeSection.End; 144 } 145 // always pop stack because post only called when child has been visited 146 this.stack.pop(); 147 } 148 149 pre(_relativeStart: number, _relativeLength: number, lineCollection: LineCollection, _parent: LineCollection, nodeType: CharRangeSection): void { 150 // currentNode corresponds to parent, but in the new tree 151 const currentNode = this.stack[this.stack.length - 1]; 152 153 if ((this.state === CharRangeSection.Entire) && (nodeType === CharRangeSection.Start)) { 154 // if range is on single line, we will never make this state transition 155 this.state = CharRangeSection.Start; 156 this.branchNode = currentNode; 157 this.lineCollectionAtBranch = lineCollection; 158 } 159 160 let child: LineCollection | undefined; 161 function fresh(node: LineCollection): LineCollection { 162 if (node.isLeaf()) { 163 return new LineLeaf(""); 164 } 165 else return new LineNode(); 166 } 167 switch (nodeType) { 168 case CharRangeSection.PreStart: 169 this.goSubtree = false; 170 if (this.state !== CharRangeSection.End) { 171 currentNode.add(lineCollection); 172 } 173 break; 174 case CharRangeSection.Start: 175 if (this.state === CharRangeSection.End) { 176 this.goSubtree = false; 177 } 178 else { 179 child = fresh(lineCollection); 180 currentNode.add(child); 181 this.startPath.push(child); 182 } 183 break; 184 case CharRangeSection.Entire: 185 if (this.state !== CharRangeSection.End) { 186 child = fresh(lineCollection); 187 currentNode.add(child); 188 this.startPath.push(child); 189 } 190 else { 191 if (!lineCollection.isLeaf()) { 192 child = fresh(lineCollection); 193 currentNode.add(child); 194 this.endBranch.push(child); 195 } 196 } 197 break; 198 case CharRangeSection.Mid: 199 this.goSubtree = false; 200 break; 201 case CharRangeSection.End: 202 if (this.state !== CharRangeSection.End) { 203 this.goSubtree = false; 204 } 205 else { 206 if (!lineCollection.isLeaf()) { 207 child = fresh(lineCollection); 208 currentNode.add(child); 209 this.endBranch.push(child); 210 } 211 } 212 break; 213 case CharRangeSection.PostEnd: 214 this.goSubtree = false; 215 if (this.state !== CharRangeSection.Start) { 216 currentNode.add(lineCollection); 217 } 218 break; 219 } 220 if (this.goSubtree) { 221 this.stack.push(child as LineNode); 222 } 223 } 224 // just gather text from the leaves 225 leaf(relativeStart: number, relativeLength: number, ll: LineLeaf) { 226 if (this.state === CharRangeSection.Start) { 227 this.initialText = ll.text.substring(0, relativeStart); 228 } 229 else if (this.state === CharRangeSection.Entire) { 230 this.initialText = ll.text.substring(0, relativeStart); 231 this.trailingText = ll.text.substring(relativeStart + relativeLength); 232 } 233 else { 234 // state is CharRangeSection.End 235 this.trailingText = ll.text.substring(relativeStart + relativeLength); 236 } 237 } 238 } 239 240 // text change information 241 class TextChange { 242 constructor(public pos: number, public deleteLen: number, public insertedText?: string) { 243 } 244 245 getTextChangeRange() { 246 return createTextChangeRange(createTextSpan(this.pos, this.deleteLen), 247 this.insertedText ? this.insertedText.length : 0); 248 } 249 } 250 251 export class ScriptVersionCache { 252 private changes: TextChange[] = []; 253 private readonly versions: LineIndexSnapshot[] = new Array<LineIndexSnapshot>(ScriptVersionCache.maxVersions); 254 private minVersion = 0; // no versions earlier than min version will maintain change history 255 256 private currentVersion = 0; 257 258 private static readonly changeNumberThreshold = 8; 259 private static readonly changeLengthThreshold = 256; 260 private static readonly maxVersions = 8; 261 262 private versionToIndex(version: number) { 263 if (version < this.minVersion || version > this.currentVersion) { 264 return undefined; 265 } 266 return version % ScriptVersionCache.maxVersions; 267 } 268 269 private currentVersionToIndex() { 270 return this.currentVersion % ScriptVersionCache.maxVersions; 271 } 272 273 // REVIEW: can optimize by coalescing simple edits 274 edit(pos: number, deleteLen: number, insertedText?: string) { 275 this.changes.push(new TextChange(pos, deleteLen, insertedText)); 276 if (this.changes.length > ScriptVersionCache.changeNumberThreshold || 277 deleteLen > ScriptVersionCache.changeLengthThreshold || 278 insertedText && insertedText.length > ScriptVersionCache.changeLengthThreshold) { 279 this.getSnapshot(); 280 } 281 } 282 283 getSnapshot(): IScriptSnapshot { return this._getSnapshot(); } 284 285 private _getSnapshot(): LineIndexSnapshot { 286 let snap = this.versions[this.currentVersionToIndex()]; 287 if (this.changes.length > 0) { 288 let snapIndex = snap.index; 289 for (const change of this.changes) { 290 snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText); 291 } 292 snap = new LineIndexSnapshot(this.currentVersion + 1, this, snapIndex, this.changes); 293 294 this.currentVersion = snap.version; 295 this.versions[this.currentVersionToIndex()] = snap; 296 this.changes = []; 297 298 if ((this.currentVersion - this.minVersion) >= ScriptVersionCache.maxVersions) { 299 this.minVersion = (this.currentVersion - ScriptVersionCache.maxVersions) + 1; 300 } 301 } 302 return snap; 303 } 304 305 getSnapshotVersion(): number { 306 return this._getSnapshot().version; 307 } 308 309 getAbsolutePositionAndLineText(oneBasedLine: number): AbsolutePositionAndLineText { 310 return this._getSnapshot().index.lineNumberToInfo(oneBasedLine); 311 } 312 313 lineOffsetToPosition(line: number, column: number): number { 314 return this._getSnapshot().index.absolutePositionOfStartOfLine(line) + (column - 1); 315 } 316 317 positionToLineOffset(position: number): protocol.Location { 318 return this._getSnapshot().index.positionToLineOffset(position); 319 } 320 321 lineToTextSpan(line: number): TextSpan { 322 const index = this._getSnapshot().index; 323 const { lineText, absolutePosition } = index.lineNumberToInfo(line + 1); 324 const len = lineText !== undefined ? lineText.length : index.absolutePositionOfStartOfLine(line + 2) - absolutePosition; 325 return createTextSpan(absolutePosition, len); 326 } 327 328 getTextChangesBetweenVersions(oldVersion: number, newVersion: number) { 329 if (oldVersion < newVersion) { 330 if (oldVersion >= this.minVersion) { 331 const textChangeRanges: TextChangeRange[] = []; 332 for (let i = oldVersion + 1; i <= newVersion; i++) { 333 const snap = this.versions[this.versionToIndex(i)!]; // TODO: GH#18217 334 for (const textChange of snap.changesSincePreviousVersion) { 335 textChangeRanges.push(textChange.getTextChangeRange()); 336 } 337 } 338 return collapseTextChangeRangesAcrossMultipleVersions(textChangeRanges); 339 } 340 else { 341 return undefined; 342 } 343 } 344 else { 345 return unchangedTextChangeRange; 346 } 347 } 348 349 getLineCount() { 350 return this._getSnapshot().index.getLineCount(); 351 } 352 353 static fromString(script: string) { 354 const svc = new ScriptVersionCache(); 355 const snap = new LineIndexSnapshot(0, svc, new LineIndex()); 356 svc.versions[svc.currentVersion] = snap; 357 const lm = LineIndex.linesFromText(script); 358 snap.index.load(lm.lines); 359 return svc; 360 } 361 } 362 363 class LineIndexSnapshot implements IScriptSnapshot { 364 constructor(readonly version: number, readonly cache: ScriptVersionCache, readonly index: LineIndex, readonly changesSincePreviousVersion: readonly TextChange[] = emptyArray) { 365 } 366 367 getText(rangeStart: number, rangeEnd: number) { 368 return this.index.getText(rangeStart, rangeEnd - rangeStart); 369 } 370 371 getLength() { 372 return this.index.getLength(); 373 } 374 375 getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined { 376 if (oldSnapshot instanceof LineIndexSnapshot && this.cache === oldSnapshot.cache) { 377 if (this.version <= oldSnapshot.version) { 378 return unchangedTextChangeRange; 379 } 380 else { 381 return this.cache.getTextChangesBetweenVersions(oldSnapshot.version, this.version); 382 } 383 } 384 } 385 } 386 387 export class LineIndex { 388 root!: LineNode; 389 // set this to true to check each edit for accuracy 390 checkEdits = false; 391 392 absolutePositionOfStartOfLine(oneBasedLine: number): number { 393 return this.lineNumberToInfo(oneBasedLine).absolutePosition; 394 } 395 396 positionToLineOffset(position: number): protocol.Location { 397 const { oneBasedLine, zeroBasedColumn } = this.root.charOffsetToLineInfo(1, position); 398 return { line: oneBasedLine, offset: zeroBasedColumn + 1 }; 399 } 400 401 private positionToColumnAndLineText(position: number): { zeroBasedColumn: number, lineText: string | undefined } { 402 return this.root.charOffsetToLineInfo(1, position); 403 } 404 405 getLineCount() { 406 return this.root.lineCount(); 407 } 408 409 lineNumberToInfo(oneBasedLine: number): AbsolutePositionAndLineText { 410 const lineCount = this.getLineCount(); 411 if (oneBasedLine <= lineCount) { 412 const { position, leaf } = this.root.lineNumberToInfo(oneBasedLine, 0); 413 return { absolutePosition: position, lineText: leaf && leaf.text }; 414 } 415 else { 416 return { absolutePosition: this.root.charCount(), lineText: undefined }; 417 } 418 } 419 420 load(lines: string[]) { 421 if (lines.length > 0) { 422 const leaves: LineLeaf[] = []; 423 for (let i = 0; i < lines.length; i++) { 424 leaves[i] = new LineLeaf(lines[i]); 425 } 426 this.root = LineIndex.buildTreeFromBottom(leaves); 427 } 428 else { 429 this.root = new LineNode(); 430 } 431 } 432 433 walk(rangeStart: number, rangeLength: number, walkFns: LineIndexWalker) { 434 this.root.walk(rangeStart, rangeLength, walkFns); 435 } 436 437 getText(rangeStart: number, rangeLength: number) { 438 let accum = ""; 439 if ((rangeLength > 0) && (rangeStart < this.root.charCount())) { 440 this.walk(rangeStart, rangeLength, { 441 goSubtree: true, 442 done: false, 443 leaf: (relativeStart: number, relativeLength: number, ll: LineLeaf) => { 444 accum = accum.concat(ll.text.substring(relativeStart, relativeStart + relativeLength)); 445 } 446 }); 447 } 448 return accum; 449 } 450 451 getLength(): number { 452 return this.root.charCount(); 453 } 454 455 every(f: (ll: LineLeaf, s: number, len: number) => boolean, rangeStart: number, rangeEnd?: number) { 456 if (!rangeEnd) { 457 rangeEnd = this.root.charCount(); 458 } 459 const walkFns = { 460 goSubtree: true, 461 done: false, 462 leaf(this: LineIndexWalker, relativeStart: number, relativeLength: number, ll: LineLeaf) { 463 if (!f(ll, relativeStart, relativeLength)) { 464 this.done = true; 465 } 466 } 467 }; 468 this.walk(rangeStart, rangeEnd - rangeStart, walkFns); 469 return !walkFns.done; 470 } 471 472 edit(pos: number, deleteLength: number, newText?: string): LineIndex { 473 if (this.root.charCount() === 0) { 474 Debug.assert(deleteLength === 0); // Can't delete from empty document 475 if (newText !== undefined) { 476 this.load(LineIndex.linesFromText(newText).lines); 477 return this; 478 } 479 return undefined!; // TODO: GH#18217 480 } 481 else { 482 let checkText: string | undefined; 483 if (this.checkEdits) { 484 const source = this.getText(0, this.root.charCount()); 485 checkText = source.slice(0, pos) + newText + source.slice(pos + deleteLength); 486 } 487 const walker = new EditWalker(); 488 let suppressTrailingText = false; 489 if (pos >= this.root.charCount()) { 490 // insert at end 491 pos = this.root.charCount() - 1; 492 const endString = this.getText(pos, 1); 493 if (newText) { 494 newText = endString + newText; 495 } 496 else { 497 newText = endString; 498 } 499 deleteLength = 0; 500 suppressTrailingText = true; 501 } 502 else if (deleteLength > 0) { 503 // check whether last characters deleted are line break 504 const e = pos + deleteLength; 505 const { zeroBasedColumn, lineText } = this.positionToColumnAndLineText(e); 506 if (zeroBasedColumn === 0) { 507 // move range end just past line that will merge with previous line 508 deleteLength += lineText!.length; // TODO: GH#18217 509 // store text by appending to end of insertedText 510 newText = newText ? newText + lineText : lineText; 511 } 512 } 513 514 this.root.walk(pos, deleteLength, walker); 515 walker.insertLines(newText, suppressTrailingText); 516 517 if (this.checkEdits) { 518 const updatedText = walker.lineIndex.getText(0, walker.lineIndex.getLength()); 519 Debug.assert(checkText === updatedText, "buffer edit mismatch"); 520 } 521 522 return walker.lineIndex; 523 } 524 } 525 526 private static buildTreeFromBottom(nodes: LineCollection[]): LineNode { 527 if (nodes.length < lineCollectionCapacity) { 528 return new LineNode(nodes); 529 } 530 531 const interiorNodes = new Array<LineNode>(Math.ceil(nodes.length / lineCollectionCapacity)); 532 let nodeIndex = 0; 533 for (let i = 0; i < interiorNodes.length; i++) { 534 const end = Math.min(nodeIndex + lineCollectionCapacity, nodes.length); 535 interiorNodes[i] = new LineNode(nodes.slice(nodeIndex, end)); 536 nodeIndex = end; 537 } 538 return this.buildTreeFromBottom(interiorNodes); 539 } 540 541 static linesFromText(text: string) { 542 const lineMap = computeLineStarts(text); 543 544 if (lineMap.length === 0) { 545 return { lines: [] as string[], lineMap }; 546 } 547 const lines = new Array(lineMap.length) as string[]; 548 const lc = lineMap.length - 1; 549 for (let lmi = 0; lmi < lc; lmi++) { 550 lines[lmi] = text.substring(lineMap[lmi], lineMap[lmi + 1]); 551 } 552 553 const endText = text.substring(lineMap[lc]); 554 if (endText.length > 0) { 555 lines[lc] = endText; 556 } 557 else { 558 lines.pop(); 559 } 560 return { lines, lineMap }; 561 } 562 } 563 564 class LineNode implements LineCollection { 565 totalChars = 0; 566 totalLines = 0; 567 568 constructor(private readonly children: LineCollection[] = []) { 569 if (children.length) this.updateCounts(); 570 } 571 572 isLeaf() { 573 return false; 574 } 575 576 updateCounts() { 577 this.totalChars = 0; 578 this.totalLines = 0; 579 for (const child of this.children) { 580 this.totalChars += child.charCount(); 581 this.totalLines += child.lineCount(); 582 } 583 } 584 585 private execWalk(rangeStart: number, rangeLength: number, walkFns: LineIndexWalker, childIndex: number, nodeType: CharRangeSection) { 586 if (walkFns.pre) { 587 walkFns.pre(rangeStart, rangeLength, this.children[childIndex], this, nodeType); 588 } 589 if (walkFns.goSubtree) { 590 this.children[childIndex].walk(rangeStart, rangeLength, walkFns); 591 if (walkFns.post) { 592 walkFns.post(rangeStart, rangeLength, this.children[childIndex], this, nodeType); 593 } 594 } 595 else { 596 walkFns.goSubtree = true; 597 } 598 return walkFns.done; 599 } 600 601 private skipChild(relativeStart: number, relativeLength: number, childIndex: number, walkFns: LineIndexWalker, nodeType: CharRangeSection) { 602 if (walkFns.pre && (!walkFns.done)) { 603 walkFns.pre(relativeStart, relativeLength, this.children[childIndex], this, nodeType); 604 walkFns.goSubtree = true; 605 } 606 } 607 608 walk(rangeStart: number, rangeLength: number, walkFns: LineIndexWalker) { 609 // assume (rangeStart < this.totalChars) && (rangeLength <= this.totalChars) 610 let childIndex = 0; 611 let childCharCount = this.children[childIndex].charCount(); 612 // find sub-tree containing start 613 let adjustedStart = rangeStart; 614 while (adjustedStart >= childCharCount) { 615 this.skipChild(adjustedStart, rangeLength, childIndex, walkFns, CharRangeSection.PreStart); 616 adjustedStart -= childCharCount; 617 childIndex++; 618 childCharCount = this.children[childIndex].charCount(); 619 } 620 // Case I: both start and end of range in same subtree 621 if ((adjustedStart + rangeLength) <= childCharCount) { 622 if (this.execWalk(adjustedStart, rangeLength, walkFns, childIndex, CharRangeSection.Entire)) { 623 return; 624 } 625 } 626 else { 627 // Case II: start and end of range in different subtrees (possibly with subtrees in the middle) 628 if (this.execWalk(adjustedStart, childCharCount - adjustedStart, walkFns, childIndex, CharRangeSection.Start)) { 629 return; 630 } 631 let adjustedLength = rangeLength - (childCharCount - adjustedStart); 632 childIndex++; 633 const child = this.children[childIndex]; 634 childCharCount = child.charCount(); 635 while (adjustedLength > childCharCount) { 636 if (this.execWalk(0, childCharCount, walkFns, childIndex, CharRangeSection.Mid)) { 637 return; 638 } 639 adjustedLength -= childCharCount; 640 childIndex++; 641 childCharCount = this.children[childIndex].charCount(); 642 } 643 if (adjustedLength > 0) { 644 if (this.execWalk(0, adjustedLength, walkFns, childIndex, CharRangeSection.End)) { 645 return; 646 } 647 } 648 } 649 // Process any subtrees after the one containing range end 650 if (walkFns.pre) { 651 const clen = this.children.length; 652 if (childIndex < (clen - 1)) { 653 for (let ej = childIndex + 1; ej < clen; ej++) { 654 this.skipChild(0, 0, ej, walkFns, CharRangeSection.PostEnd); 655 } 656 } 657 } 658 } 659 660 // Input position is relative to the start of this node. 661 // Output line number is absolute. 662 charOffsetToLineInfo(lineNumberAccumulator: number, relativePosition: number): { oneBasedLine: number, zeroBasedColumn: number, lineText: string | undefined } { 663 if (this.children.length === 0) { 664 // Root node might have no children if this is an empty document. 665 return { oneBasedLine: lineNumberAccumulator, zeroBasedColumn: relativePosition, lineText: undefined }; 666 } 667 668 for (const child of this.children) { 669 if (child.charCount() > relativePosition) { 670 if (child.isLeaf()) { 671 return { oneBasedLine: lineNumberAccumulator, zeroBasedColumn: relativePosition, lineText: child.text }; 672 } 673 else { 674 return (child as LineNode).charOffsetToLineInfo(lineNumberAccumulator, relativePosition); 675 } 676 } 677 else { 678 relativePosition -= child.charCount(); 679 lineNumberAccumulator += child.lineCount(); 680 } 681 } 682 683 // Skipped all children 684 const lineCount = this.lineCount(); 685 if (lineCount === 0) { // it's empty! (and lineNumberToInfo expects a one-based line) 686 return { oneBasedLine: 1, zeroBasedColumn: 0, lineText: undefined }; 687 } 688 const leaf = Debug.checkDefined(this.lineNumberToInfo(lineCount, 0).leaf); 689 return { oneBasedLine: lineCount, zeroBasedColumn: leaf.charCount(), lineText: undefined }; 690 } 691 692 /** 693 * Input line number is relative to the start of this node. 694 * Output line number is relative to the child. 695 * positionAccumulator will be an absolute position once relativeLineNumber reaches 0. 696 */ 697 lineNumberToInfo(relativeOneBasedLine: number, positionAccumulator: number): { position: number, leaf: LineLeaf | undefined } { 698 for (const child of this.children) { 699 const childLineCount = child.lineCount(); 700 if (childLineCount >= relativeOneBasedLine) { 701 return child.isLeaf() ? { position: positionAccumulator, leaf: child } : (child as LineNode).lineNumberToInfo(relativeOneBasedLine, positionAccumulator); 702 } 703 else { 704 relativeOneBasedLine -= childLineCount; 705 positionAccumulator += child.charCount(); 706 } 707 } 708 709 return { position: positionAccumulator, leaf: undefined }; 710 } 711 712 private splitAfter(childIndex: number) { 713 let splitNode: LineNode | undefined; 714 const clen = this.children.length; 715 childIndex++; 716 const endLength = childIndex; 717 if (childIndex < clen) { 718 splitNode = new LineNode(); 719 while (childIndex < clen) { 720 splitNode.add(this.children[childIndex]); 721 childIndex++; 722 } 723 splitNode.updateCounts(); 724 } 725 this.children.length = endLength; 726 return splitNode; 727 } 728 729 remove(child: LineCollection) { 730 const childIndex = this.findChildIndex(child); 731 const clen = this.children.length; 732 if (childIndex < (clen - 1)) { 733 for (let i = childIndex; i < (clen - 1); i++) { 734 this.children[i] = this.children[i + 1]; 735 } 736 } 737 this.children.pop(); 738 } 739 740 private findChildIndex(child: LineCollection) { 741 const childIndex = this.children.indexOf(child); 742 Debug.assert(childIndex !== -1); 743 return childIndex; 744 } 745 746 insertAt(child: LineCollection, nodes: LineCollection[]) { 747 let childIndex = this.findChildIndex(child); 748 const clen = this.children.length; 749 const nodeCount = nodes.length; 750 // if child is last and there is more room and only one node to place, place it 751 if ((clen < lineCollectionCapacity) && (childIndex === (clen - 1)) && (nodeCount === 1)) { 752 this.add(nodes[0]); 753 this.updateCounts(); 754 return []; 755 } 756 else { 757 const shiftNode = this.splitAfter(childIndex); 758 let nodeIndex = 0; 759 childIndex++; 760 while ((childIndex < lineCollectionCapacity) && (nodeIndex < nodeCount)) { 761 this.children[childIndex] = nodes[nodeIndex]; 762 childIndex++; 763 nodeIndex++; 764 } 765 let splitNodes: LineNode[] = []; 766 let splitNodeCount = 0; 767 if (nodeIndex < nodeCount) { 768 splitNodeCount = Math.ceil((nodeCount - nodeIndex) / lineCollectionCapacity); 769 splitNodes = new Array(splitNodeCount) as LineNode[]; 770 let splitNodeIndex = 0; 771 for (let i = 0; i < splitNodeCount; i++) { 772 splitNodes[i] = new LineNode(); 773 } 774 let splitNode = splitNodes[0]; 775 while (nodeIndex < nodeCount) { 776 splitNode.add(nodes[nodeIndex]); 777 nodeIndex++; 778 if (splitNode.children.length === lineCollectionCapacity) { 779 splitNodeIndex++; 780 splitNode = splitNodes[splitNodeIndex]; 781 } 782 } 783 for (let i = splitNodes.length - 1; i >= 0; i--) { 784 if (splitNodes[i].children.length === 0) { 785 splitNodes.pop(); 786 } 787 } 788 } 789 if (shiftNode) { 790 splitNodes.push(shiftNode); 791 } 792 this.updateCounts(); 793 for (let i = 0; i < splitNodeCount; i++) { 794 splitNodes[i].updateCounts(); 795 } 796 return splitNodes; 797 } 798 } 799 800 // assume there is room for the item; return true if more room 801 add(collection: LineCollection): void { 802 this.children.push(collection); 803 Debug.assert(this.children.length <= lineCollectionCapacity); 804 } 805 806 charCount() { 807 return this.totalChars; 808 } 809 810 lineCount() { 811 return this.totalLines; 812 } 813 } 814 815 class LineLeaf implements LineCollection { 816 constructor(public text: string) { 817 } 818 819 isLeaf() { 820 return true; 821 } 822 823 walk(rangeStart: number, rangeLength: number, walkFns: LineIndexWalker) { 824 walkFns.leaf(rangeStart, rangeLength, this); 825 } 826 827 charCount() { 828 return this.text.length; 829 } 830 831 lineCount() { 832 return 1; 833 } 834 } 835} 836