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