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