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