fix(tui): scope @ fuzzy search to path prefixes\n\ncloses #1423

This commit is contained in:
Mario Zechner 2026-02-12 21:26:47 +01:00
parent ed0cfcbda2
commit 31f765ff1b
2 changed files with 74 additions and 6 deletions

View file

@ -447,6 +447,42 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
return path;
}
private resolveScopedFuzzyQuery(rawQuery: string): { baseDir: string; query: string; displayBase: string } | null {
const slashIndex = rawQuery.lastIndexOf("/");
if (slashIndex === -1) {
return null;
}
const displayBase = rawQuery.slice(0, slashIndex + 1);
const query = rawQuery.slice(slashIndex + 1);
let baseDir: string;
if (displayBase.startsWith("~/")) {
baseDir = this.expandHomePath(displayBase);
} else if (displayBase.startsWith("/")) {
baseDir = displayBase;
} else {
baseDir = join(this.basePath, displayBase);
}
try {
if (!statSync(baseDir).isDirectory()) {
return null;
}
} catch {
return null;
}
return { baseDir, query, displayBase };
}
private scopedPathForDisplay(displayBase: string, relativePath: string): string {
if (displayBase === "/") {
return `/${relativePath}`;
}
return `${displayBase}${relativePath}`;
}
// Get file/directory suggestions for a given path prefix
private getFileSuggestions(prefix: string): AutocompleteItem[] {
try {
@ -610,13 +646,16 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
}
try {
const entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);
const scopedQuery = this.resolveScopedFuzzyQuery(query);
const fdBaseDir = scopedQuery?.baseDir ?? this.basePath;
const fdQuery = scopedQuery?.query ?? query;
const entries = walkDirectoryWithFd(fdBaseDir, this.fdPath, fdQuery, 100);
// Score entries
const scoredEntries = entries
.map((entry) => ({
...entry,
score: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,
score: fdQuery ? this.scoreEntry(entry.path, fdQuery, entry.isDirectory) : 1,
}))
.filter((entry) => entry.score > 0);
@ -629,8 +668,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
for (const { path: entryPath, isDirectory } of topEntries) {
// fd already includes trailing / for directories
const pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;
const displayPath = scopedQuery
? this.scopedPathForDisplay(scopedQuery.displayBase, pathWithoutSlash)
: pathWithoutSlash;
const entryName = basename(pathWithoutSlash);
const value = buildCompletionValue(entryPath, {
const completionPath = isDirectory ? `${displayPath}/` : displayPath;
const value = buildCompletionValue(completionPath, {
isDirectory,
isAtPrefix: true,
isQuotedPrefix: options.isQuotedPrefix,
@ -639,7 +682,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
suggestions.push({
value,
label: entryName + (isDirectory ? "/" : ""),
description: pathWithoutSlash,
description: displayPath,
});
}

View file

@ -107,14 +107,20 @@ describe("CombinedAutocompleteProvider", () => {
});
describe("fd @ file suggestions", { skip: !isFdInstalled }, () => {
let rootDir = "";
let baseDir = "";
let outsideDir = "";
beforeEach(() => {
baseDir = mkdtempSync(join(tmpdir(), "pi-autocomplete-"));
rootDir = mkdtempSync(join(tmpdir(), "pi-autocomplete-root-"));
baseDir = join(rootDir, "cwd");
outsideDir = join(rootDir, "outside");
mkdirSync(baseDir, { recursive: true });
mkdirSync(outsideDir, { recursive: true });
});
afterEach(() => {
rmSync(baseDir, { recursive: true, force: true });
rmSync(rootDir, { recursive: true, force: true });
});
test("returns all files and folders for empty @ query", () => {
@ -231,6 +237,25 @@ describe("CombinedAutocompleteProvider", () => {
assert.ok(!values?.includes("@src/utils/helpers.ts"));
});
test("scopes fuzzy search to relative directories and searches recursively", () => {
setupFolder(outsideDir, {
files: {
"nested/alpha.ts": "export {};",
"nested/deeper/also-alpha.ts": "export {};",
"nested/deeper/zzz.ts": "export {};",
},
});
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
const line = "@../outside/a";
const result = provider.getSuggestions([line], 0, line.length);
const values = result?.items.map((item) => item.value);
assert.ok(values?.includes("@../outside/nested/alpha.ts"));
assert.ok(values?.includes("@../outside/nested/deeper/also-alpha.ts"));
assert.ok(!values?.includes("@../outside/nested/deeper/zzz.ts"));
});
test("quotes paths with spaces for @ suggestions", () => {
setupFolder(baseDir, {
dirs: ["my folder"],