Skip to content

Commit 82dfd28

Browse files
committed
refactor: improve list-files implementation
- Remove redundant path resolution in listFilesWithRipgrep - Convert CRITICAL_IGNORE_PATTERNS to Set for better performance - Standardize error message format across all console.warn calls
1 parent 17d723a commit 82dfd28

File tree

1 file changed

+145
-77
lines changed

1 file changed

+145
-77
lines changed

src/services/glob/list-files.ts

Lines changed: 145 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ import { arePathsEqual } from "../../utils/path"
88
import { getBinPath } from "../../services/ripgrep"
99
import { DIRS_TO_IGNORE } from "./constants"
1010

11+
/**
12+
* Context object for directory scanning operations
13+
*/
14+
interface ScanContext {
15+
/** Whether this is the explicitly targeted directory */
16+
isTargetDir: boolean
17+
/** Whether we're inside an explicitly targeted hidden directory */
18+
insideExplicitHiddenTarget: boolean
19+
/** The base path for the scan operation */
20+
basePath: string
21+
/** The ignore instance for gitignore handling */
22+
ignoreInstance: ReturnType<typeof ignore>
23+
}
24+
1125
/**
1226
* List files in a directory, with optional recursive traversal
1327
*
@@ -70,7 +84,13 @@ async function getFirstLevelDirectories(dirPath: string, ignoreInstance: ReturnT
7084
for (const entry of entries) {
7185
if (entry.isDirectory() && !entry.isSymbolicLink()) {
7286
const fullDirPath = path.join(absolutePath, entry.name)
73-
if (shouldIncludeDirectory(entry.name, fullDirPath, dirPath, ignoreInstance)) {
87+
const context: ScanContext = {
88+
isTargetDir: false,
89+
insideExplicitHiddenTarget: false,
90+
basePath: dirPath,
91+
ignoreInstance,
92+
}
93+
if (shouldIncludeDirectory(entry.name, fullDirPath, context)) {
7494
const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/`
7595
directories.push(formattedPath)
7696
}
@@ -179,10 +199,14 @@ async function listFilesWithRipgrep(
179199
recursive: boolean,
180200
limit: number,
181201
): Promise<string[]> {
182-
const absolutePath = path.resolve(dirPath)
183-
const rgArgs = buildRipgrepArgs(absolutePath, recursive)
202+
const rgArgs = buildRipgrepArgs(dirPath, recursive)
203+
204+
const relativePaths = await execRipgrep(rgPath, rgArgs, limit)
184205

185-
return execRipgrep(rgPath, rgArgs, limit)
206+
// Convert relative paths from ripgrep to absolute paths
207+
// Resolve dirPath once here for the mapping operation
208+
const absolutePath = path.resolve(dirPath)
209+
return relativePaths.map((relativePath) => path.resolve(absolutePath, relativePath))
186210
}
187211

188212
/**
@@ -209,8 +233,11 @@ function buildRecursiveArgs(dirPath: string): string[] {
209233
// (ripgrep does this automatically)
210234

211235
// Check if we're explicitly targeting a hidden directory
212-
// We need to check all parts of the path, not just the basename
213-
const pathParts = dirPath.split(path.sep).filter((part) => part !== "")
236+
// Normalize the path first to handle edge cases
237+
const normalizedPath = path.normalize(dirPath)
238+
// Split by separator and filter out empty parts
239+
// This handles cases like trailing slashes, multiple separators, etc.
240+
const pathParts = normalizedPath.split(path.sep).filter((part) => part.length > 0)
214241
const isTargetingHiddenDir = pathParts.some((part) => part.startsWith("."))
215242

216243
// Get the target directory name to check if it's in the ignore list
@@ -310,7 +337,7 @@ async function createIgnoreInstance(dirPath: string): Promise<ReturnType<typeof
310337
ignoreInstance.add(content)
311338
} catch (err) {
312339
// Continue if we can't read a .gitignore file
313-
console.warn(`Error reading .gitignore at ${gitignoreFile}: ${err}`)
340+
console.warn(`Could not read .gitignore at ${gitignoreFile}: ${err}`)
314341
}
315342
}
316343

@@ -361,11 +388,20 @@ async function listFilteredDirectories(
361388
const absolutePath = path.resolve(dirPath)
362389
const directories: string[] = []
363390

364-
async function scanDirectory(
365-
currentPath: string,
366-
isTargetDir: boolean = false,
367-
insideExplicitHiddenTarget: boolean = false,
368-
): Promise<void> {
391+
// For environment details generation, we don't want to treat the root as a "target"
392+
// if we're doing a general recursive scan, as this would include hidden directories
393+
// Only treat as target if we're explicitly scanning a single hidden directory
394+
const isExplicitHiddenTarget = path.basename(absolutePath).startsWith(".")
395+
396+
// Create initial context for the scan
397+
const initialContext: ScanContext = {
398+
isTargetDir: isExplicitHiddenTarget,
399+
insideExplicitHiddenTarget: isExplicitHiddenTarget,
400+
basePath: dirPath,
401+
ignoreInstance,
402+
}
403+
404+
async function scanDirectory(currentPath: string, context: ScanContext): Promise<void> {
369405
try {
370406
// List all entries in the current directory
371407
const entries = await fs.promises.readdir(currentPath, { withFileTypes: true })
@@ -376,9 +412,15 @@ async function listFilteredDirectories(
376412
const dirName = entry.name
377413
const fullDirPath = path.join(currentPath, dirName)
378414

379-
// Check if this directory should be included
415+
// Create context for subdirectory checks
380416
// Subdirectories found during scanning are never target directories themselves
381-
if (shouldIncludeDirectory(dirName, fullDirPath, dirPath, ignoreInstance, false, insideExplicitHiddenTarget)) {
417+
const subdirContext: ScanContext = {
418+
...context,
419+
isTargetDir: false,
420+
}
421+
422+
// Check if this directory should be included
423+
if (shouldIncludeDirectory(dirName, fullDirPath, subdirContext)) {
382424
// Add the directory to our results (with trailing slash)
383425
// fullDirPath is already absolute since it's built with path.join from absolutePath
384426
const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/`
@@ -393,105 +435,132 @@ async function listFilteredDirectories(
393435
// Use the same logic as shouldIncludeDirectory for recursion decisions
394436
// When inside an explicitly targeted hidden directory, only block critical directories
395437
let shouldRecurseIntoDir = true
396-
if (insideExplicitHiddenTarget) {
438+
if (context.insideExplicitHiddenTarget) {
397439
// Only apply the most critical ignore patterns when inside explicit hidden target
398-
const criticalIgnorePatterns = ["node_modules", ".git", "__pycache__", "venv", "env"]
399-
shouldRecurseIntoDir = !criticalIgnorePatterns.includes(dirName)
440+
shouldRecurseIntoDir = !CRITICAL_IGNORE_PATTERNS.has(dirName)
400441
} else {
401442
shouldRecurseIntoDir = !isDirectoryExplicitlyIgnored(dirName)
402443
}
403444

404445
const shouldRecurse =
405446
recursive &&
406447
shouldRecurseIntoDir &&
407-
!(isHiddenDir && DIRS_TO_IGNORE.includes(".*") && !isTargetDir && !insideExplicitHiddenTarget)
448+
!(
449+
isHiddenDir &&
450+
DIRS_TO_IGNORE.includes(".*") &&
451+
!context.isTargetDir &&
452+
!context.insideExplicitHiddenTarget
453+
)
408454
if (shouldRecurse) {
409455
// If we're entering a hidden directory that's the target, or we're already inside one,
410456
// mark that we're inside an explicitly targeted hidden directory
411-
const newInsideExplicitHiddenTarget = insideExplicitHiddenTarget || (isHiddenDir && isTargetDir)
412-
await scanDirectory(fullDirPath, false, newInsideExplicitHiddenTarget)
457+
const newInsideExplicitHiddenTarget =
458+
context.insideExplicitHiddenTarget || (isHiddenDir && context.isTargetDir)
459+
const newContext: ScanContext = {
460+
...context,
461+
isTargetDir: false,
462+
insideExplicitHiddenTarget: newInsideExplicitHiddenTarget,
463+
}
464+
await scanDirectory(fullDirPath, newContext)
413465
}
414466
}
415467
}
416468
} catch (err) {
417-
// Silently continue if we can't read a directory
469+
// Continue if we can't read a directory
418470
console.warn(`Could not read directory ${currentPath}: ${err}`)
419471
}
420472
}
421473

422474
// Start scanning from the root directory
423-
// For environment details generation, we don't want to treat the root as a "target"
424-
// if we're doing a general recursive scan, as this would include hidden directories
425-
// Only treat as target if we're explicitly scanning a single hidden directory
426-
const isExplicitHiddenTarget = path.basename(absolutePath).startsWith(".")
427-
await scanDirectory(absolutePath, isExplicitHiddenTarget, isExplicitHiddenTarget)
475+
await scanDirectory(absolutePath, initialContext)
428476

429477
return directories
430478
}
431479

432480
/**
433-
* Determine if a directory should be included in results based on filters
481+
* Critical directories that should always be ignored, even inside explicitly targeted hidden directories
482+
*/
483+
const CRITICAL_IGNORE_PATTERNS = new Set(["node_modules", ".git", "__pycache__", "venv", "env"])
484+
485+
/**
486+
* Check if a directory matches any of the given patterns
434487
*/
435-
function shouldIncludeDirectory(
436-
dirName: string,
488+
function matchesIgnorePattern(dirName: string, patterns: string[]): boolean {
489+
for (const pattern of patterns) {
490+
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
491+
return true
492+
}
493+
}
494+
return false
495+
}
496+
497+
/**
498+
* Check if a directory is ignored by gitignore
499+
*/
500+
function isIgnoredByGitignore(
437501
fullDirPath: string,
438502
basePath: string,
439503
ignoreInstance: ReturnType<typeof ignore>,
440-
isTargetDir: boolean = false,
441-
insideExplicitHiddenTarget: boolean = false,
442504
): boolean {
443-
// If this is the explicitly targeted directory, allow it even if it's hidden
444-
// This preserves the ability to explicitly target hidden directories like .roo-memory
445-
if (isTargetDir) {
446-
// Only apply non-hidden-directory ignore rules to target directories
447-
const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*")
448-
for (const pattern of nonHiddenIgnorePatterns) {
449-
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
450-
return false
451-
}
452-
}
453-
return true
454-
}
505+
const relativePath = path.relative(basePath, fullDirPath)
506+
const normalizedPath = relativePath.replace(/\\/g, "/")
507+
return ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")
508+
}
455509

