@@ -24,77 +24,88 @@ const repoFileCache = new Map<
2424> ( ) ;
2525const CACHE_TTL = 30000 ; // 30 seconds
2626
27- async function getGitIgnoredFiles ( repoPath : string ) : Promise < Set < string > > {
27+ /**
28+ * List files using git ls-files - fast and works for any git repository.
29+ * This is much faster than recursive fs.readdir for large codebases.
30+ */
31+ async function listFilesWithGit (
32+ repoPath : string ,
33+ changedFiles : Set < string > ,
34+ ) : Promise < FileEntry [ ] > {
2835 try {
29- const { stdout } = await execAsync (
30- "git ls-files --others --ignored --exclude-standard" ,
31- { cwd : repoPath } ,
36+ const { stdout : trackedStdout } = await execAsync ( "git ls-files" , {
37+ cwd : repoPath ,
38+ maxBuffer : 50 * 1024 * 1024 ,
39+ } ) ;
40+
41+ const { stdout : untrackedStdout } = await execAsync (
42+ "git ls-files --others --exclude-standard" ,
43+ { cwd : repoPath , maxBuffer : 50 * 1024 * 1024 } ,
3244 ) ;
33- return new Set (
34- stdout
35- . split ( "\n" )
36- . filter ( Boolean )
37- . map ( ( f ) => path . join ( repoPath , f ) ) ,
38- ) ;
39- } catch {
40- // If git command fails, return empty set
41- return new Set ( ) ;
45+
46+ const allFiles = [
47+ ...trackedStdout . split ( "\n" ) . filter ( Boolean ) ,
48+ ...untrackedStdout . split ( "\n" ) . filter ( Boolean ) ,
49+ ] ;
50+
51+ return allFiles . map ( ( relativePath ) => ( {
52+ path : relativePath ,
53+ name : path . basename ( relativePath ) ,
54+ changed : changedFiles . has ( relativePath ) ,
55+ } ) ) ;
56+ } catch ( error ) {
57+ log . error ( "Error listing files with git:" , error ) ;
58+ return [ ] ;
4259 }
4360}
4461
45- async function listFilesRecursive (
46- dirPath : string ,
47- ignoredFiles : Set < string > ,
48- baseDir : string ,
62+ /**
63+ * List files with early termination using grep and head.
64+ * Returns limited results directly from git without loading all files into memory.
65+ */
66+ async function listFilesWithQuery (
67+ repoPath : string ,
68+ query : string ,
69+ limit : number ,
4970 changedFiles : Set < string > ,
5071) : Promise < FileEntry [ ] > {
51- const files : FileEntry [ ] = [ ] ;
52-
5372 try {
54- const entries = await fsPromises . readdir ( dirPath , { withFileTypes : true } ) ;
55-
56- for ( const entry of entries ) {
57- const fullPath = path . join ( dirPath , entry . name ) ;
58- const relativePath = path . relative ( baseDir , fullPath ) ;
59-
60- // Skip .git directory, node_modules, and common build dirs
61- if (
62- entry . name === ".git" ||
63- entry . name === "node_modules" ||
64- entry . name === "dist" ||
65- entry . name === "build" ||
66- entry . name === "__pycache__"
67- ) {
68- continue ;
69- }
70-
71- // Skip git-ignored files
72- if ( ignoredFiles . has ( fullPath ) ) {
73- continue ;
74- }
75-
76- if ( entry . isDirectory ( ) ) {
77- const subFiles = await listFilesRecursive (
78- fullPath ,
79- ignoredFiles ,
80- baseDir ,
81- changedFiles ,
82- ) ;
83- files . push ( ...subFiles ) ;
84- } else if ( entry . isFile ( ) ) {
85- files . push ( {
86- path : relativePath ,
87- name : entry . name ,
88- changed : changedFiles . has ( relativePath ) ,
89- } ) ;
90- }
91- }
73+ // escape special regex characters in the query for grep
74+ const escapedQuery = query . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
75+
76+ // grep -i for case-insensitive matching, head for early termination
77+ const grepCmd = `grep -i "${ escapedQuery } " | head -n ${ limit } ` ;
78+
79+ // the || true prevents error when grep finds no matches
80+ const [ trackedResult , untrackedResult ] = await Promise . all ( [
81+ execAsync ( `git ls-files | ${ grepCmd } || true` , {
82+ cwd : repoPath ,
83+ maxBuffer : 1024 * 1024 ,
84+ } ) ,
85+ execAsync (
86+ `git ls-files --others --exclude-standard | ${ grepCmd } || true` ,
87+ {
88+ cwd : repoPath ,
89+ maxBuffer : 1024 * 1024 ,
90+ } ,
91+ ) ,
92+ ] ) ;
93+
94+ const trackedFiles = trackedResult . stdout . split ( "\n" ) . filter ( Boolean ) ;
95+ const untrackedFiles = untrackedResult . stdout . split ( "\n" ) . filter ( Boolean ) ;
96+
97+ // combine and limit to requested amount (in case both sources have results)
98+ const allFiles = [ ...trackedFiles , ...untrackedFiles ] . slice ( 0 , limit ) ;
99+
100+ return allFiles . map ( ( relativePath ) => ( {
101+ path : relativePath ,
102+ name : path . basename ( relativePath ) ,
103+ changed : changedFiles . has ( relativePath ) ,
104+ } ) ) ;
92105 } catch ( error ) {
93- // Skip directories we can't read
94- log . error ( `Error reading directory ${ dirPath } :` , error ) ;
106+ log . error ( "Error listing files with query:" , error ) ;
107+ return [ ] ;
95108 }
96-
97- return files ;
98109}
99110
100111export function registerFsIpc ( ) : void {
@@ -104,11 +115,26 @@ export function registerFsIpc(): void {
104115 _event : IpcMainInvokeEvent ,
105116 repoPath : string ,
106117 query ?: string ,
118+ limit ?: number ,
107119 ) : Promise < FileEntry [ ] > => {
108120 if ( ! repoPath ) return [ ] ;
109121
122+ const resultLimit = limit ?? 50 ;
123+
110124 try {
111- // Check cache
125+ const changedFiles = await getChangedFilesForRepo ( repoPath ) ;
126+
127+ // when there is a query, use early termination with grep + head
128+ // this avoids loading all files into memory for filtered searches
129+ if ( query ?. trim ( ) ) {
130+ return await listFilesWithQuery (
131+ repoPath ,
132+ query . trim ( ) ,
133+ resultLimit ,
134+ changedFiles ,
135+ ) ;
136+ }
137+
112138 const cached = repoFileCache . get ( repoPath ) ;
113139 const now = Date . now ( ) ;
114140
@@ -117,40 +143,15 @@ export function registerFsIpc(): void {
117143 if ( cached && now - cached . timestamp < CACHE_TTL ) {
118144 allFiles = cached . files ;
119145 } else {
120- // Get git-ignored files
121- const ignoredFiles = await getGitIgnoredFiles ( repoPath ) ;
122-
123- // Get changed files from git
124- const changedFiles = await getChangedFilesForRepo ( repoPath ) ;
146+ allFiles = await listFilesWithGit ( repoPath , changedFiles ) ;
125147
126- // List all files
127- allFiles = await listFilesRecursive (
128- repoPath ,
129- ignoredFiles ,
130- repoPath ,
131- changedFiles ,
132- ) ;
133-
134- // Update cache
135148 repoFileCache . set ( repoPath , {
136149 files : allFiles ,
137150 timestamp : now ,
138151 } ) ;
139152 }
140153
141- // Filter by query if provided
142- if ( query ?. trim ( ) ) {
143- const lowerQuery = query . toLowerCase ( ) ;
144- return allFiles
145- . filter (
146- ( f ) =>
147- f . path . toLowerCase ( ) . includes ( lowerQuery ) ||
148- f . name . toLowerCase ( ) . includes ( lowerQuery ) ,
149- )
150- . slice ( 0 , 50 ) ; // Limit search results
151- }
152-
153- return allFiles ; // Return all files for full tree view
154+ return allFiles . slice ( 0 , resultLimit ) ;
154155 } catch ( error ) {
155156 log . error ( "Error listing repo files:" , error ) ;
156157 return [ ] ;
0 commit comments