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