From cf6795d2186ef137e7fe4b094daf6116522f1361 Mon Sep 17 00:00:00 2001 From: hrsh7th Date: Tue, 29 Dec 2020 13:50:03 +0900 Subject: [PATCH] feat(lsp): Implement textDocument/rename --- cli/lsp/capabilities.rs | 2 +- cli/lsp/language_server.rs | 125 ++++++++++++++++++ cli/lsp/tsc.rs | 99 ++++++++++++++ .../lsp/rename_did_open_notification.json | 12 ++ cli/tests/lsp/rename_request.json | 15 +++ cli/tsc/99_main_compiler.js | 12 ++ cli/tsc/compiler.d.ts | 12 +- 7 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 cli/tests/lsp/rename_did_open_notification.json create mode 100644 cli/tests/lsp/rename_request.json diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index e43e6a7e278006..3a9caae2b9c794 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -62,7 +62,7 @@ pub fn server_capabilities( document_on_type_formatting_provider: None, selection_range_provider: None, folding_range_provider: None, - rename_provider: None, + rename_provider: Some(OneOf::Left(true)), document_link_provider: None, color_provider: None, execute_command_provider: None, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 72d1b1ad386768..9591f246a7e8d6 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -802,6 +802,78 @@ impl lspower::LanguageServer for LanguageServer { } } + async fn rename( + &self, + params: RenameParams, + ) -> LSPResult> { + if !self.enabled() { + return Ok(None); + } + + let snapshot = self.snapshot(); + let specifier = + utils::normalize_url(params.text_document_position.text_document.uri); + + let line_index = + self + .get_line_index(specifier.clone()) + .await + .map_err(|err| { + error!("Failed to get line_index {:#?}", err); + LSPError::internal_error() + })?; + + let req = tsc::RequestMethod::FindRenameLocations(( + specifier, + text::to_char_pos(&line_index, params.text_document_position.position), + true, + true, + false, + )); + + let res = self + .ts_server + .request(snapshot.clone(), req) + .await + .map_err(|err| { + error!("Failed to request to tsserver {:#?}", err); + LSPError::invalid_request() + })?; + + let maybe_locations = serde_json::from_value::< + Option>, + >(res) + .map_err(|err| { + error!( + "Failed to deserialize tsserver response to Vec {:#?}", + err + ); + LSPError::internal_error() + })?; + + match maybe_locations { + Some(locations) => { + let rename_locations = tsc::RenameLocations { locations }; + let workpace_edits = rename_locations + .into_workspace_edit( + snapshot, + |s| self.get_line_index(s), + ¶ms.new_name, + ) + .await + .map_err(|err| { + error!( + "Failed to convert tsc::RenameLocations to WorkspaceEdit {:#?}", + err + ); + LSPError::internal_error() + })?; + Ok(Some(workpace_edits)) + } + None => Ok(None), + } + } + async fn request_else( &self, method: &str, @@ -1143,4 +1215,57 @@ mod tests { ]); harness.run().await; } + #[tokio::test] + async fn test_rename() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ("rename_did_open_notification.json", LspResponse::None), + ( + "rename_request.json", + LspResponse::Request( + 2, + json!({ + "documentChanges": [{ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 1, + }, + "edits": [{ + "range": { + "start": { + "line": 0, + "character": 4 + }, + "end": { + "line": 0, + "character": 12 + } + }, + "newText": "variable_modified" + }, { + "range": { + "start": { + "line": 1, + "character": 12 + }, + "end": { + "line": 1, + "character": 20 + } + }, + "newText": "variable_modified" + }] + }] + }), + ), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 2a0f7d76cf20fc..fde3e37b9cc100 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -22,6 +22,7 @@ use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; +use deno_core::url::Url; use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use deno_core::OpFn; @@ -411,6 +412,85 @@ impl QuickInfo { } } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameLocation { + // inherit from DocumentSpan + text_span: TextSpan, + file_name: String, + original_text_span: Option, + original_file_name: Option, + context_span: Option, + original_context_span: Option, + // RenameLocation props + prefix_text: Option, + suffix_text: Option, +} + +pub struct RenameLocations { + pub locations: Vec, +} + +impl RenameLocations { + pub async fn into_workspace_edit( + self, + snapshot: StateSnapshot, + index_provider: F, + new_name: &str, + ) -> Result + where + F: Fn(ModuleSpecifier) -> Fut, + Fut: Future, AnyError>>, + { + let mut text_document_edit_map: HashMap = + HashMap::new(); + for location in self.locations.iter() { + let uri = utils::normalize_file_name(&location.file_name)?; + let specifier = ModuleSpecifier::resolve_url(&location.file_name)?; + + // ensure TextDocumentEdit for `location.file_name`. + if text_document_edit_map.get(&uri).is_none() { + text_document_edit_map.insert( + uri.clone(), + lsp_types::TextDocumentEdit { + text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { + uri: uri.clone(), + version: snapshot + .doc_data + .get(&specifier) + .map_or_else(|| None, |data| data.version), + }, + edits: Vec::< + lsp_types::OneOf< + lsp_types::TextEdit, + lsp_types::AnnotatedTextEdit, + >, + >::new(), + }, + ); + } + + // push TextEdit for ensured `TextDocumentEdit.edits`. + let document_edit = text_document_edit_map.get_mut(&uri).unwrap(); + document_edit + .edits + .push(lsp_types::OneOf::Left(lsp_types::TextEdit { + range: location + .text_span + .to_range(&index_provider(specifier.clone()).await?), + new_text: new_name.to_string(), + })); + } + + Ok(lsp_types::WorkspaceEdit { + changes: None, + document_changes: Some(lsp_types::DocumentChanges::Edits( + text_document_edit_map.values().cloned().collect(), + )), + }) + } +} + #[derive(Debug, Deserialize)] pub enum HighlightSpanKind { #[serde(rename = "none")] @@ -1059,6 +1139,8 @@ pub enum RequestMethod { GetDefinition((ModuleSpecifier, u32)), /// Get completion information at a given position (IntelliSense). GetCompletions((ModuleSpecifier, u32, UserPreferences)), + /// Get rename locations at a given position. + FindRenameLocations((ModuleSpecifier, u32, bool, bool, bool)), } impl RequestMethod { @@ -1127,6 +1209,23 @@ impl RequestMethod { "preferences": preferences, }) } + RequestMethod::FindRenameLocations(( + specifier, + position, + find_in_strings, + find_in_comments, + provide_prefix_and_suffix_text_for_rename, + )) => { + json!({ + "id": id, + "method": "findRenameLocations", + "specifier": specifier, + "position": position, + "findInStrings": find_in_strings, + "findInComments": find_in_comments, + "providePrefixAndSuffixTextForRename": provide_prefix_and_suffix_text_for_rename + }) + } } } } diff --git a/cli/tests/lsp/rename_did_open_notification.json b/cli/tests/lsp/rename_did_open_notification.json new file mode 100644 index 00000000000000..c6323b742149b5 --- /dev/null +++ b/cli/tests/lsp/rename_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": "let variable = 'a';\nconsole.log(variable);" + } + } +} diff --git a/cli/tests/lsp/rename_request.json b/cli/tests/lsp/rename_request.json new file mode 100644 index 00000000000000..d9efe4b3f1001f --- /dev/null +++ b/cli/tests/lsp/rename_request.json @@ -0,0 +1,15 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/rename", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 5, + "character": 19 + }, + "newName": "variable_modified" + } +} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index de9e74d2ef440f..ddbb8fcac6aca2 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -562,6 +562,18 @@ delete Object.prototype.__proto__; ), ); } + case "findRenameLocations": { + return respond( + id, + languageService.findRenameLocations( + request.specifier, + request.position, + request.findInStrings, + request.findInComments, + request.providePrefixAndSuffixTextForRename, + ), + ); + } default: throw new TypeError( // @ts-ignore exhausted case statement sets type to never diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index 39afbe884e9509..7ba92a96fdc09c 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -50,7 +50,8 @@ declare global { | GetDocumentHighlightsRequest | GetReferencesRequest | GetDefinitionRequest - | GetCompletionsRequest; + | GetCompletionsRequest + | FindRenameLocationsRequest; interface BaseLanguageServerRequest { id: number; @@ -114,4 +115,13 @@ declare global { position: number; preferences: ts.UserPreferences; } + + interface FindRenameLocationsRequest extends BaseLanguageServerRequest { + method: "findRenameLocations"; + specifier: string; + position: number; + findInStrings: boolean; + findInComments: boolean; + providePrefixAndSuffixTextForRename: boolean; + } }