typescript-language-server
Version:
Language Server Protocol (LSP) implementation for TypeScript using tsserver
376 lines • 15.1 kB
JavaScript
/*
* Copyright (C) 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 lsp from 'vscode-languageserver';
import { KindModifiers, ScriptElementKind } from './tsp-command-types.js';
import { toTextEdit, asPlainText, asDocumentation, normalizePath } from './protocol-translation.js';
import { Commands } from './commands.js';
import { DisplayPartKind } from './ts-protocol.js';
import SnippetString from './utils/SnippetString.js';
import { Range, Position } from './utils/typeConverters.js';
export function asCompletionItem(entry, optionalReplacementSpan, file, position, document, features) {
const item = {
label: entry.name,
...features.completionLabelDetails ? { labelDetails: entry.labelDetails } : {},
kind: asCompletionItemKind(entry.kind),
sortText: entry.sortText,
commitCharacters: asCommitCharacters(entry.kind),
preselect: entry.isRecommended,
data: {
file,
line: position.line + 1,
offset: position.character + 1,
entryNames: [
entry.source || entry.data ? {
name: entry.name,
source: entry.source,
data: entry.data,
} : entry.name,
],
},
};
if (entry.source && entry.hasAction) {
// De-prioritze auto-imports
// https://github.com/Microsoft/vscode/issues/40311
item.sortText = '\uffff' + entry.sortText;
}
const { isSnippet, sourceDisplay } = entry;
if (isSnippet && !features.completionSnippets) {
return null;
}
if (features.completionSnippets && (isSnippet || entry.isImportStatementCompletion || item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method)) {
// Import statements, Functions and Methods can result in a snippet completion when resolved.
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
}
if (sourceDisplay) {
item.detail = asPlainText(sourceDisplay);
}
let insertText = entry.insertText;
const replacementSpan = entry.replacementSpan || (features.completionInsertReplaceSupport ? optionalReplacementSpan : undefined);
let replacementRange = replacementSpan && Range.fromTextSpan(replacementSpan);
// Make sure we only replace a single line at most
if (replacementRange && replacementRange.start.line !== replacementRange.end.line) {
replacementRange = lsp.Range.create(replacementRange.start, document.getLineEnd(replacementRange.start.line));
}
if (insertText && replacementRange && insertText[0] === '[') { // o.x -> o['x']
item.filterText = '.' + item.label;
}
if (entry.kindModifiers) {
const kindModifiers = new Set(entry.kindModifiers.split(/,|\s+/g));
if (kindModifiers.has(KindModifiers.optional)) {
if (!insertText) {
insertText = item.label;
}
if (!item.filterText) {
item.filterText = item.label;
}
item.label += '?';
}
if (kindModifiers.has(KindModifiers.deprecated)) {
item.tags = [lsp.CompletionItemTag.Deprecated];
}
if (kindModifiers.has(KindModifiers.color)) {
item.kind = lsp.CompletionItemKind.Color;
}
if (entry.kind === ScriptElementKind.scriptElement) {
for (const extModifier of KindModifiers.fileExtensionKindModifiers) {
if (kindModifiers.has(extModifier)) {
if (entry.name.toLowerCase().endsWith(extModifier)) {
item.detail = entry.name;
}
else {
item.detail = entry.name + extModifier;
}
break;
}
}
}
}
if (replacementRange) {
if (!insertText) {
insertText = item.label;
}
if (features.completionInsertReplaceSupport) {
const insertRange = lsp.Range.create(replacementRange.start, position);
item.textEdit = lsp.InsertReplaceEdit.create(insertText, insertRange, replacementRange);
}
else {
item.textEdit = lsp.TextEdit.replace(replacementRange, insertText);
}
}
else {
item.insertText = insertText;
}
return item;
}
function asCompletionItemKind(kind) {
switch (kind) {
case ScriptElementKind.primitiveType:
case ScriptElementKind.keyword:
return lsp.CompletionItemKind.Keyword;
case ScriptElementKind.constElement:
return lsp.CompletionItemKind.Constant;
case ScriptElementKind.letElement:
case ScriptElementKind.variableElement:
case ScriptElementKind.localVariableElement:
case ScriptElementKind.alias:
return lsp.CompletionItemKind.Variable;
case ScriptElementKind.memberVariableElement:
case ScriptElementKind.memberGetAccessorElement:
case ScriptElementKind.memberSetAccessorElement:
return lsp.CompletionItemKind.Field;
case ScriptElementKind.functionElement:
return lsp.CompletionItemKind.Function;
case ScriptElementKind.memberFunctionElement:
case ScriptElementKind.constructSignatureElement:
case ScriptElementKind.callSignatureElement:
case ScriptElementKind.indexSignatureElement:
return lsp.CompletionItemKind.Method;
case ScriptElementKind.enumElement:
return lsp.CompletionItemKind.Enum;
case ScriptElementKind.moduleElement:
case ScriptElementKind.externalModuleName:
return lsp.CompletionItemKind.Module;
case ScriptElementKind.classElement:
case ScriptElementKind.typeElement:
return lsp.CompletionItemKind.Class;
case ScriptElementKind.interfaceElement:
return lsp.CompletionItemKind.Interface;
case ScriptElementKind.warning:
case ScriptElementKind.scriptElement:
return lsp.CompletionItemKind.File;
case ScriptElementKind.directory:
return lsp.CompletionItemKind.Folder;
case ScriptElementKind.string:
return lsp.CompletionItemKind.Constant;
}
return lsp.CompletionItemKind.Property;
}
function asCommitCharacters(kind) {
const commitCharacters = [];
switch (kind) {
case ScriptElementKind.memberGetAccessorElement:
case ScriptElementKind.memberSetAccessorElement:
case ScriptElementKind.constructSignatureElement:
case ScriptElementKind.callSignatureElement:
case ScriptElementKind.indexSignatureElement:
case ScriptElementKind.enumElement:
case ScriptElementKind.interfaceElement:
commitCharacters.push('.');
break;
case ScriptElementKind.moduleElement:
case ScriptElementKind.alias:
case ScriptElementKind.constElement:
case ScriptElementKind.letElement:
case ScriptElementKind.variableElement:
case ScriptElementKind.localVariableElement:
case ScriptElementKind.memberVariableElement:
case ScriptElementKind.classElement:
case ScriptElementKind.functionElement:
case ScriptElementKind.memberFunctionElement:
commitCharacters.push('.', ',');
commitCharacters.push('(');
break;
}
return commitCharacters.length === 0 ? undefined : commitCharacters;
}
export async function asResolvedCompletionItem(item, details, client, options, features) {
item.detail = asDetail(details);
item.documentation = asDocumentation(details);
const filepath = normalizePath(item.data.file);
if (details.codeActions?.length) {
item.additionalTextEdits = asAdditionalTextEdits(details.codeActions, filepath);
item.command = asCommand(details.codeActions, item.data.file);
}
if (features.completionSnippets && options.completeFunctionCalls && (item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method)) {
const { line, offset } = item.data;
const position = Position.fromLocation({ line, offset });
const shouldCompleteFunction = await isValidFunctionCompletionContext(filepath, position, client);
if (shouldCompleteFunction) {
createSnippetOfFunctionCall(item, details);
}
}
return item;
}
export async function isValidFunctionCompletionContext(filepath, position, client) {
// Workaround for https://github.com/Microsoft/TypeScript/issues/12677
// Don't complete function calls inside of destructive assigments or imports
try {
const args = Position.toFileLocationRequestArgs(filepath, position);
const response = await client.request("quickinfo" /* CommandTypes.Quickinfo */, args);
if (response.type !== 'response') {
return true;
}
const { body } = response;
switch (body?.kind) {
case 'var':
case 'let':
case 'const':
case 'alias':
return false;
default:
return true;
}
}
catch {
return true;
}
}
function createSnippetOfFunctionCall(item, detail) {
const { displayParts } = detail;
const parameterListParts = getParameterListParts(displayParts);
const snippet = new SnippetString();
snippet.appendText(`${item.insertText || item.label}(`);
appendJoinedPlaceholders(snippet, parameterListParts.parts, ', ');
if (parameterListParts.hasOptionalParameters) {
snippet.appendTabstop();
}
snippet.appendText(')');
snippet.appendTabstop(0);
item.insertText = snippet.value;
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
}
function getParameterListParts(displayParts) {
const parts = [];
let isInMethod = false;
let hasOptionalParameters = false;
let parenCount = 0;
let braceCount = 0;
outer: for (let i = 0; i < displayParts.length; ++i) {
const part = displayParts[i];
switch (part.kind) {
case DisplayPartKind.methodName:
case DisplayPartKind.functionName:
case DisplayPartKind.text:
case DisplayPartKind.propertyName:
if (parenCount === 0 && braceCount === 0) {
isInMethod = true;
}
break;
case DisplayPartKind.parameterName:
if (parenCount === 1 && braceCount === 0 && isInMethod) {
// Only take top level paren names
const next = displayParts[i + 1];
// Skip optional parameters
const nameIsFollowedByOptionalIndicator = next && next.text === '?';
// Skip this parameter
const nameIsThis = part.text === 'this';
if (!nameIsFollowedByOptionalIndicator && !nameIsThis) {
parts.push(part);
}
hasOptionalParameters = hasOptionalParameters || nameIsFollowedByOptionalIndicator;
}
break;
case DisplayPartKind.punctuation:
if (part.text === '(') {
++parenCount;
}
else if (part.text === ')') {
--parenCount;
if (parenCount <= 0 && isInMethod) {
break outer;
}
}
else if (part.text === '...' && parenCount === 1) {
// Found rest parmeter. Do not fill in any further arguments
hasOptionalParameters = true;
break outer;
}
else if (part.text === '{') {
++braceCount;
}
else if (part.text === '}') {
--braceCount;
}
break;
}
}
return { hasOptionalParameters, parts };
}
function appendJoinedPlaceholders(snippet, parts, joiner) {
for (let i = 0; i < parts.length; ++i) {
const paramterPart = parts[i];
snippet.appendPlaceholder(paramterPart.text);
if (i !== parts.length - 1) {
snippet.appendText(joiner);
}
}
}
function asAdditionalTextEdits(codeActions, filepath) {
// Try to extract out the additionalTextEdits for the current file.
const additionalTextEdits = [];
for (const tsAction of codeActions) {
// Apply all edits in the current file using `additionalTextEdits`
if (tsAction.changes) {
for (const change of tsAction.changes) {
if (change.fileName === filepath) {
for (const textChange of change.textChanges) {
additionalTextEdits.push(toTextEdit(textChange));
}
}
}
}
}
return additionalTextEdits.length ? additionalTextEdits : undefined;
}
function asCommand(codeActions, filepath) {
let hasRemainingCommandsOrEdits = false;
for (const tsAction of codeActions) {
if (tsAction.commands) {
hasRemainingCommandsOrEdits = true;
break;
}
if (tsAction.changes) {
for (const change of tsAction.changes) {
if (change.fileName !== filepath) {
hasRemainingCommandsOrEdits = true;
break;
}
}
}
}
if (hasRemainingCommandsOrEdits) {
// Create command that applies all edits not in the current file.
return {
title: '',
command: Commands.APPLY_COMPLETION_CODE_ACTION,
arguments: [filepath, codeActions.map(codeAction => ({
commands: codeAction.commands,
description: codeAction.description,
changes: codeAction.changes.filter(x => x.fileName !== filepath),
}))],
};
}
}
function asDetail({ displayParts, sourceDisplay, source: deprecatedSource }) {
const result = [];
const source = sourceDisplay || deprecatedSource;
if (source) {
result.push(`Auto import from '${asPlainText(source)}'`);
}
const detail = asPlainText(displayParts);
if (detail) {
result.push(detail);
}
return result.join('\n');
}
export function getCompletionTriggerCharacter(character) {
switch (character) {
case '@':
case '#':
case ' ':
case '.':
case '"':
case '\'':
case '`':
case '/':
case '<':
return character;
default:
return undefined;
}
}
//# sourceMappingURL=completion.js.map