1
0
Fork 0
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:
Nayeem Rahman 2023-08-26 01:53:44 +01:00 committed by Bartek Iwańczuk
parent d34c23b8f3
commit 4dcb410b9d
No known key found for this signature in database
GPG key ID: 0C6BCDDC3B3AD750
2 changed files with 134 additions and 3 deletions

View file

@ -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()

View file

@ -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();