1namespace Harness.LanguageService { 2 3 export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService { 4 const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null 5 const langSvc: any = info.languageService; 6 for (const k of Object.keys(langSvc)) { 7 // eslint-disable-next-line local/only-arrow-functions 8 proxy[k] = function () { 9 return langSvc[k].apply(langSvc, arguments); 10 }; 11 } 12 return proxy; 13 } 14 15 export class ScriptInfo { 16 public version = 1; 17 public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; 18 private lineMap: number[] | undefined; 19 20 constructor(public fileName: string, public content: string, public isRootFile: boolean) { 21 this.setContent(content); 22 } 23 24 private setContent(content: string): void { 25 this.content = content; 26 this.lineMap = undefined; 27 } 28 29 public getLineMap(): number[] { 30 return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content)); 31 } 32 33 public updateContent(content: string): void { 34 this.editRanges = []; 35 this.setContent(content); 36 this.version++; 37 } 38 39 public editContent(start: number, end: number, newText: string): void { 40 // Apply edits 41 const prefix = this.content.substring(0, start); 42 const middle = newText; 43 const suffix = this.content.substring(end); 44 this.setContent(prefix + middle + suffix); 45 46 // Store edit range + new length of script 47 this.editRanges.push({ 48 length: this.content.length, 49 textChangeRange: ts.createTextChangeRange( 50 ts.createTextSpanFromBounds(start, end), newText.length) 51 }); 52 53 // Update version # 54 this.version++; 55 } 56 57 public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { 58 if (startVersion === endVersion) { 59 // No edits! 60 return ts.unchangedTextChangeRange; 61 } 62 63 const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion); 64 const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion); 65 66 const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex); 67 return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange)); 68 } 69 } 70 71 class ScriptSnapshot implements ts.IScriptSnapshot { 72 public textSnapshot: string; 73 public version: number; 74 75 constructor(public scriptInfo: ScriptInfo) { 76 this.textSnapshot = scriptInfo.content; 77 this.version = scriptInfo.version; 78 } 79 80 public getText(start: number, end: number): string { 81 return this.textSnapshot.substring(start, end); 82 } 83 84 public getLength(): number { 85 return this.textSnapshot.length; 86 } 87 88 public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange { 89 const oldShim = oldScript as ScriptSnapshot; 90 return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version); 91 } 92 } 93 94 class ScriptSnapshotProxy implements ts.ScriptSnapshotShim { 95 constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) { 96 } 97 98 public getText(start: number, end: number): string { 99 return this.scriptSnapshot.getText(start, end); 100 } 101 102 public getLength(): number { 103 return this.scriptSnapshot.getLength(); 104 } 105 106 public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined { 107 const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot); 108 return range && JSON.stringify(range); 109 } 110 } 111 112 class DefaultHostCancellationToken implements ts.HostCancellationToken { 113 public static readonly instance = new DefaultHostCancellationToken(); 114 115 public isCancellationRequested() { 116 return false; 117 } 118 } 119 120 export interface LanguageServiceAdapter { 121 getHost(): LanguageServiceAdapterHost; 122 getLanguageService(): ts.LanguageService; 123 getClassifier(): ts.Classifier; 124 getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo; 125 } 126 127 export abstract class LanguageServiceAdapterHost { 128 public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); 129 public typesRegistry: ts.ESMap<string, void> | undefined; 130 private scriptInfos: collections.SortedMap<string, ScriptInfo>; 131 132 constructor(protected cancellationToken = DefaultHostCancellationToken.instance, 133 protected settings = ts.getDefaultCompilerOptions()) { 134 this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); 135 } 136 137 public get vfs() { 138 return this.sys.vfs; 139 } 140 141 public getNewLine(): string { 142 return harnessNewLine; 143 } 144 145 public getFilenames(): string[] { 146 const fileNames: string[] = []; 147 this.scriptInfos.forEach(scriptInfo => { 148 if (scriptInfo.isRootFile) { 149 // only include root files here 150 // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. 151 fileNames.push(scriptInfo.fileName); 152 } 153 }); 154 return fileNames; 155 } 156 157 public realpath(path: string): string { 158 try { 159 return this.vfs.realpathSync(path); 160 } 161 catch { 162 return path; 163 } 164 } 165 166 public fileExists(path: string): boolean { 167 try { 168 return this.vfs.existsSync(path); 169 } 170 catch { 171 return false; 172 } 173 } 174 175 public readFile(path: string): string | undefined { 176 try { 177 return this.vfs.readFileSync(path).toString(); 178 } 179 catch { 180 return undefined; 181 } 182 } 183 184 public directoryExists(path: string) { 185 return this.vfs.statSync(path).isDirectory(); 186 } 187 188 public getScriptInfo(fileName: string): ScriptInfo | undefined { 189 return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName)); 190 } 191 192 public addScript(fileName: string, content: string, isRootFile: boolean): void { 193 this.vfs.mkdirpSync(vpath.dirname(fileName)); 194 this.vfs.writeFileSync(fileName, content); 195 this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile)); 196 } 197 198 public renameFileOrDirectory(oldPath: string, newPath: string): void { 199 this.vfs.mkdirpSync(ts.getDirectoryPath(newPath)); 200 this.vfs.renameSync(oldPath, newPath); 201 202 const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined); 203 this.scriptInfos.forEach((scriptInfo, key) => { 204 const newFileName = updater(key); 205 if (newFileName !== undefined) { 206 this.scriptInfos.delete(key); 207 this.scriptInfos.set(newFileName, scriptInfo); 208 scriptInfo.fileName = newFileName; 209 } 210 }); 211 } 212 213 public editScript(fileName: string, start: number, end: number, newText: string) { 214 const script = this.getScriptInfo(fileName); 215 if (script) { 216 script.editContent(start, end, newText); 217 this.vfs.mkdirpSync(vpath.dirname(fileName)); 218 this.vfs.writeFileSync(fileName, script.content); 219 return; 220 } 221 222 throw new Error("No script with name '" + fileName + "'"); 223 } 224 225 public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ } 226 227 /** 228 * @param line 0 based index 229 * @param col 0 based index 230 */ 231 public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { 232 const script: ScriptInfo = this.getScriptInfo(fileName)!; 233 assert.isOk(script); 234 return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); 235 } 236 237 public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number { 238 const script: ScriptInfo = this.getScriptInfo(fileName)!; 239 assert.isOk(script); 240 return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character); 241 } 242 243 useCaseSensitiveFileNames() { 244 return !this.vfs.ignoreCase; 245 } 246 } 247 248 /// Native adapter 249 class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { 250 isKnownTypesPackageName(name: string): boolean { 251 return !!this.typesRegistry && this.typesRegistry.has(name); 252 } 253 254 getGlobalTypingsCacheLocation() { 255 return "/Library/Caches/typescript"; 256 } 257 258 installPackage = ts.notImplemented; 259 260 getCompilationSettings() { return this.settings; } 261 262 getCancellationToken() { return this.cancellationToken; } 263 264 getDirectories(path: string): string[] { 265 return this.sys.getDirectories(path); 266 } 267 268 getCurrentDirectory(): string { return virtualFileSystemRoot; } 269 270 getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } 271 272 getScriptFileNames(): string[] { 273 return this.getFilenames().filter(ts.isAnySupportedFileExtension); 274 } 275 276 getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { 277 const script = this.getScriptInfo(fileName); 278 return script ? new ScriptSnapshot(script) : undefined; 279 } 280 281 getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } 282 283 getScriptVersion(fileName: string): string { 284 const script = this.getScriptInfo(fileName); 285 return script ? script.version.toString() : undefined!; // TODO: GH#18217 286 } 287 288 directoryExists(dirName: string): boolean { 289 return this.sys.directoryExists(dirName); 290 } 291 292 fileExists(fileName: string): boolean { 293 return this.sys.fileExists(fileName); 294 } 295 296 readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { 297 return this.sys.readDirectory(path, extensions, exclude, include, depth); 298 } 299 300 readFile(path: string): string | undefined { 301 return this.sys.readFile(path); 302 } 303 304 realpath(path: string): string { 305 return this.sys.realpath(path); 306 } 307 308 getTypeRootsVersion() { 309 return 0; 310 } 311 312 log = ts.noop; 313 trace = ts.noop; 314 error = ts.noop; 315 } 316 317 export class NativeLanguageServiceAdapter implements LanguageServiceAdapter { 318 private host: NativeLanguageServiceHost; 319 constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { 320 this.host = new NativeLanguageServiceHost(cancellationToken, options); 321 } 322 getHost(): LanguageServiceAdapterHost { return this.host; } 323 getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } 324 getClassifier(): ts.Classifier { return ts.createClassifier(); } 325 getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); } 326 } 327 328 /// Shim adapter 329 class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost { 330 private nativeHost: NativeLanguageServiceHost; 331 332 public getModuleResolutionsForFile: ((fileName: string) => string) | undefined; 333 public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined; 334 335 constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { 336 super(cancellationToken, options); 337 this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); 338 339 if (preprocessToResolve) { 340 const compilerOptions = this.nativeHost.getCompilationSettings(); 341 const moduleResolutionHost: ts.ModuleResolutionHost = { 342 fileExists: fileName => this.getScriptInfo(fileName) !== undefined, 343 readFile: fileName => { 344 const scriptInfo = this.getScriptInfo(fileName); 345 return scriptInfo && scriptInfo.content; 346 }, 347 useCaseSensitiveFileNames: this.useCaseSensitiveFileNames() 348 }; 349 this.getModuleResolutionsForFile = (fileName) => { 350 const scriptInfo = this.getScriptInfo(fileName)!; 351 const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); 352 const imports: ts.MapLike<string> = {}; 353 for (const module of preprocessInfo.importedFiles) { 354 const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); 355 if (resolutionInfo.resolvedModule) { 356 imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName; 357 } 358 } 359 return JSON.stringify(imports); 360 }; 361 this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { 362 const scriptInfo = this.getScriptInfo(fileName); 363 if (scriptInfo) { 364 const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); 365 const resolutions: ts.MapLike<ts.ResolvedTypeReferenceDirective> = {}; 366 const settings = this.nativeHost.getCompilationSettings(); 367 for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { 368 const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost); 369 if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) { 370 resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!; 371 } 372 } 373 return JSON.stringify(resolutions); 374 } 375 else { 376 return "[]"; 377 } 378 }; 379 } 380 } 381 382 getFilenames(): string[] { return this.nativeHost.getFilenames(); } 383 getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); } 384 addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } 385 editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } 386 positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } 387 388 getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); } 389 getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); } 390 getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); } 391 getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); } 392 getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); } 393 getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); } 394 getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim { 395 const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217 396 return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); 397 } 398 getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); } 399 getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } 400 getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } 401 402 readDirectory = ts.notImplemented; 403 readDirectoryNames = ts.notImplemented; 404 readFileNames = ts.notImplemented; 405 fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } 406 readFile(fileName: string) { 407 const snapshot = this.nativeHost.getScriptSnapshot(fileName); 408 return snapshot && ts.getSnapshotText(snapshot); 409 } 410 log(s: string): void { this.nativeHost.log(s); } 411 trace(s: string): void { this.nativeHost.trace(s); } 412 error(s: string): void { this.nativeHost.error(s); } 413 directoryExists(): boolean { 414 // for tests pessimistically assume that directory always exists 415 return true; 416 } 417 } 418 419 class ClassifierShimProxy implements ts.Classifier { 420 constructor(private shim: ts.ClassifierShim) { 421 } 422 getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications { 423 return ts.notImplemented(); 424 } 425 getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult { 426 const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n"); 427 const entries: ts.ClassificationInfo[] = []; 428 let i = 0; 429 let position = 0; 430 431 for (; i < result.length - 1; i += 2) { 432 const t = entries[i / 2] = { 433 length: parseInt(result[i]), 434 classification: parseInt(result[i + 1]) 435 }; 436 437 assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length); 438 position += t.length; 439 } 440 const finalLexState = parseInt(result[result.length - 1]); 441 442 assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position); 443 444 return { 445 finalLexState, 446 entries 447 }; 448 } 449 } 450 451 function unwrapJSONCallResult(result: string): any { 452 const parsedResult = JSON.parse(result); 453 if (parsedResult.error) { 454 throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error)); 455 } 456 else if (parsedResult.canceled) { 457 throw new ts.OperationCanceledException(); 458 } 459 return parsedResult.result; 460 } 461 462 class LanguageServiceShimProxy implements ts.LanguageService { 463 constructor(private shim: ts.LanguageServiceShim) { 464 } 465 cleanupSemanticCache(): void { 466 this.shim.cleanupSemanticCache(); 467 } 468 getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { 469 return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName)); 470 } 471 getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { 472 return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName)); 473 } 474 getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { 475 return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName)); 476 } 477 getCompilerOptionsDiagnostics(): ts.Diagnostic[] { 478 return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); 479 } 480 getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { 481 return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); 482 } 483 getSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] { 484 return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length, format)); 485 } 486 getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { 487 return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length)); 488 } 489 getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.Classifications { 490 const responseFormat = format || ts.SemanticClassificationFormat.Original; 491 return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length, responseFormat)); 492 } 493 getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined, formattingSettings: ts.FormatCodeSettings | undefined): ts.CompletionInfo { 494 return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences, formattingSettings)); 495 } 496 getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined): ts.CompletionEntryDetails { 497 return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences, data)); 498 } 499 getCompletionEntrySymbol(): ts.Symbol { 500 throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); 501 } 502 getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo { 503 return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position)); 504 } 505 getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan { 506 return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos)); 507 } 508 getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { 509 return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); 510 } 511 getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { 512 return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); 513 } 514 getRenameInfo(fileName: string, position: number, preferences: ts.UserPreferences): ts.RenameInfo { 515 return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, preferences)); 516 } 517 getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange { 518 return unwrapJSONCallResult(this.shim.getSmartSelectionRange(fileName, position)); 519 } 520 findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] { 521 return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); 522 } 523 getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { 524 return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); 525 } 526 getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan { 527 return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position)); 528 } 529 getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { 530 return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position)); 531 } 532 getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] { 533 return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position)); 534 } 535 getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { 536 return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position)); 537 } 538 findReferences(fileName: string, position: number): ts.ReferencedSymbol[] { 539 return unwrapJSONCallResult(this.shim.findReferences(fileName, position)); 540 } 541 getFileReferences(fileName: string): ts.ReferenceEntry[] { 542 return unwrapJSONCallResult(this.shim.getFileReferences(fileName)); 543 } 544 getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { 545 return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); 546 } 547 getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { 548 return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); 549 } 550 getNavigateToItems(searchValue: string): ts.NavigateToItem[] { 551 return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue)); 552 } 553 getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { 554 return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName)); 555 } 556 getNavigationTree(fileName: string): ts.NavigationTree { 557 return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); 558 } 559 getOutliningSpans(fileName: string): ts.OutliningSpan[] { 560 return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); 561 } 562 getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] { 563 return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors))); 564 } 565 getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { 566 return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position)); 567 } 568 getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number { 569 return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options))); 570 } 571 getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { 572 return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options))); 573 } 574 getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { 575 return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options))); 576 } 577 getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] { 578 return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options))); 579 } 580 getDocCommentTemplateAtPosition(fileName: string, position: number, options?: ts.DocCommentTemplateOptions): ts.TextInsertion { 581 return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position, options)); 582 } 583 isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { 584 return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); 585 } 586 getJsxClosingTagAtPosition(): never { 587 throw new Error("Not supported on the shim."); 588 } 589 getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan { 590 return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); 591 } 592 getCodeFixesAtPosition(): never { 593 throw new Error("Not supported on the shim."); 594 } 595 getCombinedCodeFix = ts.notImplemented; 596 applyCodeActionCommand = ts.notImplemented; 597 getCodeFixDiagnostics(): ts.Diagnostic[] { 598 throw new Error("Not supported on the shim."); 599 } 600 getEditsForRefactor(): ts.RefactorEditInfo { 601 throw new Error("Not supported on the shim."); 602 } 603 getApplicableRefactors(): ts.ApplicableRefactorInfo[] { 604 throw new Error("Not supported on the shim."); 605 } 606 organizeImports(_args: ts.OrganizeImportsArgs, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { 607 throw new Error("Not supported on the shim."); 608 } 609 getEditsForFileRename(): readonly ts.FileTextChanges[] { 610 throw new Error("Not supported on the shim."); 611 } 612 prepareCallHierarchy(fileName: string, position: number) { 613 return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position)); 614 } 615 provideCallHierarchyIncomingCalls(fileName: string, position: number) { 616 return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position)); 617 } 618 provideCallHierarchyOutgoingCalls(fileName: string, position: number) { 619 return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position)); 620 } 621 provideInlayHints(fileName: string, span: ts.TextSpan, preference: ts.UserPreferences) { 622 return unwrapJSONCallResult(this.shim.provideInlayHints(fileName, span, preference)); 623 } 624 getEmitOutput(fileName: string): ts.EmitOutput { 625 return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); 626 } 627 getProgram(): ts.Program { 628 throw new Error("Program can not be marshaled across the shim layer."); 629 } 630 getBuilderProgram(): ts.BuilderProgram | undefined { 631 throw new Error("Program can not be marshaled across the shim layer."); 632 } 633 getCurrentProgram(): ts.Program | undefined { 634 throw new Error("Program can not be marshaled across the shim layer."); 635 } 636 getAutoImportProvider(): ts.Program | undefined { 637 throw new Error("Program can not be marshaled across the shim layer."); 638 } 639 updateIsDefinitionOfReferencedSymbols(_referencedSymbols: readonly ts.ReferencedSymbol[], _knownSymbolSpans: ts.Set<ts.DocumentSpan>): boolean { 640 return ts.notImplemented(); 641 } 642 getNonBoundSourceFile(): ts.SourceFile { 643 throw new Error("SourceFile can not be marshaled across the shim layer."); 644 } 645 getSourceFile(): ts.SourceFile { 646 throw new Error("SourceFile can not be marshaled across the shim layer."); 647 } 648 getSourceMapper(): never { 649 return ts.notImplemented(); 650 } 651 clearSourceMapperCache(): never { 652 return ts.notImplemented(); 653 } 654 toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { 655 return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); 656 } 657 toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { 658 return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); 659 } 660 commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { 661 return unwrapJSONCallResult(this.shim.commentSelection(fileName, textRange)); 662 } 663 uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { 664 return unwrapJSONCallResult(this.shim.uncommentSelection(fileName, textRange)); 665 } 666 dispose(): void { this.shim.dispose({}); } 667 } 668 669 export class ShimLanguageServiceAdapter implements LanguageServiceAdapter { 670 private host: ShimLanguageServiceHost; 671 private factory: ts.TypeScriptServicesFactory; 672 constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { 673 this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options); 674 this.factory = new ts.TypeScriptServicesFactory(); 675 } 676 getHost() { return this.host; } 677 getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); } 678 getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); } 679 getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { 680 const coreServicesShim = this.factory.createCoreServicesShim(this.host); 681 const shimResult: { 682 referencedFiles: ts.ShimsFileReference[]; 683 typeReferenceDirectives: ts.ShimsFileReference[]; 684 importedFiles: ts.ShimsFileReference[]; 685 isLibFile: boolean; 686 } = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents))); 687 688 const convertResult: ts.PreProcessedFileInfo = { 689 referencedFiles: [], 690 importedFiles: [], 691 ambientExternalModules: [], 692 isLibFile: shimResult.isLibFile, 693 typeReferenceDirectives: [], 694 libReferenceDirectives: [] 695 }; 696 697 ts.forEach(shimResult.referencedFiles, refFile => { 698 convertResult.referencedFiles.push({ 699 fileName: refFile.path, 700 pos: refFile.position, 701 end: refFile.position + refFile.length 702 }); 703 }); 704 705 ts.forEach(shimResult.importedFiles, importedFile => { 706 convertResult.importedFiles.push({ 707 fileName: importedFile.path, 708 pos: importedFile.position, 709 end: importedFile.position + importedFile.length 710 }); 711 }); 712 713 ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { 714 convertResult.importedFiles.push({ 715 fileName: typeRefDirective.path, 716 pos: typeRefDirective.position, 717 end: typeRefDirective.position + typeRefDirective.length 718 }); 719 }); 720 return convertResult; 721 } 722 } 723 724 // Server adapter 725 class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { 726 private client!: ts.server.SessionClient; 727 728 constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) { 729 super(cancellationToken, settings); 730 } 731 732 onMessage = ts.noop; 733 writeMessage = ts.noop; 734 735 setClient(client: ts.server.SessionClient) { 736 this.client = client; 737 } 738 739 openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { 740 super.openFile(fileName, content, scriptKindName); 741 this.client.openFile(fileName, content, scriptKindName); 742 } 743 744 editScript(fileName: string, start: number, end: number, newText: string) { 745 const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText); 746 super.editScript(fileName, start, end, newText); 747 this.client.changeFile(fileName, changeArgs); 748 } 749 } 750 751 class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { 752 args: string[] = []; 753 newLine: string; 754 useCaseSensitiveFileNames = false; 755 756 constructor(private host: NativeLanguageServiceHost) { 757 this.newLine = this.host.getNewLine(); 758 } 759 760 onMessage = ts.noop; 761 writeMessage = ts.noop; // overridden 762 write(message: string): void { 763 this.writeMessage(message); 764 } 765 766 readFile(fileName: string): string | undefined { 767 if (ts.stringContains(fileName, Compiler.defaultLibFileName)) { 768 fileName = Compiler.defaultLibFileName; 769 } 770 771 // System FS would follow symlinks, even though snapshots are stored by original file name 772 const snapshot = this.host.getScriptSnapshot(fileName) || this.host.getScriptSnapshot(this.realpath(fileName)); 773 return snapshot && ts.getSnapshotText(snapshot); 774 } 775 776 realpath(path: string) { 777 return this.host.realpath(path); 778 } 779 780 writeFile = ts.noop; 781 782 resolvePath(path: string): string { 783 return path; 784 } 785 786 fileExists(path: string): boolean { 787 return this.host.fileExists(path); 788 } 789 790 directoryExists(): boolean { 791 // for tests assume that directory exists 792 return true; 793 } 794 795 getExecutingFilePath(): string { 796 return ""; 797 } 798 799 exit = ts.noop; 800 801 createDirectory(_directoryName: string): void { 802 return ts.notImplemented(); 803 } 804 805 getCurrentDirectory(): string { 806 return this.host.getCurrentDirectory(); 807 } 808 809 getDirectories(path: string): string[] { 810 return this.host.getDirectories(path); 811 } 812 813 getEnvironmentVariable(name: string): string { 814 return ts.sys.getEnvironmentVariable(name); 815 } 816 817 readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { 818 return this.host.readDirectory(path, extensions, exclude, include, depth); 819 } 820 821 watchFile(): ts.FileWatcher { 822 return { close: ts.noop }; 823 } 824 825 watchDirectory(): ts.FileWatcher { 826 return { close: ts.noop }; 827 } 828 829 close = ts.noop; 830 831 info(message: string): void { 832 this.host.log(message); 833 } 834 835 msg(message: string): void { 836 this.host.log(message); 837 } 838 839 loggingEnabled() { 840 return true; 841 } 842 843 getLogFileName(): string | undefined { 844 return undefined; 845 } 846 847 hasLevel() { 848 return false; 849 } 850 851 startGroup() { throw ts.notImplemented(); } 852 endGroup() { throw ts.notImplemented(); } 853 854 perftrc(message: string): void { 855 return this.host.log(message); 856 } 857 858 setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { 859 // eslint-disable-next-line no-restricted-globals 860 return setTimeout(callback, ms, ...args); 861 } 862 863 clearTimeout(timeoutId: any): void { 864 // eslint-disable-next-line no-restricted-globals 865 clearTimeout(timeoutId); 866 } 867 868 setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { 869 // eslint-disable-next-line no-restricted-globals 870 return setImmediate(callback, args); 871 } 872 873 clearImmediate(timeoutId: any): void { 874 // eslint-disable-next-line no-restricted-globals 875 clearImmediate(timeoutId); 876 } 877 878 createHash(s: string) { 879 return mockHash(s); 880 } 881 882 require(_initialDir: string, _moduleName: string): ts.RequireResult { 883 switch (_moduleName) { 884 // Adds to the Quick Info a fixed string and a string from the config file 885 // and replaces the first display part 886 case "quickinfo-augmeneter": 887 return { 888 module: () => ({ 889 create(info: ts.server.PluginCreateInfo) { 890 const proxy = makeDefaultProxy(info); 891 const langSvc: any = info.languageService; 892 // eslint-disable-next-line local/only-arrow-functions 893 proxy.getQuickInfoAtPosition = function () { 894 const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments); 895 if (parts.displayParts.length > 0) { 896 parts.displayParts[0].text = "Proxied"; 897 } 898 parts.displayParts.push({ text: info.config.message, kind: "punctuation" }); 899 return parts; 900 }; 901 902 return proxy; 903 } 904 }), 905 error: undefined 906 }; 907 908 // Throws during initialization 909 case "create-thrower": 910 return { 911 module: () => ({ 912 create() { 913 throw new Error("I am not a well-behaved plugin"); 914 } 915 }), 916 error: undefined 917 }; 918 919 // Adds another diagnostic 920 case "diagnostic-adder": 921 return { 922 module: () => ({ 923 create(info: ts.server.PluginCreateInfo) { 924 const proxy = makeDefaultProxy(info); 925 proxy.getSemanticDiagnostics = filename => { 926 const prev = info.languageService.getSemanticDiagnostics(filename); 927 const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; 928 prev.push({ 929 category: ts.DiagnosticCategory.Warning, 930 file: sourceFile, 931 code: 9999, 932 length: 3, 933 messageText: `Plugin diagnostic`, 934 start: 0 935 }); 936 return prev; 937 }; 938 return proxy; 939 } 940 }), 941 error: undefined 942 }; 943 944 // Accepts configurations 945 case "configurable-diagnostic-adder": 946 let customMessage = "default message"; 947 return { 948 module: () => ({ 949 create(info: ts.server.PluginCreateInfo) { 950 customMessage = info.config.message; 951 const proxy = makeDefaultProxy(info); 952 proxy.getSemanticDiagnostics = filename => { 953 const prev = info.languageService.getSemanticDiagnostics(filename); 954 const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; 955 prev.push({ 956 category: ts.DiagnosticCategory.Error, 957 file: sourceFile, 958 code: 9999, 959 length: 3, 960 messageText: customMessage, 961 start: 0 962 }); 963 return prev; 964 }; 965 return proxy; 966 }, 967 onConfigurationChanged(config: any) { 968 customMessage = config.message; 969 } 970 }), 971 error: undefined 972 }; 973 974 default: 975 return { 976 module: undefined, 977 error: new Error("Could not resolve module") 978 }; 979 } 980 } 981 } 982 983 class FourslashSession extends ts.server.Session { 984 getText(fileName: string) { 985 return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!); 986 } 987 } 988 989 export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { 990 private host: SessionClientHost; 991 private client: ts.server.SessionClient; 992 private server: FourslashSession; 993 constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { 994 // This is the main host that tests use to direct tests 995 const clientHost = new SessionClientHost(cancellationToken, options); 996 const client = new ts.server.SessionClient(clientHost); 997 998 // This host is just a proxy for the clientHost, it uses the client 999 // host to answer server queries about files on disk 1000 const serverHost = new SessionServerHost(clientHost); 1001 const opts: ts.server.SessionOptions = { 1002 host: serverHost, 1003 cancellationToken: ts.server.nullCancellationToken, 1004 useSingleInferredProject: false, 1005 useInferredProjectPerProjectRoot: false, 1006 typingsInstaller: { ...ts.server.nullTypingsInstaller, globalTypingsCacheLocation: "/Library/Caches/typescript" }, 1007 byteLength: Utils.byteLength, 1008 hrtime: process.hrtime, 1009 logger: serverHost, 1010 canUseEvents: true 1011 }; 1012 this.server = new FourslashSession(opts); 1013 1014 1015 // Fake the connection between the client and the server 1016 serverHost.writeMessage = client.onMessage.bind(client); 1017 clientHost.writeMessage = this.server.onMessage.bind(this.server); 1018 1019 // Wire the client to the host to get notifications when a file is open 1020 // or edited. 1021 clientHost.setClient(client); 1022 1023 // Set the properties 1024 this.client = client; 1025 this.host = clientHost; 1026 } 1027 getHost() { return this.host; } 1028 getLanguageService(): ts.LanguageService { return this.client; } 1029 getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } 1030 getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } 1031 assertTextConsistent(fileName: string) { 1032 const serverText = this.server.getText(fileName); 1033 const clientText = this.host.readFile(fileName); 1034 ts.Debug.assert(serverText === clientText, [ 1035 "Server and client text are inconsistent.", 1036 "", 1037 "\x1b[1mServer\x1b[0m\x1b[31m:", 1038 serverText, 1039 "", 1040 "\x1b[1mClient\x1b[0m\x1b[31m:", 1041 clientText, 1042 "", 1043 "This probably means something is wrong with the fourslash infrastructure, not with the test." 1044 ].join(ts.sys.newLine)); 1045 } 1046 } 1047} 1048