Add setting enableRecursiveFileSearch to control @-file completion (#1290)

This commit is contained in:
Billy Biggs 2025-06-21 18:23:35 -07:00 committed by GitHub
parent 63f6a497cb
commit 0779697da6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 77 additions and 13 deletions

View File

@ -56,13 +56,15 @@ In addition to a project settings file, a project's `.gemini` directory can cont
- **`fileFiltering`** (object): - **`fileFiltering`** (object):
- **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools. - **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools.
- **Default:** `"respectGitIgnore": true` - **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true`
- **Properties:** - **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. - **`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.
- **Example:** - **Example:**
```json ```json
"fileFiltering": { "fileFiltering": {
"respectGitIgnore": true "respectGitIgnore": true,
"enableRecursiveFileSearch": false
} }
``` ```

View File

@ -75,7 +75,9 @@ describe('Configuration Integration Tests', () => {
sandbox: false, sandbox: false,
targetDir: tempDir, targetDir: tempDir,
debugMode: false, debugMode: false,
fileFilteringRespectGitIgnore: false, fileFiltering: {
respectGitIgnore: false,
},
}; };
const config = new Config(configParams); const config = new Config(configParams);
@ -109,7 +111,9 @@ describe('Configuration Integration Tests', () => {
sandbox: false, sandbox: false,
targetDir: tempDir, targetDir: tempDir,
debugMode: false, debugMode: false,
fileFilteringRespectGitIgnore: false, fileFiltering: {
respectGitIgnore: false,
},
}; };
const config = new Config(configParams); const config = new Config(configParams);
@ -178,7 +182,9 @@ describe('Configuration Integration Tests', () => {
sandbox: false, sandbox: false,
targetDir: tempDir, targetDir: tempDir,
debugMode: false, debugMode: false,
fileFilteringRespectGitIgnore: false, // CI might need to see all files fileFiltering: {
respectGitIgnore: false,
}, // CI might need to see all files
}; };
const config = new Config(configParams); const config = new Config(configParams);

View File

@ -228,7 +228,11 @@ export async function loadCliConfig(
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts, logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
}, },
// Git-aware file filtering settings // Git-aware file filtering settings
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore, fileFiltering: {
respectGitIgnore: settings.fileFiltering?.respectGitIgnore,
enableRecursiveFileSearch:
settings.fileFiltering?.enableRecursiveFileSearch,
},
checkpointing: argv.checkpointing || settings.checkpointing?.enabled, checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
proxy: proxy:
process.env.HTTPS_PROXY || process.env.HTTPS_PROXY ||

View File

