Repository: denoland/deno
Pull request: fix(lsp): import rewrites in moved file

Fixes #30627.

Changed files:
filename: cli/lsp/analysis.rs
patch: @@ -547,6 +547,7 @@ impl<'a> TsResponseImportMapper<'a> {
     specifier: &str,
     referrer: &ModuleSpecifier,
     resolution_mode: ResolutionMode,
+    new_file_hints: &[Url],
   ) -> Option<String> {
     let specifier_stem = specifier.strip_suffix(".js").unwrap_or(specifier);
     let specifiers = std::iter::once(Cow::Borrowed(specifier)).chain(
@@ -569,9 +570,10 @@ impl<'a> TsResponseImportMapper<'a> {
         .ok()
         .and_then(|s| self.tsc_specifier_map.normalize(s.as_str()).ok())
         .filter(|s| {
-          self
-            .document_modules
-            .specifier_exists(s, self.scope.as_deref())
+          new_file_hints.contains(s)
+            || self
+              .document_modules
+              .specifier_exists(s, self.scope.as_deref())
         })
         && let Some(specifier) = self
           .check_specifier(&specifier, referrer)
@@ -692,6 +694,11 @@ pub fn fix_ts_import_changes(
   token: &CancellationToken,
 ) -> Result<Vec<tsc::FileTextChanges>, AnyError> {
   let mut r = Vec::new();
+  let new_file_hints = changes
+    .iter()
+    .filter(|c| c.is_new_file.unwrap_or(false))
+    .filter_map(|c| resolve_url(&c.file_name).ok())
+    .collect::<Vec<_>>();
   for change in changes {
     if token.is_cancelled() {
       return Err(anyhow!("request cancelled"));
@@ -733,6 +740,7 @@ pub fn fix_ts_import_changes(
                 specifier,
                 &target_specifier,
                 resolution_mode,
+                &new_file_hints,
               )
             {
               line.replace(specifier, &new_specifier)
@@ -762,7 +770,7 @@ pub fn fix_ts_import_changes(
 pub fn fix_ts_import_changes_for_file_rename(
   changes: Vec<tsc::FileTextChanges>,
   new_uri: &str,
-  module: &DocumentModule,
+  old_module: &DocumentModule,
   language_server: &language_server::Inner,
   token: &CancellationToken,
 ) -> Result<Vec<tsc::FileTextChanges>, AnyError> {
@@ -772,7 +780,7 @@ pub fn fix_ts_import_changes_for_file_rename(
   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 new_file_hints = [uri_to_url(&new_uri)];
   let mut r = Vec::with_capacity(changes.len());
   for mut change in changes {
     if token.is_cancelled() {
@@ -781,13 +789,24 @@ pub fn fix_ts_import_changes_for_file_rename(
     let Ok(target_specifier) = resolve_url(&change.file_name) else {
       continue;
     };
-    let import_mapper = language_server.get_ts_response_import_mapper(module);
+    let Some(target_module) =
+      language_server.document_modules.module_for_specifier(
+        &target_specifier,
+        old_module.scope.as_deref(),
+        Some(&old_module.compiler_options_key),
+      )
+    else {
+      continue;
+    };
+    let import_mapper =
+      language_server.get_ts_response_import_mapper(&target_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/"))
-      {
+      if let Some(new_specifier) = import_mapper.check_unresolved_specifier(
+        &text_change.new_text,
+        &target_module.specifier,
+        target_module.resolution_mode,
+        &new_file_hints,
+      ) {
         text_change.new_text = new_specifier;
       }
     }
@@ -822,6 +841,12 @@ fn fix_ts_import_action<'a>(
     specifier,
     &module.specifier,
     module.resolution_mode,
+    &action
+      .changes
+      .iter()
+      .filter(|c| c.is_new_file.unwrap_or(false))
+      .filter_map(|c| resolve_url(&c.file_name).ok())
+      .collect::<Vec<_>>(),
   ) {
     let description = action.description.replace(specifier, &new_specifier);
     let changes = action

filename: tests/integration/lsp_tests.rs
patch: @@ -19063,6 +19063,51 @@ fn lsp_will_rename_files_js_to_ts() {
   client.shutdown();
 }
 
+/// Regression test for https://github.com/denoland/deno/issues/30627.
+#[test]
+#[timeout(300_000)]
+fn lsp_will_rename_files_move_to_different_dir() {
+  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.ts\";\n");
+  temp_dir.write("other.ts", "");
+  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": file.uri(),
+          "newUri": temp_dir.path().join("subdir/main.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() {