@@ -6,6 +6,7 @@ import { globSync } from "glob"
66import ignore from "ignore"
77import { arePathsEqual , getWorkspacePath } from "../../utils/path"
88import { isPathInIgnoredDirectory } from "./ignore-utils"
9+ import { listFilesWithRipgrep } from "./ripgrep-utils"
910
1011function normalizePath ( p : string ) : string {
1112 return p . replace ( / \\ / g, "/" )
@@ -106,63 +107,60 @@ async function listAllFilesRecursively(
106107 } catch ( error ) {
107108 // Fallback to the manual method if git ls-files fails
108109 // (e.g., not a git repo, or an error with the command).
109- console . warn ( "`git ls-files` failed, falling back to manual file search :" , error )
110- return listAllFilesRecursivelyWithFs ( dir , git , workspacePath , limit )
110+ console . warn ( "`git ls-files` failed, falling back to ripgrep :" , error )
111+ return listAllFilesRecursivelyWithRipGrep ( dir , git , workspacePath , limit )
111112 }
112113}
113114
114115/**
115- * A fallback recursive file lister that manually walks the filesystem .
116- * This is slower but works if Git is not available or fails.
116+ * A fallback recursive file lister that uses ripgrep for performance .
117+ * This is used if Git is not available or fails.
117118 */
118- async function listAllFilesRecursivelyWithFs (
119+ async function listAllFilesRecursivelyWithRipGrep (
119120 dir : string ,
120121 _git : SimpleGit ,
121122 workspacePath : string ,
122123 limit : number ,
123124) : Promise < string [ ] > {
124- const result : string [ ] = [ ]
125- const queue : string [ ] = [ dir ]
126-
127- // Create ignore instance using all .gitignore files in the workspace
128125 const ig = createGitignoreFilter ( workspacePath )
129126
130- while ( queue . length > 0 && result . length < limit ) {
131- const currentDir = queue . shift ( ) !
132-
133- // Pre-check if the directory itself is ignored by custom ignore logic
134- if ( isPathInIgnoredDirectory ( path . relative ( workspacePath , currentDir ) ) ) {
135- continue
136- }
137-
138- let entries : fs . Dirent [ ]
139- try {
140- entries = await fs . promises . readdir ( currentDir , { withFileTypes : true } )
141- } catch ( err ) {
142- continue // Skip unreadable directories
143- }
127+ let files : string [ ] = [ ]
128+ let usedRipgrep = false
129+ try {
130+ files = await listFilesWithRipgrep ( dir , true , limit )
131+ usedRipgrep = true
132+ } catch ( err ) {
133+ console . warn ( "listFilesWithRipgrep failed:" , err )
134+ }
144135
145- for ( const entry of entries ) {
146- const fullPath = path . join ( currentDir , entry . name )
147- const relPath = path . relative ( workspacePath , fullPath ) . replace ( / \\ / g, "/" )
148- if ( ig . ignores ( relPath ) ) {
149- if ( entry . isDirectory ( ) ) {
150- // Still need to recurse into subdirectories to find non-ignored children
151- continue
152- }
136+ // If ripgrep failed or returned no files, fallback to manual method
137+ if ( ! files || files . length === 0 ) {
138+ const queue : string [ ] = [ dir ]
139+ while ( queue . length > 0 ) {
140+ const currentDir = queue . shift ( ) !
141+ if ( isPathInIgnoredDirectory ( path . relative ( workspacePath , currentDir ) ) ) {
153142 continue
154143 }
155- if ( entry . isDirectory ( ) ) {
156- queue . push ( fullPath )
157- } else {
158- result . push ( fullPath )
159- if ( result . length >= limit ) {
160- break
144+ let entries : fs . Dirent [ ]
145+ try {
146+ entries = await fs . promises . readdir ( currentDir , { withFileTypes : true } )
147+ } catch ( err ) {
148+ continue
149+ }
150+ for ( const entry of entries ) {
151+ const fullPath = path . join ( currentDir , entry . name )
152+ if ( entry . isDirectory ( ) ) {
153+ queue . push ( fullPath )
154+ } else {
155+ files . push ( fullPath )
161156 }
162157 }
163158 }
164159 }
165- return result
160+
161+ // Map to relative paths, filter, then map back to absolute
162+ const filtered = ig . filter ( files . map ( ( f ) => path . relative ( workspacePath , path . resolve ( f ) ) . replace ( / \\ / g, "/" ) ) )
163+ return filtered . slice ( 0 , limit ) . map ( ( rel ) => path . join ( workspacePath , rel ) )
166164}
167165
168166/**
@@ -218,18 +216,47 @@ function createGitignoreFilter(rootDir: string) {
218216
219217/**
220218 * List only top-level files and directories, filtering out ignored ones.
219+ * Uses ripgrep for performance and consistency with recursive listing.
221220 */
222221async function listNonRecursive ( dir : string , _git : SimpleGit , workspacePath : string ) : Promise < string [ ] > {
223- const entries = await fs . promises . readdir ( dir , { withFileTypes : true } )
224222 const ig = createGitignoreFilter ( workspacePath )
225223
226- const result : string [ ] = [ ]
227- for ( const entry of entries ) {
228- const fullPath = path . join ( dir , entry . name )
229- const relPath = path . relative ( workspacePath , fullPath ) . replace ( / \\ / g, "/" )
230- if ( ! ig . ignores ( relPath ) && ! isPathInIgnoredDirectory ( relPath ) ) {
231- result . push ( fullPath )
232- }
224+ let files : string [ ] = [ ]
225+ try {
226+ files = await listFilesWithRipgrep ( dir , false , 10000 )
227+ } catch ( err ) {
228+ console . warn ( "listFilesWithRipgrep (non-recursive) failed:" , err )
229+ }
230+
231+ // If ripgrep failed or returned no files, fallback to manual method
232+ if ( ! files || files . length === 0 ) {
233+ const entries = await fs . promises . readdir ( dir , { withFileTypes : true } )
234+ return entries
235+ . map ( ( entry ) => path . join ( dir , entry . name ) )
236+ . filter ( ( fullPath ) => {
237+ const relPath = path . relative ( workspacePath , fullPath ) . replace ( / \\ / g, "/" )
238+ return ! ig . ignores ( relPath ) && ! isPathInIgnoredDirectory ( relPath )
239+ } )
233240 }
234- return result
241+
242+ const filtered = ig . filter ( files . map ( ( f ) => path . relative ( workspacePath , path . resolve ( f ) ) . replace ( / \\ / g, "/" ) ) )
243+
244+ // Ripgrep --files only lists files, not directories. Add top-level directories manually.
245+ let dirEntries : string [ ] = [ ]
246+ try {
247+ const entries = await fs . promises . readdir ( dir , { withFileTypes : true } )
248+ dirEntries = entries
249+ . filter ( ( entry ) => entry . isDirectory ( ) )
250+ . map ( ( entry ) => path . join ( dir , entry . name ) )
251+ . filter ( ( fullPath ) => {
252+ const relPath = path . relative ( workspacePath , fullPath ) . replace ( / \\ / g, "/" )
253+ return ! ig . ignores ( relPath ) && ! isPathInIgnoredDirectory ( relPath )
254+ } )
255+ } catch ( err ) {
256+ // ignore
257+ }
258+
259+ // Enforce limit after combining files and directories
260+ const combined = [ ...filtered . map ( ( rel ) => path . join ( workspacePath , rel ) ) , ...dirEntries ]
261+ return combined . slice ( 0 , 10000 ) // 10000 is the same as above, will be sliced by caller
235262}
0 commit comments