typescript-language-server
Version:
Language Server Protocol (LSP) implementation for TypeScript using tsserver
1,160 lines • 48.7 kB
JavaScript
/*
* 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