@@ -34,10 +34,16 @@ export function extractRef(repoFullName: string, path: string, matchIndex: numbe
3434 * match — replacing the API-provided segments (which point at the literal
3535 * search term) with the actual regex match positions.
3636 *
37- * Line (1-based) and col (1-based) are computed from the fragment text so
38- * that `highlightFragment` can map them to the correct terminal lines.
37+ * `fragmentStartLine` is the 1-based absolute file line where the fragment
38+ * starts (derived from the existing API segments or falling back to 1). It is
39+ * used to produce absolute `line` values that match those stored by the API
40+ * path so that `output.ts` generates correct `#L{line}` GitHub anchors.
3941 */
40- function recomputeSegments ( fragment : string , regex : RegExp ) : TextMatchSegment [ ] {
42+ function recomputeSegments (
43+ fragment : string ,
44+ regex : RegExp ,
45+ fragmentStartLine : number ,
46+ ) : TextMatchSegment [ ] {
4147 // Force global flag so exec() advances; strip g and y first to avoid double-g
4248 // and to prevent sticky mode from anchoring every match at lastIndex.
4349 const re = new RegExp ( regex . source , regex . flags . replace ( / [ g y ] / g, "" ) + "g" ) ;
@@ -60,7 +66,9 @@ function recomputeSegments(fragment: string, regex: RegExp): TextMatchSegment[]
6066 if ( newlines [ mid ] < start ) lo = mid + 1 ;
6167 else hi = mid ;
6268 }
63- const line = lo + 1 ; // 1-based
69+ // `lo` = number of fragment-local lines before `start` (0-based offset).
70+ // Add `fragmentStartLine - 1` to make it absolute.
71+ const line = fragmentStartLine + lo ;
6472 const col = ( lo === 0 ? start : start - newlines [ lo - 1 ] - 1 ) + 1 ; // 1-based
6573 segments . push ( { text : m [ 0 ] , indices : [ start , end ] , line, col } ) ;
6674 if ( m [ 0 ] . length === 0 ) re . lastIndex ++ ; // guard against zero-width matches
@@ -91,7 +99,18 @@ export function aggregate(
9199 const savedLastIndex = regexFilter . lastIndex ;
92100 const updatedTextMatches : TextMatch [ ] = m . textMatches
93101 . map ( ( tm ) => {
94- const segs = recomputeSegments ( tm . fragment , regexFilter ) ;
102+ // Derive the absolute start line of this fragment from the first API
103+ // segment. If no API segment is available, fall back to 1 so that
104+ // recomputeSegments emits fragment-relative lines (which equal
105+ // absolute lines when the fragment starts at line 1).
106+ let fragmentStartLine = 1 ;
107+ const firstApiSeg = tm . matches [ 0 ] ;
108+ if ( firstApiSeg ) {
109+ const before = tm . fragment . slice ( 0 , firstApiSeg . indices [ 0 ] ) ;
110+ const fragLine = ( before . match ( / \n / g) ?. length ?? 0 ) + 1 ;
111+ fragmentStartLine = firstApiSeg . line - fragLine + 1 ;
112+ }
113+ const segs = recomputeSegments ( tm . fragment , regexFilter , fragmentStartLine ) ;
95114 return segs . length > 0 ? { fragment : tm . fragment , matches : segs } : null ;
96115 } )
97116 . filter ( ( tm ) : tm is TextMatch => tm !== null ) ;
0 commit comments