fix(tui): keep @ autocomplete responsive in large trees closes #1278
This commit is contained in:
@@ -46,15 +46,23 @@ const requireFdPath = (): string => {
|
||||
return fdPath;
|
||||
};
|
||||
|
||||
const getSuggestions = (
|
||||
provider: CombinedAutocompleteProvider,
|
||||
lines: string[],
|
||||
cursorLine: number,
|
||||
cursorCol: number,
|
||||
force: boolean = false,
|
||||
) => provider.getSuggestions(lines, cursorLine, cursorCol, { signal: new AbortController().signal, force });
|
||||
|
||||
describe("CombinedAutocompleteProvider", () => {
|
||||
describe("extractPathPrefix", () => {
|
||||
it("extracts / from 'hey /' when forced", () => {
|
||||
it("extracts / from 'hey /' when forced", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
||||
const lines = ["hey /"];
|
||||
const cursorLine = 0;
|
||||
const cursorCol = 5; // After the "/"
|
||||
|
||||
const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol);
|
||||
const result = await getSuggestions(provider, lines, cursorLine, cursorCol, true);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for root directory");
|
||||
if (result) {
|
||||
@@ -62,13 +70,13 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("extracts /A from '/A' when forced", () => {
|
||||
it("extracts /A from '/A' when forced", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
||||
const lines = ["/A"];
|
||||
const cursorLine = 0;
|
||||
const cursorCol = 2; // After the "A"
|
||||
|
||||
const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol);
|
||||
const result = await getSuggestions(provider, lines, cursorLine, cursorCol, true);
|
||||
|
||||
console.log("Result:", result);
|
||||
// This might return null if /A doesn't match anything, which is fine
|
||||
@@ -78,25 +86,25 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not trigger for slash commands", () => {
|
||||
it("does not trigger for slash commands", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
||||
const lines = ["/model"];
|
||||
const cursorLine = 0;
|
||||
const cursorCol = 6; // After "model"
|
||||
|
||||
const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol);
|
||||
const result = await getSuggestions(provider, lines, cursorLine, cursorCol, true);
|
||||
|
||||
console.log("Result:", result);
|
||||
assert.strictEqual(result, null, "Should not trigger for slash commands");
|
||||
});
|
||||
|
||||
it("triggers for absolute paths after slash command argument", () => {
|
||||
it("triggers for absolute paths after slash command argument", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
||||
const lines = ["/command /"];
|
||||
const cursorLine = 0;
|
||||
const cursorCol = 10; // After the second "/"
|
||||
|
||||
const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol);
|
||||
const result = await getSuggestions(provider, lines, cursorLine, cursorCol, true);
|
||||
|
||||
console.log("Result:", result);
|
||||
assert.notEqual(result, null, "Should trigger for absolute paths in command arguments");
|
||||
@@ -123,7 +131,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("returns all files and folders for empty @ query", () => {
|
||||
test("returns all files and folders for empty @ query", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["src"],
|
||||
files: {
|
||||
@@ -133,13 +141,13 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value).sort();
|
||||
assert.deepStrictEqual(values, ["@README.md", "@src/"].sort());
|
||||
});
|
||||
|
||||
test("matches file with extension in query", () => {
|
||||
test("matches file with extension in query", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"file.txt": "content",
|
||||
@@ -148,13 +156,13 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@file.txt";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes("@file.txt"));
|
||||
});
|
||||
|
||||
test("filters are case insensitive", () => {
|
||||
test("filters are case insensitive", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["src"],
|
||||
files: {
|
||||
@@ -164,13 +172,13 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@re";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value).sort();
|
||||
assert.deepStrictEqual(values, ["@README.md"]);
|
||||
});
|
||||
|
||||
test("ranks directories before files", () => {
|
||||
test("ranks directories before files", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["src"],
|
||||
files: {
|
||||
@@ -180,7 +188,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@src";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const firstValue = result?.items[0]?.value;
|
||||
const hasSrcFile = result?.items?.some((item) => item.value === "@src.txt");
|
||||
@@ -188,7 +196,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
assert.ok(hasSrcFile);
|
||||
});
|
||||
|
||||
test("returns nested file paths", () => {
|
||||
test("returns nested file paths", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"src/index.ts": "export {};\n",
|
||||
@@ -197,13 +205,13 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@index";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes("@src/index.ts"));
|
||||
});
|
||||
|
||||
test("matches deeply nested paths", () => {
|
||||
test("matches deeply nested paths", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"packages/tui/src/autocomplete.ts": "export {};",
|
||||
@@ -213,14 +221,14 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@tui/src/auto";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes("@packages/tui/src/autocomplete.ts"));
|
||||
assert.ok(!values?.includes("@packages/ai/src/autocomplete.ts"));
|
||||
});
|
||||
|
||||
test("matches directory in middle of path with --full-path", () => {
|
||||
test("matches directory in middle of path with --full-path", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"src/components/Button.tsx": "export {};",
|
||||
@@ -230,14 +238,14 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@components/";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes("@src/components/Button.tsx"));
|
||||
assert.ok(!values?.includes("@src/utils/helpers.ts"));
|
||||
});
|
||||
|
||||
test("scopes fuzzy search to relative directories and searches recursively", () => {
|
||||
test("scopes fuzzy search to relative directories and searches recursively", async () => {
|
||||
setupFolder(outsideDir, {
|
||||
files: {
|
||||
"nested/alpha.ts": "export {};",
|
||||
@@ -248,7 +256,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@../outside/a";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes("@../outside/nested/alpha.ts"));
|
||||
@@ -256,7 +264,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
assert.ok(!values?.includes("@../outside/nested/deeper/zzz.ts"));
|
||||
});
|
||||
|
||||
test("quotes paths with spaces for @ suggestions", () => {
|
||||
test("quotes paths with spaces for @ suggestions", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["my folder"],
|
||||
files: {
|
||||
@@ -266,13 +274,13 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@my";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes('@"my folder/"'));
|
||||
});
|
||||
|
||||
test("includes hidden paths but excludes .git", () => {
|
||||
test("includes hidden paths but excludes .git", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: [".pi", ".github", ".git"],
|
||||
files: {
|
||||
@@ -284,7 +292,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@";
|
||||
const result = provider.getSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value) ?? [];
|
||||
assert.ok(values.includes("@.pi/"));
|
||||
@@ -292,7 +300,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
assert.ok(!values.some((value) => value === "@.git" || value.startsWith("@.git/")));
|
||||
});
|
||||
|
||||
test("continues autocomplete inside quoted @ paths", () => {
|
||||
test("continues autocomplete inside quoted @ paths", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"my folder/test.txt": "content",
|
||||
@@ -302,7 +310,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = '@"my folder/"';
|
||||
const result = provider.getSuggestions([line], 0, line.length - 1);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length - 1);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for quoted folder path");
|
||||
const values = result?.items.map((item) => item.value);
|
||||
@@ -310,7 +318,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
assert.ok(values?.includes('@"my folder/other.txt"'));
|
||||
});
|
||||
|
||||
test("applies quoted @ completion without duplicating closing quote", () => {
|
||||
test("applies quoted @ completion without duplicating closing quote", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"my folder/test.txt": "content",
|
||||
@@ -320,7 +328,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = '@"my folder/te"';
|
||||
const cursorCol = line.length - 1;
|
||||
const result = provider.getSuggestions([line], 0, cursorCol);
|
||||
const result = await getSuggestions(provider, [line], 0, cursorCol);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for quoted @ path");
|
||||
const item = result?.items.find((entry) => entry.value === '@"my folder/test.txt"');
|
||||
@@ -342,7 +350,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("preserves ./ prefix when completing paths", () => {
|
||||
test("preserves ./ prefix when completing paths", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"update.sh": "#!/bin/bash",
|
||||
@@ -352,14 +360,14 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const line = "./up";
|
||||
const result = provider.getForceFileSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, true);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for ./ path");
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes("./update.sh"), `Expected ./update.sh in ${JSON.stringify(values)}`);
|
||||
});
|
||||
|
||||
test("preserves ./ prefix for directory completions", () => {
|
||||
test("preserves ./ prefix for directory completions", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["src"],
|
||||
files: {
|
||||
@@ -369,7 +377,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const line = "./sr";
|
||||
const result = provider.getForceFileSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, true);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for ./ directory path");
|
||||
const values = result?.items.map((item) => item.value);
|
||||
@@ -388,7 +396,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("quotes paths with spaces for direct completion", () => {
|
||||
test("quotes paths with spaces for direct completion", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["my folder"],
|
||||
files: {
|
||||
@@ -398,14 +406,14 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const line = "my";
|
||||
const result = provider.getForceFileSuggestions([line], 0, line.length);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, true);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for path completion");
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes('"my folder/"'));
|
||||
});
|
||||
|
||||
test("continues completion inside quoted paths", () => {
|
||||
test("continues completion inside quoted paths", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"my folder/test.txt": "content",
|
||||
@@ -415,7 +423,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const line = '"my folder/"';
|
||||
const result = provider.getForceFileSuggestions([line], 0, line.length - 1);
|
||||
const result = await getSuggestions(provider, [line], 0, line.length - 1, true);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for quoted folder path");
|
||||
const values = result?.items.map((item) => item.value);
|
||||
@@ -423,7 +431,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
assert.ok(values?.includes('"my folder/other.txt"'));
|
||||
});
|
||||
|
||||
test("applies quoted completion without duplicating closing quote", () => {
|
||||
test("applies quoted completion without duplicating closing quote", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"my folder/test.txt": "content",
|
||||
@@ -433,7 +441,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const line = '"my folder/te"';
|
||||
const cursorCol = line.length - 1;
|
||||
const result = provider.getForceFileSuggestions([line], 0, cursorCol);
|
||||
const result = await getSuggestions(provider, [line], 0, cursorCol, true);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for quoted path");
|
||||
const item = result?.items.find((entry) => entry.value === '"my folder/test.txt"');
|
||||
|
||||
Reference in New Issue
Block a user