mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
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
This commit is contained in:
parent
17d723a539
commit
82dfd28f1f
1 changed files with 146 additions and 78 deletions
|
|
@ -8,6 +8,20 @@ import { arePathsEqual } from "../../utils/path"
|
||||||
import { getBinPath } from "../../services/ripgrep"
|
import { getBinPath } from "../../services/ripgrep"
|
||||||
import { DIRS_TO_IGNORE } from "./constants"
|
import { DIRS_TO_IGNORE } from "./constants"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context object for directory scanning operations
|
||||||
|
*/
|
||||||
|
interface ScanContext {
|
||||||
|
/** Whether this is the explicitly targeted directory */
|
||||||
|
isTargetDir: boolean
|
||||||
|
/** Whether we're inside an explicitly targeted hidden directory */
|
||||||
|
insideExplicitHiddenTarget: boolean
|
||||||
|
/** The base path for the scan operation */
|
||||||
|
basePath: string
|
||||||
|
/** The ignore instance for gitignore handling */
|
||||||
|
ignoreInstance: ReturnType<typeof ignore>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List files in a directory, with optional recursive traversal
|
* List files in a directory, with optional recursive traversal
|
||||||
*
|
*
|
||||||
|
|
@ -70,7 +84,13 @@ async function getFirstLevelDirectories(dirPath: string, ignoreInstance: ReturnT
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
||||||
const fullDirPath = path.join(absolutePath, entry.name)
|
const fullDirPath = path.join(absolutePath, entry.name)
|
||||||
if (shouldIncludeDirectory(entry.name, fullDirPath, dirPath, ignoreInstance)) {
|
const context: ScanContext = {
|
||||||
|
isTargetDir: false,
|
||||||
|
insideExplicitHiddenTarget: false,
|
||||||
|
basePath: dirPath,
|
||||||
|
ignoreInstance,
|
||||||
|
}
|
||||||
|
if (shouldIncludeDirectory(entry.name, fullDirPath, context)) {
|
||||||
const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/`
|
const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/`
|
||||||
directories.push(formattedPath)
|
directories.push(formattedPath)
|
||||||
}
|
}
|
||||||
|
|
@ -179,10 +199,14 @@ async function listFilesWithRipgrep(
|
||||||
recursive: boolean,
|
recursive: boolean,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const absolutePath = path.resolve(dirPath)
|
const rgArgs = buildRipgrepArgs(dirPath, recursive)
|
||||||
const rgArgs = buildRipgrepArgs(absolutePath, recursive)
|
|
||||||
|
|
||||||
return execRipgrep(rgPath, rgArgs, limit)
|
const relativePaths = await execRipgrep(rgPath, rgArgs, limit)
|
||||||
|
|
||||||
|
// Convert relative paths from ripgrep to absolute paths
|
||||||
|
// Resolve dirPath once here for the mapping operation
|
||||||
|
const absolutePath = path.resolve(dirPath)
|
||||||
|
return relativePaths.map((relativePath) => path.resolve(absolutePath, relativePath))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -209,8 +233,11 @@ function buildRecursiveArgs(dirPath: string): string[] {
|
||||||
// (ripgrep does this automatically)
|
// (ripgrep does this automatically)
|
||||||
|
|
||||||
// Check if we're explicitly targeting a hidden directory
|
// Check if we're explicitly targeting a hidden directory
|
||||||
// We need to check all parts of the path, not just the basename
|
// Normalize the path first to handle edge cases
|
||||||
const pathParts = dirPath.split(path.sep).filter((part) => part !== "")
|
const normalizedPath = path.normalize(dirPath)
|
||||||
|
// Split by separator and filter out empty parts
|
||||||
|
// This handles cases like trailing slashes, multiple separators, etc.
|
||||||
|
const pathParts = normalizedPath.split(path.sep).filter((part) => part.length > 0)
|
||||||
const isTargetingHiddenDir = pathParts.some((part) => part.startsWith("."))
|
const isTargetingHiddenDir = pathParts.some((part) => part.startsWith("."))
|
||||||
|
|
||||||
// Get the target directory name to check if it's in the ignore list
|
// 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
|
||||||
ignoreInstance.add(content)
|
ignoreInstance.add(content)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Continue if we can't read a .gitignore file
|
// Continue if we can't read a .gitignore file
|
||||||
console.warn(`Error reading .gitignore at ${gitignoreFile}: ${err}`)
|
console.warn(`Could not read .gitignore at ${gitignoreFile}: ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,11 +388,20 @@ async function listFilteredDirectories(
|
||||||
const absolutePath = path.resolve(dirPath)
|
const absolutePath = path.resolve(dirPath)
|
||||||
const directories: string[] = []
|
const directories: string[] = []
|
||||||
|
|
||||||
async function scanDirectory(
|
// For environment details generation, we don't want to treat the root as a "target"
|
||||||
currentPath: string,
|
// if we're doing a general recursive scan, as this would include hidden directories
|
||||||
isTargetDir: boolean = false,
|
// Only treat as target if we're explicitly scanning a single hidden directory
|
||||||
insideExplicitHiddenTarget: boolean = false,
|
const isExplicitHiddenTarget = path.basename(absolutePath).startsWith(".")
|
||||||
): Promise<void> {
|
|
||||||
|
// Create initial context for the scan
|
||||||
|
const initialContext: ScanContext = {
|
||||||
|
isTargetDir: isExplicitHiddenTarget,
|
||||||
|
insideExplicitHiddenTarget: isExplicitHiddenTarget,
|
||||||
|
basePath: dirPath,
|
||||||
|
ignoreInstance,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanDirectory(currentPath: string, context: ScanContext): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// List all entries in the current directory
|
// List all entries in the current directory
|
||||||
const entries = await fs.promises.readdir(currentPath, { withFileTypes: true })
|
const entries = await fs.promises.readdir(currentPath, { withFileTypes: true })
|
||||||
|
|
@ -376,9 +412,15 @@ async function listFilteredDirectories(
|
||||||
const dirName = entry.name
|
const dirName = entry.name
|
||||||
const fullDirPath = path.join(currentPath, dirName)
|
const fullDirPath = path.join(currentPath, dirName)
|
||||||
|
|
||||||
// Check if this directory should be included
|
// Create context for subdirectory checks
|
||||||
// Subdirectories found during scanning are never target directories themselves
|
// Subdirectories found during scanning are never target directories themselves
|
||||||
if (shouldIncludeDirectory(dirName, fullDirPath, dirPath, ignoreInstance, false, insideExplicitHiddenTarget)) {
|
const subdirContext: ScanContext = {
|
||||||
|
...context,
|
||||||
|
isTargetDir: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this directory should be included
|
||||||
|
if (shouldIncludeDirectory(dirName, fullDirPath, subdirContext)) {
|
||||||
// Add the directory to our results (with trailing slash)
|
// Add the directory to our results (with trailing slash)
|
||||||
// fullDirPath is already absolute since it's built with path.join from absolutePath
|
// fullDirPath is already absolute since it's built with path.join from absolutePath
|
||||||
const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/`
|
const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/`
|
||||||
|
|
@ -393,10 +435,9 @@ async function listFilteredDirectories(
|
||||||
// Use the same logic as shouldIncludeDirectory for recursion decisions
|
// Use the same logic as shouldIncludeDirectory for recursion decisions
|
||||||
// When inside an explicitly targeted hidden directory, only block critical directories
|
// When inside an explicitly targeted hidden directory, only block critical directories
|
||||||
let shouldRecurseIntoDir = true
|
let shouldRecurseIntoDir = true
|
||||||
if (insideExplicitHiddenTarget) {
|
if (context.insideExplicitHiddenTarget) {
|
||||||
// Only apply the most critical ignore patterns when inside explicit hidden target
|
// Only apply the most critical ignore patterns when inside explicit hidden target
|
||||||
const criticalIgnorePatterns = ["node_modules", ".git", "__pycache__", "venv", "env"]
|
shouldRecurseIntoDir = !CRITICAL_IGNORE_PATTERNS.has(dirName)
|
||||||
shouldRecurseIntoDir = !criticalIgnorePatterns.includes(dirName)
|
|
||||||
} else {
|
} else {
|
||||||
shouldRecurseIntoDir = !isDirectoryExplicitlyIgnored(dirName)
|
shouldRecurseIntoDir = !isDirectoryExplicitlyIgnored(dirName)
|
||||||
}
|
}
|
||||||
|
|
@ -404,94 +445,122 @@ async function listFilteredDirectories(
|
||||||
const shouldRecurse =
|
const shouldRecurse =
|
||||||
recursive &&
|
recursive &&
|
||||||
shouldRecurseIntoDir &&
|
shouldRecurseIntoDir &&
|
||||||
!(isHiddenDir && DIRS_TO_IGNORE.includes(".*") && !isTargetDir && !insideExplicitHiddenTarget)
|
!(
|
||||||
|
isHiddenDir &&
|
||||||
|
DIRS_TO_IGNORE.includes(".*") &&
|
||||||
|
!context.isTargetDir &&
|
||||||
|
!context.insideExplicitHiddenTarget
|
||||||
|
)
|
||||||
if (shouldRecurse) {
|
if (shouldRecurse) {
|
||||||
// If we're entering a hidden directory that's the target, or we're already inside one,
|
// If we're entering a hidden directory that's the target, or we're already inside one,
|
||||||
// mark that we're inside an explicitly targeted hidden directory
|
// mark that we're inside an explicitly targeted hidden directory
|
||||||
const newInsideExplicitHiddenTarget = insideExplicitHiddenTarget || (isHiddenDir && isTargetDir)
|
const newInsideExplicitHiddenTarget =
|
||||||
await scanDirectory(fullDirPath, false, newInsideExplicitHiddenTarget)
|
context.insideExplicitHiddenTarget || (isHiddenDir && context.isTargetDir)
|
||||||
|
const newContext: ScanContext = {
|
||||||
|
...context,
|
||||||
|
isTargetDir: false,
|
||||||
|
insideExplicitHiddenTarget: newInsideExplicitHiddenTarget,
|
||||||
|
}
|
||||||
|
await scanDirectory(fullDirPath, newContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Silently continue if we can't read a directory
|
// Continue if we can't read a directory
|
||||||
console.warn(`Could not read directory ${currentPath}: ${err}`)
|
console.warn(`Could not read directory ${currentPath}: ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start scanning from the root directory
|
// Start scanning from the root directory
|
||||||
// For environment details generation, we don't want to treat the root as a "target"
|
await scanDirectory(absolutePath, initialContext)
|
||||||
// if we're doing a general recursive scan, as this would include hidden directories
|
|
||||||
// Only treat as target if we're explicitly scanning a single hidden directory
|
|
||||||
const isExplicitHiddenTarget = path.basename(absolutePath).startsWith(".")
|
|
||||||
await scanDirectory(absolutePath, isExplicitHiddenTarget, isExplicitHiddenTarget)
|
|
||||||
|
|
||||||
return directories
|
return directories
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a directory should be included in results based on filters
|
* Critical directories that should always be ignored, even inside explicitly targeted hidden directories
|
||||||
*/
|
*/
|
||||||
function shouldIncludeDirectory(
|
const CRITICAL_IGNORE_PATTERNS = new Set(["node_modules", ".git", "__pycache__", "venv", "env"])
|
||||||
dirName: string,
|
|
||||||
|
/**
|
||||||
|
* Check if a directory matches any of the given patterns
|
||||||
|
*/
|
||||||
|
function matchesIgnorePattern(dirName: string, patterns: string[]): boolean {
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a directory is ignored by gitignore
|
||||||
|
*/
|
||||||
|
function isIgnoredByGitignore(
|
||||||
fullDirPath: string,
|
fullDirPath: string,
|
||||||
basePath: string,
|
basePath: string,
|
||||||
ignoreInstance: ReturnType<typeof ignore>,
|
ignoreInstance: ReturnType<typeof ignore>,
|
||||||
isTargetDir: boolean = false,
|
|
||||||
insideExplicitHiddenTarget: boolean = false,
|
|
||||||
): boolean {
|
): boolean {
|
||||||
|
const relativePath = path.relative(basePath, fullDirPath)
|
||||||
|
const normalizedPath = relativePath.replace(/\\/g, "/")
|
||||||
|
return ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a target directory should be included
|
||||||
|
*/
|
||||||
|
function shouldIncludeTargetDirectory(dirName: string): boolean {
|
||||||
|
// Only apply non-hidden-directory ignore rules to target directories
|
||||||
|
const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*")
|
||||||
|
return !matchesIgnorePattern(dirName, nonHiddenIgnorePatterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a directory inside an explicitly targeted hidden directory should be included
|
||||||
|
*/
|
||||||
|
function shouldIncludeInsideHiddenTarget(dirName: string, fullDirPath: string, context: ScanContext): boolean {
|
||||||
|
// Only apply the most critical ignore patterns when inside explicit hidden target
|
||||||
|
if (CRITICAL_IGNORE_PATTERNS.has(dirName)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against gitignore patterns
|
||||||
|
return !isIgnoredByGitignore(fullDirPath, context.basePath, context.ignoreInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a regular directory should be included
|
||||||
|
*/
|
||||||
|
function shouldIncludeRegularDirectory(dirName: string, fullDirPath: string, context: ScanContext): boolean {
|
||||||
|
// Check against explicit ignore patterns (excluding the ".*" pattern)
|
||||||
|
const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*")
|
||||||
|
if (matchesIgnorePattern(dirName, nonHiddenIgnorePatterns)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against gitignore patterns
|
||||||
|
return !isIgnoredByGitignore(fullDirPath, context.basePath, context.ignoreInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a directory should be included in results based on filters
|
||||||
|
*/
|
||||||
|
function shouldIncludeDirectory(dirName: string, fullDirPath: string, context: ScanContext): boolean {
|
||||||
// If this is the explicitly targeted directory, allow it even if it's hidden
|
// If this is the explicitly targeted directory, allow it even if it's hidden
|
||||||
// This preserves the ability to explicitly target hidden directories like .roo-memory
|
// This preserves the ability to explicitly target hidden directories like .roo-memory
|
||||||
if (isTargetDir) {
|
if (context.isTargetDir) {
|
||||||
// Only apply non-hidden-directory ignore rules to target directories
|
return shouldIncludeTargetDirectory(dirName)
|
||||||
const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*")
|
|
||||||
for (const pattern of nonHiddenIgnorePatterns) {
|
|
||||||
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're inside an explicitly targeted hidden directory, allow subdirectories
|
// If we're inside an explicitly targeted hidden directory, allow subdirectories
|
||||||
// even if they would normally be filtered out by the ".*" pattern or other ignore rules
|
// even if they would normally be filtered out by the ".*" pattern or other ignore rules
|
||||||
if (insideExplicitHiddenTarget) {
|
if (context.insideExplicitHiddenTarget) {
|
||||||
// Only apply the most critical ignore patterns when inside explicit hidden target
|
return shouldIncludeInsideHiddenTarget(dirName, fullDirPath, context)
|
||||||
// Allow temp, rules, etc. but still block node_modules, .git, etc.
|
|
||||||
const criticalIgnorePatterns = ["node_modules", ".git", "__pycache__", "venv", "env"]
|
|
||||||
for (const pattern of criticalIgnorePatterns) {
|
|
||||||
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check against gitignore patterns using the ignore library
|
|
||||||
const relativePath = path.relative(basePath, fullDirPath)
|
|
||||||
const normalizedPath = relativePath.replace(/\\/g, "/")
|
|
||||||
if (ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check against explicit ignore patterns (excluding the ".*" pattern for now)
|
// Regular directory inclusion logic
|
||||||
const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*")
|
return shouldIncludeRegularDirectory(dirName, fullDirPath, context)
|
||||||
for (const pattern of nonHiddenIgnorePatterns) {
|
|
||||||
if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check against gitignore patterns using the ignore library
|
|
||||||
// Calculate relative path from the base directory
|
|
||||||
const relativePath = path.relative(basePath, fullDirPath)
|
|
||||||
const normalizedPath = relativePath.replace(/\\/g, "/")
|
|
||||||
|
|
||||||
// Check if the directory is ignored by .gitignore
|
|
||||||
if (ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -619,9 +688,8 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi
|
||||||
// Process each complete line
|
// Process each complete line
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim() && results.length < limit) {
|
if (line.trim() && results.length < limit) {
|
||||||
// Convert relative path from ripgrep to absolute path
|
// Keep the relative path as returned by ripgrep
|
||||||
const absolutePath = path.resolve(searchDir, line)
|
results.push(line)
|
||||||
results.push(absolutePath)
|
|
||||||
} else if (results.length >= limit) {
|
} else if (results.length >= limit) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue