diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index e777ac0104..8e9bef9bb5 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -5,6 +5,7 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; use deno_ast::SourceRange; @@ -55,6 +56,7 @@ use super::language_server; use super::resolver::LspResolver; use super::tsc; use crate::args::jsr_url; +use crate::lsp::urls::uri_to_url; use crate::tools::lint::CliLinter; use crate::util::path::relative_specifier; @@ -757,6 +759,43 @@ pub fn fix_ts_import_changes( Ok(r) } +pub fn fix_ts_import_changes_for_file_rename( + changes: Vec, + new_uri: &str, + module: &DocumentModule, + language_server: &language_server::Inner, + token: &CancellationToken, +) -> Result, AnyError> { + let Ok(new_uri) = Uri::from_str(new_uri) else { + return Ok(Vec::new()); + }; + if !new_uri.scheme().is_some_and(|s| s.eq_lowercase("file")) { + return Ok(Vec::new()); + } + let new_url = uri_to_url(&new_uri); + let mut r = Vec::with_capacity(changes.len()); + for mut change in changes { + if token.is_cancelled() { + return Err(anyhow!("request cancelled")); + } + let Ok(target_specifier) = resolve_url(&change.file_name) else { + continue; + }; + let import_mapper = language_server.get_ts_response_import_mapper(module); + for text_change in &mut change.text_changes { + if let Some(new_specifier) = import_mapper + .check_specifier(&new_url, &target_specifier) + .or_else(|| relative_specifier(&target_specifier, &new_url)) + .filter(|s| !s.contains("/node_modules/")) + { + text_change.new_text = new_specifier; + } + } + r.push(change); + } + Ok(r) +} + /// Fix tsc import code actions so that the module specifier is correct for /// resolution by Deno (includes the extension). fn fix_ts_import_action<'a>( diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 32ec573583..33bf5ec2e1 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -108,6 +108,7 @@ use crate::file_fetcher::CreateCliFileFetcherOptions; use crate::file_fetcher::create_cli_file_fetcher; use crate::graph_util; use crate::http_util::HttpClientProvider; +use crate::lsp::analysis::fix_ts_import_changes_for_file_rename; use crate::lsp::compiler_options::LspCompilerOptionsResolver; use crate::lsp::config::ConfigWatchedFileType; use crate::lsp::diagnostics::generate_module_diagnostics; @@ -3889,8 +3890,25 @@ impl Inner { LspError::internal_error() } })?; - changes_with_modules - .extend(changes.into_iter().map(|c| (c, module.clone()))); + let changes = fix_ts_import_changes_for_file_rename( + changes, + &rename.new_uri, + &module, + self, + token, + ) + .map_err(|err| { + if token.is_cancelled() { + LspError::request_cancelled() + } else { + error!("Unable to fix import changes: {:#}", err); + LspError::internal_error() + } + })?; + if !changes.is_empty() { + changes_with_modules + .extend(changes.into_iter().map(|c| (c, module.clone()))); + } } } file_text_changes_to_workspace_edit(&changes_with_modules, self, token) diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 0babef22aa..c4de9562d8 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -18893,6 +18893,51 @@ console.log(invalid, validBytes, validText); client.shutdown(); } +/// Regression test for https://github.com/denoland/deno/issues/30380. +#[test] +#[timeout(300_000)] +fn lsp_will_rename_files_js_to_ts() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.write("deno.json", json!({}).to_string()); + let file = temp_dir.source_file("main.ts", "import \"./other.js\";\n"); + let other_file = temp_dir.source_file("other.js", ""); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open_file(&file); + let res = client.write_request( + "workspace/willRenameFiles", + json!({ + "files": [ + { + "oldUri": other_file.uri(), + "newUri": temp_dir.path().join("other.ts").uri_file(), + }, + ], + }), + ); + assert_eq!( + res, + json!({ + "documentChanges": [ + { + "textDocument": { "uri": file.uri(), "version": 1 }, + "edits": [ + { + "range": { + "start": { "line": 0, "character": 8 }, + "end": { "line": 0, "character": 18 }, + }, + "newText": "./other.ts", + }, + ], + }, + ], + }), + ); + client.shutdown(); +} + #[test] #[timeout(300_000)] fn lsp_push_diagnostics() {