From e368c5d0f9d69e69438cb0a8a8deb49d7b02901a Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Mon, 8 Feb 2021 21:45:10 +1100 Subject: [PATCH] feat(lsp): add implementations code lens (#9441) --- cli/lsp/analysis.rs | 2 + cli/lsp/config.rs | 15 +- cli/lsp/language_server.rs | 217 ++++++++++++++++++ cli/lsp/tsc.rs | 21 +- .../lsp/code_lens_resolve_request_impl.json | 21 ++ .../lsp/did_open_notification_cl_impl.json | 12 + cli/tests/lsp/initialize_request.json | 1 + 7 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 cli/tests/lsp/code_lens_resolve_request_impl.json create mode 100644 cli/tests/lsp/did_open_notification_cl_impl.json diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 7b0f159bb4..0c3112ca8d 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -289,6 +289,8 @@ pub fn analyze_dependencies( #[derive(Debug, Deserialize, Serialize)] pub enum CodeLensSource { + #[serde(rename = "implementations")] + Implementations, #[serde(rename = "references")] References, } diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 47285a01ea..cb814d0fd8 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -18,7 +18,10 @@ pub struct ClientCapabilities { #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CodeLensSettings { - /// Flag for providing reference code lens. + /// Flag for providing implementation code lenses. + #[serde(default)] + pub implementations: bool, + /// Flag for providing reference code lenses. #[serde(default)] pub references: bool, /// Flag for providing reference code lens on all functions. For this to have @@ -47,7 +50,15 @@ impl WorkspaceSettings { 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.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 } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index e22934dc32..aa52866095 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -52,6 +52,7 @@ use super::tsc::TsServer; use super::utils; lazy_static! { + static ref ABSTRACT_MODIFIER: Regex = Regex::new(r"\babstract\b").unwrap(); static ref EXPORT_MODIFIER: Regex = Regex::new(r"\bexport\b").unwrap(); } @@ -1037,6 +1038,30 @@ impl Inner { navigation_tree.walk(&|i, mp| { let mut code_lenses = cl.borrow_mut(); + // TSC Implementations Code Lens + if self.config.settings.enabled_code_lens_implementations() { + let source = CodeLensSource::Implementations; + match i.kind { + tsc::ScriptElementKind::InterfaceElement => { + code_lenses.push(i.to_code_lens(&line_index, &specifier, &source)); + } + tsc::ScriptElementKind::ClassElement + | tsc::ScriptElementKind::MemberFunctionElement + | tsc::ScriptElementKind::MemberVariableElement + | tsc::ScriptElementKind::MemberGetAccessorElement + | tsc::ScriptElementKind::MemberSetAccessorElement => { + if ABSTRACT_MODIFIER.is_match(&i.kind_modifiers) { + code_lenses.push(i.to_code_lens( + &line_index, + &specifier, + &source, + )); + } + } + _ => (), + } + } + // TSC References Code Lens if self.config.settings.enabled_code_lens_references() { let source = CodeLensSource::References; @@ -1124,6 +1149,83 @@ impl Inner { let code_lens_data: CodeLensData = serde_json::from_value(data) .map_err(|err| LspError::invalid_params(err.to_string()))?; let code_lens = match code_lens_data.source { + CodeLensSource::Implementations => { + let line_index = + self.get_line_index_sync(&code_lens_data.specifier).unwrap(); + let req = tsc::RequestMethod::GetImplementation(( + code_lens_data.specifier.clone(), + line_index.offset_tsc(params.range.start)?, + )); + let res = + self.ts_server.request(self.snapshot(), req).await.map_err( + |err| { + error!("Error processing TypeScript request: {}", err); + LspError::internal_error() + }, + )?; + let maybe_implementations: Option> = + serde_json::from_value(res).map_err(|err| { + error!("Error deserializing response: {}", err); + LspError::internal_error() + })?; + if let Some(implementations) = maybe_implementations { + let mut locations = Vec::new(); + for implementation in implementations { + let implementation_specifier = ModuleSpecifier::resolve_url( + &implementation.document_span.file_name, + ) + .map_err(|err| { + error!("Invalid specifier returned from TypeScript: {}", err); + LspError::internal_error() + })?; + let implementation_location = + implementation.to_location(&line_index); + if !(implementation_specifier == code_lens_data.specifier + && implementation_location.range.start == params.range.start) + { + locations.push(implementation_location); + } + } + let command = if !locations.is_empty() { + let title = if locations.len() > 1 { + format!("{} implementations", locations.len()) + } else { + "1 implementation".to_string() + }; + Command { + title, + command: "deno.showReferences".to_string(), + arguments: Some(vec![ + serde_json::to_value(code_lens_data.specifier).unwrap(), + serde_json::to_value(params.range.start).unwrap(), + serde_json::to_value(locations).unwrap(), + ]), + } + } else { + Command { + title: "0 implementations".to_string(), + command: "".to_string(), + arguments: None, + } + }; + CodeLens { + range: params.range, + command: Some(command), + data: None, + } + } else { + let command = Command { + title: "0 implementations".to_string(), + command: "".to_string(), + arguments: None, + }; + CodeLens { + range: params.range, + command: Some(command), + data: None, + } + } + } CodeLensSource::References => { let line_index = self.get_line_index_sync(&code_lens_data.specifier).unwrap(); @@ -2372,6 +2474,121 @@ mod tests { harness.run().await; } + #[tokio::test] + async fn test_code_lens_impl_request() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ("did_open_notification_cl_impl.json", LspResponse::None), + ( + "code_lens_request.json", + LspResponse::Request( + 2, + json!([ + { + "range": { + "start": { + "line": 0, + "character": 10, + }, + "end": { + "line": 0, + "character": 11, + } + }, + "data": { + "specifier": "file:///a/file.ts", + "source": "implementations", + }, + }, + { + "range": { + "start": { + "line": 0, + "character": 10, + }, + "end": { + "line": 0, + "character": 11, + } + }, + "data": { + "specifier": "file:///a/file.ts", + "source": "references", + }, + }, + { + "range": { + "start": { + "line": 4, + "character": 6, + }, + "end": { + "line": 4, + "character": 7, + } + }, + "data": { + "specifier": "file:///a/file.ts", + "source": "references", + }, + }, + ]), + ), + ), + ( + "code_lens_resolve_request_impl.json", + LspResponse::Request( + 4, + json!({ + "range": { + "start": { + "line": 0, + "character": 10, + }, + "end": { + "line": 0, + "character": 11, + } + }, + "command": { + "title": "1 implementation", + "command": "deno.showReferences", + "arguments": [ + "file:///a/file.ts", + { + "line": 0, + "character": 10, + }, + [ + { + "uri": "file:///a/file.ts", + "range": { + "start": { + "line": 4, + "character": 6, + }, + "end": { + "line": 4, + "character": 7, + } + } + } + ], + ] + } + }), + ), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } + #[tokio::test] async fn test_code_actions() { let mut harness = LspTestHarness::new(vec![ diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 67286e2889..6614ce8e6e 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -497,8 +497,16 @@ impl NavigationTree { specifier: &ModuleSpecifier, source: &CodeLensSource, ) -> lsp::CodeLens { + let range = if let Some(name_span) = &self.name_span { + name_span.to_range(line_index) + } else if !self.spans.is_empty() { + let span = &self.spans[0]; + span.to_range(line_index) + } else { + lsp::Range::default() + }; lsp::CodeLens { - range: self.name_span.clone().unwrap().to_range(line_index), + range, command: None, data: Some(json!({ "specifier": specifier, @@ -542,6 +550,17 @@ pub struct ImplementationLocation { display_parts: Vec, } +impl ImplementationLocation { + pub fn to_location(&self, line_index: &LineIndex) -> lsp::Location { + let uri = + utils::normalize_file_name(&self.document_span.file_name).unwrap(); + lsp::Location { + uri, + range: self.document_span.text_span.to_range(line_index), + } + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RenameLocation { diff --git a/cli/tests/lsp/code_lens_resolve_request_impl.json b/cli/tests/lsp/code_lens_resolve_request_impl.json new file mode 100644 index 0000000000..e44d19675e --- /dev/null +++ b/cli/tests/lsp/code_lens_resolve_request_impl.json @@ -0,0 +1,21 @@ +{ + "jsonrpc": "2.0", + "id": 4, + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 0, + "character": 10 + }, + "end": { + "line": 0, + "character": 11 + } + }, + "data": { + "specifier": "file:///a/file.ts", + "source": "implementations" + } + } +} diff --git a/cli/tests/lsp/did_open_notification_cl_impl.json b/cli/tests/lsp/did_open_notification_cl_impl.json new file mode 100644 index 0000000000..eabcd7ccc8 --- /dev/null +++ b/cli/tests/lsp/did_open_notification_cl_impl.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "interface A {\n b(): void;\n}\n\nclass B implements A {\n b() {\n console.log(\"b\");\n }\n}\n" + } + } +} diff --git a/cli/tests/lsp/initialize_request.json b/cli/tests/lsp/initialize_request.json index eaea00a182..21e9e3b4f6 100644 --- a/cli/tests/lsp/initialize_request.json +++ b/cli/tests/lsp/initialize_request.json @@ -12,6 +12,7 @@ "initializationOptions": { "enable": true, "codeLens": { + "implementations": true, "references": true }, "lint": true,