on main help sign in
feat(diffview): implement resizer to make diff wider or shorter as asked
c608d8fAndrey Nering
internal/exp/diffview/diffview.go+28−3
@@ -137,7 +137,12 @@ func (dv *DiffView) String() string {
137 dv.convertDiffToSplit()
137 dv.convertDiffToSplit()
138 dv.adjustStyles()
138 dv.adjustStyles()
139 dv.detectNumDigits()
139 dv.detectNumDigits()
140+
141+ if dv.width <= 0 {
140− dv.detectCodeWidth()
142+ dv.detectCodeWidth()
143+ } else {
144+ dv.resizeCodeWidth()
145+ }
141
146
142 switch dv.layout {
147 switch dv.layout {
143 case layoutUnified:
148 case layoutUnified:
@@ -254,6 +259,21 @@ func (dv *DiffView) detectSplitCodeWidth() {
254 }
259 }
255}
260}
256
261
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+
257// renderUnified renders the unified diff view as a string.
277// renderUnified renders the unified diff view as a string.
258func (dv *DiffView) renderUnified() string {
278func (dv *DiffView) renderUnified() string {
259 var b strings.Builder
279 var b strings.Builder
@@ -263,7 +283,8 @@ func (dv *DiffView) renderUnified() string {
263 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
283 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
264 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
284 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
265 }
285 }
286+ content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
266− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(h)))
287+ b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
267 b.WriteRune('\n')
288 b.WriteRune('\n')
268
289
269 beforeLine := h.FromLine
290 beforeLine := h.FromLine
@@ -271,6 +292,7 @@ func (dv *DiffView) renderUnified() string {
271
292
272 for _, l := range h.Lines {
293 for _, l := range h.Lines {
273 content := strings.TrimSuffix(l.Content, "\n")
294 content := strings.TrimSuffix(l.Content, "\n")
295+ content = ansi.Truncate(content, dv.codeWidth, "…")
274
296
275 switch l.Kind {
297 switch l.Kind {
276 case udiff.Equal:
298 case udiff.Equal:
@@ -313,7 +335,8 @@ func (dv *DiffView) renderSplit() string {
313 if dv.lineNumbers {
335 if dv.lineNumbers {
314 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
336 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
315 }
337 }
338+ content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
316− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(dv.unified.Hunks[i])))
339+ b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
317 if dv.lineNumbers {
340 if dv.lineNumbers {
318 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
341 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
319 }
342 }
@@ -328,9 +351,11 @@ func (dv *DiffView) renderSplit() string {
328 var afterContent string
351 var afterContent string
329 if l.before != nil {
352 if l.before != nil {
330 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
353 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
354+ beforeContent = ansi.Truncate(beforeContent, dv.codeWidth, "…")
331 }
355 }
332 if l.after != nil {
356 if l.after != nil {
333 afterContent = strings.TrimSuffix(l.after.Content, "\n")
357 afterContent = strings.TrimSuffix(l.after.Content, "\n")
358+ afterContent = ansi.Truncate(afterContent, dv.codeWidth, "…")
334 }
359 }
335
360
336 switch {
361 switch {
internal/exp/diffview/diffview_test.go+36−1
@@ -2,8 +2,10 @@ package diffview_test
2
2
3import (
3import (
4 _ "embed"
4 _ "embed"
5+ "strings"
5 "testing"
6 "testing"
6
7
8+ "github.com/charmbracelet/x/ansi"
7 "github.com/charmbracelet/x/exp/golden"
9 "github.com/charmbracelet/x/exp/golden"
8 "github.com/opencode-ai/opencode/internal/exp/diffview"
10 "github.com/opencode-ai/opencode/internal/exp/diffview"
9)
11)
@@ -66,6 +68,18 @@ var (
66 Before("text.txt", TestNarrowBefore).
68 Before("text.txt", TestNarrowBefore).
67 After("text.txt", TestNarrowAfter)
69 After("text.txt", TestNarrowAfter)
68 }
70 }
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+ }
69
83
70 LightModeFunc = func(dv *diffview.DiffView) *diffview.DiffView {
84 LightModeFunc = func(dv *diffview.DiffView) *diffview.DiffView {
71 return dv.Style(diffview.DefaultLightStyle)
85 return dv.Style(diffview.DefaultLightStyle)
@@ -84,6 +98,8 @@ var (
84 "MultipleHunks": MultipleHunksFunc,
98 "MultipleHunks": MultipleHunksFunc,
85 "CustomContextLines": CustomContextLinesFunc,
99 "CustomContextLines": CustomContextLinesFunc,
86 "Narrow": NarrowFunc,
100 "Narrow": NarrowFunc,
101+ "SmallWidth": SmallWidthFunc,
102+ "LargeWidth": LargeWidthFunc,
87 }
103 }
88 ThemeFuncs = TestFuncs{
104 ThemeFuncs = TestFuncs{
89 "LightMode": LightModeFunc,
105 "LightMode": LightModeFunc,
@@ -102,7 +118,16 @@ func TestDiffView(t *testing.T) {
102 dv = layoutFunc(dv)
118 dv = layoutFunc(dv)
103 dv = behaviorFunc(dv)
119 dv = behaviorFunc(dv)
104 dv = themeFunc(dv)
120 dv = themeFunc(dv)
121+
122+ output := dv.String()
105− golden.RequireEqual(t, []byte(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+ }
106 })
131 })
107 }
132 }
108 })
133 })
@@ -110,3 +135,13 @@ func TestDiffView(t *testing.T) {
110 })
135 })
111 }
136 }
112}
137}
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  }