Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(lsp): properly handle snippets on completions #16274

Merged
merged 1 commit into from Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Expand Up @@ -105,7 +105,7 @@ text-size = "=1.1.0"
text_lines = "=0.6.0"
tokio = { version = "=1.21.1", features = ["full"] }
tokio-util = "=0.7.4"
tower-lsp = "=0.17.0"
tower-lsp = { version = "=0.17.0", features = ["proposed"] }
twox-hash = "=1.6.3"
typed-arena = "=2.0.1"
uuid = { version = "=1.1.2", features = ["v4", "serde"] }
Expand Down
2 changes: 2 additions & 0 deletions cli/lsp/capabilities.rs
Expand Up @@ -58,6 +58,7 @@ pub fn server_capabilities(
";".to_string(),
"(".to_string(),
]),
completion_item: None,
trigger_characters: Some(vec![
".".to_string(),
"\"".to_string(),
Expand Down Expand Up @@ -140,5 +141,6 @@ pub fn server_capabilities(
"denoConfigTasks": true,
"testingApi":true,
})),
inlay_hint_provider: None,
}
}
11 changes: 11 additions & 0 deletions cli/lsp/config.rs
Expand Up @@ -20,6 +20,7 @@ pub const SETTINGS_SECTION: &str = "deno";
pub struct ClientCapabilities {
pub code_action_disabled_support: bool,
pub line_folding_only: bool,
pub snippet_support: bool,
pub status_notification: bool,
/// The client provides the `experimental.testingApi` capability, which is
/// built around VSCode's testing API. It indicates that the server should
Expand Down Expand Up @@ -393,6 +394,16 @@ impl Config {
.as_ref()
.and_then(|it| it.disabled_support)
.unwrap_or(false);
self.client_capabilities.snippet_support =
if let Some(completion) = &text_document.completion {
completion
.completion_item
.as_ref()
.and_then(|it| it.snippet_support)
.unwrap_or(false)
} else {
false
};
}
}

Expand Down
10 changes: 7 additions & 3 deletions cli/lsp/language_server.rs
Expand Up @@ -786,6 +786,7 @@ impl Inner {
Ok(InitializeResult {
capabilities,
server_info: Some(server_info),
offset_encoding: None,
})
}

