• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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