summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDevMassive <[email protected]>2025-08-07 08:41:04 +0900
committerGitHub <[email protected]>2025-08-06 23:41:04 +0000
commit9ac3e8b79ecc584805c27d3602612c30f2adee80 (patch)
treeea7f1e12a183b4cf22af83fd7a2730a12ef492ac
parent4782113cebc990b54353e095db1eb6c5e654bdef (diff)
feat: Improve @-command file path completion with fzf integration (#5650)
Co-authored-by: Jacob Richman <[email protected]>
-rw-r--r--package-lock.json7
-rw-r--r--packages/cli/src/ui/hooks/useAtCompletion.test.ts2
-rw-r--r--packages/core/package.json1
-rw-r--r--packages/core/src/utils/filesearch/fileSearch.test.ts24
-rw-r--r--packages/core/src/utils/filesearch/fileSearch.ts17
5 files changed, 49 insertions, 2 deletions
diff --git a/package-lock.json b/package-lock.json
index b16c4904..1e5e4211 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5540,6 +5540,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/fzf": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz",
+ "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
@@ -11889,6 +11895,7 @@
"diff": "^7.0.0",
"dotenv": "^17.1.0",
"fdir": "^6.4.6",
+ "fzf": "^0.5.2",
"glob": "^10.4.5",
"google-auth-library": "^9.11.0",
"html-to-text": "^9.0.5",
diff --git a/packages/cli/src/ui/hooks/useAtCompletion.test.ts b/packages/cli/src/ui/hooks/useAtCompletion.test.ts
index 43289992..aa198fc1 100644
--- a/packages/cli/src/ui/hooks/useAtCompletion.test.ts
+++ b/packages/cli/src/ui/hooks/useAtCompletion.test.ts
@@ -114,8 +114,8 @@ describe('useAtCompletion', () => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'src/',
'src/components/',
- 'src/components/Button.tsx',
'src/index.js',
+ 'src/components/Button.tsx',
]);
});
diff --git a/packages/core/package.json b/packages/core/package.json
index 6e42a4a9..37e3687d 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -35,6 +35,7 @@
"diff": "^7.0.0",
"dotenv": "^17.1.0",
"fdir": "^6.4.6",
+ "fzf": "^0.5.2",
"glob": "^10.4.5",
"google-auth-library": "^9.11.0",
"html-to-text": "^9.0.5",
diff --git a/packages/core/src/utils/filesearch/fileSearch.test.ts b/packages/core/src/utils/filesearch/fileSearch.test.ts
index a7f59f91..38657492 100644
--- a/packages/core/src/utils/filesearch/fileSearch.test.ts
+++ b/packages/core/src/utils/filesearch/fileSearch.test.ts
@@ -290,6 +290,30 @@ describe('FileSearch', () => {
expect(results).toEqual(['src/file1.js', 'src/file2.js']); // Assuming alphabetical sort
});
+ it('should use fzf for fuzzy matching when pattern does not contain wildcards', async () => {
+ tmpDir = await createTmpDir({
+ src: {
+ 'main.js': '',
+ 'util.ts': '',
+ 'style.css': '',
+ },
+ });
+
+ const fileSearch = new FileSearch({
+ projectRoot: tmpDir,
+ useGitignore: false,
+ useGeminiignore: false,
+ ignoreDirs: [],
+ cache: false,
+ cacheTtl: 0,
+ });
+
+ await fileSearch.initialize();
+ const results = await fileSearch.search('sst');
+
+ expect(results).toEqual(['src/style.css']);
+ });
+
it('should return empty array when no matches are found', async () => {
tmpDir = await createTmpDir({
src: ['file1.js'],
diff --git a/packages/core/src/utils/filesearch/fileSearch.ts b/packages/core/src/utils/filesearch/fileSearch.ts
index db14bc65..76a099f7 100644
--- a/packages/core/src/utils/filesearch/fileSearch.ts
+++ b/packages/core/src/utils/filesearch/fileSearch.ts
@@ -11,6 +11,7 @@ import picomatch from 'picomatch';
import { Ignore } from './ignore.js';
import { ResultCache } from './result-cache.js';
import * as cache from './crawlCache.js';
+import { Fzf, FzfResultItem } from 'fzf';
export type FileSearchOptions = {
projectRoot: string;
@@ -77,6 +78,18 @@ export async function filter(
return results;
}
+/**
+ * Filters a list of paths based on a given pattern using fzf.
+ * @param allPaths The list of all paths to filter.
+ * @param pattern The fzf pattern to filter by.
+ * @returns The filtered and sorted list of paths.
+ */
+function filterByFzf(allPaths: string[], pattern: string) {
+ return new Fzf(allPaths)
+ .find(pattern)
+ .map((entry: FzfResultItem) => entry.item);
+}
+
export type SearchOptions = {
signal?: AbortSignal;
maxResults?: number;
@@ -137,7 +150,9 @@ export class FileSearch {
filteredCandidates = candidates;
} else {
// Apply the user's picomatch pattern filter
- filteredCandidates = await filter(candidates, pattern, options.signal);
+ filteredCandidates = pattern.includes('*')
+ ? await filter(candidates, pattern, options.signal)
+ : filterByFzf(this.allFiles, pattern);
this.resultCache!.set(pattern, filteredCandidates);
}