mirror of
https://github.com/denoland/deno.git
synced 2024-11-22 15:06:54 -05:00
fix(lsp): implement deno.suggest.completeFunctionCalls (#20214)
Fixes https://github.com/denoland/vscode_deno/issues/743. ```ts const items: string[] = ['foo', 'bar', 'baz']; items.map // -> items.map(callbackfn) // auto-completes with argument placeholders. ``` --- We have our own setting for `suggest.completeFunctionCalls`, which must be enabled: ```js { "deno.suggest.completeFunctionCalls": true, // Re-implementation of: // "javascript.suggest.completeFunctionCalls": true, // "typescript.suggest.completeFunctionCalls": true, } ``` But before this commit the actual implementation had been left as a TODO.
This commit is contained in:
parent
d34c23b8f3
commit
4dcb410b9d
2 changed files with 134 additions and 3 deletions
|
@ -2422,6 +2422,49 @@ fn parse_code_actions(
|
|||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/microsoft/vscode/blob/1.81.1/extensions/typescript-language-features/src/languageFeatures/util/snippetForFunctionCall.ts#L49.
|
||||
fn get_parameters_from_parts(parts: &[SymbolDisplayPart]) -> Vec<String> {
|
||||
let mut parameters = Vec::with_capacity(3);
|
||||
let mut is_in_fn = false;
|
||||
let mut paren_count = 0;
|
||||
let mut brace_count = 0;
|
||||
for (idx, part) in parts.iter().enumerate() {
|
||||
if ["methodName", "functionName", "text", "propertyName"]
|
||||
.contains(&part.kind.as_str())
|
||||
{
|
||||
if paren_count == 0 && brace_count == 0 {
|
||||
is_in_fn = true;
|
||||
}
|
||||
} else if part.kind == "parameterName" {
|
||||
if paren_count == 1 && brace_count == 0 && is_in_fn {
|
||||
let is_optional =
|
||||
matches!(parts.get(idx + 1), Some(next) if next.text == "?");
|
||||
// Skip `this` and optional parameters.
|
||||
if !is_optional && part.text != "this" {
|
||||
parameters.push(part.text.clone());
|
||||
}
|
||||
}
|
||||
} else if part.kind == "punctuation" {
|
||||
if part.text == "(" {
|
||||
paren_count += 1;
|
||||
} else if part.text == ")" {
|
||||
paren_count -= 1;
|
||||
if paren_count <= 0 && is_in_fn {
|
||||
break;
|
||||
}
|
||||
} else if part.text == "..." && paren_count == 1 {
|
||||
// Found rest parmeter. Do not fill in any further arguments.
|
||||
break;
|
||||
} else if part.text == "{" {
|
||||
brace_count += 1;
|
||||
} else if part.text == "}" {
|
||||
brace_count -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
parameters
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompletionEntryDetails {
|
||||
|
@ -2476,7 +2519,18 @@ impl CompletionEntryDetails {
|
|||
specifier,
|
||||
language_server,
|
||||
)?;
|
||||
// TODO(@kitsonk) add `use_code_snippet`
|
||||
let insert_text = if data.use_code_snippet {
|
||||
Some(format!(
|
||||
"{}({})",
|
||||
original_item
|
||||
.insert_text
|
||||
.as_ref()
|
||||
.unwrap_or(&original_item.label),
|
||||
get_parameters_from_parts(&self.display_parts).join(", "),
|
||||
))
|
||||
} else {
|
||||
original_item.insert_text.clone()
|
||||
};
|
||||
|
||||
Ok(lsp::CompletionItem {
|
||||
data: None,
|
||||
|
@ -2484,6 +2538,7 @@ impl CompletionEntryDetails {
|
|||
documentation,
|
||||
command,
|
||||
additional_text_edits,
|
||||
insert_text,
|
||||
// NOTE(bartlomieju): it's not entirely clear to me why we need to do that,
|
||||
// but when `completionItem/resolve` is called, we get a list of commit chars
|
||||
// even though we might have returned an empty list in `completion` request.
|
||||
|
@ -4831,8 +4886,6 @@ mod tests {
|
|||
position,
|
||||
GetCompletionsAtPositionOptions {
|
||||
user_preferences: UserPreferences {
|
||||
allow_incomplete_completions: Some(true),
|
||||
allow_text_changes_in_new_files: Some(true),
|
||||
include_completions_for_module_exports: Some(true),
|
||||
include_completions_with_insert_text: Some(true),
|
||||
..Default::default()
|
||||
|
|
|
@ -7721,6 +7721,84 @@ fn lsp_configuration_did_change() {
|
|||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_completions_complete_function_calls() {
|
||||
let context = TestContextBuilder::new()
|
||||
.use_http_server()
|
||||
.use_temp_cwd()
|
||||
.build();
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "[]."
|
||||
}
|
||||
}));
|
||||
client.write_notification(
|
||||
"workspace/didChangeConfiguration",
|
||||
json!({
|
||||
"settings": {}
|
||||
}),
|
||||
);
|
||||
let request = json!([{
|
||||
"enable": true,
|
||||
"suggest": {
|
||||
"completeFunctionCalls": true,
|
||||
},
|
||||
}]);
|
||||
// one for the workspace
|
||||
client.handle_configuration_request(request.clone());
|
||||
// one for the specifier
|
||||
client.handle_configuration_request(request);
|
||||
|
||||
let list = client.get_completion_list(
|
||||
"file:///a/file.ts",
|
||||
(0, 3),
|
||||
json!({
|
||||
"triggerKind": 2,
|
||||
"triggerCharacter": ".",
|
||||
}),
|
||||
);
|
||||
assert!(!list.is_incomplete);
|
||||
|
||||
let res = client.write_request(
|
||||
"completionItem/resolve",
|
||||
json!({
|
||||
"label": "map",
|
||||
"kind": 2,
|
||||
"sortText": "1",
|
||||
"insertTextFormat": 1,
|
||||
"data": {
|
||||
"tsc": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"position": 3,
|
||||
"name": "map",
|
||||
"useCodeSnippet": true
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!({
|
||||
"label": "map",
|
||||
"kind": 2,
|
||||
"detail": "(method) Array<never>.map<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any): U[]",
|
||||
"documentation": {
|
||||
"kind": "markdown",
|
||||
"value": "Calls a defined callback function on each element of an array, and returns an array that contains the results.\n\n*@param* - callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.*@param* - thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value."
|
||||
},
|
||||
"sortText": "1",
|
||||
"insertText": "map(callbackfn)",
|
||||
"insertTextFormat": 1
|
||||
})
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_workspace_symbol() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
|
|
Loading…
Reference in a new issue