@@ -20,15 +20,16 @@ type Model struct {
2020
2121 FollowOn bool
2222 ScrollOffset int
23- hunkSigs map [string ][]uint64
24- highlightedHunks map [string ]map [int ]bool
23+ prevLineSigs map [string ][]uint64
24+ highlightedLines map [string ]map [lineKey ]bool
2525 lastChangedPath string
2626 lastHighlightedPath string
27- // followTargetPath/hunk track the last auto-follow target for resize recalculation.
28- followTargetPath string
29- followTargetHunk int
30- Notification string
31- notificationSeq int
27+ // followTargetPath/hunk/line track the last auto-follow target for resize recalculation.
28+ followTargetPath string
29+ followTargetHunk int
30+ followTargetLineIdx int
31+ Notification string
32+ notificationSeq int
3233
3334 OverlayOpen bool
3435 OverlayCursor int
@@ -57,8 +58,8 @@ func NewModel(dirName, repoDir, baselineSHA string, files []internal.FileDiff) M
5758 BaselineSHA : baselineSHA ,
5859 Files : files ,
5960 FollowOn : true ,
60- hunkSigs : buildPrevHunkSigs (files ),
61- highlightedHunks : make (map [string ]map [int ]bool ),
61+ prevLineSigs : buildPrevLineSigs (files ),
62+ highlightedLines : make (map [string ]map [lineKey ]bool ),
6263 displayCache : newDisplayLineCache (),
6364 }
6465}
@@ -82,8 +83,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8283 m .Notification = "baseline reset"
8384 m .resetPending = false
8485 m .resetInFlight = false
85- m .hunkSigs = buildPrevHunkSigs (msg .Files )
86- m .highlightedHunks = make (map [string ]map [int ]bool )
86+ m .prevLineSigs = buildPrevLineSigs (msg .Files )
87+ m .highlightedLines = make (map [string ]map [lineKey ]bool )
8788 m .lastChangedPath = ""
8889 m .lastHighlightedPath = ""
8990 m .clearFollowTarget ()
@@ -181,28 +182,24 @@ func (m Model) applyFilesUpdate(msg FilesUpdatedMsg) Model {
181182 }
182183
183184 m .Files = msg .Files
184- m .highlightedHunks = make (map [string ]map [int ]bool , len (m .Files ))
185+ m .highlightedLines = make (map [string ]map [lineKey ]bool , len (m .Files ))
185186
186187 for _ , file := range m .Files {
187- changed := changedHunkIndices (m .hunkSigs [file .Path ], file .Hunks )
188+ changed := changedLineKeys (m .prevLineSigs [file .Path ], file .Hunks )
188189 if len (changed ) == 0 {
189190 continue
190191 }
191- hunks := make (map [int ]bool , len (changed ))
192- for _ , idx := range changed {
193- hunks [idx ] = true
194- }
195- m .highlightedHunks [file .Path ] = hunks
192+ m .highlightedLines [file .Path ] = changed
196193 }
197194
198195 m .lastChangedPath = lastChangedPathInFiles (msg .ChangedPaths , m .Files )
199- m .lastHighlightedPath = lastHighlightedPathInFiles (msg .ChangedPaths , m .highlightedHunks , m .Files )
196+ m .lastHighlightedPath = lastHighlightedPathInFiles (msg .ChangedPaths , m .highlightedLines , m .Files )
200197
201198 if len (m .Files ) == 0 {
202199 m .CurrentIdx = 0
203200 m .ScrollOffset = 0
204- m .hunkSigs = make (map [string ][]uint64 )
205- m .highlightedHunks = make (map [string ]map [int ]bool )
201+ m .prevLineSigs = make (map [string ][]uint64 )
202+ m .highlightedLines = make (map [string ]map [lineKey ]bool )
206203 m .lastChangedPath = ""
207204 m .lastHighlightedPath = ""
208205 m .clearFollowTarget ()
@@ -220,7 +217,7 @@ func (m Model) applyFilesUpdate(msg FilesUpdatedMsg) Model {
220217 m .anchorCurrentPath (currentPath )
221218 }
222219
223- m .hunkSigs = buildPrevHunkSigs (m .Files )
220+ m .prevLineSigs = buildPrevLineSigs (m .Files )
224221 m .clampScrollOffset ()
225222 return m
226223}
@@ -383,10 +380,11 @@ func (m *Model) setFollowTarget(targetIdx int) {
383380 file := & m .Files [targetIdx ]
384381 m .CurrentIdx = targetIdx
385382
386- if hunkIdx , ok := maxHighlightedHunkIndex (m .highlightedHunks [file .Path ]); ok {
387- m .ScrollOffset = hunkVisualOffset (file , hunkIdx , 0 , m .Width )
383+ if hunkIdx , lineIdx , ok := latestHighlightedLine (m .highlightedLines [file .Path ]); ok {
384+ m .ScrollOffset = hunkVisualOffset (file , hunkIdx , lineIdx , m .Width )
388385 m .followTargetPath = file .Path
389386 m .followTargetHunk = hunkIdx
387+ m .followTargetLineIdx = lineIdx
390388 } else {
391389 m .clearFollowTarget ()
392390 m .ScrollOffset = 0
@@ -399,6 +397,7 @@ func (m *Model) setFollowTarget(targetIdx int) {
399397func (m * Model ) clearFollowTarget () {
400398 m .followTargetPath = ""
401399 m .followTargetHunk = - 1
400+ m .followTargetLineIdx = - 1
402401}
403402
404403func (m * Model ) anchorCurrentPath (currentPath string ) {
@@ -420,7 +419,7 @@ func (m *Model) anchorCurrentPath(currentPath string) {
420419
421420// realignFollowTarget recomputes the stored follow target after width changes.
422421func (m * Model ) realignFollowTarget () {
423- if ! m .FollowOn || m .followTargetPath == "" || m .followTargetHunk < 0 {
422+ if ! m .FollowOn || m .followTargetPath == "" || m .followTargetHunk < 0 || m . followTargetLineIdx < 0 {
424423 return
425424 }
426425 if m .CurrentIdx < 0 || m .CurrentIdx >= len (m .Files ) {
@@ -433,17 +432,21 @@ func (m *Model) realignFollowTarget() {
433432 m .clearFollowTarget ()
434433 return
435434 }
435+ if m .followTargetLineIdx >= len (file .Hunks [m .followTargetHunk ].Lines ) {
436+ m .clearFollowTarget ()
437+ return
438+ }
436439
437- m .ScrollOffset = hunkVisualOffset (file , m .followTargetHunk , 0 , m .Width )
440+ m .ScrollOffset = hunkVisualOffset (file , m .followTargetHunk , m . followTargetLineIdx , m .Width )
438441}
439442
440- // buildPrevHunkSigs snapshots current file hunks for the next follow comparison.
441- func buildPrevHunkSigs (files []internal.FileDiff ) map [string ][]uint64 {
442- prevHunkSigs := make (map [string ][]uint64 , len (files ))
443+ // buildPrevLineSigs snapshots current file add/del lines for the next follow comparison.
444+ func buildPrevLineSigs (files []internal.FileDiff ) map [string ][]uint64 {
445+ prevLineSigs := make (map [string ][]uint64 , len (files ))
443446 for _ , file := range files {
444- prevHunkSigs [file .Path ] = hunkFingerprints (file .Hunks )
447+ prevLineSigs [file .Path ] = lineFingerprints (file .Hunks )
445448 }
446- return prevHunkSigs
449+ return prevLineSigs
447450}
448451
449452func fileIndexByPath (files []internal.FileDiff , path string ) int {
@@ -467,7 +470,7 @@ func lastChangedPathInFiles(changedPaths []string, files []internal.FileDiff) st
467470 return ""
468471}
469472
470- func lastHighlightedPathInFiles (changedPaths []string , highlighted map [string ]map [int ]bool , files []internal.FileDiff ) string {
473+ func lastHighlightedPathInFiles (changedPaths []string , highlighted map [string ]map [lineKey ]bool , files []internal.FileDiff ) string {
471474 for i := len (changedPaths ) - 1 ; i >= 0 ; i -- {
472475 path := changedPaths [i ]
473476 if fileIndexByPath (files , path ) < 0 {
@@ -480,17 +483,40 @@ func lastHighlightedPathInFiles(changedPaths []string, highlighted map[string]ma
480483 return ""
481484}
482485
483- func maxHighlightedHunkIndex (highlighted map [int ]bool ) (int , bool ) {
486+ // latestHighlightedLine returns the newest highlighted hunk and the first
487+ // highlighted line inside that hunk.
488+ func latestHighlightedLine (highlighted map [lineKey ]bool ) (int , int , bool ) {
484489 if len (highlighted ) == 0 {
485- return - 1 , false
490+ return - 1 , - 1 , false
486491 }
487- maxIdx := - 1
488- for idx := range highlighted {
489- if idx > maxIdx {
490- maxIdx = idx
492+
493+ bestHunkIdx := - 1
494+ bestLineIdx := - 1
495+ for key := range highlighted {
496+ if key .HunkIdx > bestHunkIdx {
497+ bestHunkIdx = key .HunkIdx
498+ bestLineIdx = key .LineIdx
499+ continue
500+ }
501+ if key .HunkIdx == bestHunkIdx && (bestLineIdx < 0 || key .LineIdx < bestLineIdx ) {
502+ bestLineIdx = key .LineIdx
491503 }
492504 }
493- return maxIdx , true
505+ return bestHunkIdx , bestLineIdx , true
506+ }
507+
508+ // highlightedHunksFromLines temporarily adapts line-level state to the old
509+ // hunk-level renderer until diffview switches to line-aware highlighting.
510+ func highlightedHunksFromLines (highlighted map [lineKey ]bool ) map [int ]bool {
511+ if len (highlighted ) == 0 {
512+ return nil
513+ }
514+
515+ hunks := make (map [int ]bool , len (highlighted ))
516+ for key := range highlighted {
517+ hunks [key .HunkIdx ] = true
518+ }
519+ return hunks
494520}
495521
496522// clampScrollOffset keeps scroll state within the current diff viewport bounds.
@@ -535,7 +561,7 @@ func (m Model) View() string {
535561 if m .OverlayOpen {
536562 content = RenderOverlay (m .OverlaySnapshot , m .OverlayCursor , diffHeight , m .Width )
537563 } else if len (m .Files ) > 0 && m .CurrentIdx < len (m .Files ) {
538- highlightSet := m . highlightedHunks [m .Files [m .CurrentIdx ].Path ]
564+ highlightSet := highlightedHunksFromLines ( m . highlightedLines [m .Files [m .CurrentIdx ].Path ])
539565 lines := m .displayCache .get (& m .Files [m .CurrentIdx ], m .Width , highlightSet , func () []string {
540566 return diffDisplayLines (& m .Files [m .CurrentIdx ], m .Width , highlightSet )
541567 })
0 commit comments