feat: add searchExcludeDirs setting for @ file autocomplete blacklist (#1202)

Implements the directory blacklist feature from #660 (incomplete items 3-4).

Users can now configure directories to exclude from the @ file picker
and fuzzy search via settings.json:

  { "searchExcludeDirs": ["node_modules", ".git", "dist", "build"] }

Changes:
- settings-manager.ts: added searchExcludeDirs setting with get/set
- autocomplete.ts (pi-tui): CombinedAutocompleteProvider accepts
  excludeDirs option, filters excluded directory names in both
  readdir-based and native fuzzy search paths
- interactive-mode.ts: passes searchExcludeDirs to the provider

The native fd fuzzy search already respects .gitignore. This setting
covers directories that aren't gitignored but shouldn't appear in
autocomplete (e.g., large vendor dirs, build outputs in projects
without comprehensive .gitignore).

Fixes #1190
This commit is contained in:
Tom Boucher 2026-03-18 13:23:40 -04:00 committed by GitHub
parent 3c50cbb504
commit 0f97f938f7
3 changed files with 34 additions and 2 deletions

View file

@ -141,6 +141,7 @@ export interface Settings {
editorPaddingX?: number; // Horizontal padding for input editor (default: 0)
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
respectGitignoreInPicker?: boolean; // When false, @ file picker shows gitignored files (default: true)
searchExcludeDirs?: string[]; // Directories to exclude from @ file search (e.g., ["node_modules", ".git", "dist"])
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
markdown?: MarkdownSettings;
memory?: MemorySettings;
@ -1041,6 +1042,16 @@ export class SettingsManager {
this.save();
}
getSearchExcludeDirs(): string[] {
return this.settings.searchExcludeDirs ?? [];
}
setSearchExcludeDirs(dirs: string[]): void {
this.globalSettings.searchExcludeDirs = dirs.filter(Boolean);
this.markModified("searchExcludeDirs");
this.save();
}
getCodeBlockIndent(): string {
return this.settings.markdown?.codeBlockIndent ?? " ";
}

View file

@ -367,7 +367,10 @@ export class InteractiveMode {
this.autocompleteProvider = new CombinedAutocompleteProvider(
[...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList],
process.cwd(),
{ respectGitignore: this.settingsManager.getRespectGitignoreInPicker() },
{
respectGitignore: this.settingsManager.getRespectGitignoreInPicker(),
excludeDirs: this.settingsManager.getSearchExcludeDirs(),
},
);
this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
if (this.editor !== this.defaultEditor) {

View file

@ -131,21 +131,27 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
private commands: (SlashCommand | AutocompleteItem)[];
private basePath: string;
private respectGitignore: boolean;
private excludeDirs: Set<string>;
constructor(
commands: (SlashCommand | AutocompleteItem)[] = [],
basePath: string = process.cwd(),
options?: { respectGitignore?: boolean },
options?: { respectGitignore?: boolean; excludeDirs?: string[] },
) {
this.commands = commands;
this.basePath = basePath;
this.respectGitignore = options?.respectGitignore ?? true;
this.excludeDirs = new Set(options?.excludeDirs ?? []);
}
setRespectGitignore(value: boolean): void {
this.respectGitignore = value;
}
setExcludeDirs(dirs: string[]): void {
this.excludeDirs = new Set(dirs.filter(Boolean));
}
getSuggestions(
lines: string[],
cursorLine: number,
@ -485,6 +491,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
continue;
}
// Skip excluded directories
if (this.excludeDirs.has(entry.name)) {
continue;
}
// Check if entry is a directory (or a symlink pointing to a directory)
let isDirectory = entry.isDirectory();
if (!isDirectory && entry.isSymbolicLink()) {
@ -578,6 +589,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
for (const { path: entryPath, isDirectory } of result.matches) {
// Native module includes trailing / for directories
const pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;
// Skip paths that start with or contain an excluded directory
if (this.excludeDirs.size > 0) {
const segments = pathWithoutSlash.split("/");
if (segments.some(seg => this.excludeDirs.has(seg))) continue;
}
const displayPath = scopedQuery
? this.scopedPathForDisplay(scopedQuery.displayBase, pathWithoutSlash)
: pathWithoutSlash;