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