diff options
author | Yuki Tanaka <uki00a@gmail.com> | 2021-02-16 11:34:09 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-16 13:34:09 +1100 |
commit | ccd6ee5c2394418c078f1a1be9e5cc1012829cbc (patch) | |
tree | 34d289fd504a89493de295ae9cd9a1cc771fede6 | |
parent | 3f5265b21ec578e543d09cdc9d8b19d9655aebd9 (diff) |
feat(lsp): Implement `textDocument/signatureHelp` (#9330)
Co-authored-by: Kitson Kelly <me@kitsonkelly.com>
-rw-r--r-- | cli/lsp/capabilities.rs | 13 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 143 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 151 | ||||
-rw-r--r-- | cli/tests/lsp/signature_help_did_change_notification.json | 25 | ||||
-rw-r--r-- | cli/tests/lsp/signature_help_did_open_notification.json | 12 | ||||
-rw-r--r-- | cli/tests/lsp/signature_help_request_01.json | 19 | ||||
-rw-r--r-- | cli/tests/lsp/signature_help_request_02.json | 14 | ||||
-rw-r--r-- | cli/tsc/99_main_compiler.js | 10 | ||||
-rw-r--r-- | cli/tsc/compiler.d.ts | 8 |
9 files changed, 382 insertions, 13 deletions
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index 9eed85b73..fb16db88b 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -16,6 +16,7 @@ use lspower::lsp::ImplementationProviderCapability; use lspower::lsp::OneOf; use lspower::lsp::SaveOptions; use lspower::lsp::ServerCapabilities; +use lspower::lsp::SignatureHelpOptions; use lspower::lsp::TextDocumentSyncCapability; use lspower::lsp::TextDocumentSyncKind; use lspower::lsp::TextDocumentSyncOptions; @@ -69,7 +70,17 @@ pub fn server_capabilities( work_done_progress: None, }, }), - signature_help_provider: None, + signature_help_provider: Some(SignatureHelpOptions { + trigger_characters: Some(vec![ + ",".to_string(), + "(".to_string(), + "<".to_string(), + ]), + retrigger_characters: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }), declaration_provider: None, definition_provider: Some(OneOf::Left(true)), type_definition_provider: None, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 6594492a4..1501249e8 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1705,6 +1705,68 @@ impl Inner { } } } + + async fn signature_help( + &self, + params: SignatureHelpParams, + ) -> LspResult<Option<SignatureHelp>> { + if !self.enabled() { + return Ok(None); + } + let mark = self.performance.mark("signature_help"); + let specifier = utils::normalize_url( + params.text_document_position_params.text_document.uri, + ); + let line_index = + if let Some(line_index) = self.get_line_index_sync(&specifier) { + line_index + } else { + return Err(LspError::invalid_params(format!( + "An unexpected specifier ({}) was provided.", + specifier + ))); + }; + let options = if let Some(context) = params.context { + tsc::SignatureHelpItemsOptions { + trigger_reason: Some(tsc::SignatureHelpTriggerReason { + kind: context.trigger_kind.into(), + trigger_character: context.trigger_character, + }), + } + } else { + tsc::SignatureHelpItemsOptions { + trigger_reason: None, + } + }; + let req = tsc::RequestMethod::GetSignatureHelpItems(( + specifier, + line_index.offset_tsc(params.text_document_position_params.position)?, + options, + )); + let res = + self + .ts_server + .request(self.snapshot(), req) + .await + .map_err(|err| { + error!("Failed to request to tsserver: {}", err); + LspError::invalid_request() + })?; + let maybe_signature_help_items: Option<tsc::SignatureHelpItems> = + serde_json::from_value(res).map_err(|err| { + error!("Failed to deserialize tsserver response: {}", err); + LspError::internal_error() + })?; + + if let Some(signature_help_items) = maybe_signature_help_items { + let signature_help = signature_help_items.into_signature_help(); + self.performance.measure(mark); + Ok(Some(signature_help)) + } else { + self.performance.measure(mark); + Ok(None) + } + } } #[lspower::async_trait] @@ -1840,6 +1902,13 @@ impl lspower::LanguageServer for LanguageServer { ) -> LspResult<Option<Value>> { self.0.lock().await.request_else(method, params).await } + + async fn signature_help( + &self, + params: SignatureHelpParams, + ) -> LspResult<Option<SignatureHelp>> { + self.0.lock().await.signature_help(params).await + } } #[derive(Debug, Deserialize, Serialize)] @@ -2539,6 +2608,80 @@ mod tests { } #[tokio::test] + async fn test_signature_help() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ( + "signature_help_did_open_notification.json", + LspResponse::None, + ), + ( + "signature_help_request_01.json", + LspResponse::Request( + 1, + json!({ + "signatures": [ + { + "label": "add(a: number, b: number): number", + "documentation": "Adds two numbers.", + "parameters": [ + { + "label": "a: number", + "documentation": "This is a first number." + }, + { + "label": "b: number", + "documentation": "This is a second number." + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + }), + ), + ), + ( + "signature_help_did_change_notification.json", + LspResponse::None, + ), + ( + "signature_help_request_02.json", + LspResponse::Request( + 2, + json!({ + "signatures": [ + { + "label": "add(a: number, b: number): number", + "documentation": "Adds two numbers.", + "parameters": [ + { + "label": "a: number", + "documentation": "This is a first number." + }, + { + "label": "b: number", + "documentation": "This is a second number." + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 1 + }), + ), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } + + #[tokio::test] async fn test_code_lens_impl_request() { let mut harness = LspTestHarness::new(vec![ ("initialize_request.json", LspResponse::RequestAny), diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index ef57682f1..5dc7200b7 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -166,16 +166,12 @@ pub async fn get_asset( } } -fn display_parts_to_string( - maybe_parts: Option<Vec<SymbolDisplayPart>>, -) -> Option<String> { - maybe_parts.map(|parts| { - parts - .into_iter() - .map(|p| p.text) - .collect::<Vec<String>>() - .join("") - }) +fn display_parts_to_string(parts: Vec<SymbolDisplayPart>) -> String { + parts + .into_iter() + .map(|p| p.text) + .collect::<Vec<String>>() + .join("") } fn get_tag_body_text(tag: &JSDocTagInfo) -> Option<String> { @@ -433,7 +429,7 @@ impl QuickInfo { pub fn to_hover(&self, line_index: &LineIndex) -> lsp::Hover { let mut contents = Vec::<lsp::MarkedString>::new(); if let Some(display_string) = - display_parts_to_string(self.display_parts.clone()) + self.display_parts.clone().map(display_parts_to_string) { contents.push(lsp::MarkedString::from_language_code( "typescript".to_string(), @@ -441,7 +437,7 @@ impl QuickInfo { )); } if let Some(documentation) = - display_parts_to_string(self.documentation.clone()) + self.documentation.clone().map(display_parts_to_string) { contents.push(lsp::MarkedString::from_markdown(documentation)); } @@ -946,6 +942,91 @@ impl CompletionEntry { } } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpItems { + items: Vec<SignatureHelpItem>, + applicable_span: TextSpan, + selected_item_index: u32, + argument_index: u32, + argument_count: u32, +} + +impl SignatureHelpItems { + pub fn into_signature_help(self) -> lsp::SignatureHelp { + lsp::SignatureHelp { + signatures: self + .items + .into_iter() + .map(|item| item.into_signature_information()) + .collect(), + active_parameter: Some(self.argument_index), + active_signature: Some(self.selected_item_index), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpItem { + is_variadic: bool, + prefix_display_parts: Vec<SymbolDisplayPart>, + suffix_display_parts: Vec<SymbolDisplayPart>, + separator_display_parts: Vec<SymbolDisplayPart>, + parameters: Vec<SignatureHelpParameter>, + documentation: Vec<SymbolDisplayPart>, + tags: Vec<JSDocTagInfo>, +} + +impl SignatureHelpItem { + pub fn into_signature_information(self) -> lsp::SignatureInformation { + 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())) + .collect::<Vec<String>>() + .join(", "); + 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, + ))), + parameters: Some( + self + .parameters + .into_iter() + .map(|param| param.into_parameter_information()) + .collect(), + ), + active_parameter: None, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpParameter { + name: String, + documentation: Vec<SymbolDisplayPart>, + display_parts: Vec<SymbolDisplayPart>, + is_optional: bool, +} + +impl SignatureHelpParameter { + pub fn into_parameter_information(self) -> lsp::ParameterInformation { + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple(display_parts_to_string( + self.display_parts, + )), + documentation: Some(lsp::Documentation::String(display_parts_to_string( + self.documentation, + ))), + } + } +} + #[derive(Debug, Clone, Deserialize)] struct Response { id: usize, @@ -1361,6 +1442,41 @@ pub struct UserPreferences { pub provide_refactor_not_applicable_reason: Option<bool>, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpItemsOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_reason: Option<SignatureHelpTriggerReason>, +} + +#[derive(Debug, Serialize)] +pub enum SignatureHelpTriggerKind { + #[serde(rename = "characterTyped")] + CharacterTyped, + #[serde(rename = "invoked")] + Invoked, + #[serde(rename = "retrigger")] + Retrigger, +} + +impl From<lsp::SignatureHelpTriggerKind> for SignatureHelpTriggerKind { + fn from(kind: lsp::SignatureHelpTriggerKind) -> Self { + match kind { + lsp::SignatureHelpTriggerKind::Invoked => Self::Invoked, + lsp::SignatureHelpTriggerKind::TriggerCharacter => Self::CharacterTyped, + lsp::SignatureHelpTriggerKind::ContentChange => Self::Retrigger, + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelpTriggerReason { + pub kind: SignatureHelpTriggerKind, + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_character: Option<String>, +} + /// Methods that are supported by the Language Service in the compiler isolate. #[derive(Debug)] pub enum RequestMethod { @@ -1390,6 +1506,8 @@ pub enum RequestMethod { GetQuickInfo((ModuleSpecifier, u32)), /// Get document references for a specific position. GetReferences((ModuleSpecifier, u32)), + /// Get signature help items for a specific position. + GetSignatureHelpItems((ModuleSpecifier, u32, SignatureHelpItemsOptions)), /// Get the diagnostic codes that support some form of code fix. GetSupportedCodeFixes, } @@ -1497,6 +1615,15 @@ impl RequestMethod { "specifier": specifier, "position": position, }), + RequestMethod::GetSignatureHelpItems((specifier, position, options)) => { + json!({ + "id": id, + "method": "getSignatureHelpItems", + "specifier": specifier, + "position": position, + "options": options, + }) + } RequestMethod::GetSupportedCodeFixes => json!({ "id": id, "method": "getSupportedCodeFixes", diff --git a/cli/tests/lsp/signature_help_did_change_notification.json b/cli/tests/lsp/signature_help_did_change_notification.json new file mode 100644 index 000000000..f88eaa9ff --- /dev/null +++ b/cli/tests/lsp/signature_help_did_change_notification.json @@ -0,0 +1,25 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 9, + "character": 4 + }, + "end": { + "line": 9, + "character": 4 + } + }, + "text": "123, " + } + ] + } +} diff --git a/cli/tests/lsp/signature_help_did_open_notification.json b/cli/tests/lsp/signature_help_did_open_notification.json new file mode 100644 index 000000000..1ba1f7586 --- /dev/null +++ b/cli/tests/lsp/signature_help_did_open_notification.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "/**\n * Adds two numbers.\n * @param a This is a first number.\n * @param b This is a second number.\n */\nfunction add(a: number, b: number) {\n return a + b;\n}\n\nadd(" + } + } +} diff --git a/cli/tests/lsp/signature_help_request_01.json b/cli/tests/lsp/signature_help_request_01.json new file mode 100644 index 000000000..c3e185e08 --- /dev/null +++ b/cli/tests/lsp/signature_help_request_01.json @@ -0,0 +1,19 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/signatureHelp", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "character": 4, + "line": 9 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "(", + "isRetrigger": false + } + } +} diff --git a/cli/tests/lsp/signature_help_request_02.json b/cli/tests/lsp/signature_help_request_02.json new file mode 100644 index 000000000..c2a6e0abb --- /dev/null +++ b/cli/tests/lsp/signature_help_request_02.json @@ -0,0 +1,14 @@ +{ + "id": 2, + "jsonrpc": "2.0", + "method": "textDocument/signatureHelp", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "character": 8, + "line": 9 + } + } +} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 50631e83f..f8eabc890 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -677,6 +677,16 @@ delete Object.prototype.__proto__; ), ); } + case "getSignatureHelpItems": { + return respond( + id, + languageService.getSignatureHelpItems( + request.specifier, + request.position, + request.options, + ), + ); + } case "getSupportedCodeFixes": { return respond( id, diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index 4e5dcdb96..7d102eb56 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -54,6 +54,7 @@ declare global { | GetNavigationTree | GetQuickInfoRequest | GetReferencesRequest + | GetSignatureHelpItemsRequest | GetSupportedCodeFixes; interface BaseLanguageServerRequest { @@ -144,6 +145,13 @@ declare global { position: number; } + interface GetSignatureHelpItemsRequest extends BaseLanguageServerRequest { + method: "getSignatureHelpItems"; + specifier: string; + position: number; + options: ts.SignatureHelpItemsOptions; + } + interface GetSupportedCodeFixes extends BaseLanguageServerRequest { method: "getSupportedCodeFixes"; } |