1namespace ts { 2 function editFlat(position: number, deletedLength: number, newText: string, source: string) { 3 return source.substring(0, position) + newText + source.substring(position + deletedLength, source.length); 4 } 5 6 function lineColToPosition(lineIndex: server.LineIndex, line: number, col: number) { 7 return lineIndex.absolutePositionOfStartOfLine(line) + (col - 1); 8 } 9 10 function validateEdit(lineIndex: server.LineIndex, sourceText: string, position: number, deleteLength: number, insertString: string): void { 11 const checkText = editFlat(position, deleteLength, insertString, sourceText); 12 const snapshot = lineIndex.edit(position, deleteLength, insertString); 13 const editedText = snapshot.getText(0, snapshot.getLength()); 14 15 assert.equal(editedText, checkText); 16 } 17 18 describe(`unittests:: tsserver:: VersionCache TS code`, () => { 19 let validateEditAtLineCharIndex: (line: number, char: number, deleteLength: number, insertString: string) => void; 20 21 before(() => { 22 const testContent = `/// <reference path="z.ts" /> 23var x = 10; 24var y = { zebra: 12, giraffe: "ell" }; 25z.a; 26class Point { 27 x: number; 28} 29k=y; 30var p:Point=new Point(); 31var q:Point=<Point>p;`; 32 33 const { lines } = server.LineIndex.linesFromText(testContent); 34 assert.isTrue(lines.length > 0, "Failed to initialize test text. Expected text to have at least one line"); 35 36 const lineIndex = new server.LineIndex(); 37 lineIndex.load(lines); 38 39 validateEditAtLineCharIndex = (line: number, char: number, deleteLength: number, insertString: string) => { 40 const position = lineColToPosition(lineIndex, line, char); 41 validateEdit(lineIndex, testContent, position, deleteLength, insertString); 42 }; 43 }); 44 45 after(() => { 46 validateEditAtLineCharIndex = undefined!; 47 }); 48 49 it("handles empty lines array", () => { 50 const lineIndex = new server.LineIndex(); 51 lineIndex.load([]); 52 assert.deepEqual(lineIndex.positionToLineOffset(0), { line: 1, offset: 1 }); 53 }); 54 55 it("handles emptying whole file (GH#44518)", () => { 56 // See below for the main thing that this tests; it would be better to have a test 57 // that uses `ScriptInfo.positionToLineOffset` but I couldn't find away to do that 58 const { lines } = server.LineIndex.linesFromText("function foo() {\n\ndsa\n\n}\n\nfo(dsa\n\n\n "); 59 const lineIndex = new server.LineIndex(); 60 lineIndex.load(lines); 61 const snapshot = lineIndex.edit(0, 39); 62 assert.equal(snapshot.getText(0, snapshot.getLength()), ""); 63 // line must always be >=1, otherwise the failIfInvalidLocation(location) assertion in ScriptInfo.positionToLineOffset will fail 64 assert.deepEqual(snapshot.positionToLineOffset(0), { line: 1, offset: 1 }); 65 }); 66 67 it(`change 9 1 0 1 {"y"}`, () => { 68 validateEditAtLineCharIndex(9, 1, 0, "y"); 69 }); 70 71 it(`change 9 2 0 1 {"."}`, () => { 72 validateEditAtLineCharIndex(9, 2, 0, "."); 73 }); 74 75 it(`change 9 3 0 1 {"\\n"}`, () => { 76 validateEditAtLineCharIndex(9, 3, 0, "\n"); 77 }); 78 79 it(`change 10 1 0 10 {"\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n"}`, () => { 80 validateEditAtLineCharIndex(10, 1, 0, "\n\n\n\n\n\n\n\n\n\n"); 81 }); 82 83 it(`change 19 1 1 0`, () => { 84 validateEditAtLineCharIndex(19, 1, 1, ""); 85 }); 86 87 it(`change 18 1 1 0`, () => { 88 validateEditAtLineCharIndex(18, 1, 1, ""); 89 }); 90 }); 91 92 describe(`unittests:: tsserver:: VersionCache simple text`, () => { 93 let validateEditAtPosition: (position: number, deleteLength: number, insertString: string) => void; 94 let testContent: string; 95 let lines: string[]; 96 let lineMap: number[]; 97 before(() => { 98 testContent = `in this story: 99the lazy brown fox 100jumped over the cow 101that ate the grass 102that was purple at the tips 103and grew 1cm per day`; 104 105 ({ lines, lineMap } = server.LineIndex.linesFromText(testContent)); 106 assert.isTrue(lines.length > 0, "Failed to initialize test text. Expected text to have at least one line"); 107 108 const lineIndex = new server.LineIndex(); 109 lineIndex.load(lines); 110 111 validateEditAtPosition = (position: number, deleteLength: number, insertString: string) => { 112 validateEdit(lineIndex, testContent, position, deleteLength, insertString); 113 }; 114 }); 115 116 after(() => { 117 validateEditAtPosition = undefined!; 118 testContent = undefined!; 119 lines = undefined!; 120 lineMap = undefined!; 121 }); 122 123 it(`Insert at end of file`, () => { 124 validateEditAtPosition(testContent.length, 0, "hmmmm...\r\n"); 125 }); 126 127 it(`Unusual line endings merge`, () => { 128 validateEditAtPosition(lines[0].length - 1, lines[1].length, ""); 129 }); 130 131 it(`Delete whole line and nothing but line (last line)`, () => { 132 validateEditAtPosition(lineMap[lineMap.length - 2], lines[lines.length - 1].length, ""); 133 }); 134 135 it(`Delete whole line and nothing but line (first line)`, () => { 136 validateEditAtPosition(0, lines[0].length, ""); 137 }); 138 139 it(`Delete whole line (first line) and insert with no line breaks`, () => { 140 validateEditAtPosition(0, lines[0].length, "moo, moo, moo! "); 141 }); 142 143 it(`Delete whole line (first line) and insert with multiple line breaks`, () => { 144 validateEditAtPosition(0, lines[0].length, "moo, \r\nmoo, \r\nmoo! "); 145 }); 146 147 it(`Delete multiple lines and nothing but lines (first and second lines)`, () => { 148 validateEditAtPosition(0, lines[0].length + lines[1].length, ""); 149 }); 150 151 it(`Delete multiple lines and nothing but lines (second and third lines)`, () => { 152 validateEditAtPosition(lines[0].length, lines[1].length + lines[2].length, ""); 153 }); 154 155 it(`Insert multiple line breaks`, () => { 156 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"); 157 }); 158 159 it(`Insert multiple line breaks`, () => { 160 validateEditAtPosition(21, 1, "cr...\r\ncr...\r\ncr"); 161 }); 162 163 it(`Insert multiple line breaks with leading \\n`, () => { 164 validateEditAtPosition(21, 1, "\ncr...\r\ncr...\r\ncr"); 165 }); 166 167 it(`Single line no line breaks deleted or inserted, delete 1 char`, () => { 168 validateEditAtPosition(21, 1, ""); 169 }); 170 171 it(`Single line no line breaks deleted or inserted, insert 1 char`, () => { 172 validateEditAtPosition(21, 0, "b"); 173 }); 174 175 it(`Single line no line breaks deleted or inserted, delete 1, insert 2 chars`, () => { 176 validateEditAtPosition(21, 1, "cr"); 177 }); 178 179 it(`Delete across line break (just the line break)`, () => { 180 validateEditAtPosition(21, 22, ""); 181 }); 182 183 it(`Delete across line break`, () => { 184 validateEditAtPosition(21, 32, ""); 185 }); 186 187 it(`Delete across multiple line breaks and insert no line breaks`, () => { 188 validateEditAtPosition(21, 42, ""); 189 }); 190 191 it(`Delete across multiple line breaks and insert text`, () => { 192 validateEditAtPosition(21, 42, "slithery "); 193 }); 194 }); 195 196 describe(`unittests:: tsserver:: VersionCache stress test`, () => { 197 let rsa: number[] = []; 198 let la: number[] = []; 199 let las: number[] = []; 200 let elas: number[] = []; 201 let ersa: number[] = []; 202 let ela: number[] = []; 203 const iterationCount = 20; 204 // const iterationCount = 20000; // uncomment for testing 205 let lines: string[]; 206 let lineMap: number[]; 207 let lineIndex: server.LineIndex; 208 let testContent: string; 209 210 before(() => { 211 // Use scanner.ts, decent size, does not change frequently 212 const testFileName = "src/compiler/scanner.ts"; 213 testContent = Harness.IO.readFile(testFileName)!; 214 const totalChars = testContent.length; 215 assert.isTrue(totalChars > 0, "Failed to read test file."); 216 217 ({ lines, lineMap } = server.LineIndex.linesFromText(testContent)); 218 assert.isTrue(lines.length > 0, "Failed to initialize test text. Expected text to have at least one line"); 219 220 lineIndex = new server.LineIndex(); 221 lineIndex.load(lines); 222 223 let etotalChars = totalChars; 224 225 for (let j = 0; j < 100000; j++) { 226 rsa[j] = Math.floor(Math.random() * totalChars); 227 la[j] = Math.floor(Math.random() * (totalChars - rsa[j])); 228 if (la[j] > 4) { 229 las[j] = 4; 230 } 231 else { 232 las[j] = la[j]; 233 } 234 if (j < 4000) { 235 ersa[j] = Math.floor(Math.random() * etotalChars); 236 ela[j] = Math.floor(Math.random() * (etotalChars - ersa[j])); 237 if (ela[j] > 4) { 238 elas[j] = 4; 239 } 240 else { 241 elas[j] = ela[j]; 242 } 243 etotalChars += (las[j] - elas[j]); 244 } 245 } 246 }); 247 248 after(() => { 249 rsa = undefined!; 250 la = undefined!; 251 las = undefined!; 252 elas = undefined!; 253 ersa = undefined!; 254 ela = undefined!; 255 lines = undefined!; 256 lineMap = undefined!; 257 lineIndex = undefined!; 258 testContent = undefined!; 259 }); 260 261 it("Range (average length 1/4 file size)", () => { 262 for (let i = 0; i < iterationCount; i++) { 263 const s2 = lineIndex.getText(rsa[i], la[i]); 264 const s1 = testContent.substring(rsa[i], rsa[i] + la[i]); 265 assert.equal(s1, s2); 266 } 267 }); 268 269 it("Range (average length 4 chars)", () => { 270 for (let j = 0; j < iterationCount; j++) { 271 const s2 = lineIndex.getText(rsa[j], las[j]); 272 const s1 = testContent.substring(rsa[j], rsa[j] + las[j]); 273 assert.equal(s1, s2); 274 } 275 }); 276 277 it("Edit (average length 4)", () => { 278 for (let i = 0; i < iterationCount; i++) { 279 const insertString = testContent.substring(rsa[100000 - i], rsa[100000 - i] + las[100000 - i]); 280 const snapshot = lineIndex.edit(rsa[i], las[i], insertString); 281 const checkText = editFlat(rsa[i], las[i], insertString, testContent); 282 const snapText = snapshot.getText(0, checkText.length); 283 assert.equal(checkText, snapText); 284 } 285 }); 286 287 it("Edit ScriptVersionCache ", () => { 288 const svc = server.ScriptVersionCache.fromString(testContent); 289 let checkText = testContent; 290 291 for (let i = 0; i < iterationCount; i++) { 292 const insertString = testContent.substring(rsa[i], rsa[i] + las[i]); 293 svc.edit(ersa[i], elas[i], insertString); 294 checkText = editFlat(ersa[i], elas[i], insertString, checkText); 295 if (0 === (i % 4)) { 296 assert.equal(checkText, getSnapshotText(svc.getSnapshot())); 297 } 298 } 299 }); 300 301 it("Edit (average length 1/4th file size)", () => { 302 for (let i = 0; i < iterationCount; i++) { 303 const insertString = testContent.substring(rsa[100000 - i], rsa[100000 - i] + la[100000 - i]); 304 const snapshot = lineIndex.edit(rsa[i], la[i], insertString); 305 const checkText = editFlat(rsa[i], la[i], insertString, testContent); 306 const snapText = snapshot.getText(0, checkText.length); 307 assert.equal(checkText, snapText); 308 } 309 }); 310 311 it("Line/offset from pos", () => { 312 for (let i = 0; i < iterationCount; i++) { 313 const lp = lineIndex.positionToLineOffset(rsa[i]); 314 const lac = computeLineAndCharacterOfPosition(lineMap, rsa[i]); 315 assert.equal(lac.line + 1, lp.line, "Line number mismatch " + (lac.line + 1) + " " + lp.line + " " + i); 316 assert.equal(lac.character, lp.offset - 1, "Character offset mismatch " + lac.character + " " + (lp.offset - 1) + " " + i); 317 } 318 }); 319 320 it("Start pos from line", () => { 321 for (let i = 0; i < iterationCount; i++) { 322 for (let j = 0; j < lines.length; j++) { 323 assert.equal(lineIndex.absolutePositionOfStartOfLine(j + 1), lineMap[j]); 324 } 325 } 326 }); 327 }); 328} 329