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