Skip to content

Commit d938fb6

Browse files
committed
fix(list-files): surface top-level symlinked files in non-recursive mode and only follow symlinks when recursive=true
1 parent 3c60c6e commit d938fb6

File tree

1 file changed

+40
-5
lines changed

1 file changed

+40
-5
lines changed

src/services/glob/list-files.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,16 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
4747
const rgPath = await getRipgrepPath()
4848

4949
if (!recursive) {
50-
// For non-recursive, use the existing approach
50+
// For non-recursive, include top-level files plus top-level symlinked files
5151
const files = await listFilesWithRipgrep(rgPath, dirPath, false, limit)
52+
const symlinkFiles = await listTopLevelSymlinkFiles(dirPath)
53+
const mergedFiles = [...files, ...symlinkFiles]
54+
5255
const ignoreInstance = await createIgnoreInstance(dirPath)
53-
// Calculate remaining limit for directories
54-
const remainingLimit = Math.max(0, limit - files.length)
56+
// Calculate remaining limit for directories after accounting for files
57+
const remainingLimit = Math.max(0, limit - mergedFiles.length)
5558
const directories = await listFilteredDirectories(dirPath, false, ignoreInstance, remainingLimit)
56-
return formatAndCombineResults(files, directories, limit)
59+
return formatAndCombineResults(mergedFiles, directories, limit)
5760
}
5861

5962
// For recursive mode, use the original approach but ensure first-level directories are included
@@ -212,15 +215,47 @@ async function listFilesWithRipgrep(
212215
const absolutePath = path.resolve(dirPath)
213216
return relativePaths.map((relativePath) => path.resolve(absolutePath, relativePath))
214217
}
218+
/**
219+
* List top-level symlinked files in a directory (non-recursive).
220+
* We include the symlink path itself if it points to a file, or if it's a broken link.
221+
*/
222+
async function listTopLevelSymlinkFiles(dirPath: string): Promise<string[]> {
223+
const absolutePath = path.resolve(dirPath)
224+
try {
225+
const entries = await fs.promises.readdir(absolutePath, { withFileTypes: true })
226+
const results: string[] = []
227+
for (const entry of entries) {
228+
if (entry.isSymbolicLink()) {
229+
const symlinkPath = path.join(absolutePath, entry.name)
230+
try {
231+
// stat follows the symlink
232+
const targetStat = await fs.promises.stat(symlinkPath)
233+
if (targetStat.isFile()) {
234+
results.push(symlinkPath)
235+
}
236+
} catch {
237+
// Broken symlink - still surface the symlink path so it is visible to the user
238+
results.push(symlinkPath)
239+
}
240+
}
241+
}
242+
return results
243+
} catch {
244+
return []
245+
}
246+
}
215247

216248
/**
217249
* Build appropriate ripgrep arguments based on whether we're doing a recursive search
218250
*/
219251
function buildRipgrepArgs(dirPath: string, recursive: boolean): string[] {
220252
// Base arguments to list files
221-
const args = ["--files", "--hidden", "--follow"]
253+
// Note: do NOT follow symlinks in non-recursive mode so that symlinked files themselves are listed.
254+
// In recursive mode we follow symlinks to traverse into linked directories when appropriate.
255+
const args = ["--files", "--hidden"]
222256

223257
if (recursive) {
258+
args.push("--follow")
224259
return [...args, ...buildRecursiveArgs(dirPath), dirPath]
225260
} else {
226261
return [...args, ...buildNonRecursiveArgs(), dirPath]

0 commit comments

Comments
 (0)