From aa8a58df3f5687e1fa70862fb07b281812d13cc8 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Mon, 26 Jan 2026 13:19:09 -0800 Subject: [PATCH] fix: exclude venv and other dependency dirs from @file search (#682) Co-authored-by: Letta --- src/cli/helpers/fileSearch.ts | 64 ++++++++++++++++++++++++++++------- src/tests/fileSearch.test.ts | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 13 deletions(-) diff --git a/src/cli/helpers/fileSearch.ts b/src/cli/helpers/fileSearch.ts index 75cfc62..1e099bd 100644 --- a/src/cli/helpers/fileSearch.ts +++ b/src/cli/helpers/fileSearch.ts @@ -6,6 +6,47 @@ interface FileMatch { type: "file" | "dir" | "url"; } +/** + * Directories to exclude from file search autocomplete. + * These are common dependency/build directories that cause lag when searched. + * All values are lowercase for case-insensitive matching (Windows compatibility). + */ +const IGNORED_DIRECTORIES = new Set([ + // JavaScript/Node + "node_modules", + "dist", + "build", + ".next", + ".nuxt", + "bower_components", + + // Python + "venv", + ".venv", + "__pycache__", + ".tox", + "env", + + // Build outputs + "target", // Rust/Maven/Java + "out", + "coverage", + ".cache", +]); + +/** + * Check if a directory entry should be excluded from search results. + * Uses case-insensitive matching for Windows compatibility. + */ +function shouldExcludeEntry(entry: string): boolean { + // Skip hidden files/directories (starts with .) + if (entry.startsWith(".")) { + return true; + } + // Case-insensitive check for Windows compatibility + return IGNORED_DIRECTORIES.has(entry.toLowerCase()); +} + export function debounce unknown>( func: T, wait: number, @@ -40,13 +81,8 @@ function searchDirectoryRecursive( const entries = readdirSync(dir); for (const entry of entries) { - // Skip hidden files and common ignore patterns - if ( - entry.startsWith(".") || - entry === "node_modules" || - entry === "dist" || - entry === "build" - ) { + // Skip hidden files and common dependency/build directories + if (shouldExcludeEntry(entry)) { continue; } @@ -151,12 +187,14 @@ export async function searchFiles( // Filter entries matching the search pattern // If pattern is empty, show all entries (for when user just types "@") - const matchingEntries = - searchPattern.length === 0 - ? entries - : entries.filter((entry) => - entry.toLowerCase().includes(searchPattern.toLowerCase()), - ); + // Also exclude common dependency/build directories + const matchingEntries = entries + .filter((entry) => !shouldExcludeEntry(entry)) + .filter( + (entry) => + searchPattern.length === 0 || + entry.toLowerCase().includes(searchPattern.toLowerCase()), + ); // Get stats for each matching entry for (const entry of matchingEntries.slice(0, 50)) { diff --git a/src/tests/fileSearch.test.ts b/src/tests/fileSearch.test.ts index f062aa0..119bb29 100644 --- a/src/tests/fileSearch.test.ts +++ b/src/tests/fileSearch.test.ts @@ -133,6 +133,67 @@ test("searchFiles skips node_modules (deep)", async () => { expect(results.some((r) => r.path.includes("index.ts"))).toBe(true); }); +test("searchFiles skips venv directories (deep)", async () => { + const originalCwd = process.cwd(); + process.chdir(TEST_DIR); + + // Create venv directory (Python virtual environment) + mkdirSync(join(TEST_DIR, "venv/lib"), { recursive: true }); + writeFileSync(join(TEST_DIR, "venv/lib/module.py"), "# python"); + + // Also test .venv (common alternative) + mkdirSync(join(TEST_DIR, ".venv/lib"), { recursive: true }); + writeFileSync(join(TEST_DIR, ".venv/lib/other.py"), "# python"); + + const results = await searchFiles("module", true); + + process.chdir(originalCwd); + + // Should not find files in venv or .venv + expect(results.some((r) => r.path.includes("venv"))).toBe(false); + expect(results.some((r) => r.path.includes(".venv"))).toBe(false); +}); + +test("searchFiles skips excluded directories in shallow search", async () => { + const originalCwd = process.cwd(); + process.chdir(TEST_DIR); + + // Create excluded directories + mkdirSync(join(TEST_DIR, "node_modules"), { recursive: true }); + mkdirSync(join(TEST_DIR, "venv"), { recursive: true }); + mkdirSync(join(TEST_DIR, "__pycache__"), { recursive: true }); + + const results = await searchFiles("", false); + + process.chdir(originalCwd); + + // Should not include excluded directories in shallow search + expect(results.some((r) => r.path === "node_modules")).toBe(false); + expect(results.some((r) => r.path === "venv")).toBe(false); + expect(results.some((r) => r.path === "__pycache__")).toBe(false); + // But should still include non-excluded directories + expect(results.some((r) => r.path === "src")).toBe(true); +}); + +test("searchFiles uses case-insensitive exclusion for directory names", async () => { + const originalCwd = process.cwd(); + process.chdir(TEST_DIR); + + // Create directory with different casing (Windows-style) + // This tests that Node_Modules or NODE_MODULES would be excluded + mkdirSync(join(TEST_DIR, "Node_Modules/pkg"), { recursive: true }); + writeFileSync(join(TEST_DIR, "Node_Modules/pkg/test.js"), "module"); + + const results = await searchFiles("test", true); + + process.chdir(originalCwd); + + // Should not find files in Node_Modules (case-insensitive match to node_modules) + expect( + results.some((r) => r.path.toLowerCase().includes("node_modules")), + ).toBe(false); +}); + test("searchFiles handles relative path queries", async () => { const originalCwd = process.cwd(); process.chdir(TEST_DIR);