@ -56,6 +56,7 @@ export interface Settings {
// Git-aware file filtering settings // Git-aware file filtering settings
fileFiltering?: { fileFiltering?: {
respectGitIgnore?: boolean; respectGitIgnore?: boolean;
enableRecursiveFileSearch?: boolean;
}; };
// UI setting. Does not display the ANSI-controlled terminal title. // UI setting. Does not display the ANSI-controlled terminal title.

View File

@ -47,6 +47,7 @@ describe('useCompletion git-aware filtering integration', () => {
mockConfig = { mockConfig = {
getFileFilteringRespectGitIgnore: vi.fn(() => true), getFileFilteringRespectGitIgnore: vi.fn(() => true),
getFileService: vi.fn().mockReturnValue(mockFileDiscoveryService), getFileService: vi.fn().mockReturnValue(mockFileDiscoveryService),
getEnableRecursiveFileSearch: vi.fn(() => true),
}; };
vi.mocked(FileDiscoveryService).mockImplementation( vi.mocked(FileDiscoveryService).mockImplementation(
@ -170,6 +171,35 @@ describe('useCompletion git-aware filtering integration', () => {
); );
}); });
it('should not perform recursive search when disabled in config', async () => {
const globResults = [`${testCwd}/data`, `${testCwd}/dist`];
vi.mocked(glob).mockResolvedValue(globResults);
// Disable recursive search in the mock config
const mockConfigNoRecursive = {
...mockConfig,
getEnableRecursiveFileSearch: vi.fn(() => false),
};
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'data', isDirectory: () => true },
{ name: 'dist', isDirectory: () => true },
] as Array<{ name: string; isDirectory: () => boolean }>);
renderHook(() =>
useCompletion('@d', testCwd, true, slashCommands, mockConfigNoRecursive),
);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 150));
});
// `glob` should not be called because recursive search is disabled
expect(glob).not.toHaveBeenCalled();
// `fs.readdir` should be called for the top-level directory instead
expect(fs.readdir).toHaveBeenCalledWith(testCwd, { withFileTypes: true });
});
it('should work without config (fallback behavior)', async () => { it('should work without config (fallback behavior)', async () => {
vi.mocked(fs.readdir).mockResolvedValue([ vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'src', isDirectory: () => true }, { name: 'src', isDirectory: () => true },

View File

@ -322,10 +322,16 @@ export function useCompletion(
let fetchedSuggestions: Suggestion[] = []; let fetchedSuggestions: Suggestion[] = [];
const fileDiscoveryService = config ? config.getFileService() : null; const fileDiscoveryService = config ? config.getFileService() : null;
const enableRecursiveSearch =
config?.getEnableRecursiveFileSearch() ?? true;
try { try {
// If there's no slash, or it's the root, do a recursive search from cwd // If there's no slash, or it's the root, do a recursive search from cwd
if (partialPath.indexOf('/') === -1 && prefix) { if (
partialPath.indexOf('/') === -1 &&
prefix &&
enableRecursiveSearch
) {
if (fileDiscoveryService) { if (fileDiscoveryService) {
fetchedSuggestions = await findFilesWithGlob( fetchedSuggestions = await findFilesWithGlob(
prefix, prefix,

View File

@ -163,7 +163,9 @@ describe('Server Config (config.ts)', () => {
it('should set custom file filtering settings when provided', () => { it('should set custom file filtering settings when provided', () => {
const paramsWithFileFiltering: ConfigParameters = { const paramsWithFileFiltering: ConfigParameters = {
...baseParams, ...baseParams,
fileFilteringRespectGitIgnore: false, fileFiltering: {
respectGitIgnore: false,
},
}; };
const config = new Config(paramsWithFileFiltering); const config = new Config(paramsWithFileFiltering);
expect(config.getFileFilteringRespectGitIgnore()).toBe(false); expect(config.getFileFilteringRespectGitIgnore()).toBe(false);

View File

@ -104,7 +104,10 @@ export interface ConfigParameters {
contextFileName?: string | string[]; contextFileName?: string | string[];
accessibility?: AccessibilitySettings; accessibility?: AccessibilitySettings;
telemetry?: TelemetrySettings; telemetry?: TelemetrySettings;
fileFilteringRespectGitIgnore?: boolean; fileFiltering?: {
respectGitIgnore?: boolean;
enableRecursiveFileSearch?: boolean;
};
checkpointing?: boolean; checkpointing?: boolean;
proxy?: string; proxy?: string;
cwd: string; cwd: string;
@ -136,7 +139,10 @@ export class Config {
private readonly accessibility: AccessibilitySettings; private readonly accessibility: AccessibilitySettings;
private readonly telemetrySettings: TelemetrySettings; private readonly telemetrySettings: TelemetrySettings;
private geminiClient!: GeminiClient; private geminiClient!: GeminiClient;
private readonly fileFilteringRespectGitIgnore: boolean; private readonly fileFiltering: {
respectGitIgnore: boolean;
enableRecursiveFileSearch: boolean;
};
private fileDiscoveryService: FileDiscoveryService | null = null; private fileDiscoveryService: FileDiscoveryService | null = null;
private gitService: GitService | undefined = undefined; private gitService: GitService | undefined = undefined;
private readonly checkpointing: boolean; private readonly checkpointing: boolean;
@ -172,8 +178,11 @@ export class Config {
logPrompts: params.telemetry?.logPrompts ?? true, logPrompts: params.telemetry?.logPrompts ?? true,
}; };
this.fileFilteringRespectGitIgnore = this.fileFiltering = {
params.fileFilteringRespectGitIgnore ?? true; respectGitIgnore: params.fileFiltering?.respectGitIgnore ?? true,
enableRecursiveFileSearch:
params.fileFiltering?.enableRecursiveFileSearch ?? true,
};
this.checkpointing = params.checkpointing ?? false; this.checkpointing = params.checkpointing ?? false;
this.proxy = params.proxy; this.proxy = params.proxy;
this.cwd = params.cwd ?? process.cwd(); this.cwd = params.cwd ?? process.cwd();
@ -330,8 +339,12 @@ export class Config {
return getProjectTempDir(this.getProjectRoot()); return getProjectTempDir(this.getProjectRoot());
} }
getEnableRecursiveFileSearch(): boolean {
return this.fileFiltering.enableRecursiveFileSearch;
}
getFileFilteringRespectGitIgnore(): boolean { getFileFilteringRespectGitIgnore(): boolean {
return this.fileFilteringRespectGitIgnore; return this.fileFiltering.respectGitIgnore;
} }
getCheckpointingEnabled(): boolean { getCheckpointingEnabled(): boolean {