Skip to content

Commit 0769d25

Browse files
committed
fix: track highlighted diff lines
1 parent af7c278 commit 0769d25

2 files changed

Lines changed: 147 additions & 68 deletions

File tree

internal/ui/model.go

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {
399397
func (m *Model) clearFollowTarget() {
400398
m.followTargetPath = ""
401399
m.followTargetHunk = -1
400+
m.followTargetLineIdx = -1
402401
}
403402

404403
func (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.
422421
func (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

449452
func 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

Comments
 (0)