456-
// If we're inside an explicitly targeted hidden directory, allow subdirectories
457-
// even if they would normally be filtered out by the ".*" pattern or other ignore rules
458-
if (insideExplicitHiddenTarget) {
459-
// Only apply the most critical ignore patterns when inside explicit hidden target
460-
// Allow temp, rules, etc. but still block node_modules, .git, etc.
461-
const criticalIgnorePatterns = ["node_modules", ".git", "__pycache__", "venv", "env"]
462-
for (const pattern of criticalIgnorePatterns) {
463-
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
464-
return false
465-
}
466-
}
467-
// Check against gitignore patterns using the ignore library
468-
const relativePath = path.relative(basePath, fullDirPath)
469-
const normalizedPath = relativePath.replace(/\\/g, "/")
470-
if (ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")) {
471-
return false
472-
}
473-
return true
510+
/**
511+
* Check if a target directory should be included
512+
*/
513+
function shouldIncludeTargetDirectory(dirName: string): boolean {
514+
// Only apply non-hidden-directory ignore rules to target directories
515+
const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*")
516+
return !matchesIgnorePattern(dirName, nonHiddenIgnorePatterns)
517+
}
518+
519+
/**
520+
* Check if a directory inside an explicitly targeted hidden directory should be included
521+
*/
522+
function shouldIncludeInsideHiddenTarget(dirName: string, fullDirPath: string, context: ScanContext): boolean {
523+
// Only apply the most critical ignore patterns when inside explicit hidden target
524+
if (CRITICAL_IGNORE_PATTERNS.has(dirName)) {
525+
return false
474526
}
475527

476-
// Check against explicit ignore patterns (excluding the ".*" pattern for now)
528+
// Check against gitignore patterns
529+
return !isIgnoredByGitignore(fullDirPath, context.basePath, context.ignoreInstance)
530+
}
531+
532+
/**
533+
* Check if a regular directory should be included
534+
*/
535+
function shouldIncludeRegularDirectory(dirName: string, fullDirPath: string, context: ScanContext): boolean {
536+
// Check against explicit ignore patterns (excluding the ".*" pattern)
477537
const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*")
478-
for (const pattern of nonHiddenIgnorePatterns) {
479-
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
480-
return false
481-
}
538+
if (matchesIgnorePattern(dirName, nonHiddenIgnorePatterns)) {
539+
return false
482540
}
483541

484-
// Check against gitignore patterns using the ignore library
485-
// Calculate relative path from the base directory
486-
const relativePath = path.relative(basePath, fullDirPath)
487-
const normalizedPath = relativePath.replace(/\\/g, "/")
542+
// Check against gitignore patterns
543+
return !isIgnoredByGitignore(fullDirPath, context.basePath, context.ignoreInstance)
544+
}
488545

