From 4ced997d6398048841d7c7a5c99b9b7a8209ef88 Mon Sep 17 00:00:00 2001 From: Bryant Chandler Date: Thu, 21 Aug 2025 23:31:39 -0700 Subject: [PATCH] feat(search): Add option to disable fuzzy search (#6510) Co-authored-by: Jacob Richman Co-authored-by: Arya Gummadi --- docs/cli/configuration.md | 14 +++- packages/cli/src/config/settingsSchema.ts | 9 +++ .../cli/src/ui/hooks/useAtCompletion.test.ts | 3 + packages/cli/src/ui/hooks/useAtCompletion.ts | 2 + packages/core/src/config/config.ts | 7 ++ .../src/utils/filesearch/fileSearch.test.ts | 78 +++++++++++++++++++ .../core/src/utils/filesearch/fileSearch.ts | 3 +- 7 files changed, 114 insertions(+), 2 deletions(-) diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index af73661ba6..a8d0414240 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -62,14 +62,26 @@ In addition to a project settings file, a project's `.gemini` directory can cont - **Properties:** - **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations. - **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt. + - **`disableFuzzySearch`** (boolean): When `true`, disables the fuzzy search capabilities when searching for files, which can improve performance on projects with a large number of files. - **Example:** ```json "fileFiltering": { "respectGitIgnore": true, - "enableRecursiveFileSearch": false + "enableRecursiveFileSearch": false, + "disableFuzzySearch": true } ``` +### Troubleshooting File Search Performance + +If you are experiencing performance issues with file searching (e.g., with `@` completions), especially in projects with a very large number of files, here are a few things you can try in order of recommendation: + +1. **Use `.geminiignore`:** Create a `.geminiignore` file in your project root to exclude directories that contain a large number of files that you don't need to reference (e.g., build artifacts, logs, `node_modules`). Reducing the total number of files crawled is the most effective way to improve performance. + +2. **Disable Fuzzy Search:** If ignoring files is not enough, you can disable fuzzy search by setting `disableFuzzySearch` to `true` in your `settings.json` file. This will use a simpler, non-fuzzy matching algorithm, which can be faster. + +3. **Disable Recursive File Search:** As a last resort, you can disable recursive file search entirely by setting `enableRecursiveFileSearch` to `false`. This will be the fastest option as it avoids a recursive crawl of your project. However, it means you will need to type the full path to files when using `@` completions. + - **`coreTools`** (array of strings): - **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed. - **Default:** All tools available for use by the Gemini model. diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index d4391cb3a6..19c4c06d6c 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -274,6 +274,15 @@ export const SETTINGS_SCHEMA = { description: 'Enable recursive file search functionality', showInDialog: true, }, + disableFuzzySearch: { + type: 'boolean', + label: 'Disable Fuzzy Search', + category: 'File Filtering', + requiresRestart: true, + default: false, + description: 'Disable fuzzy search when searching for files.', + showInDialog: true, + }, }, }, diff --git a/packages/cli/src/ui/hooks/useAtCompletion.test.ts b/packages/cli/src/ui/hooks/useAtCompletion.test.ts index b7ce447002..4a1b637938 100644 --- a/packages/cli/src/ui/hooks/useAtCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useAtCompletion.test.ts @@ -51,6 +51,7 @@ describe('useAtCompletion', () => { respectGeminiIgnore: true, })), getEnableRecursiveFileSearch: () => true, + getFileFilteringDisableFuzzySearch: () => false, } as unknown as Config; vi.clearAllMocks(); }); @@ -198,6 +199,7 @@ describe('useAtCompletion', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await realFileSearch.initialize(); @@ -468,6 +470,7 @@ describe('useAtCompletion', () => { respectGitIgnore: true, respectGeminiIgnore: true, })), + getFileFilteringDisableFuzzySearch: () => false, } as unknown as Config; const { result } = renderHook(() => diff --git a/packages/cli/src/ui/hooks/useAtCompletion.ts b/packages/cli/src/ui/hooks/useAtCompletion.ts index 5a2571a04b..a01315035f 100644 --- a/packages/cli/src/ui/hooks/useAtCompletion.ts +++ b/packages/cli/src/ui/hooks/useAtCompletion.ts @@ -172,6 +172,8 @@ export function useAtCompletion(props: UseAtCompletionProps): void { cacheTtl: 30, // 30 seconds enableRecursiveFileSearch: config?.getEnableRecursiveFileSearch() ?? true, + disableFuzzySearch: + config?.getFileFilteringDisableFuzzySearch() ?? false, }); await searcher.initialize(); fileSearch.current = searcher; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 20287b8dc6..1219fd3dc5 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -177,6 +177,7 @@ export interface ConfigParameters { respectGitIgnore?: boolean; respectGeminiIgnore?: boolean; enableRecursiveFileSearch?: boolean; + disableFuzzySearch?: boolean; }; checkpointing?: boolean; proxy?: string; @@ -237,6 +238,7 @@ export class Config { respectGitIgnore: boolean; respectGeminiIgnore: boolean; enableRecursiveFileSearch: boolean; + disableFuzzySearch: boolean; }; private fileDiscoveryService: FileDiscoveryService | null = null; private gitService: GitService | undefined = undefined; @@ -316,6 +318,7 @@ export class Config { respectGeminiIgnore: params.fileFiltering?.respectGeminiIgnore ?? true, enableRecursiveFileSearch: params.fileFiltering?.enableRecursiveFileSearch ?? true, + disableFuzzySearch: params.fileFiltering?.disableFuzzySearch ?? false, }; this.checkpointing = params.checkpointing ?? false; this.proxy = params.proxy; @@ -600,6 +603,10 @@ export class Config { return this.fileFiltering.enableRecursiveFileSearch; } + getFileFilteringDisableFuzzySearch(): boolean { + return this.fileFiltering.disableFuzzySearch; + } + getFileFilteringRespectGitIgnore(): boolean { return this.fileFiltering.respectGitIgnore; } diff --git a/packages/core/src/utils/filesearch/fileSearch.test.ts b/packages/core/src/utils/filesearch/fileSearch.test.ts index 316f17ec12..1b61cdf3f7 100644 --- a/packages/core/src/utils/filesearch/fileSearch.test.ts +++ b/packages/core/src/utils/filesearch/fileSearch.test.ts @@ -32,6 +32,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -57,6 +58,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -84,6 +86,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -112,6 +115,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -144,6 +148,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -167,6 +172,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -201,6 +207,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -230,6 +237,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -259,6 +267,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); // Expect no errors to be thrown during initialization @@ -285,6 +294,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -310,6 +320,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -318,6 +329,60 @@ describe('FileSearch', () => { expect(results).toEqual(['src/style.css']); }); + it('should not use fzf for fuzzy matching when disableFuzzySearch is true', async () => { + tmpDir = await createTmpDir({ + src: { + 'file1.js': '', + 'flexible.js': '', + 'other.ts': '', + }, + }); + + const fileSearch = FileSearchFactory.create({ + projectRoot: tmpDir, + useGitignore: false, + useGeminiignore: false, + ignoreDirs: [], + cache: false, + cacheTtl: 0, + enableRecursiveFileSearch: true, + disableFuzzySearch: true, + }); + + await fileSearch.initialize(); + const results = await fileSearch.search('fle'); + + expect(results).toEqual(['src/flexible.js']); + }); + + it('should use fzf for fuzzy matching when disableFuzzySearch is false', async () => { + tmpDir = await createTmpDir({ + src: { + 'file1.js': '', + 'flexible.js': '', + 'other.ts': '', + }, + }); + + const fileSearch = FileSearchFactory.create({ + projectRoot: tmpDir, + useGitignore: false, + useGeminiignore: false, + ignoreDirs: [], + cache: false, + cacheTtl: 0, + enableRecursiveFileSearch: true, + disableFuzzySearch: false, + }); + + await fileSearch.initialize(); + const results = await fileSearch.search('fle'); + + expect(results).toEqual( + expect.arrayContaining(['src/file1.js', 'src/flexible.js']), + ); + }); + it('should return empty array when no matches are found', async () => { tmpDir = await createTmpDir({ src: ['file1.js'], @@ -331,6 +396,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -361,6 +427,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await expect(fileSearch.search('')).rejects.toThrow( @@ -382,6 +449,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -404,6 +472,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -427,6 +496,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -463,6 +533,7 @@ describe('FileSearch', () => { cache: true, // Enable caching for this test cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -502,6 +573,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -545,6 +617,7 @@ describe('FileSearch', () => { cache: true, // Ensure caching is enabled cacheTtl: 10000, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -582,6 +655,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: true, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -611,6 +685,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: false, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -635,6 +710,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: false, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -659,6 +735,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: false, + disableFuzzySearch: false, }); await fileSearch.initialize(); @@ -681,6 +758,7 @@ describe('FileSearch', () => { cache: false, cacheTtl: 0, enableRecursiveFileSearch: false, + disableFuzzySearch: false, }); await fileSearch.initialize(); diff --git a/packages/core/src/utils/filesearch/fileSearch.ts b/packages/core/src/utils/filesearch/fileSearch.ts index 876d37f015..d01800625b 100644 --- a/packages/core/src/utils/filesearch/fileSearch.ts +++ b/packages/core/src/utils/filesearch/fileSearch.ts @@ -20,6 +20,7 @@ export interface FileSearchOptions { cache: boolean; cacheTtl: number; enableRecursiveFileSearch: boolean; + disableFuzzySearch: boolean; maxDepth?: number; } @@ -128,7 +129,7 @@ class RecursiveFileSearch implements FileSearch { filteredCandidates = candidates; } else { let shouldCache = true; - if (pattern.includes('*')) { + if (pattern.includes('*') || this.options.disableFuzzySearch) { filteredCandidates = await filter(candidates, pattern, options.signal); } else { filteredCandidates = await this.fzf