Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions packages/tui/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ async function walkDirectoryWithFd(
query: string,
maxResults: number,
signal: AbortSignal,
maxDepth?: number,
): Promise<Array<{ path: string; isDirectory: boolean }>> {
const args = [
"--base-directory",
Expand All @@ -147,6 +148,10 @@ async function walkDirectoryWithFd(
".git/**",
];

if (maxDepth !== undefined) {
args.push("--max-depth", String(maxDepth));
}

if (toDisplayPath(query).includes("/")) {
args.push("--full-path");
}
Expand Down Expand Up @@ -716,6 +721,18 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
return score;
}

private async getBaseDirSuggestions(
baseDir: string,
query: string,
signal: AbortSignal,
): Promise<Array<{ path: string; isDirectory: boolean }>> {
if (!this.fdPath || signal.aborted) {
return [];
}

return await walkDirectoryWithFd(baseDir, this.fdPath, query, 100, signal, 1);
}

// Fuzzy file search using fd (fast, respects .gitignore)
private async getFuzzyFileSuggestions(
query: string,
Expand All @@ -729,7 +746,17 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
const scopedQuery = this.resolveScopedFuzzyQuery(query);
const fdBaseDir = scopedQuery?.baseDir ?? this.basePath;
const fdQuery = scopedQuery?.query ?? query;
const entries = await walkDirectoryWithFd(fdBaseDir, this.fdPath, fdQuery, 100, options.signal);
const baseDirEntries = await this.getBaseDirSuggestions(fdBaseDir, fdQuery, options.signal);
const recursiveEntries = await walkDirectoryWithFd(fdBaseDir, this.fdPath, fdQuery, 100, options.signal);
const seenPaths = new Set(baseDirEntries.map((entry) => entry.path));
const entries = [
...baseDirEntries,
...recursiveEntries.filter((entry) => {
if (seenPaths.has(entry.path)) return false;
seenPaths.add(entry.path);
return true;
}),
];
if (options.signal.aborted) {
return [];
}
Expand All @@ -741,7 +768,20 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
}))
.filter((entry) => entry.score > 0);

scoredEntries.sort((a, b) => b.score - a.score);
scoredEntries.sort((a, b) => {
const scoreDiff = b.score - a.score;
if (scoreDiff !== 0) return scoreDiff;

const aDepth = toDisplayPath(a.path).split("/").filter(Boolean).length;
const bDepth = toDisplayPath(b.path).split("/").filter(Boolean).length;
const depthDiff = aDepth - bDepth;
if (depthDiff !== 0) return depthDiff;

const lengthDiff = a.path.length - b.path.length;
if (lengthDiff !== 0) return lengthDiff;

return a.path.localeCompare(b.path);
});
const topEntries = scoredEntries.slice(0, 20);

const suggestions: AutocompleteItem[] = [];
Expand Down
36 changes: 36 additions & 0 deletions packages/tui/test/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,42 @@ describe("CombinedAutocompleteProvider", () => {
assert.ok(!values?.includes("@../outside/nested/deeper/zzz.ts"));
});

test("ranks shallower same-score @ matches before deeper matches", async () => {
setupFolder(baseDir, {
dirs: ["scope/aaa/venv/lib/python3.12/site-packages/pkg/core/profile", "scope/projects"],
});

const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
const line = "@scope/pro";
const result = await getSuggestions(provider, [line], 0, line.length);

const values = result?.items.map((item) => item.value) ?? [];
assert.strictEqual(values[0], "@scope/projects/");
assert.ok(values.includes("@scope/aaa/venv/lib/python3.12/site-packages/pkg/core/profile/"));
});

test("includes scoped direct children when recursive @ matches are flooded", async () => {
const floodedDirs = Array.from(
{ length: 250 },
(_, index) =>
`scope/a${String(index + 1).padStart(3, "0")}/venv/lib/python3.12/site-packages/pkg/core/profile`,
);
setupFolder(baseDir, {
dirs: ["scope/projects", ...floodedDirs],
});

const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
const line = "@scope/pro";
const result = await getSuggestions(provider, [line], 0, line.length);

const values = result?.items.map((item) => item.value) ?? [];
assert.strictEqual(values[0], "@scope/projects/");
assert.ok(
values.some((value) => value.includes("/profile/")),
"Should keep deep fuzzy matches after direct children",
);
});

test("quotes paths with spaces for @ suggestions", async () => {
setupFolder(baseDir, {
dirs: ["my folder"],
Expand Down