From d938fb6307a186c6d71e95e236443acc1f4f57a7 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Fri, 5 Sep 2025 17:54:26 -0600 Subject: [PATCH] fix(list-files): surface top-level symlinked files in non-recursive mode and only follow symlinks when recursive=true --- src/services/glob/list-files.ts | 45 +++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 5366bbb84b..b3459126db 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -47,13 +47,16 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb const rgPath = await getRipgrepPath() if (!recursive) { - // For non-recursive, use the existing approach + // For non-recursive, include top-level files plus top-level symlinked files const files = await listFilesWithRipgrep(rgPath, dirPath, false, limit) + const symlinkFiles = await listTopLevelSymlinkFiles(dirPath) + const mergedFiles = [...files, ...symlinkFiles] + const ignoreInstance = await createIgnoreInstance(dirPath) - // Calculate remaining limit for directories - const remainingLimit = Math.max(0, limit - files.length) + // Calculate remaining limit for directories after accounting for files + const remainingLimit = Math.max(0, limit - mergedFiles.length) const directories = await listFilteredDirectories(dirPath, false, ignoreInstance, remainingLimit) - return formatAndCombineResults(files, directories, limit) + return formatAndCombineResults(mergedFiles, directories, limit) } // For recursive mode, use the original approach but ensure first-level directories are included @@ -212,15 +215,47 @@ async function listFilesWithRipgrep( const absolutePath = path.resolve(dirPath) return relativePaths.map((relativePath) => path.resolve(absolutePath, relativePath)) } +/** + * List top-level symlinked files in a directory (non-recursive). + * We include the symlink path itself if it points to a file, or if it's a broken link. + */ +async function listTopLevelSymlinkFiles(dirPath: string): Promise { + const absolutePath = path.resolve(dirPath) + try { + const entries = await fs.promises.readdir(absolutePath, { withFileTypes: true }) + const results: string[] = [] + for (const entry of entries) { + if (entry.isSymbolicLink()) { + const symlinkPath = path.join(absolutePath, entry.name) + try { + // stat follows the symlink + const targetStat = await fs.promises.stat(symlinkPath) + if (targetStat.isFile()) { + results.push(symlinkPath) + } + } catch { + // Broken symlink - still surface the symlink path so it is visible to the user + results.push(symlinkPath) + } + } + } + return results + } catch { + return [] + } +} /** * Build appropriate ripgrep arguments based on whether we're doing a recursive search */ function buildRipgrepArgs(dirPath: string, recursive: boolean): string[] { // Base arguments to list files - const args = ["--files", "--hidden", "--follow"] + // Note: do NOT follow symlinks in non-recursive mode so that symlinked files themselves are listed. + // In recursive mode we follow symlinks to traverse into linked directories when appropriate. + const args = ["--files", "--hidden"] if (recursive) { + args.push("--follow") return [...args, ...buildRecursiveArgs(dirPath), dirPath] } else { return [...args, ...buildNonRecursiveArgs(), dirPath]