on main help sign in
feat(diffview): basic working functionality
commitb7ab1a8091421b7ea05a47f36ec31e1e7b8765d7 authorAndrey Nering <andreynering@users.noreply.github.com> dateWed 4 Jun 2025, 14:23 (1 year ago) parent1f98b24
9 files changed+162−22
go.mod+2 go.sum+2 internal/exp/diffview/diffview.go+106−12 internal/exp/diffview/diffview_test.go+19−3 internal/exp/diffview/testdata/TestDefault.after+10 internal/exp/diffview/testdata/TestDefault.before+9 internal/exp/diffview/testdata/TestDefault.golden−7 internal/exp/diffview/testdata/TestDefault/DarkMode.golden+7 internal/exp/diffview/testdata/TestDefault/LightMode.golden+7
go.mod+2
@@ -35,6 +35,8 @@ require (
3535 github.com/stretchr/testify v1.10.0
3636)
3737
38+require github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444
39+
3840require (
3941 cloud.google.com/go v0.116.0 // indirect
4042 cloud.google.com/go/auth v0.13.0 // indirect
go.sum+2
@@ -84,6 +84,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll
8484github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
8585github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
8686github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
87+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0=
88+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
8789github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb h1:GT/STWThMsrOfYQnhnIPb165e/g1waAp0gNMFvEO6WI=
8890github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb/go.mod h1:929X+xY3LeoOZrDWIBVZcx/zyS0CYtyLiUIvE4VbKC0=
8991github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
internal/exp/diffview/diffview.go+106−12
@@ -1,11 +1,18 @@
11package diffview
22
33import (
4+ "os"
5+ "strings"
6+
47 "github.com/aymanbagabas/go-udiff"
58 "github.com/aymanbagabas/go-udiff/myers"
69 "github.com/charmbracelet/lipgloss/v2"
10+ "github.com/charmbracelet/x/ansi"
11+ "github.com/charmbracelet/x/exp/charmtone"
712)
813
14+const leadingSymbolsSize = 2
15+
916type file struct {
1017 path string
1118 content string
@@ -18,16 +25,60 @@ const (
1825 layoutSplit
1926)
2027
28+type Style struct {
29+ Base lipgloss.Style
30+ InsertLine lipgloss.Style
31+ InsertSymbols lipgloss.Style
32+ DeleteLine lipgloss.Style
33+ DeleteSymbols lipgloss.Style
34+}
35+
36+var DefaultLightStyle = Style{
37+ Base: lipgloss.NewStyle().
38+ Foreground(charmtone.Pepper).
39+ Background(charmtone.Salt),
40+ InsertLine: lipgloss.NewStyle().
41+ Foreground(charmtone.Pepper).
42+ Background(lipgloss.Color("#e8f5e9")),
43+ InsertSymbols: lipgloss.NewStyle().
44+ Foreground(charmtone.Turtle).
45+ Background(lipgloss.Color("#e8f5e9")),
46+ DeleteLine: lipgloss.NewStyle().
47+ Foreground(charmtone.Pepper).
48+ Background(lipgloss.Color("#ffebee")),
49+ DeleteSymbols: lipgloss.NewStyle().
50+ Foreground(charmtone.Cherry).
51+ Background(lipgloss.Color("#ffebee")),
52+}
53+
54+var DefaultDarkStyle = Style{
55+ Base: lipgloss.NewStyle().
56+ Foreground(charmtone.Salt).
57+ Background(charmtone.Pepper),
58+ InsertLine: lipgloss.NewStyle().
59+ Foreground(charmtone.Salt).
60+ Background(lipgloss.Color("#303a30")),
61+ InsertSymbols: lipgloss.NewStyle().
62+ Foreground(charmtone.Turtle).
63+ Background(lipgloss.Color("#303a30")),
64+ DeleteLine: lipgloss.NewStyle().
65+ Foreground(charmtone.Salt).
66+ Background(lipgloss.Color("#3a3030")),
67+ DeleteSymbols: lipgloss.NewStyle().
68+ Foreground(charmtone.Cherry).
69+ Background(lipgloss.Color("#3a3030")),
70+}
71+
2172// DiffView represents a view for displaying differences between two files.
2273type DiffView struct {
2374 layout layout
2475 before file
2576 after file
2677 contextLines int
27− baseStyle lipgloss.Style
2878 highlight bool
2979 height int
3080 width int
81+ style Style
3182
3283 isComputed bool
3384 err error
@@ -37,10 +88,16 @@ type DiffView struct {
3788
3889// New creates a new DiffView with default settings.
3990func New() *DiffView {
40− return &DiffView{
91+ dv := &DiffView{
4192 layout: layoutUnified,
4293 contextLines: udiff.DefaultContextLines,
4394 }
95+ if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
96+ dv.style = DefaultDarkStyle
97+ } else {
98+ dv.style = DefaultLightStyle
99+ }
100+ return dv
44101}
45102
46103// Unified sets the layout of the DiffView to unified.
@@ -73,10 +130,9 @@ func (dv *DiffView) ContextLines(contextLines int) *DiffView {
73130 return dv
74131}
75132
76−// BaseStyle sets the base style for the DiffView.
77−// This is useful for setting a custom background color, for example.
78−func (dv *DiffView) BaseStyle(baseStyle lipgloss.Style) *DiffView {
79− dv.baseStyle = baseStyle
133+// Style sets the style for the DiffView.
134+func (dv *DiffView) Style(style Style) *DiffView {
135+ dv.style = style
80136 return dv
81137}
82138
@@ -100,16 +156,39 @@ func (dv *DiffView) Width(width int) *DiffView {
100156
101157// String returns the string representation of the DiffView.
102158func (dv *DiffView) String() string {
103− if !dv.isComputed {
104− dv.compute()
159+ if err := dv.computeDiff(); err != nil {
160+ return err.Error()
105161 }
106− if dv.err != nil {
107− return dv.err.Error()
162+ dv.detectWidth()
163+
164+ var b strings.Builder
165+
166+ for _, h := range dv.unified.Hunks {
167+ for _, l := range h.Lines {
168+ content := strings.TrimSuffix(l.Content, "\n")
169+ width := dv.width - leadingSymbolsSize
170+
171+ switch l.Kind {
172+ case udiff.Insert:
173+ b.WriteString(dv.style.InsertSymbols.Render("+ "))
174+ b.WriteString(dv.style.InsertLine.Width(width).Render(content))
175+ case udiff.Delete:
176+ b.WriteString(dv.style.DeleteSymbols.Render("- "))
177+ b.WriteString(dv.style.DeleteLine.Width(width).Render(content))
178+ case udiff.Equal:
179+ b.WriteString(dv.style.Base.Width(width + leadingSymbolsSize).Render(" " + content))
180+ }
181+ b.WriteRune('\n')
182+ }
108183 }
109− return dv.unified.String()
184+
185+ return b.String()
110186}
111187
112−func (dv *DiffView) compute() {
188+func (dv *DiffView) computeDiff() error {
189+ if dv.isComputed {
190+ return dv.err
191+ }
113192 dv.isComputed = true
114193 dv.edits = myers.ComputeEdits(
115194 dv.before.content,
@@ -122,4 +201,19 @@ func (dv *DiffView) compute() {
122201 dv.edits,
123202 dv.contextLines,
124203 )
204+ return dv.err
205+}
206+
207+func (dv *DiffView) detectWidth() {
208+ if dv.width > 0 {
209+ return
210+ }
211+
212+ for _, h := range dv.unified.Hunks {
213+ for _, l := range h.Lines {
214+ lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
215+ lineWidth += leadingSymbolsSize
216+ dv.width = max(dv.width, lineWidth)
217+ }
218+ }
125219}
internal/exp/diffview/diffview_test.go+19−3
@@ -1,15 +1,31 @@
11package diffview_test
22
33import (
4+ _ "embed"
45 "testing"
56
67 "github.com/charmbracelet/x/exp/golden"
78 "github.com/opencode-ai/opencode/internal/exp/diffview"
89)
910
11+//go:embed testdata/TestDefault.before
12+var TestDefaultBefore string
13+
14+//go:embed testdata/TestDefault.after
15+var TestDefaultAfter string
16+
1017func TestDefault(t *testing.T) {
1118 dv := diffview.New().
12− Before("test.txt", "This is the original content.").
13− After("test.txt", "This is the modified content.")
14− golden.RequireEqual(t, []byte(dv.String()))
19+ Before("main.go", TestDefaultBefore).
20+ After("main.go", TestDefaultAfter)
21+
22+ t.Run("LightMode", func(t *testing.T) {
23+ dv = dv.Style(diffview.DefaultLightStyle)
24+ golden.RequireEqual(t, []byte(dv.String()))
25+ })
26+
27+ t.Run("DarkMode", func(t *testing.T) {
28+ dv = dv.Style(diffview.DefaultDarkStyle)
29+ golden.RequireEqual(t, []byte(dv.String()))
30+ })
1531}
internal/exp/diffview/testdata/TestDefault.afternew file+10
@@ -0,0 +1,10 @@
1+package main
2+
3+import (
4+ "fmt"
5+)
6+
7+func main() {
8+ content := "Hello, world!"
9+ fmt.Println(content)
10+}
internal/exp/diffview/testdata/TestDefault.beforenew file+9
@@ -0,0 +1,9 @@
1+package main
2+
3+import (
4+ "fmt"
5+)
6+
7+func main() {
8+ fmt.Println("Hello, world!")
9+}
internal/exp/diffview/testdata/TestDefault.goldendeleted−7
@@ -1,7 +0,0 @@
1−--- test.txt
2−+++ test.txt
3−@@ -1 +1 @@
4−-This is the original content.
5−\ No newline at end of file
6−+This is the modified content.
7−\ No newline at end of file
internal/exp/diffview/testdata/TestDefault/DarkMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ ) 
2+  
3+ func main() { 
4+-  fmt.Println("Hello, world!")
5++  content := "Hello, world!" 
6++  fmt.Println(content) 
7+ } 
internal/exp/diffview/testdata/TestDefault/LightMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ ) 
2+  
3+ func main() { 
4+-  fmt.Println("Hello, world!")
5++  content := "Hello, world!" 
6++  fmt.Println(content) 
7+ }