Repository: denoland/deno
Pull request: fix(lsp): suggest import map aliases in auto-imports for local directories

## Summary
- When an import map defines a meaningful alias like `"@app/": "./src/"`, auto-import completions now correctly suggest the alias (e.g., `@app/islands/components/Button.tsx`) instead of only offering relative paths (e.g., `../../islands/components/Button.tsx`).
- The `import_map_lookup` function was skipping import map entries whenever the referrer file was inside the mapped directory. This narrowed the skip condition to only apply to passthrough mappings (e.g., `"src/": "./src/"`) where the key matches the last path segment of the resolved address.

Fixes #32288
Fixes #31590

## Test plan
- [x] Added `lsp_auto_import_local_dir_import_map_alias` regression test that verifies `@app/` alias is suggested
- [x] Existing `lsp_auto_import_import_map_prefer_relative` test still passes (passthrough `"src/": "./src/"` still prefers relative paths)
- [x] Existing `lsp_completions_auto_import_and_quick_fix_with_import_map` test still passes
- [x] Existing `lsp_jsr_auto_import_completion_import_map` test still passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: cli/lsp/analysis.rs
patch: @@ -244,16 +244,56 @@ pub fn import_map_lookup(
   import_map: &ImportMap,
   specifier: &Url,
   referrer: &Url,
+) -> Option<String> {
+  import_map_lookup_inner(
+    import_map,
+    specifier,
+    referrer,
+    ReferrerInAddressSkip::SkipAll,
+  )
+}
+
+#[derive(Clone, Copy)]
+enum ReferrerInAddressSkip {
+  /// Skip all entries where the referrer is inside the mapped directory.
+  /// Used for diagnostics where suggesting an alias over a relative path
+  /// within the same mapped directory is not helpful.
+  SkipAll,
+  /// Only skip entries where the import map key is a passthrough mapping
+  /// (key matches the last path segment of the address, e.g.
+  /// `"src/": "./src/"`). For meaningful aliases like `"@app/": "./src/"`,
+  /// the entry is kept so the alias is suggested in auto-imports.
+  SkipPassthrough,
+}
+
+fn import_map_lookup_inner(
+  import_map: &ImportMap,
+  specifier: &Url,
+  referrer: &Url,
+  skip_mode: ReferrerInAddressSkip,
 ) -> Option<String> {
   let specifier_str = specifier.as_str();
   for entry in import_map.entries_for_referrer(referrer) {
     if let Some(address) = entry.value {
       let address_str = address.as_str();
       if referrer.as_str().starts_with(address_str) {
-        // ignore when the referrer has a common base with the
-        // import map entry (ex. `./src/a.ts` importing `./src/b.ts`
-        // and there's a `"$src/": "./src/"` import map entry)
-        continue;
+        match skip_mode {
+          ReferrerInAddressSkip::SkipAll => continue,
+          ReferrerInAddressSkip::SkipPassthrough => {
+            // Only skip if the key is a passthrough mapping, i.e. the
+            // address path ends with the key preceded by '/'
+            // (e.g. ".../src/" ends with "/src/" for key "src/").
+            // Aliases like "@app/" won't match.
+            if address_str
+              .as_bytes()
+              .get(address_str.len().wrapping_sub(entry.raw_key.len() + 1))
+              == Some(&b'/')
+              && address_str.ends_with(entry.raw_key)
+            {
+              continue;
+            }
+          }
+        }
       }
       if address_str == specifier_str {
         return Some(entry.raw_key.to_string());
@@ -474,9 +514,17 @@ impl<'a> TsResponseImportMapper<'a> {
       return Some(bare_package_specifier);
     }
 
-    // check if the import map has this specifier
+    // check if the import map has this specifier, only skipping passthrough
+    // entries (e.g. "src/": "./src/") where a relative path is cleaner.
+    // Meaningful aliases like "@app/": "./src/" are kept so they are
+    // suggested in auto-imports.
     if let Some(import_map) = self.maybe_import_map
-      && let Some(result) = import_map_lookup(import_map, specifier, referrer)
+      && let Some(result) = import_map_lookup_inner(
+        import_map,
+        specifier,
+        referrer,
+        ReferrerInAddressSkip::SkipPassthrough,
+      )
     {
       return Some(result);
     }

filename: tests/integration/lsp_tests.rs
patch: @@ -10165,6 +10165,66 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() {
   client.shutdown();
 }
 
+// Regression test for https://github.com/denoland/deno/issues/32288
+// and https://github.com/denoland/deno/issues/31590.
+// When an import map has a meaningful alias like "@app/": "./src/",
+// auto-imports should suggest the alias even when the referrer is
+// inside the mapped directory.
+#[test(timeout = 300)]
+fn lsp_auto_import_local_dir_import_map_alias() {
+  let context = TestContextBuilder::new().use_temp_cwd().build();
+  let temp_dir = context.temp_dir();
+  temp_dir.write(
+    "deno.json",
+    json!({
+      "imports": {
+        "@app/": "./src/",
+      },
+    })
+    .to_string(),
+  );
+  temp_dir.create_dir_all("src/islands/components");
+  temp_dir.create_dir_all("src/routes");
+  temp_dir.write(
+    "src/islands/components/Button.tsx",
+    "export function Button() { return 'button'; }\n",
+  );
+  let file = temp_dir.source_file("src/routes/index.ts", "Button;\n");
+  let mut client = context.new_lsp_command().build();
+  client.initialize_default();
+  let list = client.get_completion_list(
+    file.uri().as_str(),
+    (0, 6),
+    json!({ "triggerKind": 1 }),
+  );
+  let item = list
+    .items
+    .iter()
+    .find(|i| i.label == "Button")
+    .expect("should have a Button completion item");
+  let res = client.write_request("completionItem/resolve", json!(item));
+  assert_eq!(
+    res,
+    json!({
+      "label": "Button",
+      "labelDetails": { "description": "@app/islands/components/Button.tsx" },
+      "kind": 3,
+      "detail": "Add import from \"@app/islands/components/Button.tsx\"\n\nfunction Button(): string",
+      "sortText": "\u{ffff}16_0",
+      "additionalTextEdits": [
+        {
+          "range": {
+            "start": { "line": 0, "character": 0 },
+            "end": { "line": 0, "character": 0 },
+          },
+          "newText": "import { Button } from \"@app/islands/components/Button.tsx\";\n\n",
+        },
+      ],
+    }),
+  );
+  client.shutdown();
+}
+
 #[test(timeout = 300)]
 fn lsp_auto_import_import_map_prefer_relative() {
   let context = TestContextBuilder::new()