From e94a18240e5b6312358f787c19dffd3006300a4b Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Tue, 8 Dec 2020 11:36:13 +0100 Subject: [PATCH] feat(lsp): basic support for textDocument/completion (#8651) --- cli/lsp/capabilities.rs | 19 +++- cli/lsp/handlers.rs | 32 ++++++ cli/lsp/mod.rs | 1 + cli/lsp/tsc.rs | 219 +++++++++++++++++++++++++++++++++++- cli/tsc/99_main_compiler.js | 10 ++ cli/tsc/compiler.d.ts | 10 +- 6 files changed, 287 insertions(+), 4 deletions(-) diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index cf8f150cac..954baaf51b 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -6,6 +6,7 @@ ///! client. ///! use lsp_types::ClientCapabilities; +use lsp_types::CompletionOptions; use lsp_types::HoverProviderCapability; use lsp_types::OneOf; use lsp_types::SaveOptions; @@ -13,6 +14,7 @@ use lsp_types::ServerCapabilities; use lsp_types::TextDocumentSyncCapability; use lsp_types::TextDocumentSyncKind; use lsp_types::TextDocumentSyncOptions; +use lsp_types::WorkDoneProgressOptions; pub fn server_capabilities( _client_capabilities: &ClientCapabilities, @@ -28,7 +30,22 @@ pub fn server_capabilities( }, )), hover_provider: Some(HoverProviderCapability::Simple(true)), - completion_provider: None, + completion_provider: Some(CompletionOptions { + trigger_characters: Some(vec![ + ".".to_string(), + "\"".to_string(), + "'".to_string(), + "`".to_string(), + "/".to_string(), + "@".to_string(), + "<".to_string(), + "#".to_string(), + ]), + resolve_provider: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }), signature_help_provider: None, declaration_provider: None, definition_provider: Some(OneOf::Left(true)), diff --git a/cli/lsp/handlers.rs b/cli/lsp/handlers.rs index 6dd7321c79..ccda69f7d5 100644 --- a/cli/lsp/handlers.rs +++ b/cli/lsp/handlers.rs @@ -12,6 +12,8 @@ use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::ModuleSpecifier; use dprint_plugin_typescript as dprint; +use lsp_types::CompletionParams; +use lsp_types::CompletionResponse; use lsp_types::DocumentFormattingParams; use lsp_types::DocumentHighlight; use lsp_types::DocumentHighlightParams; @@ -187,6 +189,36 @@ pub fn handle_hover( } } +pub fn handle_completion( + state: &mut ServerState, + params: CompletionParams, +) -> Result, AnyError> { + let specifier = + utils::normalize_url(params.text_document_position.text_document.uri); + let line_index = get_line_index(state, &specifier)?; + let server_state = state.snapshot(); + let maybe_completion_info: Option = + serde_json::from_value(tsc::request( + &mut state.ts_runtime, + &server_state, + tsc::RequestMethod::GetCompletions(( + specifier, + text::to_char_pos(&line_index, 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() + }, + )), + )?)?; + + if let Some(completions) = maybe_completion_info { + Ok(Some(completions.into_completion_response(&line_index))) + } else { + Ok(None) + } +} + pub fn handle_references( state: &mut ServerState, params: ReferenceParams, diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index c26c5d89e0..e3092d8152 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -382,6 +382,7 @@ impl ServerState { handlers::handle_goto_definition, )? .on_sync::(handlers::handle_hover)? + .on_sync::(handlers::handle_completion)? .on_sync::(handlers::handle_references)? .on::(handlers::handle_formatting) .on::( diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 65f6ebbdb3..6ed727d9fd 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -14,6 +14,7 @@ use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::json_op_sync; use deno_core::serde::Deserialize; +use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; @@ -229,7 +230,7 @@ fn replace_links(text: &str) -> String { .to_string() } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub enum ScriptElementKind { #[serde(rename = "")] Unknown, @@ -301,7 +302,47 @@ pub enum ScriptElementKind { String, } -#[derive(Debug, Deserialize)] +impl From for lsp_types::CompletionItemKind { + fn from(kind: ScriptElementKind) -> Self { + use lsp_types::CompletionItemKind; + + match kind { + ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => { + CompletionItemKind::Keyword + } + ScriptElementKind::ConstElement => CompletionItemKind::Constant, + ScriptElementKind::LetElement + | ScriptElementKind::VariableElement + | ScriptElementKind::LocalVariableElement + | ScriptElementKind::Alias => CompletionItemKind::Variable, + ScriptElementKind::MemberVariableElement + | ScriptElementKind::MemberGetAccessorElement + | ScriptElementKind::MemberSetAccessorElement => { + CompletionItemKind::Field + } + ScriptElementKind::FunctionElement => CompletionItemKind::Function, + ScriptElementKind::MemberFunctionElement + | ScriptElementKind::ConstructSignatureElement + | ScriptElementKind::CallSignatureElement + | ScriptElementKind::IndexSignatureElement => CompletionItemKind::Method, + ScriptElementKind::EnumElement => CompletionItemKind::Enum, + ScriptElementKind::ModuleElement + | ScriptElementKind::ExternalModuleName => CompletionItemKind::Module, + ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => { + CompletionItemKind::Class + } + ScriptElementKind::InterfaceElement => CompletionItemKind::Interface, + ScriptElementKind::Warning | ScriptElementKind::ScriptElement => { + CompletionItemKind::File + } + ScriptElementKind::Directory => CompletionItemKind::Folder, + ScriptElementKind::String => CompletionItemKind::Constant, + _ => CompletionItemKind::Property, + } + } +} + +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TextSpan { start: u32, @@ -519,6 +560,104 @@ impl ReferenceEntry { } } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionInfo { + entries: Vec, + is_member_completion: bool, +} + +impl CompletionInfo { + pub fn into_completion_response( + self, + line_index: &[u32], + ) -> lsp_types::CompletionResponse { + let items = self + .entries + .into_iter() + .map(|entry| entry.into_completion_item(line_index)) + .collect(); + lsp_types::CompletionResponse::Array(items) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionEntry { + kind: ScriptElementKind, + kind_modifiers: Option, + name: String, + sort_text: String, + insert_text: Option, + replacement_span: Option, + has_action: Option, + source: Option, + is_recommended: Option, +} + +impl CompletionEntry { + pub fn into_completion_item( + self, + line_index: &[u32], + ) -> lsp_types::CompletionItem { + let mut item = lsp_types::CompletionItem { + label: self.name, + kind: Some(self.kind.into()), + sort_text: Some(self.sort_text.clone()), + // TODO(lucacasonato): missing commit_characters + ..Default::default() + }; + + 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) + } + + match item.kind { + Some(lsp_types::CompletionItemKind::Function) + | Some(lsp_types::CompletionItemKind::Method) => { + item.insert_text_format = Some(lsp_types::InsertTextFormat::Snippet); + } + _ => {} + } + + let mut insert_text = self.insert_text; + let replacement_range: Option = + self.replacement_span.map(|span| span.to_range(line_index)); + + // TODO(lucacasonato): port other special cases from https://github.com/theia-ide/typescript-language-server/blob/fdf28313833cd6216d00eb4e04dc7f00f4c04f09/server/src/completion.ts#L49-L55 + + if let Some(kind_modifiers) = self.kind_modifiers { + if kind_modifiers.contains("\\optional\\") { + if insert_text.is_none() { + insert_text = Some(item.label.clone()); + } + if item.filter_text.is_none() { + item.filter_text = Some(item.label.clone()); + } + item.label += "?"; + } + } + + if let Some(insert_text) = insert_text { + if let Some(replacement_range) = replacement_range { + item.text_edit = Some(lsp_types::CompletionTextEdit::Edit( + lsp_types::TextEdit::new(replacement_range, insert_text), + )); + } else { + item.insert_text = Some(insert_text); + } + } + + item + } +} + #[derive(Debug, Clone, Deserialize)] struct Response { id: usize, @@ -815,6 +954,71 @@ pub fn start(debug: bool) -> Result { Ok(runtime) } +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +pub enum QuotePreference { + Auto, + Double, + Single, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +pub enum ImportModuleSpecifierPreference { + Auto, + Relative, + NonRelative, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +pub enum ImportModuleSpecifierEnding { + Auto, + Minimal, + Index, + Js, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +#[allow(dead_code)] +pub enum IncludePackageJsonAutoImports { + Auto, + On, + Off, +} + +#[derive(Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserPreferences { + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_suggestions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub quote_preference: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_completions_for_module_exports: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_automatic_optional_chain_completions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_completions_with_insert_text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub import_module_specifier_preference: + Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub import_module_specifier_ending: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_text_changes_in_new_files: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub provide_prefix_and_suffix_text_for_rename: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_package_json_auto_imports: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub provide_refactor_not_applicable_reason: Option, +} + /// Methods that are supported by the Language Service in the compiler isolate. pub enum RequestMethod { /// Configure the compilation settings for the server. @@ -833,6 +1037,8 @@ pub enum RequestMethod { GetReferences((ModuleSpecifier, u32)), /// Get declaration information for a specific position. GetDefinition((ModuleSpecifier, u32)), + /// Get completion information at a given position (IntelliSense). + GetCompletions((ModuleSpecifier, u32, UserPreferences)), } impl RequestMethod { @@ -887,6 +1093,15 @@ impl RequestMethod { "specifier": specifier, "position": position, }), + RequestMethod::GetCompletions((specifier, position, preferences)) => { + json!({ + "id": id, + "method": "getCompletions", + "specifier": specifier, + "position": position, + "preferences": preferences, + }) + } } } } diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index f379d6baeb..a78b85203e 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -514,6 +514,16 @@ delete Object.prototype.__proto__; ), ); } + case "getCompletions": { + return respond( + id, + languageService.getCompletionsAtPosition( + request.specifier, + request.position, + request.preferences, + ), + ); + } case "getDocumentHighlights": { return respond( id, diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index 1a899c291a..a1f4e851cb 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -48,7 +48,8 @@ declare global { | GetQuickInfoRequest | GetDocumentHighlightsRequest | GetReferencesRequest - | GetDefinitionRequest; + | GetDefinitionRequest + | GetCompletionsRequest; interface BaseLanguageServerRequest { id: number; @@ -100,4 +101,11 @@ declare global { specifier: string; position: number; } + + interface GetCompletionsRequest extends BaseLanguageServerRequest { + method: "getCompletions"; + specifier: string; + position: number; + preferences: ts.UserPreferences; + } }