From 9ac3e8b79ecc584805c27d3602612c30f2adee80 Mon Sep 17 00:00:00 2001 From: DevMassive <76215222+DevMassive@users.noreply.github.com> Date: Thu, 7 Aug 2025 08:41:04 +0900 Subject: [PATCH] feat: Improve @-command file path completion with fzf integration (#5650) Co-authored-by: Jacob Richman --- package-lock.json | 7 ++++++ .../cli/src/ui/hooks/useAtCompletion.test.ts | 2 +- packages/core/package.json | 1 + .../src/utils/filesearch/fileSearch.test.ts | 24 +++++++++++++++++++ .../core/src/utils/filesearch/fileSearch.ts | 17 ++++++++++++- 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); }