@@ -21,15 +21,15 @@ export interface GrepSearchParams {
2121}
2222
2323export class GrepSearch {
24- private fsPath : string | undefined
24+ private path : string
2525 private query : string
2626 private caseSensitive : boolean
2727 private excludePattern ?: string
2828 private includePattern ?: string
2929 private readonly logger = getLogger ( 'grepSearch' )
3030
3131 constructor ( params : GrepSearchParams ) {
32- this . fsPath = params . path
32+ this . path = this . getSearchDirectory ( params . path )
3333 this . query = params . query
3434 this . caseSensitive = params . caseSensitive ?? false
3535 this . excludePattern = params . excludePattern
@@ -41,61 +41,58 @@ export class GrepSearch {
4141 throw new Error ( 'Grep search query cannot be empty.' )
4242 }
4343
44- // Handle optional path parameter
45- if ( ! this . fsPath || this . fsPath . trim ( ) . length === 0 ) {
46- // Use current workspace folder as default if path is not provided
47- const workspaceFolders = vscode . workspace . workspaceFolders
48- if ( ! workspaceFolders || workspaceFolders . length === 0 ) {
49- throw new Error ( 'Path cannot be empty and no workspace folder is available.' )
50- }
51- this . fsPath = workspaceFolders [ 0 ] . uri . fsPath
52- this . logger . debug ( `Using default workspace folder: ${ this . fsPath } ` )
44+ if ( this . path . trim ( ) . length === 0 ) {
45+ throw new Error ( 'Path cannot be empty and no workspace folder is available.' )
5346 }
5447
55- const sanitized = sanitizePath ( this . fsPath )
56- this . fsPath = sanitized
48+ const sanitized = sanitizePath ( this . path )
49+ this . path = sanitized
5750
58- const pathUri = vscode . Uri . file ( this . fsPath )
51+ const pathUri = vscode . Uri . file ( this . path )
5952 let pathExists : boolean
6053 try {
6154 pathExists = await fs . existsDir ( pathUri )
6255 if ( ! pathExists ) {
63- throw new Error ( `Path: "${ this . fsPath } " does not exist or cannot be accessed.` )
56+ throw new Error ( `Path: "${ this . path } " does not exist or cannot be accessed.` )
6457 }
6558 } catch ( err ) {
66- throw new Error ( `Path: "${ this . fsPath } " does not exist or cannot be accessed. (${ err } )` )
59+ throw new Error ( `Path: "${ this . path } " does not exist or cannot be accessed. (${ err } )` )
6760 }
6861 }
6962
7063 public queueDescription ( updates : Writable ) : void {
71- const searchDirectory = this . getSearchDirectory ( this . fsPath )
72- updates . write ( `Grepping for "${ this . query } " in directory: ${ searchDirectory } ` )
64+ updates . write ( `Grepping for "${ this . query } " in directory: ${ this . path } ` )
7365 updates . end ( )
7466 }
7567
7668 public async invoke ( updates ?: Writable ) : Promise < InvokeOutput > {
77- const searchDirectory = this . getSearchDirectory ( this . fsPath )
7869 try {
7970 const results = await this . executeRipgrep ( updates )
8071 return this . createOutput ( results )
8172 } catch ( error : any ) {
82- this . logger . error ( `Failed to search in "${ searchDirectory } ": ${ error . message || error } ` )
83- throw new Error ( `Failed to search in "${ searchDirectory } ": ${ error . message || error } ` )
73+ this . logger . error ( `Failed to search in "${ this . path } ": ${ error . message || error } ` )
74+ throw new Error ( `Failed to search in "${ this . path } ": ${ error . message || error } ` )
8475 }
8576 }
8677
87- private getSearchDirectory ( fsPath ?: string ) : string {
88- const workspaceFolders = vscode . workspace . workspaceFolders
89- const searchLocation = fsPath
90- ? fsPath
91- : ! workspaceFolders || workspaceFolders . length === 0
92- ? ''
93- : workspaceFolders [ 0 ] . uri . fsPath
78+ private getSearchDirectory ( path ?: string ) : string {
79+ let searchLocation = ''
80+ if ( path && path . trim ( ) . length !== 0 ) {
81+ searchLocation = path
82+ } else {
83+ // Handle optional path parameter
84+ // Use current workspace folder as default if path is not provided
85+ const workspaceFolders = vscode . workspace . workspaceFolders
86+ this . logger . info ( `Using default workspace folder: ${ workspaceFolders ?. length } ` )
87+ if ( workspaceFolders && workspaceFolders . length !== 0 ) {
88+ searchLocation = workspaceFolders [ 0 ] . uri . fsPath
89+ this . logger . debug ( `Using default workspace folder: ${ searchLocation } ` )
90+ }
91+ }
9492 return searchLocation
9593 }
9694
9795 private async executeRipgrep ( updates ?: Writable ) : Promise < string > {
98- const searchDirectory = this . getSearchDirectory ( this . fsPath )
9996 return new Promise ( async ( resolve , reject ) => {
10097 const args : string [ ] = [ ]
10198
@@ -129,7 +126,7 @@ export class GrepSearch {
129126 }
130127
131128 // Add search pattern and path
132- args . push ( this . query , searchDirectory )
129+ args . push ( this . query , this . path )
133130
134131 this . logger . debug ( `Executing ripgrep with args: ${ args . join ( ' ' ) } ` )
135132
@@ -171,36 +168,63 @@ export class GrepSearch {
171168 * 1. Remove matched content (keep only file:line)
172169 * 2. Add file URLs for clickable links
173170 */
171+ /**
172+ * Process ripgrep output to:
173+ * 1. Group results by file
174+ * 2. Format as collapsible sections
175+ * 3. Add file URLs for clickable links
176+ */
174177 private processRipgrepOutput ( output : string ) : string {
175178 if ( ! output || output . trim ( ) === '' ) {
176179 return 'No matches found.'
177180 }
178181
179182 const lines = output . split ( '\n' )
180- const processedLines = lines
181- . map ( ( line ) => {
182- if ( ! line || line . trim ( ) === '' ) {
183- return ''
184- }
185183
186- // Extract file path and line number
187- const parts = line . split ( ':' )
188- if ( parts . length < 2 ) {
189- return line
190- }
184+ // Group by file path
185+ const fileGroups : Record < string , string [ ] > = { }
186+
187+ for ( const line of lines ) {
188+ if ( ! line || line . trim ( ) === '' ) {
189+ continue
190+ }
191191
192- const filePath = parts [ 0 ]
193- const lineNumber = parts [ 1 ]
192+ // Extract file path and line number
193+ const parts = line . split ( ':' )
194+ if ( parts . length < 2 ) {
195+ continue
196+ }
197+
198+ const filePath = parts [ 0 ]
199+ const lineNumber = parts [ 1 ]
200+ // Don't include match content
194201
202+ if ( ! fileGroups [ filePath ] ) {
203+ fileGroups [ filePath ] = [ ]
204+ }
205+
206+ // Create a clickable link with line number only
207+ fileGroups [ filePath ] . push ( `- [Line ${ lineNumber } ](${ vscode . Uri . file ( filePath ) . toString ( ) } :${ lineNumber } )` )
208+ }
209+
210+ // Sort files by match count (most matches first)
211+ const sortedFiles = Object . entries ( fileGroups ) . sort ( ( a , b ) => b [ 1 ] . length - a [ 1 ] . length )
212+
213+ // Format as collapsible sections
214+ const processedOutput = sortedFiles
215+ . map ( ( [ filePath , matches ] ) => {
195216 const fileName = path . basename ( filePath )
196- const fileUri = vscode . Uri . file ( filePath )
217+ const matchCount = matches . length
218+
219+ return `<details>
220+ <summary><strong>${ fileName } (${ matchCount } )</strong></summary>
197221
198- // Format as a markdown link
199- return `[ ${ fileName } : ${ lineNumber } ]( ${ fileUri } : ${ lineNumber } ) `
222+ ${ matches . join ( '\n' ) }
223+ </details> `
200224 } )
201- . filter ( Boolean )
225+ . join ( '\n\n' )
202226
203- return processedLines . join ( '\n' )
227+ return processedOutput
204228 }
205229
206230 private createOutput ( content : string ) : InvokeOutput {
0 commit comments