diff --git a/Jakefile b/Jakefile index 2cef554cc75..2efd973a021 100644 --- a/Jakefile +++ b/Jakefile @@ -144,7 +144,8 @@ var harnessSources = [ "services/colorization.ts", "services/documentRegistry.ts", "services/preProcessFile.ts", - "services/patternMatcher.ts" + "services/patternMatcher.ts", + "versionCache.ts" ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index a3814c8fec1..366e4dbfd84 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1466,6 +1466,10 @@ module ts.server { 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(); diff --git a/tests/cases/unittests/versionCache.ts b/tests/cases/unittests/versionCache.ts new file mode 100644 index 00000000000..12cf9c1b817 --- /dev/null +++ b/tests/cases/unittests/versionCache.ts @@ -0,0 +1,279 @@ +/// +/// + +module ts { + function editFlat(position: number, deletedLength: number, newText: string, source: string) { + return source.substring(0, position) + newText + source.substring(position + deletedLength, source.length); + } + + function lineColToPosition(lineIndex: server.LineIndex, line: number, col: number) { + var lineInfo = lineIndex.lineNumberToInfo(line); + return (lineInfo.offset + col - 1); + } + + function validateEdit(lineIndex: server.LineIndex, sourceText: string, position: number, deleteLength: number, insertString: string): void { + let checkText = editFlat(position, deleteLength, insertString, sourceText); + let snapshot = lineIndex.edit(position, deleteLength, insertString); + let editedText = snapshot.getText(0, snapshot.getLength()); + + assert.equal(editedText, checkText); + } + + describe('VersionCache TS code', () => { + var testContent = `/// +var x = 10; +var y = { zebra: 12, giraffe: "ell" }; +z.a; +class Point { + x: number; +} +k=y; +var p:Point=new Point(); +var q:Point=p;` + + let {lines, lineMap} = server.LineIndex.linesFromText(testContent); + assert.isTrue(lines.length > 0, "Failed to initialize test text. Expected text to have at least one line"); + + let lineIndex = new server.LineIndex(); + lineIndex.load(lines); + + function validateEditAtLineCharIndex(line: number, char: number, deleteLength: number, insertString: string): void { + let position = lineColToPosition(lineIndex, line, char); + validateEdit(lineIndex, testContent, position, deleteLength, insertString); + } + + it('change 9 1 0 1 {"y"}', () => { + validateEditAtLineCharIndex(9, 1, 0, "y"); + }); + + it('change 9 2 0 1 {"."}', () => { + validateEditAtLineCharIndex(9, 2, 0, "."); + }); + + it('change 9 3 0 1 {"\\n"}', () => { + validateEditAtLineCharIndex(9, 3, 0, "\n"); + }); + + it('change 10 1 0 10 {"\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n"}', () => { + validateEditAtLineCharIndex(10, 1, 0, "\n\n\n\n\n\n\n\n\n\n"); + }); + + it('change 19 1 1 0', () => { + validateEditAtLineCharIndex(19, 1, 1, ""); + }); + + it('change 18 1 1 0', () => { + validateEditAtLineCharIndex(18, 1, 1, ""); + }); + }); + + describe('VersionCache simple text', () => { + let testContent = `in this story: +the lazy brown fox +jumped over the cow +that ate the grass +that was purple at the tips +and grew 1cm per day`; + + let {lines, lineMap} = server.LineIndex.linesFromText(testContent); + assert.isTrue(lines.length > 0, "Failed to initialize test text. Expected text to have at least one line"); + + let lineIndex = new server.LineIndex(); + lineIndex.load(lines); + + function validateEditAtPosition(position: number, deleteLength: number, insertString: string): void { + validateEdit(lineIndex, testContent, position, deleteLength, insertString); + } + + it('Insert at end of file', () => { + validateEditAtPosition(testContent.length, 0, "hmmmm...\r\n"); + }); + + it('Unusual line endings merge', () => { + validateEditAtPosition(lines[0].length - 1, lines[1].length, ""); + }); + + it('Delete whole line and nothing but line (last line)', () => { + validateEditAtPosition(lineMap[lineMap.length - 2], lines[lines.length - 1].length, ""); + }); + + it('Delete whole line and nothing but line (first line)', () => { + validateEditAtPosition(0, lines[0].length, ""); + }); + + it('Delete whole line (first line) and insert with no line breaks', () => { + validateEditAtPosition(0, lines[0].length, "moo, moo, moo! "); + }); + + it('Delete whole line (first line) and insert with multiple line breaks', () => { + validateEditAtPosition(0, lines[0].length, "moo, \r\nmoo, \r\nmoo! "); + }); + + it('Delete multiple lines and nothing but lines (first and second lines)', () => { + validateEditAtPosition(0, lines[0].length + lines[1].length, ""); + }); + + it('Delete multiple lines and nothing but lines (second and third lines)', () => { + validateEditAtPosition(lines[0].length, lines[1].length + lines[2].length, ""); + }); + + it('Insert multiple line breaks', () => { + validateEditAtPosition(21, 1, "cr...\r\ncr...\r\ncr...\r\ncr...\r\ncr...\r\ncr...\r\ncr...\r\ncr...\r\ncr...\r\ncr...\r\ncr...\r\ncr"); + }); + + it('Insert multiple line breaks', () => { + validateEditAtPosition(21, 1, "cr...\r\ncr...\r\ncr"); + }); + + it('Insert multiple line breaks with leading \\n', () => { + validateEditAtPosition(21, 1, "\ncr...\r\ncr...\r\ncr"); + }); + + it('Single line no line breaks deleted or inserted, delete 1 char', () => { + validateEditAtPosition(21, 1, ""); + }); + + it('Single line no line breaks deleted or inserted, insert 1 char', () => { + validateEditAtPosition(21, 0, "b"); + }); + + it('Single line no line breaks deleted or inserted, delete 1, insert 2 chars', () => { + validateEditAtPosition(21, 1, "cr"); + }); + + it('Delete across line break (just the line break)', () => { + validateEditAtPosition(21, 22, ""); + }); + + it('Delete across line break', () => { + validateEditAtPosition(21, 32, ""); + }); + + it('Delete across multiple line breaks and insert no line breaks', () => { + validateEditAtPosition(21, 42, ""); + }); + + it('Delete across multiple line breaks and insert text', () => { + validateEditAtPosition(21, 42, "slithery "); + }); + }); + + describe('VersionCache stress test', () => { + const iterationCount = 20; + //const interationCount = 20000; // uncomment for testing + + // Use scanner.ts, decent size, does not change frequentlly + let testFileName = "src/compiler/scanner.ts"; + let testContent = Harness.IO.readFile(testFileName); + let totalChars = testContent.length; + assert.isTrue(totalChars > 0, "Failed to read test file."); + + let {lines, lineMap} = server.LineIndex.linesFromText(testContent); + assert.isTrue(lines.length > 0, "Failed to initialize test text. Expected text to have at least one line"); + + let lineIndex = new server.LineIndex(); + lineIndex.load(lines); + + let rsa: number[] = []; + let la: number[] = []; + let las: number[] = []; + let elas: number[] = []; + let ersa: number[] = []; + let ela: number[] = []; + let etotalChars = totalChars; + + for (let j = 0; j < 100000; j++) { + rsa[j] = Math.floor(Math.random() * totalChars); + la[j] = Math.floor(Math.random() * (totalChars - rsa[j])); + if (la[j] > 4) { + las[j] = 4; + } + else { + las[j] = la[j]; + } + if (j < 4000) { + ersa[j] = Math.floor(Math.random() * etotalChars); + ela[j] = Math.floor(Math.random() * (etotalChars - ersa[j])); + if (ela[j] > 4) { + elas[j] = 4; + } + else { + elas[j] = ela[j]; + } + etotalChars += (las[j] - elas[j]); + } + } + + it("Range (average length 1/4 file size)", () => { + for (let i = 0; i < iterationCount; i++) { + let s2 = lineIndex.getText(rsa[i], la[i]); + let s1 = testContent.substring(rsa[i], rsa[i] + la[i]); + assert.equal(s1, s2); + } + }); + + it("Range (average length 4 chars)", () => { + for (let j = 0; j < iterationCount; j++) { + let s2 = lineIndex.getText(rsa[j], las[j]); + let s1 = testContent.substring(rsa[j], rsa[j] + las[j]); + assert.equal(s1, s2); + } + }); + + it("Edit (average length 4)", () => { + for (let i = 0; i < iterationCount; i++) { + let insertString = testContent.substring(rsa[100000 - i], rsa[100000 - i] + las[100000 - i]); + let snapshot = lineIndex.edit(rsa[i], las[i], insertString); + let checkText = editFlat(rsa[i], las[i], insertString, testContent); + let snapText = snapshot.getText(0, checkText.length); + assert.equal(checkText, snapText); + } + }); + + it("Edit ScriptVersionCache ", () => { + let svc = server.ScriptVersionCache.fromString(testContent); + let checkText = testContent; + + for (let i = 0; i < iterationCount; i++) { + let insertString = testContent.substring(rsa[i], rsa[i] + las[i]); + svc.edit(ersa[i], elas[i], insertString); + checkText = editFlat(ersa[i], elas[i], insertString, checkText); + if (0 == (i % 4)) { + let snap = svc.getSnapshot(); + let snapText = snap.getText(0, checkText.length); + assert.equal(checkText, snapText); + } + } + }); + + it("Edit (average length 1/4th file size)", () => { + for (let i = 0; i < iterationCount; i++) { + let insertString = testContent.substring(rsa[100000 - i], rsa[100000 - i] + la[100000 - i]); + let snapshot = lineIndex.edit(rsa[i], la[i], insertString); + let checkText = editFlat(rsa[i], la[i], insertString, testContent); + let snapText = snapshot.getText(0, checkText.length); + assert.equal(checkText, snapText); + } + }); + + it("Line/offset from pos", () => { + for (let i = 0; i < iterationCount; i++) { + let lp = lineIndex.charOffsetToLineNumberAndPos(rsa[i]); + let lac = ts.computeLineAndCharacterOfPosition(lineMap, rsa[i]); + assert.equal(lac.line + 1, lp.line, "Line number mismatch " + (lac.line + 1) + " " + lp.line + " " + i); + assert.equal(lac.character, (lp.offset), "Charachter offset mismatch " + lac.character + " " + lp.offset + " " + i); + } + }); + + it("Start pos from line", () => { + for (let i = 0; i < iterationCount; i++) { + for (let j = 0, llen = lines.length; j < llen; j++) { + let lineInfo = lineIndex.lineNumberToInfo(j + 1); + let lineIndexOffset = lineInfo.offset; + let lineMapOffset = lineMap[j]; + assert.equal(lineIndexOffset, lineMapOffset); + } + } + }); + }); +}