mirror of
https://github.com/denoland/deno.git
synced 2024-11-26 16:09:27 -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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CompletionEntryDetails {
|
pub struct CompletionEntryDetails {
|
||||||
|
@ -2476,7 +2519,18 @@ impl CompletionEntryDetails {
|
||||||
specifier,
|
specifier,
|
||||||
language_server,
|
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 {
|
Ok(lsp::CompletionItem {
|
||||||
data: None,
|
data: None,
|
||||||
|
@ -2484,6 +2538,7 @@ impl CompletionEntryDetails {
|
||||||
documentation,
|
documentation,
|
||||||
command,
|
command,
|
||||||
additional_text_edits,
|
additional_text_edits,
|
||||||
|
insert_text,
|
||||||
// NOTE(bartlomieju): it's not entirely clear to me why we need to do that,
|
// 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
|
// 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.
|
// even though we might have returned an empty list in `completion` request.
|
||||||
|
@ -4831,8 +4886,6 @@ mod tests {
|
||||||
position,
|
position,
|
||||||
GetCompletionsAtPositionOptions {
|
GetCompletionsAtPositionOptions {
|
||||||
user_preferences: UserPreferences {
|
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_for_module_exports: Some(true),
|
||||||
include_completions_with_insert_text: Some(true),
|
include_completions_with_insert_text: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -7721,6 +7721,84 @@ fn lsp_configuration_did_change() {
|
||||||
client.shutdown();
|
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]
|
#[test]
|
||||||
fn lsp_workspace_symbol() {
|
fn lsp_workspace_symbol() {
|
||||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||||
|
|
Loading…
Reference in a new issue