Repository: denoland/deno
Pull request: fix(lsp): fix import specifiers in file rename changes

Fixes #30380.

Changed files:
filename: cli/lsp/analysis.rs
patch: @@ -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<tsc::FileTextChanges>,
+  new_uri: &str,
+  module: &DocumentModule,
+  language_server: &language_server::Inner,
+  token: &CancellationToken,
+) -> Result<Vec<tsc::FileTextChanges>, 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>(

filename: cli/lsp/language_server.rs
patch: @@ -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)

filename: tests/integration/lsp_tests.rs
patch: @@ -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() {