• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2    ApplicableRefactorInfo, BuilderProgram, CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall,
3    Classifications, ClassifiedSpan, CodeActionCommand, CodeFixAction, CompletionEntry, CompletionEntryDetails,
4    CompletionInfo, computeLineAndCharacterOfPosition, computeLineStarts, computePositionOfLineAndCharacter,
5    createQueue, createTextSpanFromBounds, Debug, DefinitionInfo, DefinitionInfoAndBoundSpan, Diagnostic,
6    DiagnosticCategory, DiagnosticWithLocation, DocCommentTemplateOptions, DocumentHighlights, DocumentSpan,
7    EditorOptions, EmitOutput, FileTextChanges, firstDefined, FormatCodeOptions, FormatCodeSettings, getSnapshotText,
8    identity, ImplementationLocation, InlayHint, InlayHintKind, isString, JSDocTagInfo, LanguageService,
9    LanguageServiceHost, map, Map, mapOneOrMany, NavigateToItem, NavigationBarItem, NavigationTree, notImplemented,
10    OrganizeImportsArgs, OutliningSpan, PatternMatchKind, Program, QuickInfo, RefactorEditInfo, ReferencedSymbol,
11    ReferenceEntry, RenameInfo, RenameInfoFailure, RenameInfoSuccess, RenameLocation, ScriptElementKind,
12    SemanticClassificationFormat, Set, SignatureHelpItem, SignatureHelpItems, SourceFile, Symbol, TextChange,
13    TextInsertion, textPart, TextRange, TextSpan, TodoComment, TodoCommentDescriptor, UserPreferences,
14} from "./_namespaces/ts";
15import { CommandNames, protocol } from "./_namespaces/ts.server";
16
17export interface SessionClientHost extends LanguageServiceHost {
18    writeMessage(message: string): void;
19}
20
21interface RenameEntry {
22    readonly renameInfo: RenameInfo;
23    readonly inputs: {
24        readonly fileName: string;
25        readonly position: number;
26        readonly findInStrings: boolean;
27        readonly findInComments: boolean;
28    };
29    readonly locations: RenameLocation[];
30}
31
32/** @internal */
33export function extractMessage(message: string): string {
34    // Read the content length
35    const contentLengthPrefix = "Content-Length: ";
36    const lines = message.split(/\r?\n/);
37    Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response.");
38
39    const contentLengthText = lines[0];
40    Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header.");
41    const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length));
42
43    // Read the body
44    const responseBody = lines[2];
45
46    // Verify content length
47    Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length.");
48    return responseBody;
49}
50
51export class SessionClient implements LanguageService {
52    private sequence = 0;
53    private lineMaps = new Map<string, number[]>();
54    private messages = createQueue<string>();
55    private lastRenameEntry: RenameEntry | undefined;
56    private preferences: UserPreferences | undefined;
57
58    constructor(private host: SessionClientHost) {
59    }
60
61    public onMessage(message: string): void {
62        this.messages.enqueue(message);
63    }
64
65    private writeMessage(message: string): void {
66        this.host.writeMessage(message);
67    }
68
69    private getLineMap(fileName: string): number[] {
70        let lineMap = this.lineMaps.get(fileName);
71        if (!lineMap) {
72            lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!));
73            this.lineMaps.set(fileName, lineMap);
74        }
75        return lineMap;
76    }
77
78    private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number {
79        lineMap = lineMap || this.getLineMap(fileName);
80        return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1);
81    }
82
83    private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location {
84        const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position);
85        return {
86            line: lineOffset.line + 1,
87            offset: lineOffset.character + 1
88        };
89    }
90
91    private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange {
92        return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText };
93    }
94
95    private processRequest<T extends protocol.Request>(command: string, args: T["arguments"]): T {
96        const request: protocol.Request = {
97            seq: this.sequence,
98            type: "request",
99            arguments: args,
100            command
101        };
102        this.sequence++;
103
104        this.writeMessage(JSON.stringify(request));
105
106        return request as T;
107    }
108
109    private processResponse<T extends protocol.Response>(request: protocol.Request, expectEmptyBody = false): T {
110        let foundResponseMessage = false;
111        let response!: T;
112        while (!foundResponseMessage) {
113            const lastMessage = this.messages.dequeue()!;
114            Debug.assert(!!lastMessage, "Did not receive any responses.");
115            const responseBody = extractMessage(lastMessage);
116            try {
117                response = JSON.parse(responseBody);
118                // the server may emit events before emitting the response. We
119                // want to ignore these events for testing purpose.
120                if (response.type === "response") {
121                    foundResponseMessage = true;
122                }
123            }
124            catch (e) {
125                throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n  Error details: " + e.message);
126            }
127        }
128
129        // unmarshal errors
130        if (!response.success) {
131            throw new Error("Error " + response.message);
132        }
133
134        Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number.");
135        Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body.");
136        Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body.");
137
138        return response;
139    }
140
141    /** @internal */
142    configure(preferences: UserPreferences) {
143        this.preferences = preferences;
144        const args: protocol.ConfigureRequestArguments = { preferences };
145        const request = this.processRequest(CommandNames.Configure, args);
146        this.processResponse(request, /*expectEmptyBody*/ true);
147    }
148
149    /** @internal */
150    setFormattingOptions(formatOptions: FormatCodeSettings) {
151        const args: protocol.ConfigureRequestArguments = { formatOptions };
152        const request = this.processRequest(CommandNames.Configure, args);
153        this.processResponse(request, /*expectEmptyBody*/ true);
154    }
155
156    /** @internal */
157    setCompilerOptionsForInferredProjects(options: protocol.CompilerOptions) {
158        const args: protocol.SetCompilerOptionsForInferredProjectsArgs = { options };
159        const request = this.processRequest(CommandNames.CompilerOptionsForInferredProjects, args);
160        this.processResponse(request, /*expectEmptyBody*/ false);
161    }
162
163    openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void {
164        const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName };
165        this.processRequest(CommandNames.Open, args);
166    }
167
168    closeFile(file: string): void {
169        const args: protocol.FileRequestArgs = { file };
170        this.processRequest(CommandNames.Close, args);
171    }
172
173    createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs {
174        return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString };
175    }
176
177    changeFile(fileName: string, args: protocol.ChangeRequestArgs): void {
178        // clear the line map after an edit
179        this.lineMaps.set(fileName, undefined!); // TODO: GH#18217
180        this.processRequest(CommandNames.Change, args);
181    }
182
183    toLineColumnOffset(fileName: string, position: number) {
184        const { line, offset } = this.positionToOneBasedLineOffset(fileName, position);
185        return { line, character: offset };
186    }
187
188    getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {
189        const args = this.createFileLocationRequestArgs(fileName, position);
190
191        const request = this.processRequest<protocol.QuickInfoRequest>(CommandNames.Quickinfo, args);
192        const response = this.processResponse<protocol.QuickInfoResponse>(request);
193        const body = response.body!; // TODO: GH#18217
194
195        return {
196            kind: body.kind,
197            kindModifiers: body.kindModifiers,
198            textSpan: this.decodeSpan(body, fileName),
199            displayParts: [{ kind: "text", text: body.displayString }],
200            documentation: typeof body.documentation === "string" ? [{ kind: "text", text: body.documentation }] : body.documentation,
201            tags: this.decodeLinkDisplayParts(body.tags)
202        };
203    }
204
205    getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo {
206        const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList };
207
208        const request = this.processRequest<protocol.ProjectInfoRequest>(CommandNames.ProjectInfo, args);
209        const response = this.processResponse<protocol.ProjectInfoResponse>(request);
210
211        return {
212            configFileName: response.body!.configFileName, // TODO: GH#18217
213            fileNames: response.body!.fileNames
214        };
215    }
216
217    getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo {
218        // Not passing along 'preferences' because server should already have those from the 'configure' command
219        const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position);
220
221        const request = this.processRequest<protocol.CompletionsRequest>(CommandNames.CompletionInfo, args);
222        const response = this.processResponse<protocol.CompletionInfoResponse>(request);
223
224        return {
225            isGlobalCompletion: response.body!.isGlobalCompletion,
226            isMemberCompletion: response.body!.isMemberCompletion,
227            isNewIdentifierLocation: response.body!.isNewIdentifierLocation,
228            entries: response.body!.entries.map<CompletionEntry>(entry => { // TODO: GH#18217
229                if (entry.replacementSpan !== undefined) {
230                    const res: CompletionEntry = { ...entry, data: entry.data as any, replacementSpan: this.decodeSpan(entry.replacementSpan, fileName) };
231                    return res;
232                }
233
234                return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217
235            })
236        };
237    }
238
239    getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, _preferences: UserPreferences | undefined, data: unknown): CompletionEntryDetails {
240        const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source, data }] };
241
242        const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetailsFull, args);
243        const response = this.processResponse<protocol.Response>(request);
244        Debug.assert(response.body.length === 1, "Unexpected length of completion details response body.");
245        return response.body[0];
246    }
247
248    getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol {
249        return notImplemented();
250    }
251
252    getNavigateToItems(searchValue: string): NavigateToItem[] {
253        const args: protocol.NavtoRequestArgs = {
254            searchValue,
255            file: this.host.getScriptFileNames()[0]
256        };
257
258        const request = this.processRequest<protocol.NavtoRequest>(CommandNames.Navto, args);
259        const response = this.processResponse<protocol.NavtoResponse>(request);
260
261        return response.body!.map(entry => ({ // TODO: GH#18217
262            name: entry.name,
263            containerName: entry.containerName || "",
264            containerKind: entry.containerKind || ScriptElementKind.unknown,
265            kind: entry.kind,
266            kindModifiers: entry.kindModifiers || "",
267            matchKind: entry.matchKind as keyof typeof PatternMatchKind,
268            isCaseSensitive: entry.isCaseSensitive,
269            fileName: entry.file,
270            textSpan: this.decodeSpan(entry),
271        }));
272    }
273
274    getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] {
275        const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end);
276
277
278        // TODO: handle FormatCodeOptions
279        const request = this.processRequest<protocol.FormatRequest>(CommandNames.Format, args);
280        const response = this.processResponse<protocol.FormatResponse>(request);
281
282        return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217
283    }
284
285    getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] {
286        return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options);
287    }
288
289    getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] {
290        const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key };
291
292        // TODO: handle FormatCodeOptions
293        const request = this.processRequest<protocol.FormatOnKeyRequest>(CommandNames.Formatonkey, args);
294        const response = this.processResponse<protocol.FormatResponse>(request);
295
296        return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217
297    }
298
299    getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] {
300        const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);
301
302        const request = this.processRequest<protocol.DefinitionRequest>(CommandNames.Definition, args);
303        const response = this.processResponse<protocol.DefinitionResponse>(request);
304
305        return response.body!.map(entry => ({ // TODO: GH#18217
306            containerKind: ScriptElementKind.unknown,
307            containerName: "",
308            fileName: entry.file,
309            textSpan: this.decodeSpan(entry),
310            kind: ScriptElementKind.unknown,
311            name: ""
312        }));
313    }
314
315    getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan {
316        const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);
317
318        const request = this.processRequest<protocol.DefinitionAndBoundSpanRequest>(CommandNames.DefinitionAndBoundSpan, args);
319        const response = this.processResponse<protocol.DefinitionInfoAndBoundSpanResponse>(request);
320        const body = Debug.checkDefined(response.body); // TODO: GH#18217
321
322        return {
323            definitions: body.definitions.map(entry => ({
324                containerKind: ScriptElementKind.unknown,
325                containerName: "",
326                fileName: entry.file,
327                textSpan: this.decodeSpan(entry),
328                kind: ScriptElementKind.unknown,
329                name: "",
330                unverified: entry.unverified,
331            })),
332            textSpan: this.decodeSpan(body.textSpan, request.arguments.file)
333        };
334    }
335
336    getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] {
337        const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);
338
339        const request = this.processRequest<protocol.TypeDefinitionRequest>(CommandNames.TypeDefinition, args);
340        const response = this.processResponse<protocol.TypeDefinitionResponse>(request);
341
342        return response.body!.map(entry => ({ // TODO: GH#18217
343            containerKind: ScriptElementKind.unknown,
344            containerName: "",
345            fileName: entry.file,
346            textSpan: this.decodeSpan(entry),
347            kind: ScriptElementKind.unknown,
348            name: ""
349        }));
350    }
351
352    getSourceDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfo[] {
353        const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);
354        const request = this.processRequest<protocol.FindSourceDefinitionRequest>(CommandNames.FindSourceDefinition, args);
355        const response = this.processResponse<protocol.DefinitionResponse>(request);
356        const body = Debug.checkDefined(response.body); // TODO: GH#18217
357
358        return body.map(entry => ({
359            containerKind: ScriptElementKind.unknown,
360            containerName: "",
361            fileName: entry.file,
362            textSpan: this.decodeSpan(entry),
363            kind: ScriptElementKind.unknown,
364            name: "",
365            unverified: entry.unverified,
366        }));
367    }
368
369    getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] {
370        const args = this.createFileLocationRequestArgs(fileName, position);
371
372        const request = this.processRequest<protocol.ImplementationRequest>(CommandNames.Implementation, args);
373        const response = this.processResponse<protocol.ImplementationResponse>(request);
374
375        return response.body!.map(entry => ({ // TODO: GH#18217
376            fileName: entry.file,
377            textSpan: this.decodeSpan(entry),
378            kind: ScriptElementKind.unknown,
379            displayParts: []
380        }));
381    }
382
383    findReferences(fileName: string, position: number): ReferencedSymbol[] {
384        const args = this.createFileLocationRequestArgs(fileName, position);
385        const request = this.processRequest<protocol.ReferencesRequest>(CommandNames.ReferencesFull, args);
386        const response = this.processResponse(request);
387        return response.body;
388    }
389
390    getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] {
391        const args = this.createFileLocationRequestArgs(fileName, position);
392
393        const request = this.processRequest<protocol.ReferencesRequest>(CommandNames.References, args);
394        const response = this.processResponse<protocol.ReferencesResponse>(request);
395
396        return response.body!.refs.map(entry => ({ // TODO: GH#18217
397            fileName: entry.file,
398            textSpan: this.decodeSpan(entry),
399            isWriteAccess: entry.isWriteAccess,
400            isDefinition: entry.isDefinition,
401        }));
402    }
403
404    getFileReferences(fileName: string): ReferenceEntry[] {
405        const request = this.processRequest<protocol.FileReferencesRequest>(CommandNames.FileReferences, { file: fileName });
406        const response = this.processResponse<protocol.FileReferencesResponse>(request);
407
408        return response.body!.refs.map(entry => ({ // TODO: GH#18217
409            fileName: entry.file,
410            textSpan: this.decodeSpan(entry),
411            isWriteAccess: entry.isWriteAccess,
412            isDefinition: entry.isDefinition,
413        }));
414    }
415
416    getEmitOutput(file: string): EmitOutput {
417        const request = this.processRequest<protocol.EmitOutputRequest>(protocol.CommandTypes.EmitOutput, { file });
418        const response = this.processResponse<protocol.EmitOutputResponse>(request);
419        return response.body as EmitOutput;
420    }
421
422    getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] {
423        return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync);
424    }
425    getSemanticDiagnostics(file: string): Diagnostic[] {
426        return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync);
427    }
428    getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] {
429        return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync);
430    }
431
432    private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] {
433        const request = this.processRequest<protocol.SyntacticDiagnosticsSyncRequest | protocol.SemanticDiagnosticsSyncRequest | protocol.SuggestionDiagnosticsSyncRequest>(command, { file, includeLinePosition: true });
434        const response = this.processResponse<protocol.SyntacticDiagnosticsSyncResponse | protocol.SemanticDiagnosticsSyncResponse | protocol.SuggestionDiagnosticsSyncResponse>(request);
435        const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!);
436        const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie!
437
438        return (response.body as protocol.DiagnosticWithLinePosition[]).map((entry): DiagnosticWithLocation => {
439            const category = firstDefined(Object.keys(DiagnosticCategory), id =>
440                isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory as any)[id] : undefined);
441            return {
442                file: fakeSourceFile,
443                start: entry.start,
444                length: entry.length,
445                messageText: entry.message,
446                category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"),
447                code: entry.code,
448                reportsUnnecessary: entry.reportsUnnecessary,
449                reportsDeprecated: entry.reportsDeprecated,
450            };
451        });
452    }
453
454    getCompilerOptionsDiagnostics(): Diagnostic[] {
455        return notImplemented();
456    }
457
458    getRenameInfo(fileName: string, position: number, _preferences: UserPreferences, findInStrings?: boolean, findInComments?: boolean): RenameInfo {
459        // Not passing along 'options' because server should already have those from the 'configure' command
460        const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments };
461
462        const request = this.processRequest<protocol.RenameRequest>(CommandNames.Rename, args);
463        const response = this.processResponse<protocol.RenameResponse>(request);
464        const body = response.body!; // TODO: GH#18217
465        const locations: RenameLocation[] = [];
466        for (const entry of body.locs) {
467            const fileName = entry.file;
468            for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) {
469                locations.push({
470                    textSpan: this.decodeSpan({ start, end }, fileName),
471                    fileName,
472                    ...(contextStart !== undefined ?
473                        { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } :
474                        undefined),
475                    ...prefixSuffixText
476                });
477            }
478        }
479
480        const renameInfo = body.info.canRename
481            ? identity<RenameInfoSuccess>({
482                canRename: body.info.canRename,
483                fileToRename: body.info.fileToRename,
484                displayName: body.info.displayName,
485                fullDisplayName: body.info.fullDisplayName,
486                kind: body.info.kind,
487                kindModifiers: body.info.kindModifiers,
488                triggerSpan: createTextSpanFromBounds(position, position),
489            })
490            : identity<RenameInfoFailure>({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage });
491        this.lastRenameEntry = {
492            renameInfo,
493            inputs: {
494                fileName,
495                position,
496                findInStrings: !!findInStrings,
497                findInComments: !!findInComments,
498            },
499            locations,
500        };
501        return renameInfo;
502    }
503
504    getSmartSelectionRange() {
505        return notImplemented();
506    }
507
508    findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] {
509        if (!this.lastRenameEntry ||
510            this.lastRenameEntry.inputs.fileName !== fileName ||
511            this.lastRenameEntry.inputs.position !== position ||
512            this.lastRenameEntry.inputs.findInStrings !== findInStrings ||
513            this.lastRenameEntry.inputs.findInComments !== findInComments) {
514            if (providePrefixAndSuffixTextForRename !== undefined) {
515                // User preferences have to be set through the `Configure` command
516                this.configure({ providePrefixAndSuffixTextForRename });
517                // Options argument is not used, so don't pass in options
518                this.getRenameInfo(fileName, position, /*preferences*/{}, findInStrings, findInComments);
519                // Restore previous user preferences
520                if (this.preferences) {
521                    this.configure(this.preferences);
522                }
523            }
524            else {
525                this.getRenameInfo(fileName, position, /*preferences*/{}, findInStrings, findInComments);
526            }
527        }
528
529        return this.lastRenameEntry!.locations;
530    }
531
532    private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] {
533        if (!items) {
534            return [];
535        }
536
537        return items.map(item => ({
538            text: item.text,
539            kind: item.kind,
540            kindModifiers: item.kindModifiers || "",
541            spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)),
542            childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap),
543            indent: item.indent,
544            bolded: false,
545            grayed: false
546        }));
547    }
548
549    getNavigationBarItems(file: string): NavigationBarItem[] {
550        const request = this.processRequest<protocol.NavBarRequest>(CommandNames.NavBar, { file });
551        const response = this.processResponse<protocol.NavBarResponse>(request);
552
553        const lineMap = this.getLineMap(file);
554        return this.decodeNavigationBarItems(response.body, file, lineMap);
555    }
556
557    private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree {
558        return {
559            text: tree.text,
560            kind: tree.kind,
561            kindModifiers: tree.kindModifiers,
562            spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)),
563            nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap),
564            childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap))
565        };
566    }
567
568    getNavigationTree(file: string): NavigationTree {
569        const request = this.processRequest<protocol.NavTreeRequest>(CommandNames.NavTree, { file });
570        const response = this.processResponse<protocol.NavTreeResponse>(request);
571
572        const lineMap = this.getLineMap(file);
573        return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217
574    }
575
576    private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan;
577    private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan;
578    private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan {
579        if (span.start.line === 1 && span.start.offset === 1 && span.end.line === 1 && span.end.offset === 1) {
580            return { start: 0, length: 0 };
581        }
582        fileName = fileName || span.file;
583        lineMap = lineMap || this.getLineMap(fileName);
584        return createTextSpanFromBounds(
585            this.lineOffsetToPosition(fileName, span.start, lineMap),
586            this.lineOffsetToPosition(fileName, span.end, lineMap));
587    }
588
589    private decodeLinkDisplayParts(tags: (protocol.JSDocTagInfo | JSDocTagInfo)[]): JSDocTagInfo[] {
590        return tags.map(tag => typeof tag.text === "string" ? {
591            ...tag,
592            text: [textPart(tag.text)]
593        } : (tag as JSDocTagInfo));
594    }
595
596    getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan {
597        return notImplemented();
598    }
599
600    getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan {
601        return notImplemented();
602    }
603
604    getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined {
605        const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position);
606
607        const request = this.processRequest<protocol.SignatureHelpRequest>(CommandNames.SignatureHelp, args);
608        const response = this.processResponse<protocol.SignatureHelpResponse>(request);
609
610        if (!response.body) {
611            return undefined;
612        }
613
614        const { items: encodedItems, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body;
615
616        const applicableSpan = encodedApplicableSpan as unknown as TextSpan;
617        const items = (encodedItems as (SignatureHelpItem | protocol.SignatureHelpItem)[]).map(item => ({ ...item, tags: this.decodeLinkDisplayParts(item.tags) }));
618
619        return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
620    }
621
622    getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] {
623        const args = this.createFileLocationRequestArgs(fileName, position);
624
625        const request = this.processRequest<protocol.OccurrencesRequest>(CommandNames.Occurrences, args);
626        const response = this.processResponse<protocol.OccurrencesResponse>(request);
627
628        return response.body!.map(entry => ({ // TODO: GH#18217
629            fileName: entry.file,
630            textSpan: this.decodeSpan(entry),
631            isWriteAccess: entry.isWriteAccess,
632        }));
633    }
634
635    getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] {
636        const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch };
637
638        const request = this.processRequest<protocol.DocumentHighlightsRequest>(CommandNames.DocumentHighlights, args);
639        const response = this.processResponse<protocol.DocumentHighlightsResponse>(request);
640
641        return response.body!.map(item => ({ // TODO: GH#18217
642            fileName: item.file,
643            highlightSpans: item.highlightSpans.map(span => ({
644                textSpan: this.decodeSpan(span, item.file),
645                kind: span.kind
646            })),
647        }));
648    }
649
650    getOutliningSpans(file: string): OutliningSpan[] {
651        const request = this.processRequest<protocol.OutliningSpansRequest>(CommandNames.GetOutliningSpans, { file });
652        const response = this.processResponse<protocol.OutliningSpansResponse>(request);
653
654        return response.body!.map<OutliningSpan>(item => ({
655            textSpan: this.decodeSpan(item.textSpan, file),
656            hintSpan: this.decodeSpan(item.hintSpan, file),
657            bannerText: item.bannerText,
658            autoCollapse: item.autoCollapse,
659            kind: item.kind
660        }));
661    }
662
663    getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] {
664        return notImplemented();
665    }
666
667    getDocCommentTemplateAtPosition(_fileName: string, _position: number, _options?: DocCommentTemplateOptions): TextInsertion {
668        return notImplemented();
669    }
670
671    isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean {
672        return notImplemented();
673    }
674
675    getJsxClosingTagAtPosition(_fileName: string, _position: number): never {
676        return notImplemented();
677    }
678
679    getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan {
680        return notImplemented();
681    }
682
683    getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] {
684        const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes };
685
686        const request = this.processRequest<protocol.CodeFixRequest>(CommandNames.GetCodeFixes, args);
687        const response = this.processResponse<protocol.CodeFixResponse>(request);
688
689        return response.body!.map<CodeFixAction>(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217
690            ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription }));
691    }
692
693    getCombinedCodeFix = notImplemented;
694
695    applyCodeActionCommand = notImplemented;
696
697    provideInlayHints(file: string, span: TextSpan): InlayHint[] {
698        const { start, length } = span;
699        const args: protocol.InlayHintsRequestArgs = { file, start, length };
700
701        const request = this.processRequest<protocol.InlayHintsRequest>(CommandNames.ProvideInlayHints, args);
702        const response = this.processResponse<protocol.InlayHintsResponse>(request);
703
704        return response.body!.map(item => ({ // TODO: GH#18217
705            ...item,
706            kind: item.kind as InlayHintKind,
707            position: this.lineOffsetToPosition(file, item.position),
708        }));
709    }
710
711    private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
712        return typeof positionOrRange === "number"
713            ? this.createFileLocationRequestArgs(fileName, positionOrRange)
714            : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end);
715    }
716
717    private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs {
718        const { line, offset } = this.positionToOneBasedLineOffset(file, position);
719        return { file, line, offset };
720    }
721
722    private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs {
723        const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start);
724        const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end);
725        return { file, startLine, startOffset, endLine, endOffset };
726    }
727
728    private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } {
729        const { line, offset } = this.positionToOneBasedLineOffset(file, start);
730        const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end);
731        return { file, line, offset, endLine, endOffset };
732    }
733
734    getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] {
735        const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName);
736
737        const request = this.processRequest<protocol.GetApplicableRefactorsRequest>(CommandNames.GetApplicableRefactors, args);
738        const response = this.processResponse<protocol.GetApplicableRefactorsResponse>(request);
739        return response.body!; // TODO: GH#18217
740    }
741
742    getEditsForRefactor(
743        fileName: string,
744        _formatOptions: FormatCodeSettings,
745        positionOrRange: number | TextRange,
746        refactorName: string,
747        actionName: string): RefactorEditInfo {
748
749        const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs;
750        args.refactor = refactorName;
751        args.action = actionName;
752
753        const request = this.processRequest<protocol.GetEditsForRefactorRequest>(CommandNames.GetEditsForRefactor, args);
754        const response = this.processResponse<protocol.GetEditsForRefactorResponse>(request);
755
756        if (!response.body) {
757            return { edits: [], renameFilename: undefined, renameLocation: undefined };
758        }
759
760        const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits);
761
762        const renameFilename: string | undefined = response.body.renameFilename;
763        let renameLocation: number | undefined;
764        if (renameFilename !== undefined) {
765            renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217
766        }
767
768        return {
769            edits,
770            renameFilename,
771            renameLocation
772        };
773    }
774
775    organizeImports(_args: OrganizeImportsArgs, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] {
776        return notImplemented();
777    }
778
779    getEditsForFileRename() {
780        return notImplemented();
781    }
782
783    private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] {
784        return edits.map(edit => {
785            const fileName = edit.fileName;
786            return {
787                fileName,
788                textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName))
789            };
790        });
791    }
792
793    private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] {
794        return changes.map(change => ({
795            fileName: change.fileName,
796            textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName))
797        }));
798    }
799
800    convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange {
801        return {
802            span: this.decodeSpan(change, fileName),
803            newText: change.newText ? change.newText : ""
804        };
805    }
806
807    getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
808        const args = this.createFileLocationRequestArgs(fileName, position);
809
810        const request = this.processRequest<protocol.BraceRequest>(CommandNames.Brace, args);
811        const response = this.processResponse<protocol.BraceResponse>(request);
812
813        return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217
814    }
815
816    configurePlugin(pluginName: string, configuration: any): void {
817        const request = this.processRequest<protocol.ConfigurePluginRequest>("configurePlugin", { pluginName, configuration });
818        this.processResponse<protocol.ConfigurePluginResponse>(request, /*expectEmptyBody*/ true);
819    }
820
821    getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number {
822        return notImplemented();
823    }
824
825    getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] {
826        return notImplemented();
827    }
828
829    getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] {
830        return notImplemented();
831    }
832
833    getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications {
834        return notImplemented();
835    }
836
837    getEncodedSemanticClassifications(file: string, span: TextSpan, format?: SemanticClassificationFormat): Classifications {
838        const request = this.processRequest<protocol.EncodedSemanticClassificationsRequest>(protocol.CommandTypes.EncodedSemanticClassificationsFull, { file, start: span.start, length: span.length, format });
839        const r = this.processResponse<protocol.EncodedSemanticClassificationsResponse>(request);
840        return r.body!;
841    }
842
843    private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem {
844        return {
845            file: item.file,
846            name: item.name,
847            kind: item.kind,
848            kindModifiers: item.kindModifiers,
849            containerName: item.containerName,
850            span: this.decodeSpan(item.span, item.file),
851            selectionSpan: this.decodeSpan(item.selectionSpan, item.file)
852        };
853    }
854
855    prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined {
856        const args = this.createFileLocationRequestArgs(fileName, position);
857        const request = this.processRequest<protocol.PrepareCallHierarchyRequest>(CommandNames.PrepareCallHierarchy, args);
858        const response = this.processResponse<protocol.PrepareCallHierarchyResponse>(request);
859        return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item));
860    }
861
862    private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall {
863        return {
864            from: this.convertCallHierarchyItem(item.from),
865            fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file))
866        };
867    }
868
869    provideCallHierarchyIncomingCalls(fileName: string, position: number) {
870        const args = this.createFileLocationRequestArgs(fileName, position);
871        const request = this.processRequest<protocol.ProvideCallHierarchyIncomingCallsRequest>(CommandNames.ProvideCallHierarchyIncomingCalls, args);
872        const response = this.processResponse<protocol.ProvideCallHierarchyIncomingCallsResponse>(request);
873        return response.body.map(item => this.convertCallHierarchyIncomingCall(item));
874    }
875
876    private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall {
877        return {
878            to: this.convertCallHierarchyItem(item.to),
879            fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file))
880        };
881    }
882
883    provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
884        const args = this.createFileLocationRequestArgs(fileName, position);
885        const request = this.processRequest<protocol.ProvideCallHierarchyOutgoingCallsRequest>(CommandNames.ProvideCallHierarchyOutgoingCalls, args);
886        const response = this.processResponse<protocol.ProvideCallHierarchyOutgoingCallsResponse>(request);
887        return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item));
888    }
889
890    getProgram(): Program {
891        throw new Error("Program objects are not serializable through the server protocol.");
892    }
893
894    getBuilderProgram(): BuilderProgram | undefined {
895        throw new Error("Program objects are not serializable through the server protocol.");
896    }
897
898    getCurrentProgram(): Program | undefined {
899        throw new Error("Program objects are not serializable through the server protocol.");
900    }
901
902    getAutoImportProvider(): Program | undefined {
903        throw new Error("Program objects are not serializable through the server protocol.");
904    }
905
906    updateIsDefinitionOfReferencedSymbols(_referencedSymbols: readonly ReferencedSymbol[], _knownSymbolSpans: Set<DocumentSpan>): boolean {
907        return notImplemented();
908    }
909
910    getNonBoundSourceFile(_fileName: string): SourceFile {
911        throw new Error("SourceFile objects are not serializable through the server protocol.");
912    }
913
914    getSourceFile(_fileName: string): SourceFile {
915        throw new Error("SourceFile objects are not serializable through the server protocol.");
916    }
917
918    cleanupSemanticCache(): void {
919        throw new Error("cleanupSemanticCache is not available through the server layer.");
920    }
921
922    getSourceMapper(): never {
923        return notImplemented();
924    }
925
926    clearSourceMapperCache(): never {
927        return notImplemented();
928    }
929
930    toggleLineComment(): TextChange[] {
931        return notImplemented();
932    }
933
934    toggleMultilineComment(): TextChange[] {
935        return notImplemented();
936    }
937
938    commentSelection(): TextChange[] {
939        return notImplemented();
940    }
941
942    uncommentSelection(): TextChange[] {
943        return notImplemented();
944    }
945
946    dispose(): void {
947        throw new Error("dispose is not available through the server layer.");
948    }
949}
950