489-
// Check if the directory is ignored by .gitignore
490-
if (ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")) {
491-
return false
546+
/**
547+
* Determine if a directory should be included in results based on filters
548+
*/
549+
function shouldIncludeDirectory(dirName: string, fullDirPath: string, context: ScanContext): boolean {
550+
// If this is the explicitly targeted directory, allow it even if it's hidden
551+
// This preserves the ability to explicitly target hidden directories like .roo-memory
552+
if (context.isTargetDir) {
553+
return shouldIncludeTargetDirectory(dirName)
554+
}
555+
556+
// If we're inside an explicitly targeted hidden directory, allow subdirectories
557+
// even if they would normally be filtered out by the ".*" pattern or other ignore rules
558+
if (context.insideExplicitHiddenTarget) {
559+
return shouldIncludeInsideHiddenTarget(dirName, fullDirPath, context)
492560
}
493561

494-
return true
562+
// Regular directory inclusion logic
563+
return shouldIncludeRegularDirectory(dirName, fullDirPath, context)
495564
}
496565

497566
/**
@@ -619,9 +688,8 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi
619688
// Process each complete line
620689
for (const line of lines) {
621690
if (line.trim() && results.length < limit) {
622-
// Convert relative path from ripgrep to absolute path
623-
const absolutePath = path.resolve(searchDir, line)
624-
results.push(absolutePath)
691+
// Keep the relative path as returned by ripgrep
692+
results.push(line)
625693
} else if (results.length >= limit) {
626694
break
627695
}

0 commit comments

Comments
 (0)