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