fix: exclude venv and other dependency dirs from @file search (#682)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-26 13:19:09 -08:00
committed by GitHub
parent 2a1fde3f62
commit aa8a58df3f
2 changed files with 112 additions and 13 deletions

View File

@@ -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<T extends (...args: never[]) => 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)) {

View File

@@ -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);