summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/lsp/capabilities.rs2
-rw-r--r--cli/lsp/language_server.rs125
-rw-r--r--cli/lsp/tsc.rs99
-rw-r--r--cli/tests/lsp/rename_did_open_notification.json12
-rw-r--r--cli/tests/lsp/rename_request.json15
-rw-r--r--cli/tsc/99_main_compiler.js12
-rw-r--r--cli/tsc/compiler.d.ts12
7 files changed, 275 insertions, 2 deletions
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
index e43e6a7e2..3a9caae2b 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 72d1b1ad3..9591f246a 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<Option<WorkspaceEdit>> {
+ 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<Vec<tsc::RenameLocation>>,
+ >(res)
+ .map_err(|err| {
+ error!(
+ "Failed to deserialize tsserver response to Vec<RenameLocation> {:#?}",
+ 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),
+ &params.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 2a0f7d76c..fde3e37b9 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;
@@ -412,6 +413,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<TextSpan>,
+ original_file_name: Option<String>,
+ context_span: Option<TextSpan>,
+ original_context_span: Option<TextSpan>,
+ // RenameLocation props
+ prefix_text: Option<String>,
+ suffix_text: Option<String>,
+}
+
+pub struct RenameLocations {
+ pub locations: Vec<RenameLocation>,
+}
+
+impl RenameLocations {
+ pub async fn into_workspace_edit<F, Fut>(
+ self,
+ snapshot: StateSnapshot,
+ index_provider: F,
+ new_name: &str,
+ ) -> Result<lsp_types::WorkspaceEdit, AnyError>
+ where
+ F: Fn(ModuleSpecifier) -> Fut,
+ Fut: Future<Output = Result<Vec<u32>, AnyError>>,
+ {
+ let mut text_document_edit_map: HashMap<Url, lsp_types::TextDocumentEdit> =
+ 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")]
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 000000000..c6323b742
--- /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 000000000..d9efe4b3f
--- /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 de9e74d2e..ddbb8fcac 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 39afbe884..7ba92a96f 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;
+ }
}