Expand Down Expand Up @@ -1777,6 +1778,7 @@ impl Inner {
};
let position =
line_index.offset_tsc(params.text_document_position.position)?;
let use_snippets = self.config.client_capabilities.snippet_support;
let req = tsc::RequestMethod::GetCompletions((
specifier.clone(),
position,
Expand All @@ -1792,10 +1794,12 @@ impl Inner {
self.config.get_workspace_settings().suggest.auto_imports,
),
include_completions_for_module_exports: Some(true),
include_completions_with_object_literal_method_snippets: Some(true),
include_completions_with_class_member_snippets: Some(true),
include_completions_with_object_literal_method_snippets: Some(
use_snippets,
),
include_completions_with_class_member_snippets: Some(use_snippets),
include_completions_with_insert_text: Some(true),
include_completions_with_snippet_text: Some(true),
include_completions_with_snippet_text: Some(use_snippets),
jsx_attribute_completion_style: Some(
tsc::JsxAttributeCompletionStyle::Auto,
),
Expand Down
1 change: 1 addition & 0 deletions cli/lsp/repl.rs
Expand Up @@ -74,6 +74,7 @@ impl ReplLanguageServer {
window: None,
general: None,
experimental: None,
offset_encoding: None,
},
trace: None,
workspace_folders: None,
Expand Down
9 changes: 9 additions & 0 deletions cli/lsp/tsc.rs
Expand Up @@ -2196,6 +2196,10 @@ impl CompletionEntry {
|| kind == Some(lsp::CompletionItemKind::METHOD));
let commit_characters = self.get_commit_characters(info, settings);
let mut insert_text = self.insert_text.clone();
let insert_text_format = match self.is_snippet {
Some(true) => Some(lsp::InsertTextFormat::SNIPPET),
_ => None,
};
let range = self.replacement_span.clone();
let mut filter_text = self.get_filter_text();
let mut tags = None;
Expand Down Expand Up @@ -2262,6 +2266,7 @@ impl CompletionEntry {
text_edit,
filter_text,
insert_text,
insert_text_format,
detail,
tags,
commit_characters,
Expand Down Expand Up @@ -2910,6 +2915,10 @@ pub struct UserPreferences {
pub include_inlay_function_like_return_type_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_enum_member_value_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_rename_of_import_path: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_import_file_exclude_patterns: Option<Vec<String>>,
}

#[derive(Debug, Serialize)]
Expand Down
185 changes: 185 additions & 0 deletions cli/tests/integration/lsp_tests.rs
Expand Up @@ -3654,6 +3654,191 @@ fn lsp_completions_auto_import() {
);
}

#[test]
fn lsp_completions_snippet() {
let mut client = init("initialize_params.json");
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/a.tsx",
"languageId": "typescriptreact",
"version": 1,
"text": "function A({ type }: { type: string }) {\n return type;\n}\n\nfunction B() {\n return <A t\n}",
}
}),
);
let (maybe_res, maybe_err) = client
.write_request(
"textDocument/completion",
json!({
"textDocument": {
"uri": "file:///a/a.tsx"
},
"position": {
"line": 5,
"character": 13,
},
"context": {
"triggerKind": 1,
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
if let Some(lsp::CompletionResponse::List(list)) = maybe_res {
assert!(!list.is_incomplete);
assert_eq!(
json!(list),
json!({
"isIncomplete": false,
"items": [
{
"label": "type",
"kind": 5,
"sortText": "11",
"filterText": "type=\"$1\"",
"insertText": "type=\"$1\"",
"insertTextFormat": 2,
"commitCharacters": [
".",
",",
";",
"("
],
"data": {
"tsc": {
"specifier": "file:///a/a.tsx",
"position": 87,
"name": "type",
"useCodeSnippet": false
}
}
}
]
})
);
} else {
panic!("unexpected completion response");
}
let (maybe_res, maybe_err) = client
.write_request(
"completionItem/resolve",
json!({
"label": "type",
"kind": 5,
"sortText": "11",
"filterText": "type=\"$1\"",
"insertText": "type=\"$1\"",
"insertTextFormat": 2,
"commitCharacters": [
".",
",",
";",
"("
],
"data": {
"tsc": {
"specifier": "file:///a/a.tsx",
"position": 87,
"name": "type",
"useCodeSnippet": false
}
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!({
"label": "type",
"kind": 5,
"detail": "(property) type: string",
"documentation": {
"kind": "markdown",
"value": ""
},
"sortText": "11",
"filterText": "type=\"$1\"",
"insertText": "type=\"$1\"",
"insertTextFormat": 2,
"commitCharacters": [
".",
",",
";",
"("
]
}))
);
}

#[test]
fn lsp_completions_no_snippet() {
let mut client = init("initialize_params_no_snippet.json");
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/a.tsx",
"languageId": "typescriptreact",
"version": 1,
"text": "function A({ type }: { type: string }) {\n return type;\n}\n\nfunction B() {\n return <A t\n}",
}
}),
);
let (maybe_res, maybe_err) = client
.write_request(
"textDocument/completion",
json!({
"textDocument": {
"uri": "file:///a/a.tsx"
},
"position": {
"line": 5,
"character": 13,
},
"context": {
"triggerKind": 1,
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
if let Some(lsp::CompletionResponse::List(list)) = maybe_res {
assert!(!list.is_incomplete);
assert_eq!(
json!(list),
json!({
"isIncomplete": false,
"items": [
{
"label": "type",
"kind": 5,
"sortText": "11",
"commitCharacters": [
".",
",",
";",
"("
],
"data": {
"tsc": {
"specifier": "file:///a/a.tsx",
"position": 87,
"name": "type",
"useCodeSnippet": false
}
}
}
]
})
);
} else {
panic!("unexpected completion response");
}
}

#[test]
fn lsp_completions_registry() {
let _g = http_server();
Expand Down
5 changes: 5 additions & 0 deletions cli/tests/testdata/lsp/initialize_params.json
Expand Up @@ -56,6 +56,11 @@
]
}
},
"completion": {
"completionItem": {
"snippetSupport": true
}
},
"foldingRange": {
"lineFoldingOnly": true
},
Expand Down