on main help sign in
feat(diffview): implement resizer to make diff wider or shorter as asked
commitc608d8f06e95d3e0a7c1990bd36cc6da62599426 authorAndrey Nering <andreynering@users.noreply.github.com> dateMon 9 Jun 2025, 18:15 (1 year ago) parentac33d35
10 files changed+188−4
internal/exp/diffview/diffview.go+28−3 internal/exp/diffview/diffview_test.go+36−1 internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/DarkMode.golden+15 internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/LightMode.golden+15 internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/DarkMode.golden+15 internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/LightMode.golden+15 internal/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/DarkMode.golden+16 internal/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/LightMode.golden+16 internal/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/DarkMode.golden+16 internal/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/LightMode.golden+16
internal/exp/diffview/diffview.go+28−3
@@ -137,7 +137,12 @@ func (dv *DiffView) String() string {
137137 dv.convertDiffToSplit()
138138 dv.adjustStyles()
139139 dv.detectNumDigits()
140− dv.detectCodeWidth()
140+
141+ if dv.width <= 0 {
142+ dv.detectCodeWidth()
143+ } else {
144+ dv.resizeCodeWidth()
145+ }
141146
142147 switch dv.layout {
143148 case layoutUnified:
@@ -254,6 +259,21 @@ func (dv *DiffView) detectSplitCodeWidth() {
254259 }
255260}
256261
262+// resizeCodeWidth resizes the code width to fit within the specified width.
263+func (dv *DiffView) resizeCodeWidth() {
264+ fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
265+ fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
266+
267+ switch dv.layout {
268+ case layoutUnified:
269+ dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
270+ case layoutSplit:
271+ dv.codeWidth = (dv.width - fullNumWidth - leadingSymbolsSize*2) / 2
272+ }
273+
274+ dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
275+}
276+
257277// renderUnified renders the unified diff view as a string.
258278func (dv *DiffView) renderUnified() string {
259279 var b strings.Builder
@@ -263,7 +283,8 @@ func (dv *DiffView) renderUnified() string {
263283 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
264284 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
265285 }
266− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(h)))
286+ content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
287+ b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
267288 b.WriteRune('\n')
268289
269290 beforeLine := h.FromLine
@@ -271,6 +292,7 @@ func (dv *DiffView) renderUnified() string {
271292
272293 for _, l := range h.Lines {
273294 content := strings.TrimSuffix(l.Content, "\n")
295+ content = ansi.Truncate(content, dv.codeWidth, "…")
274296
275297 switch l.Kind {
276298 case udiff.Equal:
@@ -313,7 +335,8 @@ func (dv *DiffView) renderSplit() string {
313335 if dv.lineNumbers {
314336 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
315337 }
316− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(dv.unified.Hunks[i])))
338+ content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
339+ b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
317340 if dv.lineNumbers {
318341 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
319342 }
@@ -328,9 +351,11 @@ func (dv *DiffView) renderSplit() string {
328351 var afterContent string
329352 if l.before != nil {
330353 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
354+ beforeContent = ansi.Truncate(beforeContent, dv.codeWidth, "…")
331355 }
332356 if l.after != nil {
333357 afterContent = strings.TrimSuffix(l.after.Content, "\n")
358+ afterContent = ansi.Truncate(afterContent, dv.codeWidth, "…")
334359 }
335360
336361 switch {
internal/exp/diffview/diffview_test.go+36−1
@@ -2,8 +2,10 @@ package diffview_test
22
33import (
44 _ "embed"
5+ "strings"
56 "testing"
67
8+ "github.com/charmbracelet/x/ansi"
79 "github.com/charmbracelet/x/exp/golden"
810 "github.com/opencode-ai/opencode/internal/exp/diffview"
911)
@@ -66,6 +68,18 @@ var (
6668 Before("text.txt", TestNarrowBefore).
6769 After("text.txt", TestNarrowAfter)
6870 }
71+ SmallWidthFunc = func(dv *diffview.DiffView) *diffview.DiffView {
72+ return dv.
73+ Before("text.txt", TestMultipleHunksBefore).
74+ After("text.txt", TestMultipleHunksAfter).
75+ Width(40)
76+ }
77+ LargeWidthFunc = func(dv *diffview.DiffView) *diffview.DiffView {
78+ return dv.
79+ Before("text.txt", TestMultipleHunksBefore).
80+ After("text.txt", TestMultipleHunksAfter).
81+ Width(120)
82+ }
6983
7084 LightModeFunc = func(dv *diffview.DiffView) *diffview.DiffView {
7185 return dv.Style(diffview.DefaultLightStyle)
@@ -84,6 +98,8 @@ var (
8498 "MultipleHunks": MultipleHunksFunc,
8599 "CustomContextLines": CustomContextLinesFunc,
86100 "Narrow": NarrowFunc,
101+ "SmallWidth": SmallWidthFunc,
102+ "LargeWidth": LargeWidthFunc,
87103 }
88104 ThemeFuncs = TestFuncs{
89105 "LightMode": LightModeFunc,
@@ -102,7 +118,16 @@ func TestDiffView(t *testing.T) {
102118 dv = layoutFunc(dv)
103119 dv = behaviorFunc(dv)
104120 dv = themeFunc(dv)
105− golden.RequireEqual(t, []byte(dv.String()))
121+
122+ output := dv.String()
123+ golden.RequireEqual(t, []byte(output))
124+
125+ switch behaviorName {
126+ case "SmallWidth":
127+ assertLineWidth(t, 40, output)
128+ case "LargeWidth":
129+ assertLineWidth(t, 120, output)
130+ }
106131 })
107132 }
108133 })
@@ -110,3 +135,13 @@ func TestDiffView(t *testing.T) {
110135 })
111136 }
112137}
138+
139+func assertLineWidth(t *testing.T, expected int, output string) {
140+ var lineWidth int
141+ for line := range strings.SplitSeq(output, "\n") {
142+ lineWidth = max(lineWidth, ansi.StringWidth(line))
143+ }
144+ if lineWidth != expected {
145+ t.Errorf("expected output width to be == %d, got %d", expected, lineWidth)
146+ }
147+}
internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/DarkMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4  "fmt"   4  "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/LightMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4  "fmt"   4  "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/DarkMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4  "fmt"   4  "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConte… 12  func getConte…
13+ 12 -  return "H… 13 +  content :…
14+      14 +  return co…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/LightMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4  "fmt"   4  "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConte… 12  func getConte…
13+ 12 -  return "H… 13 +  content :…
14+      14 +  return co…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/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/TestDiffView/Unified/LargeWidth/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/TestDiffView/Unified/SmallWidth/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.ToUppe…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/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.ToUppe…
15+    14 +  return content 
16+ 13  15  }