UNPKG

typescript-language-server

Version:

Language Server Protocol (LSP) implementation for TypeScript using tsserver

1,160 lines 48.7 kB
/* * Copyright (C) 2017, 2018 TypeFox and others. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ import * as path from 'node:path'; import fs from 'fs-extra'; import debounce from 'p-debounce'; import { temporaryFile } from 'tempy'; import * as lsp from 'vscode-languageserver'; import * as lspcalls from './lsp-protocol.calls.proposed.js'; import * as lspsemanticTokens from './semantic-tokens.js'; import API from './utils/api.js'; import { PrefixingLogger } from './logger.js'; import { TspClient } from './tsp-client.js'; import { DiagnosticEventQueue } from './diagnostic-queue.js'; import { toDocumentHighlight, asTagsDocumentation, uriToPath, toSymbolKind, toLocation, pathToUri, toTextEdit, asPlainText, normalizePath } from './protocol-translation.js'; import { LspDocuments } from './document.js'; import { asCompletionItem, asResolvedCompletionItem, getCompletionTriggerCharacter } from './completion.js'; import { asSignatureHelp, toTsTriggerReason } from './hover.js'; import { Commands } from './commands.js'; import { provideQuickFix } from './quickfix.js'; import { provideRefactors } from './refactor.js'; import { provideOrganizeImports } from './organize-imports.js'; import { collectDocumentSymbols, collectSymbolInformation } from './document-symbol.js'; import { computeCallers, computeCallees } from './calls.js'; import { TypeScriptAutoFixProvider } from './features/fix-all.js'; import { TypeScriptInlayHintsProvider } from './features/inlay-hints.js'; import { SourceDefinitionCommand } from './features/source-definition.js'; import { TypeScriptVersionProvider } from './tsServer/versionProvider.js'; import { Position, Range } from './utils/typeConverters.js'; import { CodeActionKind } from './utils/types.js'; import { ConfigurationManager } from './configuration-manager.js'; export class LspServer { constructor(options) { this.options = options; this._tspClient = null; this.initializeParams = null; this.typeScriptAutoFixProvider = null; this.features = {}; this.documents = new LspDocuments(); // True if diagnostic request is currently debouncing or the request is in progress. False only if there are // no pending requests. this.pendingDebouncedRequest = false; this.doRequestDiagnosticsDebounced = debounce(() => this.doRequestDiagnostics(), 200); this.configurationManager = new ConfigurationManager(this.documents); this.logger = new PrefixingLogger(options.logger, '[lspserver]'); } closeAll() { for (const file of [...this.documents.files]) { this.closeDocument(file); } } shutdown() { if (this._tspClient) { this._tspClient.shutdown(); this._tspClient = null; } } get tspClient() { if (!this._tspClient) { throw new Error('TS client not created. Did you forget to send the "initialize" request?'); } return this._tspClient; } findTypescriptVersion() { const typescriptVersionProvider = new TypeScriptVersionProvider(this.options, this.logger); // User-provided tsserver path. const userSettingVersion = typescriptVersionProvider.getUserSettingVersion(); if (userSettingVersion) { if (userSettingVersion.isValid) { return userSettingVersion; } this.logger.warn(`Typescript specified through --tsserver-path ignored due to invalid path "${userSettingVersion.path}"`); } // Workspace version. if (this.workspaceRoot) { const workspaceVersion = typescriptVersionProvider.getWorkspaceVersion([this.workspaceRoot]); if (workspaceVersion) { return workspaceVersion; } } // Bundled version const bundledVersion = typescriptVersionProvider.bundledVersion(); if (bundledVersion && bundledVersion.isValid) { return bundledVersion; } return null; } async initialize(params) { this.logger.log('initialize', params); if (this._tspClient) { throw new Error('The "initialize" request has already called before.'); } this.initializeParams = params; const clientCapabilities = this.initializeParams.capabilities; this.workspaceRoot = this.initializeParams.rootUri ? uriToPath(this.initializeParams.rootUri) : this.initializeParams.rootPath || undefined; const userInitializationOptions = this.initializeParams.initializationOptions || {}; const { disableAutomaticTypingAcquisition, hostInfo, maxTsServerMemory, npmLocation, locale } = userInitializationOptions; const { logVerbosity, plugins } = { logVerbosity: userInitializationOptions.logVerbosity || this.options.tsserverLogVerbosity, plugins: userInitializationOptions.plugins || [], }; const logFile = this.getLogFile(logVerbosity); const globalPlugins = []; const pluginProbeLocations = []; for (const plugin of plugins) { globalPlugins.push(plugin.name); pluginProbeLocations.push(plugin.location); } const typescriptVersion = this.findTypescriptVersion(); if (typescriptVersion) { this.logger.info(`Using Typescript version (${typescriptVersion.source}) ${typescriptVersion.versionString} from path "${typescriptVersion.tsServerPath}"`); } else { throw Error('Could not find a valid TypeScript installation. Please ensure that the "typescript" dependency is installed in the workspace or that a valid --tsserver-path is specified. Exiting.'); } this.configurationManager.mergeTsPreferences(userInitializationOptions.preferences || {}); // Setup supported features. const { textDocument } = clientCapabilities; if (textDocument) { const completionCapabilities = textDocument.completion; this.features.codeActionDisabledSupport = textDocument.codeAction?.disabledSupport; this.features.definitionLinkSupport = textDocument.definition?.linkSupport && typescriptVersion.version?.gte(API.v270); this.features.completionInsertReplaceSupport = completionCapabilities?.completionItem?.insertReplaceSupport; if (completionCapabilities?.completionItem) { if (this.configurationManager.tsPreferences.useLabelDetailsInCompletionEntries && completionCapabilities.completionItem.labelDetailsSupport && typescriptVersion.version?.gte(API.v470)) { this.features.completionLabelDetails = true; } if (completionCapabilities.completionItem.snippetSupport) { this.features.completionSnippets = true; } if (textDocument.publishDiagnostics?.tagSupport) { this.features.diagnosticsTagSupport = true; } } } this.configurationManager.mergeTsPreferences({ useLabelDetailsInCompletionEntries: this.features.completionLabelDetails, }); this.diagnosticQueue = new DiagnosticEventQueue(diagnostics => this.options.lspClient.publishDiagnostics(diagnostics), this.documents, this.features, this.logger); this._tspClient = new TspClient({ lspClient: this.options.lspClient, typescriptVersion, logFile, logVerbosity, disableAutomaticTypingAcquisition, maxTsServerMemory, npmLocation, locale, globalPlugins, pluginProbeLocations, logger: this.options.logger, onEvent: this.onTsEvent.bind(this), onExit: (exitCode, signal) => { if (exitCode) { this.logger.error(`tsserver process has exited (exit code: ${exitCode}, signal: ${signal}). Stopping the server.`); } this.shutdown(); }, }); const started = this.tspClient.start(); if (!started) { throw new Error('tsserver process has failed to start.'); } process.on('exit', () => { this.shutdown(); }); process.on('SIGINT', () => { process.exit(); }); this.typeScriptAutoFixProvider = new TypeScriptAutoFixProvider(this.tspClient); await Promise.all([ this.configurationManager.setAndConfigureTspClient(this.workspaceRoot, this._tspClient, hostInfo), this.tspClient.request("compilerOptionsForInferredProjects" /* CommandTypes.CompilerOptionsForInferredProjects */, { options: { module: "CommonJS" /* tsp.ModuleKind.CommonJS */, target: "ES2016" /* tsp.ScriptTarget.ES2016 */, jsx: "Preserve" /* tsp.JsxEmit.Preserve */, allowJs: true, allowSyntheticDefaultImports: true, allowNonTsExtensions: true, }, }), ]); const logFileUri = logFile && pathToUri(logFile, undefined); const initializeResult = { capabilities: { textDocumentSync: lsp.TextDocumentSyncKind.Incremental, completionProvider: { triggerCharacters: ['.', '"', '\'', '/', '@', '<'], resolveProvider: true, }, codeActionProvider: clientCapabilities.textDocument?.codeAction?.codeActionLiteralSupport ? { codeActionKinds: [ ...TypeScriptAutoFixProvider.kinds.map(kind => kind.value), CodeActionKind.SourceOrganizeImportsTs.value, CodeActionKind.QuickFix.value, CodeActionKind.Refactor.value, ] } : true, definitionProvider: true, documentFormattingProvider: true, documentRangeFormattingProvider: true, documentHighlightProvider: true, documentSymbolProvider: true, executeCommandProvider: { commands: [ Commands.APPLY_WORKSPACE_EDIT, Commands.APPLY_CODE_ACTION, Commands.APPLY_REFACTORING, Commands.ORGANIZE_IMPORTS, Commands.APPLY_RENAME_FILE, Commands.SOURCE_DEFINITION, ], }, hoverProvider: true, inlayHintProvider: true, renameProvider: true, referencesProvider: true, signatureHelpProvider: { triggerCharacters: ['(', ',', '<'], retriggerCharacters: [')'], }, workspaceSymbolProvider: true, implementationProvider: true, typeDefinitionProvider: true, foldingRangeProvider: true, semanticTokensProvider: { documentSelector: null, legend: { // list taken from: https://github.com/microsoft/TypeScript/blob/main/src/services/classifier2020.ts#L10 tokenTypes: [ 'class', 'enum', 'interface', 'namespace', 'typeParameter', 'type', 'parameter', 'variable', 'enumMember', 'property', 'function', 'member', ], // token from: https://github.com/microsoft/TypeScript/blob/main/src/services/classifier2020.ts#L14 tokenModifiers: [ 'declaration', 'static', 'async', 'readonly', 'defaultLibrary', 'local', ], }, full: true, range: true, }, }, logFileUri, }; initializeResult.capabilities.callsProvider = true; this.logger.log('onInitialize result', initializeResult); return initializeResult; } getLogFile(logVerbosity) { if (logVerbosity === undefined || logVerbosity === 'off') { return undefined; } const logFile = this.doGetLogFile(); if (logFile) { fs.ensureFileSync(logFile); return logFile; } return temporaryFile({ name: 'tsserver.log' }); } doGetLogFile() { if (process.env.TSSERVER_LOG_FILE) { return process.env.TSSERVER_LOG_FILE; } if (this.options.tsserverLogFile) { return this.options.tsserverLogFile; } if (this.workspaceRoot) { return path.join(this.workspaceRoot, '.log/tsserver.log'); } return undefined; } didChangeConfiguration(params) { this.configurationManager.setWorkspaceConfiguration(params.settings || {}); const ignoredDiagnosticCodes = this.configurationManager.workspaceConfiguration.diagnostics?.ignoredCodes || []; this.diagnosticQueue?.updateIgnoredDiagnosticCodes(ignoredDiagnosticCodes); } interuptDiagnostics(f) { if (!this.diagnosticsTokenSource) { return f(); } this.cancelDiagnostics(); const result = f(); this.requestDiagnostics(); return result; } async requestDiagnostics() { this.pendingDebouncedRequest = true; await this.doRequestDiagnosticsDebounced(); } async doRequestDiagnostics() { this.cancelDiagnostics(); const geterrTokenSource = new lsp.CancellationTokenSource(); this.diagnosticsTokenSource = geterrTokenSource; const { files } = this.documents; try { return await this.tspClient.requestGeterr({ delay: 0, files }, this.diagnosticsTokenSource.token); } finally { if (this.diagnosticsTokenSource === geterrTokenSource) { this.diagnosticsTokenSource = undefined; this.pendingDebouncedRequest = false; } } } cancelDiagnostics() { if (this.diagnosticsTokenSource) { this.diagnosticsTokenSource = undefined; } } didOpenTextDocument(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('onDidOpenTextDocument', params, file); if (!file) { return; } if (this.documents.open(file, params.textDocument)) { this.tspClient.notify("open" /* CommandTypes.Open */, { file, fileContent: params.textDocument.text, scriptKindName: this.getScriptKindName(params.textDocument.languageId), projectRootPath: this.workspaceRoot, }); this.requestDiagnostics(); } else { this.logger.log(`Cannot open already opened doc '${params.textDocument.uri}'.`); this.didChangeTextDocument({ textDocument: params.textDocument, contentChanges: [ { text: params.textDocument.text, }, ], }); } } getScriptKindName(languageId) { switch (languageId) { case 'typescript': return 'TS'; case 'typescriptreact': return 'TSX'; case 'javascript': return 'JS'; case 'javascriptreact': return 'JSX'; } return undefined; } didCloseTextDocument(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('onDidCloseTextDocument', params, file); if (!file) { return; } this.closeDocument(file); } closeDocument(file) { const document = this.documents.close(file); if (!document) { return; } this.tspClient.notify("close" /* CommandTypes.Close */, { file }); // We won't be updating diagnostics anymore for that file, so clear them // so we don't leave stale ones. this.options.lspClient.publishDiagnostics({ uri: document.uri, diagnostics: [], }); } didChangeTextDocument(params) { const { textDocument } = params; const file = uriToPath(textDocument.uri); this.logger.log('onDidChangeTextDocument', params, file); if (!file) { return; } const document = this.documents.get(file); if (!document) { this.logger.error('Received change on non-opened document ' + textDocument.uri); throw new Error('Received change on non-opened document ' + textDocument.uri); } if (textDocument.version === null) { throw new Error(`Received document change event for ${textDocument.uri} without valid version identifier`); } for (const change of params.contentChanges) { let line = 0; let offset = 0; let endLine = 0; let endOffset = 0; if (lsp.TextDocumentContentChangeEvent.isIncremental(change)) { line = change.range.start.line + 1; offset = change.range.start.character + 1; endLine = change.range.end.line + 1; endOffset = change.range.end.character + 1; } else { line = 1; offset = 1; const endPos = document.positionAt(document.getText().length); endLine = endPos.line + 1; endOffset = endPos.character + 1; } this.tspClient.notify("change" /* CommandTypes.Change */, { file, line, offset, endLine, endOffset, insertString: change.text, }); document.applyEdit(textDocument.version, change); } this.requestDiagnostics(); } didSaveTextDocument(_params) { // do nothing } async definition(params) { return this.getDefinition({ type: this.features.definitionLinkSupport ? "definitionAndBoundSpan" /* CommandTypes.DefinitionAndBoundSpan */ : "definition" /* CommandTypes.Definition */, params, }); } async implementation(params) { return this.getSymbolLocations({ type: "implementation" /* CommandTypes.Implementation */, params, }); } async typeDefinition(params) { return this.getSymbolLocations({ type: "typeDefinition" /* CommandTypes.TypeDefinition */, params, }); } async getDefinition({ type, params }) { const file = uriToPath(params.textDocument.uri); this.logger.log(type, params, file); if (!file) { return undefined; } if (type === "definitionAndBoundSpan" /* CommandTypes.DefinitionAndBoundSpan */) { const args = Position.toFileLocationRequestArgs(file, params.position); const response = await this.tspClient.request(type, args); if (response.type !== 'response' || !response.body) { return undefined; } // `textSpan` can be undefined in older TypeScript versions, despite type saying otherwise. const span = response.body.textSpan ? Range.fromTextSpan(response.body.textSpan) : undefined; return response.body.definitions .map((location) => { const target = toLocation(location, this.documents); const targetRange = location.contextStart && location.contextEnd ? Range.fromLocations(location.contextStart, location.contextEnd) : target.range; return { originSelectionRange: span, targetRange, targetUri: target.uri, targetSelectionRange: target.range, }; }); } return this.getSymbolLocations({ type: "definition" /* CommandTypes.Definition */, params }); } async getSymbolLocations({ type, params }) { const file = uriToPath(params.textDocument.uri); this.logger.log(type, params, file); if (!file) { return []; } const args = Position.toFileLocationRequestArgs(file, params.position); const response = await this.tspClient.request(type, args); if (response.type !== 'response' || !response.body) { return undefined; } return response.body.map(fileSpan => toLocation(fileSpan, this.documents)); } async documentSymbol(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('symbol', params, file); if (!file) { return []; } const response = await this.tspClient.request("navtree" /* CommandTypes.NavTree */, { file, }); const tree = response.body; if (!tree || !tree.childItems) { return []; } if (this.supportHierarchicalDocumentSymbol) { const symbols = []; for (const item of tree.childItems) { collectDocumentSymbols(item, symbols); } return symbols; } const symbols = []; for (const item of tree.childItems) { collectSymbolInformation(params.textDocument.uri, item, symbols); } return symbols; } get supportHierarchicalDocumentSymbol() { const textDocument = this.initializeParams?.capabilities.textDocument; const documentSymbol = textDocument && textDocument.documentSymbol; return !!documentSymbol && !!documentSymbol.hierarchicalDocumentSymbolSupport; } /* * implemented based on * https://github.com/Microsoft/vscode/blob/master/extensions/typescript-language-features/src/features/completions.ts */ async completion(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('completion', params, file); if (!file) { return lsp.CompletionList.create([]); } const document = this.documents.get(file); if (!document) { throw new Error('The document should be opened for completion, file: ' + file); } try { const result = await this.interuptDiagnostics(() => this.tspClient.request("completionInfo" /* CommandTypes.CompletionInfo */, { file, line: params.position.line + 1, offset: params.position.character + 1, triggerCharacter: getCompletionTriggerCharacter(params.context?.triggerCharacter), triggerKind: params.context?.triggerKind, })); const { body } = result; if (!body) { return lsp.CompletionList.create(); } const { entries, isIncomplete, optionalReplacementSpan } = body; const completions = []; for (const entry of entries || []) { if (entry.kind === 'warning') { continue; } const completion = asCompletionItem(entry, optionalReplacementSpan, file, params.position, document, this.features); if (!completion) { continue; } completions.push(completion); } return lsp.CompletionList.create(completions, isIncomplete); } catch (error) { if (error.message === 'No content available.') { this.logger.info('No content was available for completion request'); return null; } else { throw error; } } } async completionResolve(item) { this.logger.log('completion/resolve', item); await this.configurationManager.configureGloballyFromDocument(item.data.file); const { body } = await this.interuptDiagnostics(() => this.tspClient.request("completionEntryDetails" /* CommandTypes.CompletionDetails */, item.data)); const details = body && body.length && body[0]; if (!details) { return item; } return asResolvedCompletionItem(item, details, this.tspClient, this.configurationManager.workspaceConfiguration.completions || {}, this.features); } async hover(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('hover', params, file); if (!file) { return { contents: [] }; } const result = await this.interuptDiagnostics(() => this.getQuickInfo(file, params.position)); if (!result || !result.body) { return { contents: [] }; } const range = Range.fromTextSpan(result.body); const contents = []; if (result.body.displayString) { contents.push({ language: 'typescript', value: result.body.displayString }); } const tags = asTagsDocumentation(result.body.tags); const documentation = asPlainText(result.body.documentation); contents.push(documentation + (tags ? '\n\n' + tags : '')); return { contents, range, }; } async getQuickInfo(file, position) { try { return await this.tspClient.request("quickinfo" /* CommandTypes.Quickinfo */, { file, line: position.line + 1, offset: position.character + 1, }); } catch (err) { return undefined; } } async rename(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('onRename', params, file); if (!file) { return undefined; } const result = await this.tspClient.request("rename" /* CommandTypes.Rename */, { file, line: params.position.line + 1, offset: params.position.character + 1, }); if (!result.body || !result.body.info.canRename || result.body.locs.length === 0) { return undefined; } const workspaceEdit = {}; result.body.locs .forEach((spanGroup) => { const uri = pathToUri(spanGroup.file, this.documents); if (!workspaceEdit.changes) { workspaceEdit.changes = {}; } const textEdits = workspaceEdit.changes[uri] || (workspaceEdit.changes[uri] = []); spanGroup.locs.forEach((textSpan) => { textEdits.push({ newText: `${textSpan.prefixText || ''}${params.newName}${textSpan.suffixText || ''}`, range: { start: Position.fromLocation(textSpan.start), end: Position.fromLocation(textSpan.end), }, }); }); }); return workspaceEdit; } async references(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('onReferences', params, file); if (!file) { return []; } const result = await this.tspClient.request("references" /* CommandTypes.References */, { file, line: params.position.line + 1, offset: params.position.character + 1, }); if (!result.body) { return []; } return result.body.refs .filter(fileSpan => params.context.includeDeclaration || !fileSpan.isDefinition) .map(fileSpan => toLocation(fileSpan, this.documents)); } async documentFormatting(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('documentFormatting', params, file); if (!file) { return []; } const formatOptions = params.options; await this.configurationManager.configureGloballyFromDocument(file, formatOptions); const response = await this.tspClient.request("format" /* CommandTypes.Format */, { file, line: 1, offset: 1, endLine: Number.MAX_SAFE_INTEGER, endOffset: Number.MAX_SAFE_INTEGER, options: formatOptions, }); if (response.body) { return response.body.map(e => toTextEdit(e)); } return []; } async documentRangeFormatting(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('documentRangeFormatting', params, file); if (!file) { return []; } const formatOptions = params.options; await this.configurationManager.configureGloballyFromDocument(file, formatOptions); const response = await this.tspClient.request("format" /* CommandTypes.Format */, { file, line: params.range.start.line + 1, offset: params.range.start.character + 1, endLine: params.range.end.line + 1, endOffset: params.range.end.character + 1, options: formatOptions, }); if (response.body) { return response.body.map(e => toTextEdit(e)); } return []; } async signatureHelp(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('signatureHelp', params, file); if (!file) { return undefined; } const response = await this.interuptDiagnostics(() => this.getSignatureHelp(file, params)); if (!response || !response.body) { return undefined; } return asSignatureHelp(response.body, params.context); } async getSignatureHelp(file, params) { try { const { position, context } = params; return await this.tspClient.request("signatureHelp" /* CommandTypes.SignatureHelp */, { file, line: position.line + 1, offset: position.character + 1, triggerReason: context ? toTsTriggerReason(context) : undefined, }); } catch (err) { return undefined; } } async codeAction(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('codeAction', params, file); if (!file) { return []; } const fileRangeArgs = Range.toFileRangeRequestArgs(file, params.range); const actions = []; const kinds = params.context.only?.map(kind => new CodeActionKind(kind)); if (!kinds || kinds.some(kind => kind.contains(CodeActionKind.QuickFix))) { actions.push(...provideQuickFix(await this.getCodeFixes(fileRangeArgs, params.context), this.documents)); } if (!kinds || kinds.some(kind => kind.contains(CodeActionKind.Refactor))) { actions.push(...provideRefactors(await this.getRefactors(fileRangeArgs, params.context), fileRangeArgs, this.features)); } // organize import is provided by tsserver for any line, so we only get it if explicitly requested if (kinds?.some(kind => kind.contains(CodeActionKind.SourceOrganizeImportsTs))) { // see this issue for more context about how this argument is used // https://github.com/microsoft/TypeScript/issues/43051 const skipDestructiveCodeActions = params.context.diagnostics.some( // assume no severity is an error d => (d.severity ?? 0) <= 2); const response = await this.getOrganizeImports({ scope: { type: 'file', args: fileRangeArgs }, skipDestructiveCodeActions, }); actions.push(...provideOrganizeImports(response, this.documents)); } // TODO: Since we rely on diagnostics pointing at errors in the correct places, we can't proceed if we are not // sure that diagnostics are up-to-date. Thus we check `pendingDebouncedRequest` to see if there are *any* // pending diagnostic requests (regardless of for which file). // In general would be better to replace the whole diagnostics handling logic with the one from // bufferSyncSupport.ts in VSCode's typescript language features. if (kinds && !this.pendingDebouncedRequest) { const diagnostics = this.diagnosticQueue?.getDiagnosticsForFile(file) || []; if (diagnostics.length) { actions.push(...await this.typeScriptAutoFixProvider.provideCodeActions(kinds, file, diagnostics, this.documents)); } } return actions; } async getCodeFixes(fileRangeArgs, context) { const errorCodes = context.diagnostics.map(diagnostic => Number(diagnostic.code)); const args = { ...fileRangeArgs, errorCodes, }; try { return await this.tspClient.request("getCodeFixes" /* CommandTypes.GetCodeFixes */, args); } catch (err) { return undefined; } } async getRefactors(fileRangeArgs, context) { const args = { ...fileRangeArgs, triggerReason: context.triggerKind === lsp.CodeActionTriggerKind.Invoked ? 'invoked' : undefined, kind: context.only?.length === 1 ? context.only[0] : undefined, }; try { return await this.tspClient.request("getApplicableRefactors" /* CommandTypes.GetApplicableRefactors */, args); } catch (err) { return undefined; } } async getOrganizeImports(args) { try { await this.configurationManager.configureGloballyFromDocument(args.scope.args.file); return await this.tspClient.request("organizeImports" /* CommandTypes.OrganizeImports */, args); } catch (err) { return undefined; } } async executeCommand(arg, token, workDoneProgress) { this.logger.log('executeCommand', arg); if (arg.command === Commands.APPLY_WORKSPACE_EDIT && arg.arguments) { const edit = arg.arguments[0]; await this.options.lspClient.applyWorkspaceEdit({ edit }); } else if (arg.command === Commands.APPLY_CODE_ACTION && arg.arguments) { const codeAction = arg.arguments[0]; if (!await this.applyFileCodeEdits(codeAction.changes)) { return; } if (codeAction.commands && codeAction.commands.length) { for (const command of codeAction.commands) { await this.tspClient.request("applyCodeActionCommand" /* CommandTypes.ApplyCodeActionCommand */, { command }); } } } else if (arg.command === Commands.APPLY_REFACTORING && arg.arguments) { const args = arg.arguments[0]; const { body } = await this.tspClient.request("getEditsForRefactor" /* CommandTypes.GetEditsForRefactor */, args); if (!body || !body.edits.length) { return; } for (const edit of body.edits) { await fs.ensureFile(edit.fileName); } if (!await this.applyFileCodeEdits(body.edits)) { return; } const renameLocation = body.renameLocation; if (renameLocation) { await this.options.lspClient.rename({ textDocument: { uri: pathToUri(args.file, this.documents), }, position: Position.fromLocation(renameLocation), }); } } else if (arg.command === Commands.ORGANIZE_IMPORTS && arg.arguments) { const file = arg.arguments[0]; const additionalArguments = arg.arguments[1] || {}; await this.configurationManager.configureGloballyFromDocument(file); const { body } = await this.tspClient.request("organizeImports" /* CommandTypes.OrganizeImports */, { scope: { type: 'file', args: { file }, }, skipDestructiveCodeActions: additionalArguments.skipDestructiveCodeActions, }); await this.applyFileCodeEdits(body); } else if (arg.command === Commands.APPLY_RENAME_FILE && arg.arguments) { const { sourceUri, targetUri } = arg.arguments[0]; this.applyRenameFile(sourceUri, targetUri); } else if (arg.command === Commands.APPLY_COMPLETION_CODE_ACTION && arg.arguments) { const [_, codeActions] = arg.arguments; for (const codeAction of codeActions) { await this.applyFileCodeEdits(codeAction.changes); if (codeAction.commands && codeAction.commands.length) { for (const command of codeAction.commands) { await this.tspClient.request("applyCodeActionCommand" /* CommandTypes.ApplyCodeActionCommand */, { command }); } } // Execute only the first code action. break; } } else if (arg.command === Commands.SOURCE_DEFINITION) { const [uri, position] = (arg.arguments || []); const reporter = await this.options.lspClient.createProgressReporter(token, workDoneProgress); return SourceDefinitionCommand.execute(uri, position, this.documents, this.tspClient, this.options.lspClient, reporter); } else { this.logger.error(`Unknown command ${arg.command}.`); } } async applyFileCodeEdits(edits) { if (!edits.length) { return false; } const changes = {}; for (const edit of edits) { changes[pathToUri(edit.fileName, this.documents)] = edit.textChanges.map(toTextEdit); } const { applied } = await this.options.lspClient.applyWorkspaceEdit({ edit: { changes }, }); return applied; } async applyRenameFile(sourceUri, targetUri) { const edits = await this.getEditsForFileRename(sourceUri, targetUri); this.applyFileCodeEdits(edits); } async getEditsForFileRename(sourceUri, targetUri) { const newFilePath = uriToPath(targetUri); const oldFilePath = uriToPath(sourceUri); if (!newFilePath || !oldFilePath) { return []; } try { const { body } = await this.tspClient.request("getEditsForFileRename" /* CommandTypes.GetEditsForFileRename */, { oldFilePath, newFilePath, }); return body; } catch (err) { return []; } } async documentHighlight(arg) { const file = uriToPath(arg.textDocument.uri); this.logger.log('documentHighlight', arg, file); if (!file) { return []; } let response; try { response = await this.tspClient.request("documentHighlights" /* CommandTypes.DocumentHighlights */, { file, line: arg.position.line + 1, offset: arg.position.character + 1, filesToSearch: [file], }); } catch (err) { return []; } if (!response.body) { return []; } const result = []; for (const item of response.body) { // tsp returns item.file with POSIX path delimiters, whereas file is platform specific. // Converting to a URI and back to a path ensures consistency. if (normalizePath(item.file) === file) { const highlights = toDocumentHighlight(item); result.push(...highlights); } } return result; } lastFileOrDummy() { return this.documents.files[0] || this.workspaceRoot; } async workspaceSymbol(params) { const result = await this.tspClient.request("navto" /* CommandTypes.Navto */, { file: this.lastFileOrDummy(), searchValue: params.query, }); if (!result.body) { return []; } return result.body.map(item => { return { location: { uri: pathToUri(item.file, this.documents), range: { start: Position.fromLocation(item.start), end: Position.fromLocation(item.end), }, }, kind: toSymbolKind(item.kind), name: item.name, }; }); } /** * implemented based on https://github.com/Microsoft/vscode/blob/master/extensions/typescript-language-features/src/features/folding.ts */ async foldingRanges(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('foldingRanges', params, file); if (!file) { return undefined; } const document = this.documents.get(file); if (!document) { throw new Error("The document should be opened for foldingRanges', file: " + file); } const { body } = await this.tspClient.request("getOutliningSpans" /* CommandTypes.GetOutliningSpans */, { file }); if (!body) { return undefined; } const foldingRanges = []; for (const span of body) { const foldingRange = this.asFoldingRange(span, document); if (foldingRange) { foldingRanges.push(foldingRange); } } return foldingRanges; } asFoldingRange(span, document) { const range = Range.fromTextSpan(span.textSpan); const kind = this.asFoldingRangeKind(span); // workaround for https://github.com/Microsoft/vscode/issues/49904 if (span.kind === 'comment') { const line = document.getLine(range.start.line); if (line.match(/\/\/\s*#endregion/gi)) { return undefined; } } const startLine = range.start.line; // workaround for https://github.com/Microsoft/vscode/issues/47240 const endLine = range.end.character > 0 && document.getText(lsp.Range.create(lsp.Position.create(range.end.line, range.end.character - 1), range.end)) === '}' ? Math.max(range.end.line - 1, range.start.line) : range.end.line; return { startLine, endLine, kind, }; } asFoldingRangeKind(span) { switch (span.kind) { case 'comment': return lsp.FoldingRangeKind.Comment; case 'region': return lsp.FoldingRangeKind.Region; case 'imports': return lsp.FoldingRangeKind.Imports; case 'code': default: return undefined; } } async onTsEvent(event) { if (event.event === "semanticDiag" /* EventTypes.SementicDiag */ || event.event === "syntaxDiag" /* EventTypes.SyntaxDiag */ || event.event === "suggestionDiag" /* EventTypes.SuggestionDiag */) { this.diagnosticQueue?.updateDiagnostics(event.event, event); } } async calls(params) { let callsResult = { calls: [] }; const file = uriToPath(params.textDocument.uri); this.logger.log('calls', params, file); if (!file) { return callsResult; } if (params.direction === lspcalls.CallDirection.Outgoing) { const documentProvider = (file) => this.documents.get(file); callsResult = await computeCallees(this.tspClient, params, documentProvider); } else { callsResult = await computeCallers(this.tspClient, params); } return callsResult; } async inlayHints(params) { return await TypeScriptInlayHintsProvider.provideInlayHints(params.textDocument.uri, params.range, this.documents, this.tspClient, this.options.lspClient, this.configurationManager); } async inlayHintsLegacy(params) { this.options.lspClient.logMessage({ message: 'Support for experimental "typescript/inlayHints" request is deprecated. Use spec-compliant "textDocument/inlayHint" instead.', type: lsp.MessageType.Warning, }); const file = uriToPath(params.textDocument.uri); this.logger.log('inlayHints', params, file); if (!file) { return { inlayHints: [] }; } await this.configurationManager.configureGloballyFromDocument(file); const doc = this.documents.get(file); if (!doc) { return { inlayHints: [] }; } const start = doc.offsetAt(params.range?.start ?? { line: 0, character: 0, }); const end = doc.offsetAt(params.range?.end ?? { line: doc.lineCount + 1, character: 0, }); try { const result = await this.tspClient.request("provideInlayHints" /* CommandTypes.ProvideInlayHints */, { file, start: start, length: end - start, }); return { inlayHints: result.body?.map((item) => ({ text: item.text, position: Position.fromLocation(item.position), whitespaceAfter: item.whitespaceAfter, whitespaceBefore: item.whitespaceBefore, kind: item.kind, })) ?? [], }; } catch { return { inlayHints: [], }; } } async semanticTokensFull(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('semanticTokensFull', params, file); if (!file) { return { data: [] }; } const doc = this.documents.get(file); if (!doc) { return { data: [] }; } const start = doc.offsetAt({ line: 0, character: 0, }); const end = doc.offsetAt({ line: doc.lineCount, character: 0, }); return this.getSemanticTokens(doc, file, start, end); } async semanticTokensRange(params) { const file = uriToPath(params.textDocument.uri); this.logger.log('semanticTokensRange', params, file); if (!file) { return { data: [] }; } const doc = this.documents.get(file); if (!doc) { return { data: [] }; } const start = doc.offsetAt(params.range.start); const end = doc.offsetAt(params.range.end); return this.getSemanticTokens(doc, file, start, end); } async getSemanticTokens(doc, file, startOffset, endOffset) { try { const result = await this.tspClient.request("encodedSemanticClassifications-full" /* CommandTypes.EncodedSemanticClassificationsFull */, { file, start: startOffset, length: endOffset - startOffset, format: '2020', }); const spans = result.body?.spans ?? []; return { data: lspsemanticTokens.transformSpans(doc, spans) }; } catch { return { data: [] }; } } } //# sourceMappingURL=lsp-server.js.map