From 506b321d472005d0cf916823dfa8ea37fa0b064a Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Tue, 16 Mar 2021 09:01:41 +1100 Subject: [PATCH] refactor(lsp): refactor completions and add tests (#9789) --- cli/lsp/capabilities.rs | 11 +- cli/lsp/config.rs | 72 +- cli/lsp/language_server.rs | 139 +++- cli/lsp/tsc.rs | 623 +++++++++++++++--- cli/tests/lsp/completion_request.json | 18 + cli/tests/lsp/completion_resolve_request.json | 17 + .../did_open_notification_completions.json | 12 + cli/tsc/99_main_compiler.js | 16 + cli/tsc/compiler.d.ts | 14 +- 9 files changed, 787 insertions(+), 135 deletions(-) create mode 100644 cli/tests/lsp/completion_request.json create mode 100644 cli/tests/lsp/completion_resolve_request.json create mode 100644 cli/tests/lsp/did_open_notification_completions.json diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index be318e7f388a49..82bb910bb3428b 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -55,7 +55,12 @@ pub fn server_capabilities( )), hover_provider: Some(HoverProviderCapability::Simple(true)), completion_provider: Some(CompletionOptions { - all_commit_characters: None, + all_commit_characters: Some(vec![ + ".".to_string(), + ",".to_string(), + ";".to_string(), + "(".to_string(), + ]), trigger_characters: Some(vec![ ".".to_string(), "\"".to_string(), @@ -66,7 +71,7 @@ pub fn server_capabilities( "<".to_string(), "#".to_string(), ]), - resolve_provider: None, + resolve_provider: Some(true), work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None, }, @@ -77,7 +82,7 @@ pub fn server_capabilities( "(".to_string(), "<".to_string(), ]), - retrigger_characters: None, + retrigger_characters: Some(vec![")".to_string()]), work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None, }, diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 8d31e3d54c7ad9..201e5f23c34087 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -15,7 +15,7 @@ pub struct ClientCapabilities { pub workspace_did_change_watched_files: bool, } -#[derive(Debug, Default, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CodeLensSettings { /// Flag for providing implementation code lenses. @@ -30,13 +30,50 @@ pub struct CodeLensSettings { pub references_all_functions: bool, } +impl Default for CodeLensSettings { + fn default() -> Self { + Self { + implementations: false, + references: false, + references_all_functions: false, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionSettings { + #[serde(default)] + pub complete_function_calls: bool, + #[serde(default)] + pub names: bool, + #[serde(default)] + pub paths: bool, + #[serde(default)] + pub auto_imports: bool, +} + +impl Default for CompletionSettings { + fn default() -> Self { + Self { + complete_function_calls: false, + names: true, + paths: true, + auto_imports: true, + } + } +} + #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WorkspaceSettings { pub enable: bool, pub config: Option, pub import_map: Option, - pub code_lens: Option, + #[serde(default)] + pub code_lens: CodeLensSettings, + #[serde(default)] + pub suggest: CompletionSettings, #[serde(default)] pub lint: bool, @@ -48,36 +85,7 @@ impl WorkspaceSettings { /// Determine if any code lenses are enabled at all. This allows short /// circuiting when there are no code lenses enabled. pub fn enabled_code_lens(&self) -> bool { - if let Some(code_lens) = &self.code_lens { - // This should contain all the "top level" code lens references - code_lens.implementations || code_lens.references - } else { - false - } - } - - pub fn enabled_code_lens_implementations(&self) -> bool { - if let Some(code_lens) = &self.code_lens { - code_lens.implementations - } else { - false - } - } - - pub fn enabled_code_lens_references(&self) -> bool { - if let Some(code_lens) = &self.code_lens { - code_lens.references - } else { - false - } - } - - pub fn enabled_code_lens_references_all_functions(&self) -> bool { - if let Some(code_lens) = &self.code_lens { - code_lens.references_all_functions - } else { - false - } + self.code_lens.implementations || self.code_lens.references } } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 96983dc5233e84..3c3d82b3b73918 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -917,7 +917,7 @@ impl Inner { let mut code_lenses = cl.borrow_mut(); // TSC Implementations Code Lens - if self.config.settings.enabled_code_lens_implementations() { + if self.config.settings.code_lens.implementations { let source = CodeLensSource::Implementations; match i.kind { tsc::ScriptElementKind::InterfaceElement => { @@ -941,7 +941,7 @@ impl Inner { } // TSC References Code Lens - if self.config.settings.enabled_code_lens_references() { + if self.config.settings.code_lens.references { let source = CodeLensSource::References; if let Some(parent) = &mp { if parent.kind == tsc::ScriptElementKind::EnumElement { @@ -950,11 +950,7 @@ impl Inner { } match i.kind { tsc::ScriptElementKind::FunctionElement => { - if self - .config - .settings - .enabled_code_lens_references_all_functions() - { + if self.config.settings.code_lens.references_all_functions { code_lenses.push(i.to_code_lens( &line_index, &specifier, @@ -1358,7 +1354,6 @@ impl Inner { let specifier = self .url_map .normalize_url(¶ms.text_document_position.text_document.uri); - // TODO(lucacasonato): handle error correctly let line_index = if let Some(line_index) = self.get_line_index_sync(&specifier) { line_index @@ -1368,13 +1363,22 @@ impl Inner { specifier ))); }; + let trigger_character = if let Some(context) = ¶ms.context { + context.trigger_character.clone() + } else { + None + }; + let position = + line_index.offset_tsc(params.text_document_position.position)?; let req = tsc::RequestMethod::GetCompletions(( - specifier, - line_index.offset_tsc(params.text_document_position.position)?, - tsc::UserPreferences { - // TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651 - include_completions_with_insert_text: Some(false), - ..Default::default() + specifier.clone(), + position, + tsc::GetCompletionsAtPositionOptions { + user_preferences: tsc::UserPreferences { + include_completions_with_insert_text: Some(true), + ..Default::default() + }, + trigger_character, }, )); let maybe_completion_info: Option = self @@ -1387,7 +1391,12 @@ impl Inner { })?; if let Some(completions) = maybe_completion_info { - let results = completions.into_completion_response(&line_index); + let results = completions.as_completion_response( + &line_index, + &self.config.settings.suggest, + &specifier, + position, + ); self.performance.measure(mark); Ok(Some(results)) } else { @@ -1396,6 +1405,47 @@ impl Inner { } } + async fn completion_resolve( + &mut self, + params: CompletionItem, + ) -> LspResult { + let mark = self.performance.mark("completion_resolve"); + if let Some(data) = ¶ms.data { + let data: tsc::CompletionItemData = serde_json::from_value(data.clone()) + .map_err(|err| { + error!("{}", err); + LspError::invalid_params( + "Could not decode data field of completion item.", + ) + })?; + let req = tsc::RequestMethod::GetCompletionDetails(data.into()); + let maybe_completion_info: Option = self + .ts_server + .request(self.snapshot(), req) + .await + .map_err(|err| { + error!("Unable to get completion info from TypeScript: {}", err); + LspError::internal_error() + })?; + if let Some(completion_info) = maybe_completion_info { + let completion_item = completion_info.as_completion_item(¶ms); + self.performance.measure(mark); + Ok(completion_item) + } else { + error!( + "Received an undefined response from tsc for completion details." + ); + self.performance.measure(mark); + Ok(params) + } + } else { + self.performance.measure(mark); + Err(LspError::invalid_params( + "The completion item is missing the data field.", + )) + } + } + async fn goto_implementation( &mut self, params: GotoImplementationParams, @@ -1715,6 +1765,13 @@ impl lspower::LanguageServer for LanguageServer { self.0.lock().await.completion(params).await } + async fn completion_resolve( + &self, + params: CompletionItem, + ) -> LspResult { + self.0.lock().await.completion_resolve(params).await + } + async fn goto_implementation( &self, params: GotoImplementationParams, @@ -2740,6 +2797,58 @@ mod tests { harness.run().await; } + #[derive(Deserialize)] + struct CompletionResult { + pub result: Option, + } + + #[tokio::test] + async fn test_completions() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ("did_open_notification_completions.json", LspResponse::None), + ( + "completion_request.json", + LspResponse::RequestAssert(|value| { + let response: CompletionResult = + serde_json::from_value(value).unwrap(); + let result = response.result.unwrap(); + match result { + CompletionResponse::List(list) => { + // there should be at least 90 completions for `Deno.` + assert!(list.items.len() > 90); + } + _ => panic!("unexpected result"), + } + }), + ), + ( + "completion_resolve_request.json", + LspResponse::Request( + 4, + json!({ + "label": "build", + "kind": 6, + "detail": "const Deno.build: {\n target: string;\n arch: \"x86_64\";\n os: \"darwin\" | \"linux\" | \"windows\";\n vendor: string;\n env?: string | undefined;\n}", + "documentation": { + "kind": "markdown", + "value": "Build related information." + }, + "sortText": "1", + "insertTextFormat": 1, + }), + ), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } + #[derive(Deserialize)] struct PerformanceAverages { averages: Vec, diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 31434f52975d36..a60f15eb8ede58 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -3,6 +3,7 @@ use super::analysis::CodeLensSource; use super::analysis::ResolvedDependency; use super::analysis::ResolvedDependencyErr; +use super::config; use super::language_server; use super::language_server::StateSnapshot; use super::text; @@ -35,11 +36,15 @@ use regex::Captures; use regex::Regex; use std::borrow::Cow; use std::collections::HashMap; +use std::collections::HashSet; use std::thread; use text_size::TextSize; use tokio::sync::mpsc; use tokio::sync::oneshot; +const FILE_EXTENSION_KIND_MODIFIERS: &[&str] = + &[".d.ts", ".ts", ".tsx", ".js", ".jsx", ".json"]; + type Request = ( RequestMethod, StateSnapshot, @@ -170,10 +175,10 @@ pub async fn get_asset( } } -fn display_parts_to_string(parts: Vec) -> String { +fn display_parts_to_string(parts: &[SymbolDisplayPart]) -> String { parts - .into_iter() - .map(|p| p.text) + .iter() + .map(|p| p.text.to_string()) .collect::>() .join("") } @@ -276,7 +281,12 @@ fn replace_links(text: &str) -> String { .to_string() } -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +fn parse_kind_modifier(kind_modifiers: &str) -> HashSet<&str> { + let re = Regex::new(r",|\s+").unwrap(); + re.split(kind_modifiers).collect() +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub enum ScriptElementKind { #[serde(rename = "")] Unknown, @@ -348,42 +358,58 @@ pub enum ScriptElementKind { String, } +impl Default for ScriptElementKind { + fn default() -> Self { + Self::Unknown + } +} + impl From for lsp::CompletionItemKind { fn from(kind: ScriptElementKind) -> Self { - use lspower::lsp::CompletionItemKind; - match kind { ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => { - CompletionItemKind::Keyword + lsp::CompletionItemKind::Keyword } - ScriptElementKind::ConstElement => CompletionItemKind::Constant, - ScriptElementKind::LetElement + ScriptElementKind::ConstElement + | ScriptElementKind::LetElement | ScriptElementKind::VariableElement | ScriptElementKind::LocalVariableElement - | ScriptElementKind::Alias => CompletionItemKind::Variable, + | ScriptElementKind::Alias + | ScriptElementKind::ParameterElement => { + lsp::CompletionItemKind::Variable + } ScriptElementKind::MemberVariableElement | ScriptElementKind::MemberGetAccessorElement | ScriptElementKind::MemberSetAccessorElement => { - CompletionItemKind::Field + lsp::CompletionItemKind::Field + } + ScriptElementKind::FunctionElement + | ScriptElementKind::LocalFunctionElement => { + lsp::CompletionItemKind::Function } - ScriptElementKind::FunctionElement => CompletionItemKind::Function, ScriptElementKind::MemberFunctionElement | ScriptElementKind::ConstructSignatureElement | ScriptElementKind::CallSignatureElement - | ScriptElementKind::IndexSignatureElement => CompletionItemKind::Method, - ScriptElementKind::EnumElement => CompletionItemKind::Enum, + | ScriptElementKind::IndexSignatureElement => { + lsp::CompletionItemKind::Method + } + ScriptElementKind::EnumElement => lsp::CompletionItemKind::Enum, + ScriptElementKind::EnumMemberElement => { + lsp::CompletionItemKind::EnumMember + } ScriptElementKind::ModuleElement - | ScriptElementKind::ExternalModuleName => CompletionItemKind::Module, - ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => { - CompletionItemKind::Class + | ScriptElementKind::ExternalModuleName => { + lsp::CompletionItemKind::Module } - ScriptElementKind::InterfaceElement => CompletionItemKind::Interface, - ScriptElementKind::Warning | ScriptElementKind::ScriptElement => { - CompletionItemKind::File + ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => { + lsp::CompletionItemKind::Class } - ScriptElementKind::Directory => CompletionItemKind::Folder, - ScriptElementKind::String => CompletionItemKind::Constant, - _ => CompletionItemKind::Property, + ScriptElementKind::InterfaceElement => lsp::CompletionItemKind::Interface, + ScriptElementKind::Warning => lsp::CompletionItemKind::Text, + ScriptElementKind::ScriptElement => lsp::CompletionItemKind::File, + ScriptElementKind::Directory => lsp::CompletionItemKind::Folder, + ScriptElementKind::String => lsp::CompletionItemKind::Constant, + _ => lsp::CompletionItemKind::Property, } } } @@ -432,16 +458,20 @@ pub struct QuickInfo { impl QuickInfo { pub fn to_hover(&self, line_index: &LineIndex) -> lsp::Hover { let mut contents = Vec::::new(); - if let Some(display_string) = - self.display_parts.clone().map(display_parts_to_string) + if let Some(display_string) = self + .display_parts + .clone() + .map(|p| display_parts_to_string(&p)) { contents.push(lsp::MarkedString::from_language_code( "typescript".to_string(), display_string, )); } - if let Some(documentation) = - self.documentation.clone().map(display_parts_to_string) + if let Some(documentation) = self + .documentation + .clone() + .map(|p| display_parts_to_string(&p)) { contents.push(lsp::MarkedString::from_markdown(documentation)); } @@ -824,6 +854,15 @@ impl FileTextChanges { } } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeAction { + description: String, + changes: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + commands: Option>, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CodeFixAction { @@ -882,99 +921,308 @@ impl ReferenceEntry { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] +pub struct CompletionEntryDetails { + name: String, + kind: ScriptElementKind, + kind_modifiers: String, + display_parts: Vec, + documentation: Option>, + tags: Option>, + code_actions: Option>, + source: Option>, +} + +impl CompletionEntryDetails { + pub fn as_completion_item( + &self, + original_item: &lsp::CompletionItem, + ) -> lsp::CompletionItem { + let detail = if original_item.detail.is_some() { + original_item.detail.clone() + } else if !self.display_parts.is_empty() { + Some(replace_links(&display_parts_to_string(&self.display_parts))) + } else { + None + }; + let documentation = if let Some(parts) = &self.documentation { + let mut value = display_parts_to_string(parts); + if let Some(tags) = &self.tags { + let tag_documentation = tags + .iter() + .map(get_tag_documentation) + .collect::>() + .join(""); + value = format!("{}\n\n{}", value, tag_documentation); + } + Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value, + })) + } else { + None + }; + // TODO(@kitsonk) add `self.code_actions` + // TODO(@kitsonk) add `use_code_snippet` + + lsp::CompletionItem { + data: None, + detail, + documentation, + ..original_item.clone() + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct CompletionInfo { entries: Vec, + is_global_completion: bool, is_member_completion: bool, + is_new_identifier_location: bool, + metadata: Option, + optional_replacement_span: Option, } impl CompletionInfo { - pub fn into_completion_response( - self, + pub fn as_completion_response( + &self, line_index: &LineIndex, + settings: &config::CompletionSettings, + specifier: &ModuleSpecifier, + position: u32, ) -> lsp::CompletionResponse { let items = self .entries - .into_iter() - .map(|entry| entry.into_completion_item(line_index)) + .iter() + .map(|entry| { + entry + .as_completion_item(line_index, self, settings, specifier, position) + }) .collect(); - lsp::CompletionResponse::Array(items) + let is_incomplete = self + .metadata + .clone() + .map(|v| { + v.as_object() + .unwrap() + .get("isIncomplete") + .unwrap_or(&json!(false)) + .as_bool() + .unwrap() + }) + .unwrap_or(false); + lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete, + items, + }) } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionItemData { + pub specifier: ModuleSpecifier, + pub position: u32, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + pub use_code_snippet: bool, +} + +#[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionEntry { + name: String, kind: ScriptElementKind, + #[serde(skip_serializing_if = "Option::is_none")] kind_modifiers: Option, - name: String, sort_text: String, + #[serde(skip_serializing_if = "Option::is_none")] insert_text: Option, + #[serde(skip_serializing_if = "Option::is_none")] replacement_span: Option, + #[serde(skip_serializing_if = "Option::is_none")] has_action: Option, + #[serde(skip_serializing_if = "Option::is_none")] source: Option, + #[serde(skip_serializing_if = "Option::is_none")] is_recommended: Option, + #[serde(skip_serializing_if = "Option::is_none")] + is_from_unchecked_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, } impl CompletionEntry { - pub fn into_completion_item( - self, - line_index: &LineIndex, - ) -> lsp::CompletionItem { - let mut item = lsp::CompletionItem { - label: self.name, - kind: Some(self.kind.into()), - sort_text: Some(self.sort_text.clone()), - // TODO(lucacasonato): missing commit_characters - ..Default::default() - }; + fn get_commit_characters( + &self, + info: &CompletionInfo, + settings: &config::CompletionSettings, + ) -> Option> { + if info.is_new_identifier_location { + return None; + } - if let Some(true) = self.is_recommended { - // Make sure isRecommended property always comes first - // https://github.com/Microsoft/vscode/issues/40325 - item.preselect = Some(true); - } else if self.source.is_some() { - // De-prioritze auto-imports - // https://github.com/Microsoft/vscode/issues/40311 - item.sort_text = Some("\u{ffff}".to_string() + &self.sort_text) + let mut commit_characters = vec![]; + match self.kind { + ScriptElementKind::MemberGetAccessorElement + | ScriptElementKind::MemberSetAccessorElement + | ScriptElementKind::ConstructSignatureElement + | ScriptElementKind::CallSignatureElement + | ScriptElementKind::IndexSignatureElement + | ScriptElementKind::EnumElement + | ScriptElementKind::InterfaceElement => { + commit_characters.push("."); + commit_characters.push(";"); + } + ScriptElementKind::ModuleElement + | ScriptElementKind::Alias + | ScriptElementKind::ConstElement + | ScriptElementKind::LetElement + | ScriptElementKind::VariableElement + | ScriptElementKind::LocalVariableElement + | ScriptElementKind::MemberVariableElement + | ScriptElementKind::ClassElement + | ScriptElementKind::FunctionElement + | ScriptElementKind::MemberFunctionElement + | ScriptElementKind::Keyword + | ScriptElementKind::ParameterElement => { + commit_characters.push("."); + commit_characters.push(","); + commit_characters.push(";"); + if !settings.complete_function_calls { + commit_characters.push("("); + } + } + _ => (), } - match item.kind { - Some(lsp::CompletionItemKind::Function) - | Some(lsp::CompletionItemKind::Method) => { - item.insert_text_format = Some(lsp::InsertTextFormat::Snippet); + if commit_characters.is_empty() { + None + } else { + Some(commit_characters.into_iter().map(String::from).collect()) + } + } + + fn get_filter_text(&self) -> Option { + // TODO(@kitsonk) this is actually quite a bit more complex. + // See `MyCompletionItem.getFilterText` in vscode completion.ts. + if self.name.starts_with('#') && self.insert_text.is_none() { + return Some(self.name.clone()); + } + + if let Some(insert_text) = &self.insert_text { + if insert_text.starts_with("this.") { + return None; + } + if insert_text.starts_with('[') { + let re = Regex::new(r#"^\[['"](.+)['"]\]$"#).unwrap(); + let insert_text = re.replace(insert_text, ".$1").to_string(); + return Some(insert_text); } - _ => {} } - let mut insert_text = self.insert_text; - let replacement_range: Option = - self.replacement_span.map(|span| span.to_range(line_index)); + self.insert_text.clone() + } - // TODO(lucacasonato): port other special cases from https://github.com/theia-ide/typescript-language-server/blob/fdf28313833cd6216d00eb4e04dc7f00f4c04f09/server/src/completion.ts#L49-L55 + pub fn as_completion_item( + &self, + line_index: &LineIndex, + info: &CompletionInfo, + settings: &config::CompletionSettings, + specifier: &ModuleSpecifier, + position: u32, + ) -> lsp::CompletionItem { + let mut label = self.name.clone(); + let mut kind: Option = + Some(self.kind.clone().into()); - if let Some(kind_modifiers) = self.kind_modifiers { - if kind_modifiers.contains("\\optional\\") { + let sort_text = if self.source.is_some() { + Some(format!("\u{ffff}{}", self.sort_text)) + } else { + Some(self.sort_text.clone()) + }; + + let preselect = self.is_recommended; + 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 mut insert_text = self.insert_text.clone(); + let range = self.replacement_span.clone(); + let mut filter_text = self.get_filter_text(); + let mut tags = None; + let mut detail = None; + + if let Some(kind_modifiers) = &self.kind_modifiers { + let kind_modifiers = parse_kind_modifier(kind_modifiers); + if kind_modifiers.contains("optional") { if insert_text.is_none() { - insert_text = Some(item.label.clone()); + insert_text = Some(label.clone()); } - if item.filter_text.is_none() { - item.filter_text = Some(item.label.clone()); + if filter_text.is_none() { + filter_text = Some(label.clone()); + } + label += "?"; + } + if kind_modifiers.contains("deprecated") { + tags = Some(vec![lsp::CompletionItemTag::Deprecated]); + } + if kind_modifiers.contains("color") { + kind = Some(lsp::CompletionItemKind::Color); + } + if self.kind == ScriptElementKind::ScriptElement { + for ext_modifier in FILE_EXTENSION_KIND_MODIFIERS { + if kind_modifiers.contains(ext_modifier) { + detail = if self.name.to_lowercase().ends_with(ext_modifier) { + Some(self.name.clone()) + } else { + Some(format!("{}{}", self.name, ext_modifier)) + }; + break; + } } - item.label += "?"; } } - if let Some(insert_text) = insert_text { - if let Some(replacement_range) = replacement_range { - item.text_edit = Some(lsp::CompletionTextEdit::Edit( - lsp::TextEdit::new(replacement_range, insert_text), - )); + let text_edit = + if let (Some(text_span), Some(new_text)) = (range, insert_text) { + let range = text_span.to_range(line_index); + let insert_replace_edit = lsp::InsertReplaceEdit { + new_text, + insert: range, + replace: range, + }; + Some(insert_replace_edit.into()) } else { - item.insert_text = Some(insert_text); - } - } + None + }; + + let data = CompletionItemData { + specifier: specifier.clone(), + position, + name: self.name.clone(), + source: self.source.clone(), + data: self.data.clone(), + use_code_snippet, + }; - item + lsp::CompletionItem { + label, + kind, + sort_text, + preselect, + text_edit, + filter_text, + detail, + tags, + data: Some(serde_json::to_value(data).unwrap()), + ..Default::default() + } } } @@ -1016,18 +1264,18 @@ pub struct SignatureHelpItem { impl SignatureHelpItem { pub fn into_signature_information(self) -> lsp::SignatureInformation { - let prefix_text = display_parts_to_string(self.prefix_display_parts); + let prefix_text = display_parts_to_string(&self.prefix_display_parts); let params_text = self .parameters .iter() - .map(|param| display_parts_to_string(param.display_parts.clone())) + .map(|param| display_parts_to_string(¶m.display_parts)) .collect::>() .join(", "); - let suffix_text = display_parts_to_string(self.suffix_display_parts); + let suffix_text = display_parts_to_string(&self.suffix_display_parts); lsp::SignatureInformation { label: format!("{}{}{}", prefix_text, params_text, suffix_text), documentation: Some(lsp::Documentation::String(display_parts_to_string( - self.documentation, + &self.documentation, ))), parameters: Some( self @@ -1054,10 +1302,10 @@ impl SignatureHelpParameter { pub fn into_parameter_information(self) -> lsp::ParameterInformation { lsp::ParameterInformation { label: lsp::ParameterLabel::Simple(display_parts_to_string( - self.display_parts, + &self.display_parts, )), documentation: Some(lsp::Documentation::String(display_parts_to_string( - self.documentation, + &self.documentation, ))), } } @@ -1479,6 +1727,15 @@ pub enum IncludePackageJsonAutoImports { Off, } +#[derive(Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionsAtPositionOptions { + #[serde(flatten)] + pub user_preferences: UserPreferences, + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_character: Option, +} + #[derive(Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserPreferences { @@ -1542,6 +1799,30 @@ pub struct SignatureHelpTriggerReason { pub trigger_character: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionDetailsArgs { + pub specifier: ModuleSpecifier, + pub position: u32, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +impl From for GetCompletionDetailsArgs { + fn from(item_data: CompletionItemData) -> Self { + Self { + specifier: item_data.specifier, + position: item_data.position, + name: item_data.name, + source: item_data.source, + data: item_data.data, + } + } +} + /// Methods that are supported by the Language Service in the compiler isolate. #[derive(Debug)] pub enum RequestMethod { @@ -1554,7 +1835,9 @@ pub enum RequestMethod { /// Retrieve code fixes for a range of a file with the provided error codes. GetCodeFixes((ModuleSpecifier, u32, u32, Vec)), /// Get completion information at a given position (IntelliSense). - GetCompletions((ModuleSpecifier, u32, UserPreferences)), + GetCompletions((ModuleSpecifier, u32, GetCompletionsAtPositionOptions)), + /// Get details about a specific completion entry. + GetCompletionDetails(GetCompletionDetailsArgs), /// Retrieve the combined code fixes for a fix id for a module. GetCombinedCodeFix((ModuleSpecifier, Value)), /// Get declaration information for a specific position. @@ -1626,6 +1909,11 @@ impl RequestMethod { "specifier": specifier, "fixId": fix_id, }), + RequestMethod::GetCompletionDetails(args) => json!({ + "id": id, + "method": "getCompletionDetails", + "args": args + }), RequestMethod::GetCompletions((specifier, position, preferences)) => { json!({ "id": id, @@ -1738,6 +2026,7 @@ mod tests { use crate::lsp::analysis; use crate::lsp::documents::DocumentCache; use crate::lsp::sources::Sources; + use crate::lsp::text::LineIndex; use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; @@ -2228,4 +2517,170 @@ mod tests { }) ); } + + #[test] + fn test_completion_entry_filter_text() { + let fixture = CompletionEntry { + kind: ScriptElementKind::MemberVariableElement, + name: "['foo']".to_string(), + insert_text: Some("['foo']".to_string()), + ..Default::default() + }; + let actual = fixture.get_filter_text(); + assert_eq!(actual, Some(".foo".to_string())); + } + + #[test] + fn test_completions() { + let fixture = r#" + import { B } from "https://deno.land/x/b/mod.ts"; + + const b = new B(); + + console. + "#; + let line_index = LineIndex::new(fixture); + let position = line_index + .offset_tsc(lsp::Position { + line: 5, + character: 16, + }) + .unwrap(); + let (mut runtime, state_snapshot, _) = setup( + false, + json!({ + "target": "esnext", + "module": "esnext", + "lib": ["deno.ns", "deno.window"], + "noEmit": true, + }), + &[("file:///a.ts", fixture, 1)], + ); + let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); + let result = request( + &mut runtime, + state_snapshot.clone(), + RequestMethod::GetDiagnostics(vec![specifier.clone()]), + ); + assert!(result.is_ok()); + let result = request( + &mut runtime, + state_snapshot.clone(), + RequestMethod::GetCompletions(( + specifier.clone(), + position, + GetCompletionsAtPositionOptions { + user_preferences: UserPreferences { + include_completions_with_insert_text: Some(true), + ..Default::default() + }, + trigger_character: Some(".".to_string()), + }, + )), + ); + assert!(result.is_ok()); + let response: CompletionInfo = + serde_json::from_value(result.unwrap()).unwrap(); + assert_eq!(response.entries.len(), 20); + let result = request( + &mut runtime, + state_snapshot, + RequestMethod::GetCompletionDetails(GetCompletionDetailsArgs { + specifier, + position, + name: "log".to_string(), + source: None, + data: None, + }), + ); + assert!(result.is_ok()); + let response = result.unwrap(); + assert_eq!( + response, + json!({ + "name": "log", + "kindModifiers": "declare", + "kind": "method", + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "method", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "Console", + "kind": "interfaceName" + }, + { + "text": ".", + "kind": "punctuation" + }, + { + "text": "log", + "kind": "methodName" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "...", + "kind": "punctuation" + }, + { + "text": "data", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "any", + "kind": "keyword" + }, + { + "text": "[", + "kind": "punctuation" + }, + { + "text": "]", + "kind": "punctuation" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "documentation": [] + }) + ); + } } diff --git a/cli/tests/lsp/completion_request.json b/cli/tests/lsp/completion_request.json new file mode 100644 index 00000000000000..81bf719a9e0c7a --- /dev/null +++ b/cli/tests/lsp/completion_request.json @@ -0,0 +1,18 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 5 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + } +} diff --git a/cli/tests/lsp/completion_resolve_request.json b/cli/tests/lsp/completion_resolve_request.json new file mode 100644 index 00000000000000..c176a431e46243 --- /dev/null +++ b/cli/tests/lsp/completion_resolve_request.json @@ -0,0 +1,17 @@ +{ + "jsonrpc": "2.0", + "id": 4, + "method": "completionItem/resolve", + "params": { + "label": "build", + "kind": 6, + "sortText": "1", + "insertTextFormat": 1, + "data": { + "specifier": "file:///a/file.ts", + "position": 5, + "name": "build", + "useCodeSnippet": false + } + } +} diff --git a/cli/tests/lsp/did_open_notification_completions.json b/cli/tests/lsp/did_open_notification_completions.json new file mode 100644 index 00000000000000..edcdc937345062 --- /dev/null +++ b/cli/tests/lsp/did_open_notification_completions.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "Deno." + } + } +} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index f8eabc890e5dc7..c84c2365c80698 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -594,6 +594,22 @@ delete Object.prototype.__proto__; ), ); } + case "getCompletionDetails": { + debug("request", request); + return respond( + id, + languageService.getCompletionEntryDetails( + 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.data, + ), + ); + } case "getCompletions": { return respond( id, diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index d37b56c0608a80..a3200469c81e30 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -51,6 +51,7 @@ declare global { | GetAsset | GetCodeFixes | GetCombinedCodeFix + | GetCompletionDetails | GetCompletionsRequest | GetDefinitionRequest | GetDiagnosticsRequest @@ -102,11 +103,22 @@ declare global { fixId: {}; } + interface GetCompletionDetails extends BaseLanguageServerRequest { + method: "getCompletionDetails"; + args: { + specifier: string; + position: number; + name: string; + source?: string; + data?: unknown; + }; + } + interface GetCompletionsRequest extends BaseLanguageServerRequest { method: "getCompletions"; specifier: string; position: number; - preferences: ts.UserPreferences; + preferences: ts.GetCompletionsAtPositionOptions; } interface GetDiagnosticsRequest extends BaseLanguageServerRequest {