@@ -136,37 +136,43 @@ function getSliceLimits(beforeCount: number, afterCount: number, totalLimit: num
136136}
137137/* v8 ignore stop */
138138
139+ type ThreadEntry = { thread : CommentThread ; index : number } ;
140+
141+ /** Inline threads for a single file, bucketed by line number for each version. */
142+ interface FileThreadIndex {
143+ before : Map < number , ThreadEntry [ ] > ;
144+ after : Map < number , ThreadEntry [ ] > ;
145+ }
146+
147+ /**
148+ * Returns the inline-thread entries anchored to `line`, or undefined when none.
149+ * Uses integer line-number lookups so the hot loop never concatenates keys.
150+ */
139151function findMatchingThreadEntries (
140- threadsByKey : Map < string , { thread : CommentThread ; index : number } [ ] > ,
141- filePath : string ,
152+ fileThreads : FileThreadIndex ,
142153 line : DisplayLine ,
143- ) : { thread : CommentThread ; index : number } [ ] {
144- const results : { thread : CommentThread ; index : number } [ ] = [ ] ;
145-
146- if ( line . type === "delete" && line . beforeLineNumber ) {
147- const key = `${ filePath } :${ line . beforeLineNumber } :BEFORE` ;
148- results . push ( ...( threadsByKey . get ( key ) ?? [ ] ) ) ;
154+ ) : ThreadEntry [ ] | undefined {
155+ if ( line . type === "delete" ) {
156+ return line . beforeLineNumber ? fileThreads . before . get ( line . beforeLineNumber ) : undefined ;
149157 }
150158
151- if ( line . type === "add" && line . afterLineNumber ) {
152- const key = `${ filePath } :${ line . afterLineNumber } :AFTER` ;
153- results . push ( ...( threadsByKey . get ( key ) ?? [ ] ) ) ;
159+ if ( line . type === "add" ) {
160+ return line . afterLineNumber ? fileThreads . after . get ( line . afterLineNumber ) : undefined ;
154161 }
155162
156163 /* v8 ignore start -- context lines always have both line numbers in practice */
157164 if ( line . type === "context" ) {
158- if ( line . beforeLineNumber ) {
159- const key = `${ filePath } :${ line . beforeLineNumber } :BEFORE` ;
160- results . push ( ...( threadsByKey . get ( key ) ?? [ ] ) ) ;
161- }
162- if ( line . afterLineNumber ) {
163- const key = `${ filePath } :${ line . afterLineNumber } :AFTER` ;
164- results . push ( ...( threadsByKey . get ( key ) ?? [ ] ) ) ;
165- }
165+ const before = line . beforeLineNumber
166+ ? fileThreads . before . get ( line . beforeLineNumber )
167+ : undefined ;
168+ const after = line . afterLineNumber ? fileThreads . after . get ( line . afterLineNumber ) : undefined ;
169+ if ( before && after ) return [ ...before , ...after ] ;
170+ return before ?? after ;
166171 }
167172 /* v8 ignore stop */
168173
169- return results ;
174+ /* v8 ignore next -- diff lines are only ever add/delete/context */
175+ return undefined ;
170176}
171177
172178export function buildDisplayLines (
@@ -181,15 +187,25 @@ export function buildDisplayLines(
181187) : DisplayLine [ ] {
182188 const lines : DisplayLine [ ] = [ ] ;
183189
184- // Index inline comments by file:position:version for efficient lookup
185- const inlineThreadsByKey = new Map < string , { thread : CommentThread ; index : number } [ ] > ( ) ;
190+ // Index inline comments by file, then by line number per version, so the
191+ // diff loop below can resolve anchors with integer lookups (no key strings).
192+ const inlineThreadsByFile = new Map < string , FileThreadIndex > ( ) ;
186193 for ( let i = 0 ; i < commentThreads . length ; i ++ ) {
187194 const thread = commentThreads [ i ] ! ;
188- if ( thread . location ) {
189- const key = `${ thread . location . filePath } :${ thread . location . filePosition } :${ thread . location . relativeFileVersion } ` ;
190- const existing = inlineThreadsByKey . get ( key ) ?? [ ] ;
195+ const location = thread . location ;
196+ if ( ! location ) continue ;
197+
198+ let fileIndex = inlineThreadsByFile . get ( location . filePath ) ;
199+ if ( ! fileIndex ) {
200+ fileIndex = { before : new Map ( ) , after : new Map ( ) } ;
201+ inlineThreadsByFile . set ( location . filePath , fileIndex ) ;
202+ }
203+ const bucket = location . relativeFileVersion === "BEFORE" ? fileIndex . before : fileIndex . after ;
204+ const existing = bucket . get ( location . filePosition ) ;
205+ if ( existing ) {
191206 existing . push ( { thread, index : i } ) ;
192- inlineThreadsByKey . set ( key , existing ) ;
207+ } else {
208+ bucket . set ( location . filePosition , [ { thread, index : i } ] ) ;
193209 }
194210 }
195211
@@ -220,23 +236,28 @@ export function buildDisplayLines(
220236 afterLines . length ,
221237 displayLimit ,
222238 ) ;
223- diffLines = computeSimpleDiff (
224- beforeLines . slice ( 0 , beforeLimit ) ,
225- afterLines . slice ( 0 , afterLimit ) ,
226- ) ;
239+ // Skip the slice copy when the limit already covers the whole array
240+ // (the common non-truncated case); computeSimpleDiff never mutates its inputs.
241+ const beforeSlice =
242+ beforeLimit === beforeLines . length ? beforeLines : beforeLines . slice ( 0 , beforeLimit ) ;
243+ const afterSlice =
244+ afterLimit === afterLines . length ? afterLines : afterLines . slice ( 0 , afterLimit ) ;
245+ diffLines = computeSimpleDiff ( beforeSlice , afterSlice ) ;
227246 // Enrich once here so the hot loop below can push lines without per-call copies
228247 for ( const dl of diffLines ) {
229248 dl . filePath = filePath ;
230249 dl . diffKey = blobKey ;
231250 }
232251 diffCache ?. set ( cacheKey , diffLines ) ;
233252 }
234- const hasInlineThreads = inlineThreadsByKey . size > 0 ;
253+ // Only files that actually have inline threads pay the per-line lookup cost.
254+ const fileThreads = inlineThreadsByFile . get ( filePath ) ;
235255 for ( const dl of diffLines ) {
236256 lines . push ( dl ) ;
237257
238- if ( ! hasInlineThreads ) continue ;
239- const matchingEntries = findMatchingThreadEntries ( inlineThreadsByKey , filePath , dl ) ;
258+ if ( ! fileThreads ) continue ;
259+ const matchingEntries = findMatchingThreadEntries ( fileThreads , dl ) ;
260+ if ( ! matchingEntries ) continue ;
240261 for ( const { thread, index : threadIdx } of matchingEntries ) {
241262 appendThreadLines (
242263 lines ,
0 commit comments