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