From 780d536727653ae07983621ba44d69377f7ec1e4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 22 Apr 2026 00:35:19 +0200 Subject: [PATCH] fix(tui): follow symlinks in fuzzy autocomplete closes #3507 --- packages/tui/CHANGELOG.md | 4 ++ packages/tui/src/autocomplete.ts | 1 + packages/tui/test/autocomplete.test.ts | 57 +++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 324092c5..24cd8446 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `@` autocomplete fuzzy search to follow symlinked directories and include symlinked paths in results ([#3507](https://github.com/badlogic/pi-mono/issues/3507)) + ## [0.68.0] - 2026-04-20 ### Added diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 9c950f0e..80795192 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -137,6 +137,7 @@ async function walkDirectoryWithFd( "f", "--type", "d", + "--follow", "--hidden", "--exclude", ".git", diff --git a/packages/tui/test/autocomplete.test.ts b/packages/tui/test/autocomplete.test.ts index 5ca2a028..6940edb6 100644 --- a/packages/tui/test/autocomplete.test.ts +++ b/packages/tui/test/autocomplete.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, beforeEach, describe, it, test } from "node:test"; @@ -300,6 +300,61 @@ describe("CombinedAutocompleteProvider", () => { assert.ok(!values.some((value) => value === "@.git" || value.startsWith("@.git/"))); }); + test("follows symlinked directories for fuzzy @ search", async () => { + setupFolder(baseDir, { + files: { + "dir/some_file.txt": "real", + }, + }); + setupFolder(outsideDir, { + files: { + "some_file.txt": "symlinked", + }, + }); + symlinkSync("../outside", join(baseDir, "symlinked_dir")); + + const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath()); + const line = "@some"; + const result = await getSuggestions(provider, [line], 0, line.length); + + const values = result?.items.map((item) => item.value) ?? []; + assert.ok(values.includes("@dir/some_file.txt")); + assert.ok(values.includes("@symlinked_dir/some_file.txt")); + }); + + test("returns symlinked directories when matching their name", async () => { + setupFolder(outsideDir, { + files: { + "nested/file.txt": "symlinked", + }, + }); + symlinkSync("../outside", join(baseDir, "symlinked_dir")); + + const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath()); + const line = "@symlinked"; + const result = await getSuggestions(provider, [line], 0, line.length); + + const values = result?.items.map((item) => item.value) ?? []; + assert.ok(values.includes("@symlinked_dir/")); + }); + + test("returns symlinked files without requiring type l", async () => { + setupFolder(baseDir, { + files: { + "original.txt": "content", + }, + }); + const linkPath = join(baseDir, "link.txt"); + symlinkSync("original.txt", linkPath); + + const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath()); + const line = "@link"; + const result = await getSuggestions(provider, [line], 0, line.length); + + const values = result?.items.map((item) => item.value) ?? []; + assert.ok(values.includes("@link.txt")); + }); + test("returns the same @ suggestions when the cwd path contains the query", async () => { const normalBaseDir = join(rootDir, "cwd-normal"); const queryInPathBaseDir = join(rootDir, "cwd-plan-repro");