on main help sign in
feat(diffview): show hunk lines and test for multiple hunks
commit07d4221c589fe068d98c949bdf1461d6e29fee98 authorAndrey Nering <andreynering@users.noreply.github.com> dateThu 5 Jun 2025, 14:35 (1 year ago) parentc8e653e
11 files changed+158−11
internal/exp/diffview/diffview.go+48−8 internal/exp/diffview/diffview_test.go+22 internal/exp/diffview/style.go+24−3 internal/exp/diffview/testdata/TestDefault/DarkMode.golden+1 internal/exp/diffview/testdata/TestDefault/LightMode.golden+1 internal/exp/diffview/testdata/TestMultipleHunks.after+15 internal/exp/diffview/testdata/TestMultipleHunks.before+13 internal/exp/diffview/testdata/TestMultipleHunks/DarkMode.golden+16 internal/exp/diffview/testdata/TestMultipleHunks/LightMode.golden+16 internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.golden+1 internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden+1
internal/exp/diffview/diffview.go+48−8
@@ -1,6 +1,7 @@
11package diffview
22
33import (
4+ "fmt"
45 "os"
56 "strconv"
67 "strings"
@@ -125,22 +126,34 @@ func (dv *DiffView) String() string {
125126 }
126127 dv.detectWidth()
127128
128− lineNumberWidth := func(start, num int) int {
129− return len(strconv.Itoa(start + num))
130− }
129+ codeWidth := dv.width - leadingSymbolsSize
130+ beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
131131
132132 var b strings.Builder
133133
134− for _, h := range dv.unified.Hunks {
134+ for i, h := range dv.unified.Hunks {
135+ beforeShownLines, afterShownLines := dv.hunkShownLines(i)
136+
137+ if dv.lineNumbers {
138+ b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
139+ b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
140+ }
141+ b.WriteString(dv.style.DividerLine.Code.Width(codeWidth + leadingSymbolsSize).Render(
142+ fmt.Sprintf(
143+ " @@ -%d,%d +%d,%d @@",
144+ h.FromLine,
145+ beforeShownLines,
146+ h.ToLine,
147+ afterShownLines,
148+ ),
149+ ))
150+ b.WriteRune('\n')
151+
135152 beforeLine := h.FromLine
136153 afterLine := h.ToLine
137154
138− beforeNumDigits := lineNumberWidth(h.FromLine, len(h.Lines))
139− afterNumDigits := lineNumberWidth(h.ToLine, len(h.Lines))
140−
141155 for _, l := range h.Lines {
142156 content := strings.TrimSuffix(l.Content, "\n")
143− codeWidth := dv.width - leadingSymbolsSize
144157
145158 switch l.Kind {
146159 case udiff.Equal:
@@ -194,6 +207,33 @@ func (dv *DiffView) computeDiff() error {
194207 return dv.err
195208}
196209
210+// lineNumberDigits calculates the maximum number of digits needed for before and
211+// after line numbers.
212+func (dv *DiffView) lineNumberDigits() (maxBefore, maxAfter int) {
213+ for _, h := range dv.unified.Hunks {
214+ maxBefore = max(maxBefore, len(strconv.Itoa(h.FromLine+len(h.Lines))))
215+ maxAfter = max(maxAfter, len(strconv.Itoa(h.ToLine+len(h.Lines))))
216+ }
217+ return
218+}
219+
220+// hunkShownLines calculates the number of lines shown in a hunk for both before
221+// and after versions.
222+func (dv *DiffView) hunkShownLines(i int) (before, after int) {
223+ for _, l := range dv.unified.Hunks[i].Lines {
224+ switch l.Kind {
225+ case udiff.Equal:
226+ before++
227+ after++
228+ case udiff.Insert:
229+ after++
230+ case udiff.Delete:
231+ before++
232+ }
233+ }
234+ return
235+}
236+
197237func (dv *DiffView) detectWidth() {
198238 if dv.width > 0 {
199239 return
internal/exp/diffview/diffview_test.go+22
@@ -14,6 +14,12 @@ var TestDefaultBefore string
1414//go:embed testdata/TestDefault.after
1515var TestDefaultAfter string
1616
17+//go:embed testdata/TestMultipleHunks.before
18+var TestMultipleHunksBefore string
19+
20+//go:embed testdata/TestMultipleHunks.after
21+var TestMultipleHunksAfter string
22+
1723func TestDefault(t *testing.T) {
1824 dv := diffview.New().
1925 Before("main.go", TestDefaultBefore).
@@ -46,3 +52,19 @@ func TestNoLineNumbers(t *testing.T) {
4652 golden.RequireEqual(t, []byte(dv.String()))
4753 })
4854}
55+
56+func TestMultipleHunks(t *testing.T) {
57+ dv := diffview.New().
58+ Before("main.go", TestMultipleHunksBefore).
59+ After("main.go", TestMultipleHunksAfter)
60+
61+ t.Run("LightMode", func(t *testing.T) {
62+ dv = dv.Style(diffview.DefaultLightStyle)
63+ golden.RequireEqual(t, []byte(dv.String()))
64+ })
65+
66+ t.Run("DarkMode", func(t *testing.T) {
67+ dv = dv.Style(diffview.DefaultDarkStyle)
68+ golden.RequireEqual(t, []byte(dv.String()))
69+ })
70+}
internal/exp/diffview/style.go+24−3
@@ -12,12 +12,23 @@ type LineStyle struct {
1212}
1313
1414type Style struct {
15− EqualLine LineStyle
16− InsertLine LineStyle
17− DeleteLine LineStyle
15+ DividerLine LineStyle
16+ EqualLine LineStyle
17+ InsertLine LineStyle
18+ DeleteLine LineStyle
1819}
1920
2021var DefaultLightStyle = Style{
22+ DividerLine: LineStyle{
23+ LineNumber: lipgloss.NewStyle().
24+ Foreground(charmtone.Iron).
25+ Background(charmtone.Thunder).
26+ Align(lipgloss.Right).
27+ Padding(0, 1),
28+ Code: lipgloss.NewStyle().
29+ Foreground(charmtone.Oyster).
30+ Background(charmtone.Anchovy),
31+ },
2132 EqualLine: LineStyle{
2233 LineNumber: lipgloss.NewStyle().
2334 Foreground(charmtone.Charcoal).
@@ -57,6 +68,16 @@ var DefaultLightStyle = Style{
5768}
5869
5970var DefaultDarkStyle = Style{
71+ DividerLine: LineStyle{
72+ LineNumber: lipgloss.NewStyle().
73+ Foreground(charmtone.Smoke).
74+ Background(charmtone.Sapphire).
75+ Align(lipgloss.Right).
76+ Padding(0, 1),
77+ Code: lipgloss.NewStyle().
78+ Foreground(charmtone.Smoke).
79+ Background(charmtone.Ox),
80+ },
6081 EqualLine: LineStyle{
6182 LineNumber: lipgloss.NewStyle().
6283 Foreground(charmtone.Ash).
internal/exp/diffview/testdata/TestDefault/DarkMode.golden+1
@@ -1,3 +1,4 @@
1+  …   …  @@ -5,5 +5,6 @@ 
12  5   5  ) 
23  6   6   
34  7   7  func main() { 
internal/exp/diffview/testdata/TestDefault/LightMode.golden+1
@@ -1,3 +1,4 @@
1+  …   …  @@ -5,5 +5,6 @@ 
12  5   5  ) 
23  6   6   
34  7   7  func main() { 
internal/exp/diffview/testdata/TestMultipleHunks.afternew file+15
@@ -0,0 +1,15 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "strings"
6+)
7+
8+func main() {
9+ fmt.Println(getContent())
10+}
11+
12+func getContent() string {
13+ content := strings.ToUpper("Hello, World!")
14+ return content
15+}
internal/exp/diffview/testdata/TestMultipleHunks.beforenew file+13
@@ -0,0 +1,13 @@
1+package main
2+
3+import (
4+ "fmt"
5+)
6+
7+func main() {
8+ fmt.Println(getContent())
9+}
10+
11+func getContent() string {
12+ return "Hello, world!"
13+}
internal/exp/diffview/testdata/TestMultipleHunks/DarkMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@ 
2+  2   2   
3+  3   3  import ( 
4+  4   4  "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@ 
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!")
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestMultipleHunks/LightMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@ 
2+  2   2   
3+  3   3  import ( 
4+  4   4  "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@ 
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!")
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.golden+1
@@ -1,3 +1,4 @@
1+ @@ -5,5 +5,6 @@ 
12 ) 
23  
34 func main() { 
internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden+1
@@ -1,3 +1,4 @@
1+ @@ -5,5 +5,6 @@ 
12 ) 
23  
34 func main() {