From 1fb9e8f23f5093b5798c2877e17aa3233573790c Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 23 Oct 2022 00:39:42 +0800 Subject: [PATCH] feat: support for `completeFunctionCalls` close #956 --- .../typescript/src/createLanguageService.ts | 2 +- .../src/services/completions/resolve.ts | 62 +++++++++++ .../src/utils/snippetForFunctionCall.ts | 104 ++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 plugins/typescript/src/utils/snippetForFunctionCall.ts diff --git a/plugins/typescript/src/createLanguageService.ts b/plugins/typescript/src/createLanguageService.ts index 7cd932822..5326d164f 100644 --- a/plugins/typescript/src/createLanguageService.ts +++ b/plugins/typescript/src/createLanguageService.ts @@ -36,7 +36,7 @@ export * from './configs/getFormatCodeSettings'; export * from './configs/getUserPreferences'; export interface GetConfiguration { - (section: string, scopeUri: string): Promise; + (section: string, scopeUri: string): Promise; }; export function createLanguageService( diff --git a/plugins/typescript/src/services/completions/resolve.ts b/plugins/typescript/src/services/completions/resolve.ts index 410d75f2a..a0d0352bb 100644 --- a/plugins/typescript/src/services/completions/resolve.ts +++ b/plugins/typescript/src/services/completions/resolve.ts @@ -10,6 +10,8 @@ import type { GetConfiguration } from '../../createLanguageService'; import { URI } from 'vscode-uri'; import { getFormatCodeSettings } from '../../configs/getFormatCodeSettings'; import { getUserPreferences } from '../../configs/getUserPreferences'; +import { snippetForFunctionCall } from '../../utils/snippetForFunctionCall'; +import { isTypeScriptDocument } from '../../configs/shared'; export function register( rootUri: URI, @@ -80,6 +82,36 @@ export function register( handleKindModifiers(item, details); } + if (document) { + + const useCodeSnippetsOnMethodSuggest = await getConfiguration((isTypeScriptDocument(document.uri) ? 'typescript' : 'javascript') + '.suggest.completeFunctionCalls', document.uri) ?? false; + const useCodeSnippet = useCodeSnippetsOnMethodSuggest && (item.kind === vscode.CompletionItemKind.Function || item.kind === vscode.CompletionItemKind.Method); + + if (useCodeSnippet) { + const shouldCompleteFunction = isValidFunctionCompletionContext(languageService, fileName, offset, document); + if (shouldCompleteFunction) { + const { snippet, parameterCount } = snippetForFunctionCall(item, details.displayParts); + if (item.textEdit) { + item.textEdit.newText = snippet; + } + if (item.insertText) { + item.insertText = snippet; + } + item.insertTextFormat = vscode.InsertTextFormat.Snippet; + if (parameterCount > 0) { + //Fix for https://github.com/microsoft/vscode/issues/104059 + //Don't show parameter hints if "editor.parameterHints.enabled": false + // if (await getConfiguration('editor.parameterHints.enabled', document.uri)) { + // item.command = { + // title: 'triggerParameterHints', + // command: 'editor.action.triggerParameterHints', + // }; + // } + } + } + } + } + return item; function toResource(path: string) { @@ -87,3 +119,33 @@ export function register( } }; } + +function isValidFunctionCompletionContext( + client: ts.LanguageService, + filepath: string, + offset: number, + document: TextDocument, +): boolean { + // Workaround for https://github.com/microsoft/TypeScript/issues/12677 + // Don't complete function calls inside of destructive assignments or imports + try { + const response = client.getQuickInfoAtPosition(filepath, offset); + if (response) { + switch (response.kind) { + case 'var': + case 'let': + case 'const': + case 'alias': + return false; + } + } + } catch { + // Noop + } + + // Don't complete function call if there is already something that looks like a function call + // https://github.com/microsoft/vscode/issues/18131 + const position = document.positionAt(offset); + const after = shared.getLineText(document, position.line).slice(position.character); + return after.match(/^[a-z_$0-9]*\s*\(/gi) === null; +} diff --git a/plugins/typescript/src/utils/snippetForFunctionCall.ts b/plugins/typescript/src/utils/snippetForFunctionCall.ts new file mode 100644 index 000000000..8771f088c --- /dev/null +++ b/plugins/typescript/src/utils/snippetForFunctionCall.ts @@ -0,0 +1,104 @@ +import type * as Proto from '../protocol'; +import * as PConst from '../protocol.const'; + +export function snippetForFunctionCall( + item: { insertText?: string; label: string; }, + displayParts: ReadonlyArray +): { snippet: string; parameterCount: number; } { + if (item.insertText && typeof item.insertText !== 'string') { + return { snippet: item.insertText, parameterCount: 0 }; + } + + let _tabstop = 1; + + const parameterListParts = getParameterListParts(displayParts); + let snippet = ''; + snippet += `${item.insertText || item.label}(`; + snippet = appendJoinedPlaceholders(snippet, parameterListParts.parts, ', '); + if (parameterListParts.hasOptionalParameters) { + snippet += '$' + _tabstop++; + } + snippet += ')'; + snippet += '$' + _tabstop++; + return { snippet, parameterCount: parameterListParts.parts.length + (parameterListParts.hasOptionalParameters ? 1 : 0) }; + + function appendJoinedPlaceholders( + snippet: string, + parts: ReadonlyArray, + joiner: string + ) { + for (let i = 0; i < parts.length; ++i) { + const paramterPart = parts[i]; + snippet += '${' + _tabstop++ + ':' + paramterPart.text + '}'; + if (i !== parts.length - 1) { + snippet += joiner; + } + } + return snippet; + } +} + +interface ParamterListParts { + readonly parts: ReadonlyArray; + readonly hasOptionalParameters: boolean; +} + +function getParameterListParts( + displayParts: ReadonlyArray +): ParamterListParts { + const parts: Proto.SymbolDisplayPart[] = []; + 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 PConst.DisplayPartKind.methodName: + case PConst.DisplayPartKind.functionName: + case PConst.DisplayPartKind.text: + case PConst.DisplayPartKind.propertyName: + if (parenCount === 0 && braceCount === 0) { + isInMethod = true; + } + break; + + case PConst.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 PConst.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 }; +}