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