diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index 82bb910bb3..dbfb42b59c 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -15,6 +15,7 @@ use lspower::lsp::HoverProviderCapability; use lspower::lsp::ImplementationProviderCapability; use lspower::lsp::OneOf; use lspower::lsp::SaveOptions; +use lspower::lsp::SelectionRangeProviderCapability; use lspower::lsp::ServerCapabilities; use lspower::lsp::SignatureHelpOptions; use lspower::lsp::TextDocumentSyncCapability; @@ -104,7 +105,9 @@ pub fn server_capabilities( document_formatting_provider: Some(OneOf::Left(true)), document_range_formatting_provider: None, document_on_type_formatting_provider: None, - selection_range_provider: None, + selection_range_provider: Some(SelectionRangeProviderCapability::Simple( + true, + )), folding_range_provider: None, rename_provider: Some(OneOf::Left(true)), document_link_provider: None, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 3c3d82b3b7..5489a0b9bf 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1590,6 +1590,48 @@ impl Inner { } } + async fn selection_range( + &self, + params: SelectionRangeParams, + ) -> LspResult>> { + if !self.enabled() { + return Ok(None); + } + let mark = self.performance.mark("selection_range"); + let specifier = self.url_map.normalize_url(¶ms.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 mut selection_ranges = Vec::::new(); + for position in params.positions { + let req = tsc::RequestMethod::GetSmartSelectionRange(( + specifier.clone(), + line_index.offset_tsc(position)?, + )); + + let selection_range: tsc::SelectionRange = self + .ts_server + .request(self.snapshot(), req) + .await + .map_err(|err| { + error!("Failed to request to tsserver {}", err); + LspError::invalid_request() + })?; + + selection_ranges.push(selection_range.to_selection_range(&line_index)); + } + self.performance.measure(mark); + Ok(Some(selection_ranges)) + } + async fn signature_help( &self, params: SignatureHelpParams, @@ -1794,6 +1836,13 @@ impl lspower::LanguageServer for LanguageServer { self.0.lock().await.request_else(method, params).await } + async fn selection_range( + &self, + params: SelectionRangeParams, + ) -> LspResult>> { + self.0.lock().await.selection_range(params).await + } + async fn signature_help( &self, params: SignatureHelpParams, @@ -2411,6 +2460,114 @@ mod tests { harness.run().await; } + #[tokio::test] + async fn test_selection_range() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ( + "selection_range_did_open_notification.json", + LspResponse::None, + ), + ( + "selection_range_request.json", + LspResponse::Request( + 2, + json!([{ + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 15 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 4 + }, + "end": { + "line": 4, + "character": 5 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 13 + }, + "end": { + "line": 6, + "character": 2 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 6, + "character": 3 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 11 + }, + "end": { + "line": 7, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 7, + "character": 1 + } + } + } + } + } + } + } + } + }]), + ), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } + #[tokio::test] async fn test_code_lens_request() { let mut harness = LspTestHarness::new(vec![ diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index a60f15eb8e..c2418d1578 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -1311,6 +1311,31 @@ impl SignatureHelpParameter { } } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SelectionRange { + text_span: TextSpan, + #[serde(skip_serializing_if = "Option::is_none")] + parent: Option>, +} + +impl SelectionRange { + pub fn to_selection_range( + &self, + line_index: &LineIndex, + ) -> lsp::SelectionRange { + lsp::SelectionRange { + range: self.text_span.to_range(line_index), + parent: match &self.parent { + Some(parent_selection) => { + Some(Box::new(parent_selection.to_selection_range(line_index))) + } + None => None, + }, + } + } +} + #[derive(Debug, Clone, Deserialize)] struct Response { id: usize, @@ -1856,6 +1881,8 @@ pub enum RequestMethod { GetReferences((ModuleSpecifier, u32)), /// Get signature help items for a specific position. GetSignatureHelpItems((ModuleSpecifier, u32, SignatureHelpItemsOptions)), + /// Get a selection range for a specific position. + GetSmartSelectionRange((ModuleSpecifier, u32)), /// Get the diagnostic codes that support some form of code fix. GetSupportedCodeFixes, } @@ -1977,6 +2004,14 @@ impl RequestMethod { "options": options, }) } + RequestMethod::GetSmartSelectionRange((specifier, position)) => { + json!({ + "id": id, + "method": "getSmartSelectionRange", + "specifier": specifier, + "position": position + }) + } RequestMethod::GetSupportedCodeFixes => json!({ "id": id, "method": "getSupportedCodeFixes", diff --git a/cli/tests/lsp/selection_range_did_open_notification.json b/cli/tests/lsp/selection_range_did_open_notification.json new file mode 100644 index 0000000000..a6b3d9d39c --- /dev/null +++ b/cli/tests/lsp/selection_range_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": "class Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}" + } + } +} diff --git a/cli/tests/lsp/selection_range_request.json b/cli/tests/lsp/selection_range_request.json new file mode 100644 index 0000000000..5125fa6a02 --- /dev/null +++ b/cli/tests/lsp/selection_range_request.json @@ -0,0 +1,16 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/selectionRange", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "positions": [ + { + "line": 2, + "character": 8 + } + ] + } +} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index c84c2365c8..8aba3dca90 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -703,6 +703,15 @@ delete Object.prototype.__proto__; ), ); } + case "getSmartSelectionRange": { + return respond( + id, + languageService.getSmartSelectionRange( + request.specifier, + request.position, + ), + ); + } case "getSupportedCodeFixes": { return respond( id, diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index a3200469c8..bdbc89a872 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -61,6 +61,7 @@ declare global { | GetQuickInfoRequest | GetReferencesRequest | GetSignatureHelpItemsRequest + | GetSmartSelectionRange | GetSupportedCodeFixes; interface BaseLanguageServerRequest { @@ -169,6 +170,12 @@ declare global { options: ts.SignatureHelpItemsOptions; } + interface GetSmartSelectionRange extends BaseLanguageServerRequest { + method: "getSmartSelectionRange"; + specifier: string; + position: number; + } + interface GetSupportedCodeFixes extends BaseLanguageServerRequest { method: "getSupportedCodeFixes"; }