• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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