Skip to content

Commit

Permalink
feat: support for completeFunctionCalls
Browse files Browse the repository at this point in the history
close #956
  • Loading branch information
johnsoncodehk committed Oct 22, 2022
1 parent aa3e362 commit 1fb9e8f
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 1 deletion.
2 changes: 1 addition & 1 deletion plugins/typescript/src/createLanguageService.ts
Expand Up @@ -36,7 +36,7 @@ export * from './configs/getFormatCodeSettings';
export * from './configs/getUserPreferences';

export interface GetConfiguration {
(section: string, scopeUri: string): Promise<any>;
<T = any>(section: string, scopeUri: string): Promise<T | undefined>;
};

export function createLanguageService(
Expand Down
62 changes: 62 additions & 0 deletions plugins/typescript/src/services/completions/resolve.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -80,10 +82,70 @@ export function register(
handleKindModifiers(item, details);
}

if (document) {

const useCodeSnippetsOnMethodSuggest = await getConfiguration<boolean>((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) {
return shared.getUriByPath(rootUri, path);
}
};
}

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;
}
104 changes: 104 additions & 0 deletions 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<Proto.SymbolDisplayPart>
): { 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<Proto.SymbolDisplayPart>,
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<Proto.SymbolDisplayPart>;
readonly hasOptionalParameters: boolean;
}

function getParameterListParts(
displayParts: ReadonlyArray<Proto.SymbolDisplayPart>
): 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 };
}

0 comments on commit 1fb9e8f

Please sign in to comment.