@@ -166,6 +166,8 @@ Usage:
166166 let truncated = false ;
167167 const MAX_LINE_LENGTH = 500 ;
168168 const GLOBAL_CAP = 1000 ;
169+ const normalizedContextLines = Math . max ( 0 , Math . floor ( contextLines ) ) ;
170+ let emittedContextHunk = false ;
169171
170172 for ( const filePath of filePaths ) {
171173 if ( truncated ) break ;
@@ -181,6 +183,7 @@ Usage:
181183
182184 const lines = content . split ( '\n' ) ;
183185 let fileMatchCount = 0 ;
186+ const fileMatches : Array < { lineIndex : number ; columnIndex : number } > = [ ] ;
184187
185188 for ( let i = 0 ; i < lines . length ; i ++ ) {
186189 const currentLine = lines [ i ] ! ;
@@ -191,31 +194,7 @@ Usage:
191194
192195 filesWithMatches . add ( filePath ) ;
193196
194- let lineContent = currentLine ;
195- if ( lineContent . length > MAX_LINE_LENGTH ) {
196- lineContent = lineContent . slice ( 0 , MAX_LINE_LENGTH ) + '...' ;
197- }
198-
199- // Add context lines before the match
200- if ( contextLines > 0 ) {
201- const beforeStart = Math . max ( 0 , i - contextLines ) ;
202- for ( let b = beforeStart ; b < i ; b ++ ) {
203- outputLines . push ( `${ filePath } :${ b + 1 } - ${ lines [ b ] } ` ) ;
204- }
205- }
206-
207- // Add the matching line
208- outputLines . push ( `${ filePath } :${ i + 1 } :${ lineMatch . index + 1 } : ${ lineContent } ` ) ;
209-
210- // Add context lines after the match
211- if ( contextLines > 0 ) {
212- const afterEnd = Math . min ( lines . length - 1 , i + contextLines ) ;
213- for ( let a = i + 1 ; a <= afterEnd ; a ++ ) {
214- outputLines . push ( `${ filePath } :${ a + 1 } - ${ lines [ a ] } ` ) ;
215- }
216- // Separator between context groups
217- outputLines . push ( '--' ) ;
218- }
197+ fileMatches . push ( { lineIndex : i , columnIndex : lineMatch . index } ) ;
219198
220199 totalMatchCount ++ ;
221200 fileMatchCount ++ ;
@@ -229,6 +208,60 @@ Usage:
229208 break ;
230209 }
231210 }
211+
212+ if ( normalizedContextLines > 0 ) {
213+ const hunks : Array < {
214+ start : number ;
215+ end : number ;
216+ matchesByLine : Map < number , number > ;
217+ } > = [ ] ;
218+
219+ for ( const match of fileMatches ) {
220+ const start = Math . max ( 0 , match . lineIndex - normalizedContextLines ) ;
221+ const end = Math . min ( lines . length - 1 , match . lineIndex + normalizedContextLines ) ;
222+ const previousHunk = hunks [ hunks . length - 1 ] ;
223+
224+ if ( previousHunk && start <= previousHunk . end + 1 ) {
225+ previousHunk . end = Math . max ( previousHunk . end , end ) ;
226+ previousHunk . matchesByLine . set ( match . lineIndex , match . columnIndex ) ;
227+ } else {
228+ hunks . push ( {
229+ start,
230+ end,
231+ matchesByLine : new Map ( [ [ match . lineIndex , match . columnIndex ] ] ) ,
232+ } ) ;
233+ }
234+ }
235+
236+ for ( const hunk of hunks ) {
237+ if ( emittedContextHunk ) {
238+ outputLines . push ( '--' ) ;
239+ }
240+ emittedContextHunk = true ;
241+
242+ for ( let i = hunk . start ; i <= hunk . end ; i ++ ) {
243+ const columnIndex = hunk . matchesByLine . get ( i ) ;
244+
245+ if ( columnIndex !== undefined ) {
246+ let lineContent = lines [ i ] ! ;
247+ if ( lineContent . length > MAX_LINE_LENGTH ) {
248+ lineContent = lineContent . slice ( 0 , MAX_LINE_LENGTH ) + '...' ;
249+ }
250+ outputLines . push ( `${ filePath } :${ i + 1 } :${ columnIndex + 1 } : ${ lineContent } ` ) ;
251+ } else {
252+ outputLines . push ( `${ filePath } :${ i + 1 } - ${ lines [ i ] } ` ) ;
253+ }
254+ }
255+ }
256+ } else {
257+ for ( const match of fileMatches ) {
258+ let lineContent = lines [ match . lineIndex ] ! ;
259+ if ( lineContent . length > MAX_LINE_LENGTH ) {
260+ lineContent = lineContent . slice ( 0 , MAX_LINE_LENGTH ) + '...' ;
261+ }
262+ outputLines . push ( `${ filePath } :${ match . lineIndex + 1 } :${ match . columnIndex + 1 } : ${ lineContent } ` ) ;
263+ }
264+ }
232265 }
233266
234267 // Summary line — placed at the top so it's always visible after truncation
0 commit comments