• 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) {
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