Skip to content

Commit 653ebaf

Browse files
committed
fix: render line-level diff highlights
1 parent 0769d25 commit 653ebaf

3 files changed

Lines changed: 59 additions & 32 deletions

File tree

internal/ui/diffview.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func newDisplayLineCache() *displayLineCache {
4040

4141
// get returns cached rendered lines when the file signature, width, and
4242
// highlight state match.
43-
func (c *displayLineCache) get(file *internal.FileDiff, width int, highlightSet map[int]bool, build func() []string) []string {
43+
func (c *displayLineCache) get(file *internal.FileDiff, width int, highlightSet map[lineKey]bool, build func() []string) []string {
4444
if c == nil {
4545
return build()
4646
}
@@ -67,7 +67,7 @@ func (c *displayLineCache) get(file *internal.FileDiff, width int, highlightSet
6767
}
6868

6969
// newDisplayLineCacheKey fingerprints the rendered inputs that affect visual lines.
70-
func newDisplayLineCacheKey(file *internal.FileDiff, width int, highlightSet map[int]bool) displayLineCacheKey {
70+
func newDisplayLineCacheKey(file *internal.FileDiff, width int, highlightSet map[lineKey]bool) displayLineCacheKey {
7171
key := displayLineCacheKey{width: width}
7272
if file == nil {
7373
return key
@@ -90,7 +90,7 @@ func newDisplayLineCacheKey(file *internal.FileDiff, width int, highlightSet map
9090
}
9191

9292
// RenderDiffView renders the current file diff within the viewport.
93-
func RenderDiffView(file *internal.FileDiff, scrollOffset, width, height int, highlightSet map[int]bool) string {
93+
func RenderDiffView(file *internal.FileDiff, scrollOffset, width, height int, highlightSet map[lineKey]bool) string {
9494
lines := diffDisplayLines(file, width, highlightSet)
9595
return renderDisplayLines(lines, scrollOffset, height)
9696
}
@@ -179,7 +179,7 @@ func renderSeparator(width int) string {
179179
}
180180

181181
// diffDisplayLines expands one file diff into the exact visual lines shown in the viewport.
182-
func diffDisplayLines(file *internal.FileDiff, width int, highlightSet map[int]bool) []string {
182+
func diffDisplayLines(file *internal.FileDiff, width int, highlightSet map[lineKey]bool) []string {
183183
if file == nil {
184184
return nil
185185
}
@@ -202,17 +202,17 @@ func diffDisplayLines(file *internal.FileDiff, width int, highlightSet map[int]b
202202

203203
var lines []string
204204
for hunkIdx, hunk := range file.Hunks {
205-
highlightedHunk := highlightSet[hunkIdx]
206205
lines = append(lines, renderSeparator(width))
207-
for _, diffLine := range hunk.Lines {
206+
for lineIdx, diffLine := range hunk.Lines {
207+
highlightedLine := highlightSet[lineKey{HunkIdx: hunkIdx, LineIdx: lineIdx}]
208208
lineNo := displayedLineNo(diffLine)
209209
for i, segment := range wrapLineParts(diffLine.Content, contentWidth) {
210210
if compactGutter {
211211
prefix := "↳"
212212
if i == 0 {
213213
prefix = diffPrefix(diffLine.Type)
214214
}
215-
lines = append(lines, renderDiffSegment("", prefix, segment, diffLine.Type, width, file.Path, highlightedHunk))
215+
lines = append(lines, renderDiffSegment("", prefix, segment, diffLine.Type, width, file.Path, highlightedLine))
216216
continue
217217
}
218218

@@ -222,7 +222,7 @@ func diffDisplayLines(file *internal.FileDiff, width int, highlightSet map[int]b
222222
lineNoText = formatDisplayedLineNo(lineNo, lineNumberWidth)
223223
prefix = diffPrefix(diffLine.Type)
224224
}
225-
lines = append(lines, renderDiffSegment(lineNoText, prefix, segment, diffLine.Type, width, file.Path, highlightedHunk))
225+
lines = append(lines, renderDiffSegment(lineNoText, prefix, segment, diffLine.Type, width, file.Path, highlightedLine))
226226
}
227227
}
228228
}
@@ -427,20 +427,25 @@ func renderDiffSegment(lineNoText, prefix, code string, lineType internal.LineTy
427427
}
428428
}
429429

430-
func highlightSignature(highlightSet map[int]bool) uint64 {
430+
func highlightSignature(highlightSet map[lineKey]bool) uint64 {
431431
if len(highlightSet) == 0 {
432432
return 0
433433
}
434434

435-
indices := make([]int, 0, len(highlightSet))
436-
for idx := range highlightSet {
437-
indices = append(indices, idx)
435+
keys := make([]lineKey, 0, len(highlightSet))
436+
for key := range highlightSet {
437+
keys = append(keys, key)
438438
}
439-
sort.Ints(indices)
439+
sort.Slice(keys, func(i, j int) bool {
440+
if keys[i].HunkIdx != keys[j].HunkIdx {
441+
return keys[i].HunkIdx < keys[j].HunkIdx
442+
}
443+
return keys[i].LineIdx < keys[j].LineIdx
444+
})
440445

441446
hasher := fnv.New64a()
442-
for _, idx := range indices {
443-
_, _ = hasher.Write([]byte(fmt.Sprintf("%d\x00", idx)))
447+
for _, key := range keys {
448+
_, _ = hasher.Write([]byte(fmt.Sprintf("%d\x00%d\x00", key.HunkIdx, key.LineIdx)))
444449
}
445450

446451
return hasher.Sum64()

internal/ui/diffview_test.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ func TestDisplayLineCacheMissesWhenHighlightStateChanges(t *testing.T) {
425425
}
426426

427427
builds := 0
428-
cache.get(file, 80, map[int]bool{0: true}, func() []string {
428+
cache.get(file, 80, map[lineKey]bool{{HunkIdx: 0, LineIdx: 0}: true}, func() []string {
429429
builds++
430430
return []string{"first"}
431431
})
@@ -675,7 +675,7 @@ func TestDiffDisplayLinesTrueColorPadsBackgroundAcrossViewport(t *testing.T) {
675675
}},
676676
}
677677

678-
line := diffDisplayLines(file, 30, map[int]bool{0: true})[1]
678+
line := diffDisplayLines(file, 30, map[lineKey]bool{{HunkIdx: 0, LineIdx: 0}: true})[1]
679679
if !strings.Contains(line, "\033[48;2;") {
680680
t.Fatalf("true-color add line should contain background ANSI, got %q", line)
681681
}
@@ -684,6 +684,42 @@ func TestDiffDisplayLinesTrueColorPadsBackgroundAcrossViewport(t *testing.T) {
684684
}
685685
}
686686

687+
// TestDiffDisplayLinesHighlightsOnlyMarkedLine verifies line-level highlight
688+
// state does not spill across other lines in the same hunk.
689+
func TestDiffDisplayLinesHighlightsOnlyMarkedLine(t *testing.T) {
690+
prevProfile := colorProfileFn
691+
colorProfileFn = func() termenv.Profile { return termenv.TrueColor }
692+
defer func() { colorProfileFn = prevProfile }()
693+
defer setThemeForTest(ThemeDark)()
694+
695+
file := &internal.FileDiff{
696+
Path: "main.go",
697+
Hunks: []internal.DiffHunk{{
698+
Header: "@@ -0,0 +1,2 @@",
699+
Lines: []internal.DiffLine{
700+
{
701+
Type: internal.LineAdd,
702+
Content: "first",
703+
NewLineNo: 1,
704+
},
705+
{
706+
Type: internal.LineAdd,
707+
Content: "second",
708+
NewLineNo: 2,
709+
},
710+
},
711+
}},
712+
}
713+
714+
lines := diffDisplayLines(file, 30, map[lineKey]bool{{HunkIdx: 0, LineIdx: 1}: true})
715+
if strings.Contains(lines[1], "\033[48;2;") {
716+
t.Fatalf("first line should not contain background ANSI, got %q", lines[1])
717+
}
718+
if !strings.Contains(lines[2], "\033[48;2;") {
719+
t.Fatalf("second line should contain background ANSI, got %q", lines[2])
720+
}
721+
}
722+
687723
// TestRenderDiffSegmentTrueColorNonHighlightedForcesPrefixColor verifies
688724
// true-color terminals still color add/delete prefixes when backgrounds are off.
689725
func TestRenderDiffSegmentTrueColorNonHighlightedForcesPrefixColor(t *testing.T) {

internal/ui/model.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -505,20 +505,6 @@ func latestHighlightedLine(highlighted map[lineKey]bool) (int, int, bool) {
505505
return bestHunkIdx, bestLineIdx, true
506506
}
507507

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
520-
}
521-
522508
// clampScrollOffset keeps scroll state within the current diff viewport bounds.
523509
func (m *Model) clampScrollOffset() {
524510
if m.ScrollOffset < 0 {
@@ -561,7 +547,7 @@ func (m Model) View() string {
561547
if m.OverlayOpen {
562548
content = RenderOverlay(m.OverlaySnapshot, m.OverlayCursor, diffHeight, m.Width)
563549
} else if len(m.Files) > 0 && m.CurrentIdx < len(m.Files) {
564-
highlightSet := highlightedHunksFromLines(m.highlightedLines[m.Files[m.CurrentIdx].Path])
550+
highlightSet := m.highlightedLines[m.Files[m.CurrentIdx].Path]
565551
lines := m.displayCache.get(&m.Files[m.CurrentIdx], m.Width, highlightSet, func() []string {
566552
return diffDisplayLines(&m.Files[m.CurrentIdx], m.Width, highlightSet)
567553
})

0 commit comments

Comments
 (0)