From 5db16d122914336124620a5152655917e58f05a6 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Tue, 12 Jul 2022 09:35:18 +1000 Subject: [PATCH] fix(lsp): enable auto imports (#15145) Fixes: #15111 --- cli/lsp/completions.rs | 27 +- cli/lsp/language_server.rs | 51 ++- cli/lsp/tsc.rs | 322 ++++++++++++++++-- cli/tests/integration/lsp_tests.rs | 137 +++++++- .../completion_request_response_empty.json | 14 +- cli/tsc/99_main_compiler.js | 5 +- cli/tsc/compiler.d.ts | 6 +- 7 files changed, 506 insertions(+), 56 deletions(-) diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index a41e26bf5e..e69df80799 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -31,6 +31,7 @@ static FILE_PROTO_RE: Lazy = const CURRENT_PATH: &str = "."; const PARENT_PATH: &str = ".."; const LOCAL_PATHS: &[&str] = &[CURRENT_PATH, PARENT_PATH]; +const IMPORT_COMMIT_CHARS: &[&str] = &["\"", "'", "/"]; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -182,6 +183,9 @@ pub async fn get_import_completions( detail: Some("(local)".to_string()), sort_text: Some("1".to_string()), insert_text: Some(s.to_string()), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), ..Default::default() }) .collect(); @@ -231,6 +235,9 @@ fn get_base_import_map_completions( detail: Some("(import map)".to_string()), sort_text: Some(label.clone()), insert_text: Some(label), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), ..Default::default() } }) @@ -284,6 +291,9 @@ fn get_import_map_completions( sort_text: Some("1".to_string()), filter_text: Some(new_text), text_edit, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), ..Default::default() }) } else { @@ -311,6 +321,9 @@ fn get_import_map_completions( detail: Some("(import map)".to_string()), sort_text: Some("1".to_string()), text_edit, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), ..Default::default() }); } @@ -382,6 +395,9 @@ fn get_local_completions( filter_text, sort_text: Some("1".to_string()), text_edit, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), ..Default::default() }), Ok(file_type) if file_type.is_file() => { @@ -393,6 +409,9 @@ fn get_local_completions( filter_text, sort_text: Some("1".to_string()), text_edit, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), ..Default::default() }) } else { @@ -463,6 +482,9 @@ fn get_workspace_completions( detail, sort_text: Some("1".to_string()), text_edit, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), ..Default::default() }) } else { @@ -484,7 +506,7 @@ fn get_workspace_completions( /// assert_eq!(relative_specifier(&specifier, &base), "../b.ts"); /// ``` /// -fn relative_specifier( +pub fn relative_specifier( specifier: &ModuleSpecifier, base: &ModuleSpecifier, ) -> String { @@ -812,6 +834,9 @@ mod tests { }, new_text: "https://deno.land/x/a/b/c.ts".to_string(), })), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), ..Default::default() }] ); diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index d5fa03e248..05d23bf008 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1764,11 +1764,15 @@ impl Inner { Some(response) } else { let line_index = asset_or_doc.line_index(); - let trigger_character = if let Some(context) = ¶ms.context { - context.trigger_character.clone() - } else { - None - }; + let (trigger_character, trigger_kind) = + if let Some(context) = ¶ms.context { + ( + context.trigger_character.clone(), + Some(context.trigger_kind.into()), + ) + } else { + (None, None) + }; let position = line_index.offset_tsc(params.text_document_position.position)?; let req = tsc::RequestMethod::GetCompletions(( @@ -1776,14 +1780,30 @@ impl Inner { position, tsc::GetCompletionsAtPositionOptions { user_preferences: tsc::UserPreferences { - allow_text_changes_in_new_files: Some(specifier.scheme() == "file"), - include_automatic_optional_chain_completions: Some(true), - provide_refactor_not_applicable_reason: Some(true), - include_completions_with_insert_text: Some(true), allow_incomplete_completions: Some(true), + allow_text_changes_in_new_files: Some(specifier.scheme() == "file"), + import_module_specifier_ending: Some( + tsc::ImportModuleSpecifierEnding::Index, + ), + include_automatic_optional_chain_completions: Some(true), + include_completions_for_import_statements: Some( + 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_insert_text: Some(true), + include_completions_with_snippet_text: Some(true), + jsx_attribute_completion_style: Some( + tsc::JsxAttributeCompletionStyle::Auto, + ), + provide_prefix_and_suffix_text_for_rename: Some(true), + provide_refactor_not_applicable_reason: Some(true), + use_label_details_in_completion_entries: Some(true), ..Default::default() }, trigger_character, + trigger_kind, }, )); let snapshot = self.snapshot(); @@ -1822,7 +1842,8 @@ impl Inner { "Could not decode data field of completion item.", ) })?; - if let Some(data) = data.tsc { + if let Some(data) = &data.tsc { + let specifier = data.specifier.clone(); let req = tsc::RequestMethod::GetCompletionDetails(data.into()); let maybe_completion_info: Option = self.ts_server.request(self.snapshot(), req).await.map_err( @@ -1832,7 +1853,15 @@ impl Inner { }, )?; if let Some(completion_info) = maybe_completion_info { - completion_info.as_completion_item(¶ms, self) + completion_info + .as_completion_item(¶ms, data, &specifier, self) + .map_err(|err| { + error!( + "Failed to serialize virtual_text_document response: {}", + err + ); + LspError::internal_error() + })? } else { error!( "Received an undefined response from tsc for completion details." diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index c362d4ce3b..b701893df4 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -1,6 +1,7 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use super::code_lens; +use super::completions::relative_specifier; use super::config; use super::documents::AssetOrDocument; use super::language_server; @@ -511,7 +512,7 @@ pub enum ScriptElementKind { Link, #[serde(rename = "link name")] LinkName, - #[serde(rename = "link test")] + #[serde(rename = "link text")] LinkText, } @@ -636,7 +637,7 @@ pub struct SymbolDisplayPart { target: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct JsDocTagInfo { name: String, @@ -1285,7 +1286,14 @@ pub struct TextChange { } impl TextChange { - pub fn as_text_edit( + pub fn as_text_edit(&self, line_index: Arc) -> lsp::TextEdit { + lsp::TextEdit { + range: self.span.to_range(line_index), + new_text: self.new_text.clone(), + } + } + + pub fn as_text_or_annotated_text_edit( &self, line_index: Arc, ) -> lsp::OneOf { @@ -1315,7 +1323,7 @@ impl FileTextChanges { let edits = self .text_changes .iter() - .map(|tc| tc.as_text_edit(asset_or_doc.line_index())) + .map(|tc| tc.as_text_or_annotated_text_edit(asset_or_doc.line_index())) .collect(); Ok(lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { @@ -1359,7 +1367,7 @@ impl FileTextChanges { let edits = self .text_changes .iter() - .map(|tc| tc.as_text_edit(line_index.clone())) + .map(|tc| tc.as_text_or_annotated_text_edit(line_index.clone())) .collect(); ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { @@ -1579,13 +1587,13 @@ impl RefactorEditInfo { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeAction { - // description: String, - // changes: Vec, - // #[serde(skip_serializing_if = "Option::is_none")] - // commands: Option>, + description: String, + changes: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + commands: Option>, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] @@ -1816,25 +1824,97 @@ impl CallHierarchyOutgoingCall { } } -#[derive(Debug, Deserialize)] +/// Used to convert completion code actions into a command and additional text +/// edits to pass in the completion item. +fn parse_code_actions( + maybe_code_actions: Option<&Vec>, + data: &CompletionItemData, + specifier: &ModuleSpecifier, + language_server: &language_server::Inner, +) -> Result<(Option, Option>), AnyError> { + if let Some(code_actions) = maybe_code_actions { + let mut additional_text_edits: Vec = Vec::new(); + let mut has_remaining_commands_or_edits = false; + for ts_action in code_actions { + if ts_action.commands.is_some() { + has_remaining_commands_or_edits = true; + } + + let asset_or_doc = + language_server.get_asset_or_document(&data.specifier)?; + for change in &ts_action.changes { + let change_specifier = normalize_specifier(&change.file_name)?; + if data.specifier == change_specifier { + additional_text_edits.extend(change.text_changes.iter().map(|tc| { + update_import_statement( + tc.as_text_edit(asset_or_doc.line_index()), + data, + ) + })); + } else { + has_remaining_commands_or_edits = true; + } + } + } + + let mut command: Option = None; + if has_remaining_commands_or_edits { + let actions: Vec = code_actions + .iter() + .map(|ca| { + let changes: Vec = ca + .changes + .clone() + .into_iter() + .filter(|ch| { + normalize_specifier(&ch.file_name).unwrap() == data.specifier + }) + .collect(); + json!({ + "commands": ca.commands, + "description": ca.description, + "changes": changes, + }) + }) + .collect(); + command = Some(lsp::Command { + title: "".to_string(), + command: "_typescript.applyCompletionCodeAction".to_string(), + arguments: Some(vec![json!(specifier.to_string()), json!(actions)]), + }); + } + + if additional_text_edits.is_empty() { + Ok((command, None)) + } else { + Ok((command, Some(additional_text_edits))) + } + } else { + Ok((None, None)) + } +} + +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionEntryDetails { display_parts: Vec, documentation: Option>, tags: Option>, - // name: String, - // kind: ScriptElementKind, - // kind_modifiers: String, - // code_actions: Option>, - // source_display: Option>, + name: String, + kind: ScriptElementKind, + kind_modifiers: String, + code_actions: Option>, + source_display: Option>, } impl CompletionEntryDetails { pub fn as_completion_item( &self, original_item: &lsp::CompletionItem, + data: &CompletionItemData, + specifier: &ModuleSpecifier, language_server: &language_server::Inner, - ) -> lsp::CompletionItem { + ) -> Result { let detail = if original_item.detail.is_some() { original_item.detail.clone() } else if !self.display_parts.is_empty() { @@ -1862,15 +1942,22 @@ impl CompletionEntryDetails { } else { None }; - // TODO(@kitsonk) add `self.code_actions` + let (command, additional_text_edits) = parse_code_actions( + self.code_actions.as_ref(), + data, + specifier, + language_server, + )?; // TODO(@kitsonk) add `use_code_snippet` - lsp::CompletionItem { + Ok(lsp::CompletionItem { data: None, detail, documentation, + command, + additional_text_edits, ..original_item.clone() - } + }) } } @@ -1951,6 +2038,36 @@ pub struct CompletionItemData { pub use_code_snippet: bool, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct CompletionEntryDataImport { + module_specifier: String, + file_name: String, +} + +/// Modify an import statement text replacement to have the correct import +/// specifier to work with Deno module resolution. +fn update_import_statement( + mut text_edit: lsp::TextEdit, + item_data: &CompletionItemData, +) -> lsp::TextEdit { + if let Some(data) = &item_data.data { + if let Ok(import_data) = + serde_json::from_value::(data.clone()) + { + if let Ok(import_specifier) = normalize_specifier(&import_data.file_name) + { + let new_module_specifier = + relative_specifier(&import_specifier, &item_data.specifier); + text_edit.new_text = text_edit + .new_text + .replace(&import_data.module_specifier, &new_module_specifier); + } + } + } + text_edit +} + #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionEntry { @@ -2087,8 +2204,7 @@ impl CompletionEntry { let use_code_snippet = settings.complete_function_calls && (kind == Some(lsp::CompletionItemKind::FUNCTION) || kind == Some(lsp::CompletionItemKind::METHOD)); - // TODO(@kitsonk) missing from types: https://github.com/gluon-lang/lsp-types/issues/204 - let _commit_characters = self.get_commit_characters(info, settings); + let commit_characters = self.get_commit_characters(info, settings); let mut insert_text = self.insert_text.clone(); let range = self.replacement_span.clone(); let mut filter_text = self.get_filter_text(); @@ -2158,9 +2274,8 @@ impl CompletionEntry { insert_text, detail, tags, - data: Some(json!({ - "tsc": tsc, - })), + commit_characters, + data: Some(json!({ "tsc": tsc })), ..Default::default() } } @@ -2662,6 +2777,27 @@ fn start( Ok(()) } +#[derive(Debug, Deserialize_repr, Serialize_repr)] +#[repr(u32)] +pub enum CompletionTriggerKind { + Invoked = 1, + TriggerCharacter = 2, + TriggerForIncompleteCompletions = 3, +} + +impl From for CompletionTriggerKind { + fn from(kind: lsp::CompletionTriggerKind) -> Self { + match kind { + lsp::CompletionTriggerKind::INVOKED => Self::Invoked, + lsp::CompletionTriggerKind::TRIGGER_CHARACTER => Self::TriggerCharacter, + lsp::CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS => { + Self::TriggerForIncompleteCompletions + } + _ => Self::Invoked, + } + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] @@ -2690,6 +2826,15 @@ pub enum ImportModuleSpecifierEnding { Js, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +pub enum IncludeInlayParameterNameHints { + None, + Literals, + All, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] @@ -2699,6 +2844,15 @@ pub enum IncludePackageJsonAutoImports { Off, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +pub enum JsxAttributeCompletionStyle { + Auto, + Braces, + None, +} + #[derive(Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetCompletionsAtPositionOptions { @@ -2706,6 +2860,8 @@ pub struct GetCompletionsAtPositionOptions { pub user_preferences: UserPreferences, #[serde(skip_serializing_if = "Option::is_none")] pub trigger_character: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_kind: Option, } #[derive(Debug, Default, Serialize)] @@ -2726,6 +2882,12 @@ pub struct UserPreferences { #[serde(skip_serializing_if = "Option::is_none")] pub include_completions_with_insert_text: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub include_completions_with_class_member_snippets: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_completions_with_object_literal_method_snippets: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub use_label_details_in_completion_entries: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub allow_incomplete_completions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub import_module_specifier_preference: @@ -2740,6 +2902,24 @@ pub struct UserPreferences { pub include_package_json_auto_imports: Option, #[serde(skip_serializing_if = "Option::is_none")] pub provide_refactor_not_applicable_reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub jsx_attribute_completion_style: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_parameter_name_hints: + Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_parameter_name_hints_when_argument_matches_name: + Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_function_parameter_type_hints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_variable_type_hints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_property_declaration_type_hints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_function_like_return_type_hints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_enum_member_value_hints: Option, } #[derive(Debug, Serialize)] @@ -2789,17 +2969,20 @@ pub struct GetCompletionDetailsArgs { #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub preferences: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, } -impl From for GetCompletionDetailsArgs { - fn from(item_data: CompletionItemData) -> Self { +impl From<&CompletionItemData> for GetCompletionDetailsArgs { + fn from(item_data: &CompletionItemData) -> Self { Self { - specifier: item_data.specifier, + specifier: item_data.specifier.clone(), position: item_data.position, - name: item_data.name, - source: item_data.source, - data: item_data.data, + name: item_data.name.clone(), + source: item_data.source.clone(), + preferences: None, + data: item_data.data.clone(), } } } @@ -3809,6 +3992,7 @@ mod tests { ..Default::default() }, trigger_character: Some(".".to_string()), + trigger_kind: None, }, )), Default::default(), @@ -3825,6 +4009,7 @@ mod tests { position, name: "log".to_string(), source: None, + preferences: None, data: None, }), Default::default(), @@ -3919,4 +4104,79 @@ mod tests { }) ); } + + #[test] + fn test_update_import_statement() { + let fixtures = vec![ + ( + "file:///a/a.ts", + "./b", + "file:///a/b.ts", + "import { b } from \"./b\";\n\n", + "import { b } from \"./b.ts\";\n\n", + ), + ( + "file:///a/a.ts", + "../b/b", + "file:///b/b.ts", + "import { b } from \"../b/b\";\n\n", + "import { b } from \"../b/b.ts\";\n\n", + ), + ("file:///a/a.ts", "./b", "file:///a/b.ts", ", b", ", b"), + ]; + + for ( + specifier_text, + module_specifier, + file_name, + orig_text, + expected_text, + ) in fixtures + { + let specifier = ModuleSpecifier::parse(specifier_text).unwrap(); + let item_data = CompletionItemData { + specifier: specifier.clone(), + position: 0, + name: "b".to_string(), + source: None, + data: Some(json!({ + "moduleSpecifier": module_specifier, + "fileName": file_name, + })), + use_code_snippet: false, + }; + let actual = update_import_statement( + lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 0, + }, + end: lsp::Position { + line: 0, + character: 0, + }, + }, + new_text: orig_text.to_string(), + }, + &item_data, + ); + assert_eq!( + actual, + lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 0, + }, + end: lsp::Position { + line: 0, + character: 0, + }, + }, + new_text: expected_text.to_string(), + } + ); + } + } } diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 2138270571..568ac6e7cc 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -774,35 +774,40 @@ fn lsp_import_map_import_completions() { "kind": 19, "detail": "(local)", "sortText": "1", - "insertText": "." + "insertText": ".", + "commitCharacters": ["\"", "'", "/"], }, { "label": "..", "kind": 19, "detail": "(local)", "sortText": "1", - "insertText": ".." + "insertText": "..", + "commitCharacters": ["\"", "'", "/"], }, { "label": "std", "kind": 19, "detail": "(import map)", "sortText": "std", - "insertText": "std" + "insertText": "std", + "commitCharacters": ["\"", "'", "/"], }, { "label": "fs", "kind": 17, "detail": "(import map)", "sortText": "fs", - "insertText": "fs" + "insertText": "fs", + "commitCharacters": ["\"", "'", "/"], }, { "label": "/~", "kind": 19, "detail": "(import map)", "sortText": "/~", - "insertText": "/~" + "insertText": "/~", + "commitCharacters": ["\"", "'", "/"], } ] })) @@ -883,7 +888,8 @@ fn lsp_import_map_import_completions() { } }, "newText": "/~/b.ts" - } + }, + "commitCharacters": ["\"", "'", "/"], } ] })) @@ -3490,6 +3496,7 @@ fn lsp_completions_optional() { "sortText": "11", "filterText": "b", "insertText": "b", + "commitCharacters": [".", ",", ";", "("], "data": { "tsc": { "specifier": "file:///a/file.ts", @@ -3527,6 +3534,124 @@ fn lsp_completions_optional() { shutdown(&mut client); } +#[test] +fn lsp_completions_auto_import() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/b.ts", + "languageId": "typescript", + "version": 1, + "text": "export const foo = \"foo\";\n", + } + }), + ); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "export {};\n\n", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 2, + "character": 0, + }, + "context": { + "triggerKind": 1, + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + if !list.items.iter().any(|item| item.label == "foo") { + panic!("completions items missing 'foo' symbol"); + } + } else { + panic!("unexpected completion response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + json!({ + "label": "foo", + "kind": 6, + "sortText": "￿16", + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/file.ts", + "position": 12, + "name": "foo", + "source": "./b", + "data": { + "exportName": "foo", + "moduleSpecifier": "./b", + "fileName": "file:///a/b.ts" + }, + "useCodeSnippet": false + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "label": "foo", + "kind": 6, + "detail": "const foo: \"foo\"", + "documentation": { + "kind": "markdown", + "value": "" + }, + "sortText": "￿16", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "import { foo } from \"./b.ts\";\n\n" + } + ], + "commitCharacters": [ + ".", + ",", + ";", + "(" + ] + })) + ); +} + #[test] fn lsp_completions_registry() { let _g = http_server(); diff --git a/cli/tests/testdata/lsp/completion_request_response_empty.json b/cli/tests/testdata/lsp/completion_request_response_empty.json index 272dfb4756..c2218aaa75 100644 --- a/cli/tests/testdata/lsp/completion_request_response_empty.json +++ b/cli/tests/testdata/lsp/completion_request_response_empty.json @@ -6,14 +6,24 @@ "kind": 19, "detail": "(local)", "sortText": "1", - "insertText": "." + "insertText": ".", + "commitCharacters": [ + "\"", + "'", + "/" + ] }, { "label": "..", "kind": 19, "detail": "(local)", "sortText": "1", - "insertText": ".." + "insertText": "..", + "commitCharacters": [ + "\"", + "'", + "/" + ] }, { "label": "http://localhost:4545", diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index c2b50ba166..85ab38cccc 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -718,10 +718,9 @@ delete Object.prototype.__proto__; request.args.specifier, request.args.position, request.args.name, - undefined, + {}, request.args.source, - undefined, - // @ts-expect-error this exists in 4.3 but not part of the d.ts + request.args.preferences, request.args.data, ), ); diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index 1ba1161707..bf06604709 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -50,6 +50,7 @@ declare global { } type LanguageServerRequest = + | Restart | ConfigureRequest | FindRenameLocationsRequest | GetAssets @@ -138,7 +139,8 @@ declare global { position: number; name: string; source?: string; - data?: unknown; + preferences?: ts.UserPreferences; + data?: ts.CompletionEntryData; }; } @@ -252,7 +254,7 @@ declare global { position: number; } - interface Restart { + interface Restart extends BaseLanguageServerRequest { method: "restart"; } }