namespace ts.server { export interface SessionClientHost extends LanguageServiceHost { writeMessage(message: string): void; } interface RenameEntry { readonly renameInfo: RenameInfo; readonly inputs: { readonly fileName: string; readonly position: number; readonly findInStrings: boolean; readonly findInComments: boolean; }; readonly locations: RenameLocation[]; } /* @internal */ export function extractMessage(message: string): string { // Read the content length const contentLengthPrefix = "Content-Length: "; const lines = message.split(/\r?\n/); Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); const contentLengthText = lines[0]; Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); // Read the body const responseBody = lines[2]; // Verify content length Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); return responseBody; } export class SessionClient implements LanguageService { private sequence = 0; private lineMaps = new Map(); private messages = createQueue(); private lastRenameEntry: RenameEntry | undefined; private preferences: UserPreferences | undefined; constructor(private host: SessionClientHost) { } public onMessage(message: string): void { this.messages.enqueue(message); } private writeMessage(message: string): void { this.host.writeMessage(message); } private getLineMap(fileName: string): number[] { let lineMap = this.lineMaps.get(fileName); if (!lineMap) { lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); this.lineMaps.set(fileName, lineMap); } return lineMap; } private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { lineMap = lineMap || this.getLineMap(fileName); return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); } private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); return { line: lineOffset.line + 1, offset: lineOffset.character + 1 }; } private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; } private processRequest(command: string, args: T["arguments"]): T { const request: protocol.Request = { seq: this.sequence, type: "request", arguments: args, command }; this.sequence++; this.writeMessage(JSON.stringify(request)); return request as T; } private processResponse(request: protocol.Request, expectEmptyBody = false): T { let foundResponseMessage = false; let response!: T; while (!foundResponseMessage) { const lastMessage = this.messages.dequeue()!; Debug.assert(!!lastMessage, "Did not receive any responses."); const responseBody = extractMessage(lastMessage); try { response = JSON.parse(responseBody); // the server may emit events before emitting the response. We // want to ignore these events for testing purpose. if (response.type === "response") { foundResponseMessage = true; } } catch (e) { throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); } } // unmarshal errors if (!response.success) { throw new Error("Error " + response.message); } Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); return response; } /*@internal*/ configure(preferences: UserPreferences) { this.preferences = preferences; const args: protocol.ConfigureRequestArguments = { preferences }; const request = this.processRequest(CommandNames.Configure, args); this.processResponse(request, /*expectEmptyBody*/ true); } /*@internal*/ setFormattingOptions(formatOptions: FormatCodeSettings) { const args: protocol.ConfigureRequestArguments = { formatOptions }; const request = this.processRequest(CommandNames.Configure, args); this.processResponse(request, /*expectEmptyBody*/ true); } /*@internal*/ setCompilerOptionsForInferredProjects(options: protocol.CompilerOptions) { const args: protocol.SetCompilerOptionsForInferredProjectsArgs = { options }; const request = this.processRequest(CommandNames.CompilerOptionsForInferredProjects, args); this.processResponse(request, /*expectEmptyBody*/ false); } openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; this.processRequest(CommandNames.Open, args); } closeFile(file: string): void { const args: protocol.FileRequestArgs = { file }; this.processRequest(CommandNames.Close, args); } createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; } changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { // clear the line map after an edit this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 this.processRequest(CommandNames.Change, args); } toLineColumnOffset(fileName: string, position: number) { const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); return { line, character: offset }; } getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.Quickinfo, args); const response = this.processResponse(request); const body = response.body!; // TODO: GH#18217 return { kind: body.kind, kindModifiers: body.kindModifiers, textSpan: this.decodeSpan(body, fileName), displayParts: [{ kind: "text", text: body.displayString }], documentation: typeof body.documentation === "string" ? [{ kind: "text", text: body.documentation }] : body.documentation, tags: this.decodeLinkDisplayParts(body.tags) }; } getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; const request = this.processRequest(CommandNames.ProjectInfo, args); const response = this.processResponse(request); return { configFileName: response.body!.configFileName, // TODO: GH#18217 fileNames: response.body!.fileNames }; } getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { // Not passing along 'preferences' because server should already have those from the 'configure' command const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.CompletionInfo, args); const response = this.processResponse(request); return { isGlobalCompletion: response.body!.isGlobalCompletion, isMemberCompletion: response.body!.isMemberCompletion, isNewIdentifierLocation: response.body!.isNewIdentifierLocation, entries: response.body!.entries.map(entry => { // TODO: GH#18217 if (entry.replacementSpan !== undefined) { const res: CompletionEntry = { ...entry, data: entry.data as any, replacementSpan: this.decodeSpan(entry.replacementSpan, fileName) }; return res; } return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 }) }; } getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, _preferences: UserPreferences | undefined, data: unknown): CompletionEntryDetails { const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source, data }] }; const request = this.processRequest(CommandNames.CompletionDetailsFull, args); const response = this.processResponse(request); Debug.assert(response.body.length === 1, "Unexpected length of completion details response body."); return response.body[0]; } getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { return notImplemented(); } getNavigateToItems(searchValue: string): NavigateToItem[] { const args: protocol.NavtoRequestArgs = { searchValue, file: this.host.getScriptFileNames()[0] }; const request = this.processRequest(CommandNames.Navto, args); const response = this.processResponse(request); return response.body!.map(entry => ({ // TODO: GH#18217 name: entry.name, containerName: entry.containerName || "", containerKind: entry.containerKind || ScriptElementKind.unknown, kind: entry.kind, kindModifiers: entry.kindModifiers || "", matchKind: entry.matchKind as keyof typeof PatternMatchKind, isCaseSensitive: entry.isCaseSensitive, fileName: entry.file, textSpan: this.decodeSpan(entry), })); } getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); // TODO: handle FormatCodeOptions const request = this.processRequest(CommandNames.Format, args); const response = this.processResponse(request); return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 } getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); } getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; // TODO: handle FormatCodeOptions const request = this.processRequest(CommandNames.Formatonkey, args); const response = this.processResponse(request); return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 } getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.Definition, args); const response = this.processResponse(request); return response.body!.map(entry => ({ // TODO: GH#18217 containerKind: ScriptElementKind.unknown, containerName: "", fileName: entry.file, textSpan: this.decodeSpan(entry), kind: ScriptElementKind.unknown, name: "" })); } getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); const response = this.processResponse(request); const body = Debug.checkDefined(response.body); // TODO: GH#18217 return { definitions: body.definitions.map(entry => ({ containerKind: ScriptElementKind.unknown, containerName: "", fileName: entry.file, textSpan: this.decodeSpan(entry), kind: ScriptElementKind.unknown, name: "", unverified: entry.unverified, })), textSpan: this.decodeSpan(body.textSpan, request.arguments.file) }; } getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.TypeDefinition, args); const response = this.processResponse(request); return response.body!.map(entry => ({ // TODO: GH#18217 containerKind: ScriptElementKind.unknown, containerName: "", fileName: entry.file, textSpan: this.decodeSpan(entry), kind: ScriptElementKind.unknown, name: "" })); } getSourceDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfo[] { const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.FindSourceDefinition, args); const response = this.processResponse(request); const body = Debug.checkDefined(response.body); // TODO: GH#18217 return body.map(entry => ({ containerKind: ScriptElementKind.unknown, containerName: "", fileName: entry.file, textSpan: this.decodeSpan(entry), kind: ScriptElementKind.unknown, name: "", unverified: entry.unverified, })); } getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.Implementation, args); const response = this.processResponse(request); return response.body!.map(entry => ({ // TODO: GH#18217 fileName: entry.file, textSpan: this.decodeSpan(entry), kind: ScriptElementKind.unknown, displayParts: [] })); } findReferences(fileName: string, position: number): ReferencedSymbol[] { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.ReferencesFull, args); const response = this.processResponse(request); return response.body; } getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.References, args); const response = this.processResponse(request); return response.body!.refs.map(entry => ({ // TODO: GH#18217 fileName: entry.file, textSpan: this.decodeSpan(entry), isWriteAccess: entry.isWriteAccess, isDefinition: entry.isDefinition, })); } getFileReferences(fileName: string): ReferenceEntry[] { const request = this.processRequest(CommandNames.FileReferences, { file: fileName }); const response = this.processResponse(request); return response.body!.refs.map(entry => ({ // TODO: GH#18217 fileName: entry.file, textSpan: this.decodeSpan(entry), isWriteAccess: entry.isWriteAccess, isDefinition: entry.isDefinition, })); } getEmitOutput(file: string): EmitOutput { const request = this.processRequest(protocol.CommandTypes.EmitOutput, { file }); const response = this.processResponse(request); return response.body as EmitOutput; } getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); } getSemanticDiagnostics(file: string): Diagnostic[] { return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); } getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); } private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { const request = this.processRequest(command, { file, includeLinePosition: true }); const response = this.processResponse(request); const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! return (response.body as protocol.DiagnosticWithLinePosition[]).map((entry): DiagnosticWithLocation => { const category = firstDefined(Object.keys(DiagnosticCategory), id => isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory as any)[id] : undefined); return { file: fakeSourceFile, start: entry.start, length: entry.length, messageText: entry.message, category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), code: entry.code, reportsUnnecessary: entry.reportsUnnecessary, reportsDeprecated: entry.reportsDeprecated, }; }); } getCompilerOptionsDiagnostics(): Diagnostic[] { return notImplemented(); } getRenameInfo(fileName: string, position: number, _preferences: UserPreferences, findInStrings?: boolean, findInComments?: boolean): RenameInfo { // Not passing along 'options' because server should already have those from the 'configure' command const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; const request = this.processRequest(CommandNames.Rename, args); const response = this.processResponse(request); const body = response.body!; // TODO: GH#18217 const locations: RenameLocation[] = []; for (const entry of body.locs) { const fileName = entry.file; for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { locations.push({ textSpan: this.decodeSpan({ start, end }, fileName), fileName, ...(contextStart !== undefined ? { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : undefined), ...prefixSuffixText }); } } const renameInfo = body.info.canRename ? identity({ canRename: body.info.canRename, fileToRename: body.info.fileToRename, displayName: body.info.displayName, fullDisplayName: body.info.fullDisplayName, kind: body.info.kind, kindModifiers: body.info.kindModifiers, triggerSpan: createTextSpanFromBounds(position, position), }) : identity({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); this.lastRenameEntry = { renameInfo, inputs: { fileName, position, findInStrings: !!findInStrings, findInComments: !!findInComments, }, locations, }; return renameInfo; } getSmartSelectionRange() { return notImplemented(); } findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] { if (!this.lastRenameEntry || this.lastRenameEntry.inputs.fileName !== fileName || this.lastRenameEntry.inputs.position !== position || this.lastRenameEntry.inputs.findInStrings !== findInStrings || this.lastRenameEntry.inputs.findInComments !== findInComments) { if (providePrefixAndSuffixTextForRename !== undefined) { // User preferences have to be set through the `Configure` command this.configure({ providePrefixAndSuffixTextForRename }); // Options argument is not used, so don't pass in options this.getRenameInfo(fileName, position, /*preferences*/{}, findInStrings, findInComments); // Restore previous user preferences if (this.preferences) { this.configure(this.preferences); } } else { this.getRenameInfo(fileName, position, /*preferences*/{}, findInStrings, findInComments); } } return this.lastRenameEntry!.locations; } private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { if (!items) { return []; } return items.map(item => ({ text: item.text, kind: item.kind, kindModifiers: item.kindModifiers || "", spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), indent: item.indent, bolded: false, grayed: false })); } getNavigationBarItems(file: string): NavigationBarItem[] { const request = this.processRequest(CommandNames.NavBar, { file }); const response = this.processResponse(request); const lineMap = this.getLineMap(file); return this.decodeNavigationBarItems(response.body, file, lineMap); } private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { return { text: tree.text, kind: tree.kind, kindModifiers: tree.kindModifiers, spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) }; } getNavigationTree(file: string): NavigationTree { const request = this.processRequest(CommandNames.NavTree, { file }); const response = this.processResponse(request); const lineMap = this.getLineMap(file); return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 } private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { if (span.start.line === 1 && span.start.offset === 1 && span.end.line === 1 && span.end.offset === 1) { return { start: 0, length: 0 }; } fileName = fileName || span.file; lineMap = lineMap || this.getLineMap(fileName); return createTextSpanFromBounds( this.lineOffsetToPosition(fileName, span.start, lineMap), this.lineOffsetToPosition(fileName, span.end, lineMap)); } private decodeLinkDisplayParts(tags: (protocol.JSDocTagInfo | JSDocTagInfo)[]): JSDocTagInfo[] { return tags.map(tag => typeof tag.text === "string" ? { ...tag, text: [textPart(tag.text)] } : (tag as JSDocTagInfo)); } getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { return notImplemented(); } getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { return notImplemented(); } getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.SignatureHelp, args); const response = this.processResponse(request); if (!response.body) { return undefined; } const { items: encodedItems, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; const applicableSpan = encodedApplicableSpan as unknown as TextSpan; const items = (encodedItems as (SignatureHelpItem | protocol.SignatureHelpItem)[]).map(item => ({ ...item, tags: this.decodeLinkDisplayParts(item.tags) })); return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; } getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.Occurrences, args); const response = this.processResponse(request); return response.body!.map(entry => ({ // TODO: GH#18217 fileName: entry.file, textSpan: this.decodeSpan(entry), isWriteAccess: entry.isWriteAccess, })); } getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; const request = this.processRequest(CommandNames.DocumentHighlights, args); const response = this.processResponse(request); return response.body!.map(item => ({ // TODO: GH#18217 fileName: item.file, highlightSpans: item.highlightSpans.map(span => ({ textSpan: this.decodeSpan(span, item.file), kind: span.kind })), })); } getOutliningSpans(file: string): OutliningSpan[] { const request = this.processRequest(CommandNames.GetOutliningSpans, { file }); const response = this.processResponse(request); return response.body!.map(item => ({ textSpan: this.decodeSpan(item.textSpan, file), hintSpan: this.decodeSpan(item.hintSpan, file), bannerText: item.bannerText, autoCollapse: item.autoCollapse, kind: item.kind })); } getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { return notImplemented(); } getDocCommentTemplateAtPosition(_fileName: string, _position: number, _options?: DocCommentTemplateOptions): TextInsertion { return notImplemented(); } isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { return notImplemented(); } getJsxClosingTagAtPosition(_fileName: string, _position: number): never { return notImplemented(); } getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { return notImplemented(); } getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; const request = this.processRequest(CommandNames.GetCodeFixes, args); const response = this.processResponse(request); return response.body!.map(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); } getCombinedCodeFix = notImplemented; applyCodeActionCommand = notImplemented; provideInlayHints(file: string, span: TextSpan): InlayHint[] { const { start, length } = span; const args: protocol.InlayHintsRequestArgs = { file, start, length }; const request = this.processRequest(CommandNames.ProvideInlayHints, args); const response = this.processResponse(request); return response.body!.map(item => ({ // TODO: GH#18217 ...item, kind: item.kind as InlayHintKind, position: this.lineOffsetToPosition(file, item.position), })); } private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { return typeof positionOrRange === "number" ? this.createFileLocationRequestArgs(fileName, positionOrRange) : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); } private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { const { line, offset } = this.positionToOneBasedLineOffset(file, position); return { file, line, offset }; } private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); return { file, startLine, startOffset, endLine, endOffset }; } private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { const { line, offset } = this.positionToOneBasedLineOffset(file, start); const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); return { file, line, offset, endLine, endOffset }; } getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); const request = this.processRequest(CommandNames.GetApplicableRefactors, args); const response = this.processResponse(request); return response.body!; // TODO: GH#18217 } getEditsForRefactor( fileName: string, _formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo { const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; args.refactor = refactorName; args.action = actionName; const request = this.processRequest(CommandNames.GetEditsForRefactor, args); const response = this.processResponse(request); if (!response.body) { return { edits: [], renameFilename: undefined, renameLocation: undefined }; } const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); const renameFilename: string | undefined = response.body.renameFilename; let renameLocation: number | undefined; if (renameFilename !== undefined) { renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 } return { edits, renameFilename, renameLocation }; } organizeImports(_args: OrganizeImportsArgs, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { return notImplemented(); } getEditsForFileRename() { return notImplemented(); } private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { return edits.map(edit => { const fileName = edit.fileName; return { fileName, textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) }; }); } private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { return changes.map(change => ({ fileName: change.fileName, textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) })); } convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { return { span: this.decodeSpan(change, fileName), newText: change.newText ? change.newText : "" }; } getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.Brace, args); const response = this.processResponse(request); return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 } configurePlugin(pluginName: string, configuration: any): void { const request = this.processRequest("configurePlugin", { pluginName, configuration }); this.processResponse(request, /*expectEmptyBody*/ true); } getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { return notImplemented(); } getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { return notImplemented(); } getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { return notImplemented(); } getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { return notImplemented(); } getEncodedSemanticClassifications(file: string, span: TextSpan, format?: SemanticClassificationFormat): Classifications { const request = this.processRequest(protocol.CommandTypes.EncodedSemanticClassificationsFull, { file, start: span.start, length: span.length, format }); const r = this.processResponse(request); return r.body!; } private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { return { file: item.file, name: item.name, kind: item.kind, kindModifiers: item.kindModifiers, containerName: item.containerName, span: this.decodeSpan(item.span, item.file), selectionSpan: this.decodeSpan(item.selectionSpan, item.file) }; } prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); const response = this.processResponse(request); return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); } private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { return { from: this.convertCallHierarchyItem(item.from), fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) }; } provideCallHierarchyIncomingCalls(fileName: string, position: number) { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.ProvideCallHierarchyIncomingCalls, args); const response = this.processResponse(request); return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); } private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { return { to: this.convertCallHierarchyItem(item.to), fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) }; } provideCallHierarchyOutgoingCalls(fileName: string, position: number) { const args = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.ProvideCallHierarchyOutgoingCalls, args); const response = this.processResponse(request); return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); } getProgram(): Program { throw new Error("Program objects are not serializable through the server protocol."); } getBuilderProgram(): BuilderProgram | undefined { throw new Error("Program objects are not serializable through the server protocol."); } getCurrentProgram(): Program | undefined { throw new Error("Program objects are not serializable through the server protocol."); } getAutoImportProvider(): Program | undefined { throw new Error("Program objects are not serializable through the server protocol."); } updateIsDefinitionOfReferencedSymbols(_referencedSymbols: readonly ReferencedSymbol[], _knownSymbolSpans: Set): boolean { return notImplemented(); } getNonBoundSourceFile(_fileName: string): SourceFile { throw new Error("SourceFile objects are not serializable through the server protocol."); } getSourceFile(_fileName: string): SourceFile { throw new Error("SourceFile objects are not serializable through the server protocol."); } cleanupSemanticCache(): void { throw new Error("cleanupSemanticCache is not available through the server layer."); } getSourceMapper(): never { return notImplemented(); } clearSourceMapperCache(): never { return notImplemented(); } toggleLineComment(): TextChange[] { return notImplemented(); } toggleMultilineComment(): TextChange[] { return notImplemented(); } commentSelection(): TextChange[] { return notImplemented(); } uncommentSelection(): TextChange[] { return notImplemented(); } dispose(): void { throw new Error("dispose is not available through the server layer."); } } }