diff --git a/Jakefile.js b/Jakefile.js index 2f0ce4aa399..501892bf840 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -100,6 +100,7 @@ var servicesSources = [ var serverCoreSources = [ "node.d.ts", + "scriptVersionCache.ts", "editorServices.ts", "protocol.d.ts", "session.ts", @@ -160,6 +161,7 @@ var harnessSources = harnessCoreSources.concat([ "protocol.d.ts", "session.ts", "client.ts", + "scriptVersionCache.ts", "editorServices.ts" ].map(function (f) { return path.join(serverDirectory, f); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f2ab320c265..01f98a2b954 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2,6 +2,7 @@ /// /// /// +/// namespace ts.server { export interface Logger { @@ -15,7 +16,6 @@ namespace ts.server { msg(s: string, type?: string): void; } - const lineCollectionCapacity = 4; function getDefaultFormatCodeOptions(host: ServerHost): ts.FormatCodeOptions { return ts.clone({ IndentSize: 4, @@ -444,7 +444,8 @@ namespace ts.server { this.lsHost.setCompilationSettings(projectOptions.compilerOptions); } } - saveTo(filename: string, tmpfilename: string) { + + saveTo(filename: string, tmpfilename: string) { const script = this.getScriptInfo(filename); if (script) { const snap = script.snap(); @@ -1338,939 +1339,5 @@ namespace ts.server { project.projectFilename = projectFilename; return project; } - - } - - export interface LineCollection { - charCount(): number; - lineCount(): number; - isLeaf(): boolean; - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker): void; - } - - export interface ILineInfo { - line: number; - offset: number; - text?: string; - leaf?: LineLeaf; - } - - export enum CharRangeSection { - PreStart, - Start, - Entire, - Mid, - End, - PostEnd - } - - export interface ILineIndexWalker { - goSubtree: boolean; - done: boolean; - leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void; - pre?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, - parent: LineNode, nodeType: CharRangeSection): LineCollection; - post?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, - parent: LineNode, nodeType: CharRangeSection): LineCollection; - } - - class BaseLineIndexWalker implements ILineIndexWalker { - goSubtree = true; - done = false; - leaf(rangeStart: number, rangeLength: number, ll: LineLeaf) { - } - } - - class EditWalker extends BaseLineIndexWalker { - lineIndex = new LineIndex(); - // path to start of range - startPath: LineCollection[]; - endBranch: LineCollection[] = []; - branchNode: LineNode; - // path to current node - stack: LineNode[]; - state = CharRangeSection.Entire; - lineCollectionAtBranch: LineCollection; - initialText = ""; - trailingText = ""; - suppressTrailingText = false; - - constructor() { - super(); - this.lineIndex.root = new LineNode(); - this.startPath = [this.lineIndex.root]; - this.stack = [this.lineIndex.root]; - } - - insertLines(insertedText: string) { - if (this.suppressTrailingText) { - this.trailingText = ""; - } - if (insertedText) { - insertedText = this.initialText + insertedText + this.trailingText; - } - else { - insertedText = this.initialText + this.trailingText; - } - const lm = LineIndex.linesFromText(insertedText); - const lines = lm.lines; - if (lines.length > 1) { - if (lines[lines.length - 1] == "") { - lines.length--; - } - } - let branchParent: LineNode; - let lastZeroCount: LineCollection; - - for (let k = this.endBranch.length - 1; k >= 0; k--) { - (this.endBranch[k]).updateCounts(); - if (this.endBranch[k].charCount() === 0) { - lastZeroCount = this.endBranch[k]; - if (k > 0) { - branchParent = this.endBranch[k - 1]; - } - else { - branchParent = this.branchNode; - } - } - } - if (lastZeroCount) { - branchParent.remove(lastZeroCount); - } - - // path at least length two (root and leaf) - let insertionNode = this.startPath[this.startPath.length - 2]; - const leafNode = this.startPath[this.startPath.length - 1]; - const len = lines.length; - - if (len > 0) { - leafNode.text = lines[0]; - - if (len > 1) { - let insertedNodes = new Array(len - 1); - let startNode = leafNode; - for (let i = 1, len = lines.length; i < len; i++) { - insertedNodes[i - 1] = new LineLeaf(lines[i]); - } - let pathIndex = this.startPath.length - 2; - while (pathIndex >= 0) { - insertionNode = this.startPath[pathIndex]; - insertedNodes = insertionNode.insertAt(startNode, insertedNodes); - pathIndex--; - startNode = insertionNode; - } - let insertedNodesLen = insertedNodes.length; - while (insertedNodesLen > 0) { - const newRoot = new LineNode(); - newRoot.add(this.lineIndex.root); - insertedNodes = newRoot.insertAt(this.lineIndex.root, insertedNodes); - insertedNodesLen = insertedNodes.length; - this.lineIndex.root = newRoot; - } - this.lineIndex.root.updateCounts(); - } - else { - for (let j = this.startPath.length - 2; j >= 0; j--) { - (this.startPath[j]).updateCounts(); - } - } - } - else { - // no content for leaf node, so delete it - insertionNode.remove(leafNode); - for (let j = this.startPath.length - 2; j >= 0; j--) { - (this.startPath[j]).updateCounts(); - } - } - - return this.lineIndex; - } - - post(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection): LineCollection { - // have visited the path for start of range, now looking for end - // if range is on single line, we will never make this state transition - if (lineCollection === this.lineCollectionAtBranch) { - this.state = CharRangeSection.End; - } - // always pop stack because post only called when child has been visited - this.stack.length--; - return undefined; - } - - pre(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection) { - // currentNode corresponds to parent, but in the new tree - const currentNode = this.stack[this.stack.length - 1]; - - if ((this.state === CharRangeSection.Entire) && (nodeType === CharRangeSection.Start)) { - // if range is on single line, we will never make this state transition - this.state = CharRangeSection.Start; - this.branchNode = currentNode; - this.lineCollectionAtBranch = lineCollection; - } - - let child: LineCollection; - function fresh(node: LineCollection): LineCollection { - if (node.isLeaf()) { - return new LineLeaf(""); - } - else return new LineNode(); - } - switch (nodeType) { - case CharRangeSection.PreStart: - this.goSubtree = false; - if (this.state !== CharRangeSection.End) { - currentNode.add(lineCollection); - } - break; - case CharRangeSection.Start: - if (this.state === CharRangeSection.End) { - this.goSubtree = false; - } - else { - child = fresh(lineCollection); - currentNode.add(child); - this.startPath[this.startPath.length] = child; - } - break; - case CharRangeSection.Entire: - if (this.state !== CharRangeSection.End) { - child = fresh(lineCollection); - currentNode.add(child); - this.startPath[this.startPath.length] = child; - } - else { - if (!lineCollection.isLeaf()) { - child = fresh(lineCollection); - currentNode.add(child); - this.endBranch[this.endBranch.length] = child; - } - } - break; - case CharRangeSection.Mid: - this.goSubtree = false; - break; - case CharRangeSection.End: - if (this.state !== CharRangeSection.End) { - this.goSubtree = false; - } - else { - if (!lineCollection.isLeaf()) { - child = fresh(lineCollection); - currentNode.add(child); - this.endBranch[this.endBranch.length] = child; - } - } - break; - case CharRangeSection.PostEnd: - this.goSubtree = false; - if (this.state !== CharRangeSection.Start) { - currentNode.add(lineCollection); - } - break; - } - if (this.goSubtree) { - this.stack[this.stack.length] = child; - } - return lineCollection; - } - // just gather text from the leaves - leaf(relativeStart: number, relativeLength: number, ll: LineLeaf) { - if (this.state === CharRangeSection.Start) { - this.initialText = ll.text.substring(0, relativeStart); - } - else if (this.state === CharRangeSection.Entire) { - this.initialText = ll.text.substring(0, relativeStart); - this.trailingText = ll.text.substring(relativeStart + relativeLength); - } - else { - // state is CharRangeSection.End - this.trailingText = ll.text.substring(relativeStart + relativeLength); - } - } - } - - // text change information - export class TextChange { - constructor(public pos: number, public deleteLen: number, public insertedText?: string) { - } - - getTextChangeRange() { - return ts.createTextChangeRange(ts.createTextSpan(this.pos, this.deleteLen), - this.insertedText ? this.insertedText.length : 0); - } - } - - export class ScriptVersionCache { - changes: TextChange[] = []; - versions: LineIndexSnapshot[] = []; - minVersion = 0; // no versions earlier than min version will maintain change history - private currentVersion = 0; - private host: ServerHost; - - static changeNumberThreshold = 8; - static changeLengthThreshold = 256; - static maxVersions = 8; - - // REVIEW: can optimize by coalescing simple edits - edit(pos: number, deleteLen: number, insertedText?: string) { - this.changes[this.changes.length] = new TextChange(pos, deleteLen, insertedText); - if ((this.changes.length > ScriptVersionCache.changeNumberThreshold) || - (deleteLen > ScriptVersionCache.changeLengthThreshold) || - (insertedText && (insertedText.length > ScriptVersionCache.changeLengthThreshold))) { - this.getSnapshot(); - } - } - - latest() { - return this.versions[this.currentVersion]; - } - - latestVersion() { - if (this.changes.length > 0) { - this.getSnapshot(); - } - return this.currentVersion; - } - - reloadFromFile(filename: string, cb?: () => any) { - let content = this.host.readFile(filename); - // If the file doesn't exist or cannot be read, we should - // wipe out its cached content on the server to avoid side effects. - if (!content) { - content = ""; - } - this.reload(content); - if (cb) - cb(); - } - - // reload whole script, leaving no change history behind reload - reload(script: string) { - this.currentVersion++; - this.changes = []; // history wiped out by reload - const snap = new LineIndexSnapshot(this.currentVersion, this); - this.versions[this.currentVersion] = snap; - snap.index = new LineIndex(); - const lm = LineIndex.linesFromText(script); - snap.index.load(lm.lines); - // REVIEW: could use linked list - for (let i = this.minVersion; i < this.currentVersion; i++) { - this.versions[i] = undefined; - } - this.minVersion = this.currentVersion; - - } - - getSnapshot() { - let snap = this.versions[this.currentVersion]; - if (this.changes.length > 0) { - let snapIndex = this.latest().index; - for (let i = 0, len = this.changes.length; i < len; i++) { - const change = this.changes[i]; - snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText); - } - snap = new LineIndexSnapshot(this.currentVersion + 1, this); - snap.index = snapIndex; - snap.changesSincePreviousVersion = this.changes; - this.currentVersion = snap.version; - this.versions[snap.version] = snap; - this.changes = []; - if ((this.currentVersion - this.minVersion) >= ScriptVersionCache.maxVersions) { - const oldMin = this.minVersion; - this.minVersion = (this.currentVersion - ScriptVersionCache.maxVersions) + 1; - for (let j = oldMin; j < this.minVersion; j++) { - this.versions[j] = undefined; - } - } - } - return snap; - } - - getTextChangesBetweenVersions(oldVersion: number, newVersion: number) { - if (oldVersion < newVersion) { - if (oldVersion >= this.minVersion) { - const textChangeRanges: ts.TextChangeRange[] = []; - for (let i = oldVersion + 1; i <= newVersion; i++) { - const snap = this.versions[i]; - for (let j = 0, len = snap.changesSincePreviousVersion.length; j < len; j++) { - const textChange = snap.changesSincePreviousVersion[j]; - textChangeRanges[textChangeRanges.length] = textChange.getTextChangeRange(); - } - } - return ts.collapseTextChangeRangesAcrossMultipleVersions(textChangeRanges); - } - else { - return undefined; - } - } - else { - return ts.unchangedTextChangeRange; - } - } - - static fromString(host: ServerHost, script: string) { - const svc = new ScriptVersionCache(); - const snap = new LineIndexSnapshot(0, svc); - svc.versions[svc.currentVersion] = snap; - svc.host = host; - snap.index = new LineIndex(); - const lm = LineIndex.linesFromText(script); - snap.index.load(lm.lines); - return svc; - } - } - - export class LineIndexSnapshot implements ts.IScriptSnapshot { - index: LineIndex; - changesSincePreviousVersion: TextChange[] = []; - - constructor(public version: number, public cache: ScriptVersionCache) { - } - - getText(rangeStart: number, rangeEnd: number) { - return this.index.getText(rangeStart, rangeEnd - rangeStart); - } - - getLength() { - return this.index.root.charCount(); - } - - // this requires linear space so don't hold on to these - getLineStartPositions(): number[] { - const starts: number[] = [-1]; - let count = 1; - let pos = 0; - this.index.every((ll, s, len) => { - starts[count] = pos; - count++; - pos += ll.text.length; - return true; - }, 0); - return starts; - } - - getLineMapper() { - return (line: number) => { - return this.index.lineNumberToInfo(line).offset; - }; - } - - getTextChangeRangeSinceVersion(scriptVersion: number) { - if (this.version <= scriptVersion) { - return ts.unchangedTextChangeRange; - } - else { - return this.cache.getTextChangesBetweenVersions(scriptVersion, this.version); - } - } - getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { - const oldSnap = oldSnapshot; - return this.getTextChangeRangeSinceVersion(oldSnap.version); - } - } - - export class LineIndex { - root: LineNode; - // set this to true to check each edit for accuracy - checkEdits = false; - - charOffsetToLineNumberAndPos(charOffset: number) { - return this.root.charOffsetToLineNumberAndPos(1, charOffset); - } - - lineNumberToInfo(lineNumber: number): ILineInfo { - const lineCount = this.root.lineCount(); - if (lineNumber <= lineCount) { - const lineInfo = this.root.lineNumberToInfo(lineNumber, 0); - lineInfo.line = lineNumber; - return lineInfo; - } - else { - return { - line: lineNumber, - offset: this.root.charCount() - }; - } - } - - load(lines: string[]) { - if (lines.length > 0) { - const leaves: LineLeaf[] = []; - for (let i = 0, len = lines.length; i < len; i++) { - leaves[i] = new LineLeaf(lines[i]); - } - this.root = LineIndex.buildTreeFromBottom(leaves); - } - else { - this.root = new LineNode(); - } - } - - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { - this.root.walk(rangeStart, rangeLength, walkFns); - } - - getText(rangeStart: number, rangeLength: number) { - let accum = ""; - if ((rangeLength > 0) && (rangeStart < this.root.charCount())) { - this.walk(rangeStart, rangeLength, { - goSubtree: true, - done: false, - leaf: (relativeStart: number, relativeLength: number, ll: LineLeaf) => { - accum = accum.concat(ll.text.substring(relativeStart, relativeStart + relativeLength)); - } - }); - } - return accum; - } - - getLength(): number { - return this.root.charCount(); - } - - every(f: (ll: LineLeaf, s: number, len: number) => boolean, rangeStart: number, rangeEnd?: number) { - if (!rangeEnd) { - rangeEnd = this.root.charCount(); - } - const walkFns = { - goSubtree: true, - done: false, - leaf: function (relativeStart: number, relativeLength: number, ll: LineLeaf) { - if (!f(ll, relativeStart, relativeLength)) { - this.done = true; - } - } - }; - this.walk(rangeStart, rangeEnd - rangeStart, walkFns); - return !walkFns.done; - } - - edit(pos: number, deleteLength: number, newText?: string) { - function editFlat(source: string, s: number, dl: number, nt = "") { - return source.substring(0, s) + nt + source.substring(s + dl, source.length); - } - if (this.root.charCount() === 0) { - // TODO: assert deleteLength === 0 - if (newText) { - this.load(LineIndex.linesFromText(newText).lines); - return this; - } - } - else { - let checkText: string; - if (this.checkEdits) { - checkText = editFlat(this.getText(0, this.root.charCount()), pos, deleteLength, newText); - } - const walker = new EditWalker(); - if (pos >= this.root.charCount()) { - // insert at end - pos = this.root.charCount() - 1; - const endString = this.getText(pos, 1); - if (newText) { - newText = endString + newText; - } - else { - newText = endString; - } - deleteLength = 0; - walker.suppressTrailingText = true; - } - else if (deleteLength > 0) { - // check whether last characters deleted are line break - const e = pos + deleteLength; - const lineInfo = this.charOffsetToLineNumberAndPos(e); - if ((lineInfo && (lineInfo.offset === 0))) { - // move range end just past line that will merge with previous line - deleteLength += lineInfo.text.length; - // store text by appending to end of insertedText - if (newText) { - newText = newText + lineInfo.text; - } - else { - newText = lineInfo.text; - } - } - } - if (pos < this.root.charCount()) { - this.root.walk(pos, deleteLength, walker); - walker.insertLines(newText); - } - if (this.checkEdits) { - const updatedText = this.getText(0, this.root.charCount()); - Debug.assert(checkText == updatedText, "buffer edit mismatch"); - } - return walker.lineIndex; - } - } - - static buildTreeFromBottom(nodes: LineCollection[]): LineNode { - const nodeCount = Math.ceil(nodes.length / lineCollectionCapacity); - const interiorNodes: LineNode[] = []; - let nodeIndex = 0; - for (let i = 0; i < nodeCount; i++) { - interiorNodes[i] = new LineNode(); - let charCount = 0; - let lineCount = 0; - for (let j = 0; j < lineCollectionCapacity; j++) { - if (nodeIndex < nodes.length) { - interiorNodes[i].add(nodes[nodeIndex]); - charCount += nodes[nodeIndex].charCount(); - lineCount += nodes[nodeIndex].lineCount(); - } - else { - break; - } - nodeIndex++; - } - interiorNodes[i].totalChars = charCount; - interiorNodes[i].totalLines = lineCount; - } - if (interiorNodes.length === 1) { - return interiorNodes[0]; - } - else { - return this.buildTreeFromBottom(interiorNodes); - } - } - - static linesFromText(text: string) { - const lineStarts = ts.computeLineStarts(text); - - if (lineStarts.length === 0) { - return { lines: [], lineMap: lineStarts }; - } - const lines = new Array(lineStarts.length); - const lc = lineStarts.length - 1; - for (let lmi = 0; lmi < lc; lmi++) { - lines[lmi] = text.substring(lineStarts[lmi], lineStarts[lmi + 1]); - } - - const endText = text.substring(lineStarts[lc]); - if (endText.length > 0) { - lines[lc] = endText; - } - else { - lines.length--; - } - return { lines: lines, lineMap: lineStarts }; - } - } - - export class LineNode implements LineCollection { - totalChars = 0; - totalLines = 0; - children: LineCollection[] = []; - - isLeaf() { - return false; - } - - updateCounts() { - this.totalChars = 0; - this.totalLines = 0; - for (let i = 0, len = this.children.length; i < len; i++) { - const child = this.children[i]; - this.totalChars += child.charCount(); - this.totalLines += child.lineCount(); - } - } - - execWalk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker, childIndex: number, nodeType: CharRangeSection) { - if (walkFns.pre) { - walkFns.pre(rangeStart, rangeLength, this.children[childIndex], this, nodeType); - } - if (walkFns.goSubtree) { - this.children[childIndex].walk(rangeStart, rangeLength, walkFns); - if (walkFns.post) { - walkFns.post(rangeStart, rangeLength, this.children[childIndex], this, nodeType); - } - } - else { - walkFns.goSubtree = true; - } - return walkFns.done; - } - - skipChild(relativeStart: number, relativeLength: number, childIndex: number, walkFns: ILineIndexWalker, nodeType: CharRangeSection) { - if (walkFns.pre && (!walkFns.done)) { - walkFns.pre(relativeStart, relativeLength, this.children[childIndex], this, nodeType); - walkFns.goSubtree = true; - } - } - - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { - // assume (rangeStart < this.totalChars) && (rangeLength <= this.totalChars) - let childIndex = 0; - let child = this.children[0]; - let childCharCount = child.charCount(); - // find sub-tree containing start - let adjustedStart = rangeStart; - while (adjustedStart >= childCharCount) { - this.skipChild(adjustedStart, rangeLength, childIndex, walkFns, CharRangeSection.PreStart); - adjustedStart -= childCharCount; - childIndex++; - child = this.children[childIndex]; - childCharCount = child.charCount(); - } - // Case I: both start and end of range in same subtree - if ((adjustedStart + rangeLength) <= childCharCount) { - if (this.execWalk(adjustedStart, rangeLength, walkFns, childIndex, CharRangeSection.Entire)) { - return; - } - } - else { - // Case II: start and end of range in different subtrees (possibly with subtrees in the middle) - if (this.execWalk(adjustedStart, childCharCount - adjustedStart, walkFns, childIndex, CharRangeSection.Start)) { - return; - } - let adjustedLength = rangeLength - (childCharCount - adjustedStart); - childIndex++; - child = this.children[childIndex]; - childCharCount = child.charCount(); - while (adjustedLength > childCharCount) { - if (this.execWalk(0, childCharCount, walkFns, childIndex, CharRangeSection.Mid)) { - return; - } - adjustedLength -= childCharCount; - childIndex++; - child = this.children[childIndex]; - childCharCount = child.charCount(); - } - if (adjustedLength > 0) { - if (this.execWalk(0, adjustedLength, walkFns, childIndex, CharRangeSection.End)) { - return; - } - } - } - // Process any subtrees after the one containing range end - if (walkFns.pre) { - const clen = this.children.length; - if (childIndex < (clen - 1)) { - for (let ej = childIndex + 1; ej < clen; ej++) { - this.skipChild(0, 0, ej, walkFns, CharRangeSection.PostEnd); - } - } - } - } - - charOffsetToLineNumberAndPos(lineNumber: number, charOffset: number): ILineInfo { - const childInfo = this.childFromCharOffset(lineNumber, charOffset); - if (!childInfo.child) { - return { - line: lineNumber, - offset: charOffset, - }; - } - else if (childInfo.childIndex < this.children.length) { - if (childInfo.child.isLeaf()) { - return { - line: childInfo.lineNumber, - offset: childInfo.charOffset, - text: ((childInfo.child)).text, - leaf: ((childInfo.child)) - }; - } - else { - const lineNode = (childInfo.child); - return lineNode.charOffsetToLineNumberAndPos(childInfo.lineNumber, childInfo.charOffset); - } - } - else { - const lineInfo = this.lineNumberToInfo(this.lineCount(), 0); - return { line: this.lineCount(), offset: lineInfo.leaf.charCount() }; - } - } - - lineNumberToInfo(lineNumber: number, charOffset: number): ILineInfo { - const childInfo = this.childFromLineNumber(lineNumber, charOffset); - if (!childInfo.child) { - return { - line: lineNumber, - offset: charOffset - }; - } - else if (childInfo.child.isLeaf()) { - return { - line: lineNumber, - offset: childInfo.charOffset, - text: ((childInfo.child)).text, - leaf: ((childInfo.child)) - }; - } - else { - const lineNode = (childInfo.child); - return lineNode.lineNumberToInfo(childInfo.relativeLineNumber, childInfo.charOffset); - } - } - - childFromLineNumber(lineNumber: number, charOffset: number) { - let child: LineCollection; - let relativeLineNumber = lineNumber; - let i: number; - let len: number; - for (i = 0, len = this.children.length; i < len; i++) { - child = this.children[i]; - const childLineCount = child.lineCount(); - if (childLineCount >= relativeLineNumber) { - break; - } - else { - relativeLineNumber -= childLineCount; - charOffset += child.charCount(); - } - } - return { - child: child, - childIndex: i, - relativeLineNumber: relativeLineNumber, - charOffset: charOffset - }; - } - - childFromCharOffset(lineNumber: number, charOffset: number) { - let child: LineCollection; - let i: number; - let len: number; - for (i = 0, len = this.children.length; i < len; i++) { - child = this.children[i]; - if (child.charCount() > charOffset) { - break; - } - else { - charOffset -= child.charCount(); - lineNumber += child.lineCount(); - } - } - return { - child: child, - childIndex: i, - charOffset: charOffset, - lineNumber: lineNumber - }; - } - - splitAfter(childIndex: number) { - let splitNode: LineNode; - const clen = this.children.length; - childIndex++; - const endLength = childIndex; - if (childIndex < clen) { - splitNode = new LineNode(); - while (childIndex < clen) { - splitNode.add(this.children[childIndex]); - childIndex++; - } - splitNode.updateCounts(); - } - this.children.length = endLength; - return splitNode; - } - - remove(child: LineCollection) { - const childIndex = this.findChildIndex(child); - const clen = this.children.length; - if (childIndex < (clen - 1)) { - for (let i = childIndex; i < (clen - 1); i++) { - this.children[i] = this.children[i + 1]; - } - } - this.children.length--; - } - - findChildIndex(child: LineCollection) { - let childIndex = 0; - const clen = this.children.length; - while ((this.children[childIndex] !== child) && (childIndex < clen)) childIndex++; - return childIndex; - } - - insertAt(child: LineCollection, nodes: LineCollection[]) { - let childIndex = this.findChildIndex(child); - const clen = this.children.length; - const nodeCount = nodes.length; - // if child is last and there is more room and only one node to place, place it - if ((clen < lineCollectionCapacity) && (childIndex === (clen - 1)) && (nodeCount === 1)) { - this.add(nodes[0]); - this.updateCounts(); - return []; - } - else { - const shiftNode = this.splitAfter(childIndex); - let nodeIndex = 0; - childIndex++; - while ((childIndex < lineCollectionCapacity) && (nodeIndex < nodeCount)) { - this.children[childIndex] = nodes[nodeIndex]; - childIndex++; - nodeIndex++; - } - let splitNodes: LineNode[] = []; - let splitNodeCount = 0; - if (nodeIndex < nodeCount) { - splitNodeCount = Math.ceil((nodeCount - nodeIndex) / lineCollectionCapacity); - splitNodes = new Array(splitNodeCount); - let splitNodeIndex = 0; - for (let i = 0; i < splitNodeCount; i++) { - splitNodes[i] = new LineNode(); - } - let splitNode = splitNodes[0]; - while (nodeIndex < nodeCount) { - splitNode.add(nodes[nodeIndex]); - nodeIndex++; - if (splitNode.children.length === lineCollectionCapacity) { - splitNodeIndex++; - splitNode = splitNodes[splitNodeIndex]; - } - } - for (let i = splitNodes.length - 1; i >= 0; i--) { - if (splitNodes[i].children.length === 0) { - splitNodes.length--; - } - } - } - if (shiftNode) { - splitNodes[splitNodes.length] = shiftNode; - } - this.updateCounts(); - for (let i = 0; i < splitNodeCount; i++) { - (splitNodes[i]).updateCounts(); - } - return splitNodes; - } - } - - // assume there is room for the item; return true if more room - add(collection: LineCollection) { - this.children[this.children.length] = collection; - return (this.children.length < lineCollectionCapacity); - } - - charCount() { - return this.totalChars; - } - - lineCount() { - return this.totalLines; - } - } - - export class LineLeaf implements LineCollection { - constructor(public text: string) { - } - - isLeaf() { - return true; - } - - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { - walkFns.leaf(rangeStart, rangeLength, this); - } - - charCount() { - return this.text.length; - } - - lineCount() { - return 1; - } } } diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts new file mode 100644 index 00000000000..4acb99b66d2 --- /dev/null +++ b/src/server/scriptVersionCache.ts @@ -0,0 +1,941 @@ +/// +/// +/// +/// + +namespace ts.server { + const lineCollectionCapacity = 4; + + export interface LineCollection { + charCount(): number; + lineCount(): number; + isLeaf(): boolean; + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker): void; + } + + export interface ILineInfo { + line: number; + offset: number; + text?: string; + leaf?: LineLeaf; + } + + export enum CharRangeSection { + PreStart, + Start, + Entire, + Mid, + End, + PostEnd + } + + export interface ILineIndexWalker { + goSubtree: boolean; + done: boolean; + leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void; + pre?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, + parent: LineNode, nodeType: CharRangeSection): LineCollection; + post?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, + parent: LineNode, nodeType: CharRangeSection): LineCollection; + } + + class BaseLineIndexWalker implements ILineIndexWalker { + goSubtree = true; + done = false; + leaf(rangeStart: number, rangeLength: number, ll: LineLeaf) { + } + } + + class EditWalker extends BaseLineIndexWalker { + lineIndex = new LineIndex(); + // path to start of range + startPath: LineCollection[]; + endBranch: LineCollection[] = []; + branchNode: LineNode; + // path to current node + stack: LineNode[]; + state = CharRangeSection.Entire; + lineCollectionAtBranch: LineCollection; + initialText = ""; + trailingText = ""; + suppressTrailingText = false; + + constructor() { + super(); + this.lineIndex.root = new LineNode(); + this.startPath = [this.lineIndex.root]; + this.stack = [this.lineIndex.root]; + } + + insertLines(insertedText: string) { + if (this.suppressTrailingText) { + this.trailingText = ""; + } + if (insertedText) { + insertedText = this.initialText + insertedText + this.trailingText; + } + else { + insertedText = this.initialText + this.trailingText; + } + const lm = LineIndex.linesFromText(insertedText); + const lines = lm.lines; + if (lines.length > 1) { + if (lines[lines.length - 1] == "") { + lines.length--; + } + } + let branchParent: LineNode; + let lastZeroCount: LineCollection; + + for (let k = this.endBranch.length - 1; k >= 0; k--) { + (this.endBranch[k]).updateCounts(); + if (this.endBranch[k].charCount() === 0) { + lastZeroCount = this.endBranch[k]; + if (k > 0) { + branchParent = this.endBranch[k - 1]; + } + else { + branchParent = this.branchNode; + } + } + } + if (lastZeroCount) { + branchParent.remove(lastZeroCount); + } + + // path at least length two (root and leaf) + let insertionNode = this.startPath[this.startPath.length - 2]; + const leafNode = this.startPath[this.startPath.length - 1]; + const len = lines.length; + + if (len > 0) { + leafNode.text = lines[0]; + + if (len > 1) { + let insertedNodes = new Array(len - 1); + let startNode = leafNode; + for (let i = 1, len = lines.length; i < len; i++) { + insertedNodes[i - 1] = new LineLeaf(lines[i]); + } + let pathIndex = this.startPath.length - 2; + while (pathIndex >= 0) { + insertionNode = this.startPath[pathIndex]; + insertedNodes = insertionNode.insertAt(startNode, insertedNodes); + pathIndex--; + startNode = insertionNode; + } + let insertedNodesLen = insertedNodes.length; + while (insertedNodesLen > 0) { + const newRoot = new LineNode(); + newRoot.add(this.lineIndex.root); + insertedNodes = newRoot.insertAt(this.lineIndex.root, insertedNodes); + insertedNodesLen = insertedNodes.length; + this.lineIndex.root = newRoot; + } + this.lineIndex.root.updateCounts(); + } + else { + for (let j = this.startPath.length - 2; j >= 0; j--) { + (this.startPath[j]).updateCounts(); + } + } + } + else { + // no content for leaf node, so delete it + insertionNode.remove(leafNode); + for (let j = this.startPath.length - 2; j >= 0; j--) { + (this.startPath[j]).updateCounts(); + } + } + + return this.lineIndex; + } + + post(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection): LineCollection { + // have visited the path for start of range, now looking for end + // if range is on single line, we will never make this state transition + if (lineCollection === this.lineCollectionAtBranch) { + this.state = CharRangeSection.End; + } + // always pop stack because post only called when child has been visited + this.stack.length--; + return undefined; + } + + pre(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection) { + // currentNode corresponds to parent, but in the new tree + const currentNode = this.stack[this.stack.length - 1]; + + if ((this.state === CharRangeSection.Entire) && (nodeType === CharRangeSection.Start)) { + // if range is on single line, we will never make this state transition + this.state = CharRangeSection.Start; + this.branchNode = currentNode; + this.lineCollectionAtBranch = lineCollection; + } + + let child: LineCollection; + function fresh(node: LineCollection): LineCollection { + if (node.isLeaf()) { + return new LineLeaf(""); + } + else return new LineNode(); + } + switch (nodeType) { + case CharRangeSection.PreStart: + this.goSubtree = false; + if (this.state !== CharRangeSection.End) { + currentNode.add(lineCollection); + } + break; + case CharRangeSection.Start: + if (this.state === CharRangeSection.End) { + this.goSubtree = false; + } + else { + child = fresh(lineCollection); + currentNode.add(child); + this.startPath[this.startPath.length] = child; + } + break; + case CharRangeSection.Entire: + if (this.state !== CharRangeSection.End) { + child = fresh(lineCollection); + currentNode.add(child); + this.startPath[this.startPath.length] = child; + } + else { + if (!lineCollection.isLeaf()) { + child = fresh(lineCollection); + currentNode.add(child); + this.endBranch[this.endBranch.length] = child; + } + } + break; + case CharRangeSection.Mid: + this.goSubtree = false; + break; + case CharRangeSection.End: + if (this.state !== CharRangeSection.End) { + this.goSubtree = false; + } + else { + if (!lineCollection.isLeaf()) { + child = fresh(lineCollection); + currentNode.add(child); + this.endBranch[this.endBranch.length] = child; + } + } + break; + case CharRangeSection.PostEnd: + this.goSubtree = false; + if (this.state !== CharRangeSection.Start) { + currentNode.add(lineCollection); + } + break; + } + if (this.goSubtree) { + this.stack[this.stack.length] = child; + } + return lineCollection; + } + // just gather text from the leaves + leaf(relativeStart: number, relativeLength: number, ll: LineLeaf) { + if (this.state === CharRangeSection.Start) { + this.initialText = ll.text.substring(0, relativeStart); + } + else if (this.state === CharRangeSection.Entire) { + this.initialText = ll.text.substring(0, relativeStart); + this.trailingText = ll.text.substring(relativeStart + relativeLength); + } + else { + // state is CharRangeSection.End + this.trailingText = ll.text.substring(relativeStart + relativeLength); + } + } + } + + // text change information + export class TextChange { + constructor(public pos: number, public deleteLen: number, public insertedText?: string) { + } + + getTextChangeRange() { + return ts.createTextChangeRange(ts.createTextSpan(this.pos, this.deleteLen), + this.insertedText ? this.insertedText.length : 0); + } + } + + export class ScriptVersionCache { + changes: TextChange[] = []; + versions: LineIndexSnapshot[] = []; + minVersion = 0; // no versions earlier than min version will maintain change history + private currentVersion = 0; + private host: ServerHost; + + static changeNumberThreshold = 8; + static changeLengthThreshold = 256; + static maxVersions = 8; + + // REVIEW: can optimize by coalescing simple edits + edit(pos: number, deleteLen: number, insertedText?: string) { + this.changes[this.changes.length] = new TextChange(pos, deleteLen, insertedText); + if ((this.changes.length > ScriptVersionCache.changeNumberThreshold) || + (deleteLen > ScriptVersionCache.changeLengthThreshold) || + (insertedText && (insertedText.length > ScriptVersionCache.changeLengthThreshold))) { + this.getSnapshot(); + } + } + + latest() { + return this.versions[this.currentVersion]; + } + + latestVersion() { + if (this.changes.length > 0) { + this.getSnapshot(); + } + return this.currentVersion; + } + + reloadFromFile(filename: string, cb?: () => any) { + let content = this.host.readFile(filename); + // If the file doesn't exist or cannot be read, we should + // wipe out its cached content on the server to avoid side effects. + if (!content) { + content = ""; + } + this.reload(content); + if (cb) + cb(); + } + + // reload whole script, leaving no change history behind reload + reload(script: string) { + this.currentVersion++; + this.changes = []; // history wiped out by reload + const snap = new LineIndexSnapshot(this.currentVersion, this); + this.versions[this.currentVersion] = snap; + snap.index = new LineIndex(); + const lm = LineIndex.linesFromText(script); + snap.index.load(lm.lines); + // REVIEW: could use linked list + for (let i = this.minVersion; i < this.currentVersion; i++) { + this.versions[i] = undefined; + } + this.minVersion = this.currentVersion; + + } + + getSnapshot() { + let snap = this.versions[this.currentVersion]; + if (this.changes.length > 0) { + let snapIndex = this.latest().index; + for (let i = 0, len = this.changes.length; i < len; i++) { + const change = this.changes[i]; + snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText); + } + snap = new LineIndexSnapshot(this.currentVersion + 1, this); + snap.index = snapIndex; + snap.changesSincePreviousVersion = this.changes; + this.currentVersion = snap.version; + this.versions[snap.version] = snap; + this.changes = []; + if ((this.currentVersion - this.minVersion) >= ScriptVersionCache.maxVersions) { + const oldMin = this.minVersion; + this.minVersion = (this.currentVersion - ScriptVersionCache.maxVersions) + 1; + for (let j = oldMin; j < this.minVersion; j++) { + this.versions[j] = undefined; + } + } + } + return snap; + } + + getTextChangesBetweenVersions(oldVersion: number, newVersion: number) { + if (oldVersion < newVersion) { + if (oldVersion >= this.minVersion) { + const textChangeRanges: ts.TextChangeRange[] = []; + for (let i = oldVersion + 1; i <= newVersion; i++) { + const snap = this.versions[i]; + for (let j = 0, len = snap.changesSincePreviousVersion.length; j < len; j++) { + const textChange = snap.changesSincePreviousVersion[j]; + textChangeRanges[textChangeRanges.length] = textChange.getTextChangeRange(); + } + } + return ts.collapseTextChangeRangesAcrossMultipleVersions(textChangeRanges); + } + else { + return undefined; + } + } + else { + return ts.unchangedTextChangeRange; + } + } + + static fromString(host: ServerHost, script: string) { + const svc = new ScriptVersionCache(); + const snap = new LineIndexSnapshot(0, svc); + svc.versions[svc.currentVersion] = snap; + svc.host = host; + snap.index = new LineIndex(); + const lm = LineIndex.linesFromText(script); + snap.index.load(lm.lines); + return svc; + } + } + + export class LineIndexSnapshot implements ts.IScriptSnapshot { + index: LineIndex; + changesSincePreviousVersion: TextChange[] = []; + + constructor(public version: number, public cache: ScriptVersionCache) { + } + + getText(rangeStart: number, rangeEnd: number) { + return this.index.getText(rangeStart, rangeEnd - rangeStart); + } + + getLength() { + return this.index.root.charCount(); + } + + // this requires linear space so don't hold on to these + getLineStartPositions(): number[] { + const starts: number[] = [-1]; + let count = 1; + let pos = 0; + this.index.every((ll, s, len) => { + starts[count] = pos; + count++; + pos += ll.text.length; + return true; + }, 0); + return starts; + } + + getLineMapper() { + return (line: number) => { + return this.index.lineNumberToInfo(line).offset; + }; + } + + getTextChangeRangeSinceVersion(scriptVersion: number) { + if (this.version <= scriptVersion) { + return ts.unchangedTextChangeRange; + } + else { + return this.cache.getTextChangesBetweenVersions(scriptVersion, this.version); + } + } + getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { + const oldSnap = oldSnapshot; + return this.getTextChangeRangeSinceVersion(oldSnap.version); + } + } + + export class LineIndex { + root: LineNode; + // set this to true to check each edit for accuracy + checkEdits = false; + + charOffsetToLineNumberAndPos(charOffset: number) { + return this.root.charOffsetToLineNumberAndPos(1, charOffset); + } + + lineNumberToInfo(lineNumber: number): ILineInfo { + const lineCount = this.root.lineCount(); + if (lineNumber <= lineCount) { + const lineInfo = this.root.lineNumberToInfo(lineNumber, 0); + lineInfo.line = lineNumber; + return lineInfo; + } + else { + return { + line: lineNumber, + offset: this.root.charCount() + }; + } + } + + load(lines: string[]) { + if (lines.length > 0) { + const leaves: LineLeaf[] = []; + for (let i = 0, len = lines.length; i < len; i++) { + leaves[i] = new LineLeaf(lines[i]); + } + this.root = LineIndex.buildTreeFromBottom(leaves); + } + else { + this.root = new LineNode(); + } + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + this.root.walk(rangeStart, rangeLength, walkFns); + } + + getText(rangeStart: number, rangeLength: number) { + let accum = ""; + if ((rangeLength > 0) && (rangeStart < this.root.charCount())) { + this.walk(rangeStart, rangeLength, { + goSubtree: true, + done: false, + leaf: (relativeStart: number, relativeLength: number, ll: LineLeaf) => { + accum = accum.concat(ll.text.substring(relativeStart, relativeStart + relativeLength)); + } + }); + } + return accum; + } + + getLength(): number { + return this.root.charCount(); + } + + every(f: (ll: LineLeaf, s: number, len: number) => boolean, rangeStart: number, rangeEnd?: number) { + if (!rangeEnd) { + rangeEnd = this.root.charCount(); + } + const walkFns = { + goSubtree: true, + done: false, + leaf: function (relativeStart: number, relativeLength: number, ll: LineLeaf) { + if (!f(ll, relativeStart, relativeLength)) { + this.done = true; + } + } + }; + this.walk(rangeStart, rangeEnd - rangeStart, walkFns); + return !walkFns.done; + } + + edit(pos: number, deleteLength: number, newText?: string) { + function editFlat(source: string, s: number, dl: number, nt = "") { + return source.substring(0, s) + nt + source.substring(s + dl, source.length); + } + if (this.root.charCount() === 0) { + // TODO: assert deleteLength === 0 + if (newText) { + this.load(LineIndex.linesFromText(newText).lines); + return this; + } + } + else { + let checkText: string; + if (this.checkEdits) { + checkText = editFlat(this.getText(0, this.root.charCount()), pos, deleteLength, newText); + } + const walker = new EditWalker(); + if (pos >= this.root.charCount()) { + // insert at end + pos = this.root.charCount() - 1; + const endString = this.getText(pos, 1); + if (newText) { + newText = endString + newText; + } + else { + newText = endString; + } + deleteLength = 0; + walker.suppressTrailingText = true; + } + else if (deleteLength > 0) { + // check whether last characters deleted are line break + const e = pos + deleteLength; + const lineInfo = this.charOffsetToLineNumberAndPos(e); + if ((lineInfo && (lineInfo.offset === 0))) { + // move range end just past line that will merge with previous line + deleteLength += lineInfo.text.length; + // store text by appending to end of insertedText + if (newText) { + newText = newText + lineInfo.text; + } + else { + newText = lineInfo.text; + } + } + } + if (pos < this.root.charCount()) { + this.root.walk(pos, deleteLength, walker); + walker.insertLines(newText); + } + if (this.checkEdits) { + const updatedText = this.getText(0, this.root.charCount()); + Debug.assert(checkText == updatedText, "buffer edit mismatch"); + } + return walker.lineIndex; + } + } + + static buildTreeFromBottom(nodes: LineCollection[]): LineNode { + const nodeCount = Math.ceil(nodes.length / lineCollectionCapacity); + const interiorNodes: LineNode[] = []; + let nodeIndex = 0; + for (let i = 0; i < nodeCount; i++) { + interiorNodes[i] = new LineNode(); + let charCount = 0; + let lineCount = 0; + for (let j = 0; j < lineCollectionCapacity; j++) { + if (nodeIndex < nodes.length) { + interiorNodes[i].add(nodes[nodeIndex]); + charCount += nodes[nodeIndex].charCount(); + lineCount += nodes[nodeIndex].lineCount(); + } + else { + break; + } + nodeIndex++; + } + interiorNodes[i].totalChars = charCount; + interiorNodes[i].totalLines = lineCount; + } + if (interiorNodes.length === 1) { + return interiorNodes[0]; + } + else { + return this.buildTreeFromBottom(interiorNodes); + } + } + + static linesFromText(text: string) { + const lineStarts = ts.computeLineStarts(text); + + if (lineStarts.length === 0) { + return { lines: [], lineMap: lineStarts }; + } + const lines = new Array(lineStarts.length); + const lc = lineStarts.length - 1; + for (let lmi = 0; lmi < lc; lmi++) { + lines[lmi] = text.substring(lineStarts[lmi], lineStarts[lmi + 1]); + } + + const endText = text.substring(lineStarts[lc]); + if (endText.length > 0) { + lines[lc] = endText; + } + else { + lines.length--; + } + return { lines: lines, lineMap: lineStarts }; + } + } + + export class LineNode implements LineCollection { + totalChars = 0; + totalLines = 0; + children: LineCollection[] = []; + + isLeaf() { + return false; + } + + updateCounts() { + this.totalChars = 0; + this.totalLines = 0; + for (let i = 0, len = this.children.length; i < len; i++) { + const child = this.children[i]; + this.totalChars += child.charCount(); + this.totalLines += child.lineCount(); + } + } + + execWalk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker, childIndex: number, nodeType: CharRangeSection) { + if (walkFns.pre) { + walkFns.pre(rangeStart, rangeLength, this.children[childIndex], this, nodeType); + } + if (walkFns.goSubtree) { + this.children[childIndex].walk(rangeStart, rangeLength, walkFns); + if (walkFns.post) { + walkFns.post(rangeStart, rangeLength, this.children[childIndex], this, nodeType); + } + } + else { + walkFns.goSubtree = true; + } + return walkFns.done; + } + + skipChild(relativeStart: number, relativeLength: number, childIndex: number, walkFns: ILineIndexWalker, nodeType: CharRangeSection) { + if (walkFns.pre && (!walkFns.done)) { + walkFns.pre(relativeStart, relativeLength, this.children[childIndex], this, nodeType); + walkFns.goSubtree = true; + } + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + // assume (rangeStart < this.totalChars) && (rangeLength <= this.totalChars) + let childIndex = 0; + let child = this.children[0]; + let childCharCount = child.charCount(); + // find sub-tree containing start + let adjustedStart = rangeStart; + while (adjustedStart >= childCharCount) { + this.skipChild(adjustedStart, rangeLength, childIndex, walkFns, CharRangeSection.PreStart); + adjustedStart -= childCharCount; + childIndex++; + child = this.children[childIndex]; + childCharCount = child.charCount(); + } + // Case I: both start and end of range in same subtree + if ((adjustedStart + rangeLength) <= childCharCount) { + if (this.execWalk(adjustedStart, rangeLength, walkFns, childIndex, CharRangeSection.Entire)) { + return; + } + } + else { + // Case II: start and end of range in different subtrees (possibly with subtrees in the middle) + if (this.execWalk(adjustedStart, childCharCount - adjustedStart, walkFns, childIndex, CharRangeSection.Start)) { + return; + } + let adjustedLength = rangeLength - (childCharCount - adjustedStart); + childIndex++; + child = this.children[childIndex]; + childCharCount = child.charCount(); + while (adjustedLength > childCharCount) { + if (this.execWalk(0, childCharCount, walkFns, childIndex, CharRangeSection.Mid)) { + return; + } + adjustedLength -= childCharCount; + childIndex++; + child = this.children[childIndex]; + childCharCount = child.charCount(); + } + if (adjustedLength > 0) { + if (this.execWalk(0, adjustedLength, walkFns, childIndex, CharRangeSection.End)) { + return; + } + } + } + // Process any subtrees after the one containing range end + if (walkFns.pre) { + const clen = this.children.length; + if (childIndex < (clen - 1)) { + for (let ej = childIndex + 1; ej < clen; ej++) { + this.skipChild(0, 0, ej, walkFns, CharRangeSection.PostEnd); + } + } + } + } + + charOffsetToLineNumberAndPos(lineNumber: number, charOffset: number): ILineInfo { + const childInfo = this.childFromCharOffset(lineNumber, charOffset); + if (!childInfo.child) { + return { + line: lineNumber, + offset: charOffset, + }; + } + else if (childInfo.childIndex < this.children.length) { + if (childInfo.child.isLeaf()) { + return { + line: childInfo.lineNumber, + offset: childInfo.charOffset, + text: ((childInfo.child)).text, + leaf: ((childInfo.child)) + }; + } + else { + const lineNode = (childInfo.child); + return lineNode.charOffsetToLineNumberAndPos(childInfo.lineNumber, childInfo.charOffset); + } + } + else { + const lineInfo = this.lineNumberToInfo(this.lineCount(), 0); + return { line: this.lineCount(), offset: lineInfo.leaf.charCount() }; + } + } + + lineNumberToInfo(lineNumber: number, charOffset: number): ILineInfo { + const childInfo = this.childFromLineNumber(lineNumber, charOffset); + if (!childInfo.child) { + return { + line: lineNumber, + offset: charOffset + }; + } + else if (childInfo.child.isLeaf()) { + return { + line: lineNumber, + offset: childInfo.charOffset, + text: ((childInfo.child)).text, + leaf: ((childInfo.child)) + }; + } + else { + const lineNode = (childInfo.child); + return lineNode.lineNumberToInfo(childInfo.relativeLineNumber, childInfo.charOffset); + } + } + + childFromLineNumber(lineNumber: number, charOffset: number) { + let child: LineCollection; + let relativeLineNumber = lineNumber; + let i: number; + let len: number; + for (i = 0, len = this.children.length; i < len; i++) { + child = this.children[i]; + const childLineCount = child.lineCount(); + if (childLineCount >= relativeLineNumber) { + break; + } + else { + relativeLineNumber -= childLineCount; + charOffset += child.charCount(); + } + } + return { + child: child, + childIndex: i, + relativeLineNumber: relativeLineNumber, + charOffset: charOffset + }; + } + + childFromCharOffset(lineNumber: number, charOffset: number) { + let child: LineCollection; + let i: number; + let len: number; + for (i = 0, len = this.children.length; i < len; i++) { + child = this.children[i]; + if (child.charCount() > charOffset) { + break; + } + else { + charOffset -= child.charCount(); + lineNumber += child.lineCount(); + } + } + return { + child: child, + childIndex: i, + charOffset: charOffset, + lineNumber: lineNumber + }; + } + + splitAfter(childIndex: number) { + let splitNode: LineNode; + const clen = this.children.length; + childIndex++; + const endLength = childIndex; + if (childIndex < clen) { + splitNode = new LineNode(); + while (childIndex < clen) { + splitNode.add(this.children[childIndex]); + childIndex++; + } + splitNode.updateCounts(); + } + this.children.length = endLength; + return splitNode; + } + + remove(child: LineCollection) { + const childIndex = this.findChildIndex(child); + const clen = this.children.length; + if (childIndex < (clen - 1)) { + for (let i = childIndex; i < (clen - 1); i++) { + this.children[i] = this.children[i + 1]; + } + } + this.children.length--; + } + + findChildIndex(child: LineCollection) { + let childIndex = 0; + const clen = this.children.length; + while ((this.children[childIndex] !== child) && (childIndex < clen)) childIndex++; + return childIndex; + } + + insertAt(child: LineCollection, nodes: LineCollection[]) { + let childIndex = this.findChildIndex(child); + const clen = this.children.length; + const nodeCount = nodes.length; + // if child is last and there is more room and only one node to place, place it + if ((clen < lineCollectionCapacity) && (childIndex === (clen - 1)) && (nodeCount === 1)) { + this.add(nodes[0]); + this.updateCounts(); + return []; + } + else { + const shiftNode = this.splitAfter(childIndex); + let nodeIndex = 0; + childIndex++; + while ((childIndex < lineCollectionCapacity) && (nodeIndex < nodeCount)) { + this.children[childIndex] = nodes[nodeIndex]; + childIndex++; + nodeIndex++; + } + let splitNodes: LineNode[] = []; + let splitNodeCount = 0; + if (nodeIndex < nodeCount) { + splitNodeCount = Math.ceil((nodeCount - nodeIndex) / lineCollectionCapacity); + splitNodes = new Array(splitNodeCount); + let splitNodeIndex = 0; + for (let i = 0; i < splitNodeCount; i++) { + splitNodes[i] = new LineNode(); + } + let splitNode = splitNodes[0]; + while (nodeIndex < nodeCount) { + splitNode.add(nodes[nodeIndex]); + nodeIndex++; + if (splitNode.children.length === lineCollectionCapacity) { + splitNodeIndex++; + splitNode = splitNodes[splitNodeIndex]; + } + } + for (let i = splitNodes.length - 1; i >= 0; i--) { + if (splitNodes[i].children.length === 0) { + splitNodes.length--; + } + } + } + if (shiftNode) { + splitNodes[splitNodes.length] = shiftNode; + } + this.updateCounts(); + for (let i = 0; i < splitNodeCount; i++) { + (splitNodes[i]).updateCounts(); + } + return splitNodes; + } + } + + // assume there is room for the item; return true if more room + add(collection: LineCollection) { + this.children[this.children.length] = collection; + return (this.children.length < lineCollectionCapacity); + } + + charCount() { + return this.totalChars; + } + + lineCount() { + return this.totalLines; + } + } + + export class LineLeaf implements LineCollection { + constructor(public text: string) { + } + + isLeaf() { + return true; + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + walkFns.leaf(rangeStart, rangeLength, this); + } + + charCount() { + return this.text.length; + } + + lineCount() { + return 1; + } + } +} \ No newline at end of file