on main help sign in
feat(diffview): basic working functionality
b7ab1a8Andrey Nering
@@ -35,6 +35,8 @@ require (
35 github.com/stretchr/testify v1.10.0
35 github.com/stretchr/testify v1.10.0
36)
36)
37
37
38+require github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444
39+
38require (
40require (
39 cloud.google.com/go v0.116.0 // indirect
41 cloud.google.com/go v0.116.0 // indirect
40 cloud.google.com/go/auth v0.13.0 // indirect
42 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
84github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
84github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
85github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
85github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
86github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
86github.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=
87github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb h1:GT/STWThMsrOfYQnhnIPb165e/g1waAp0gNMFvEO6WI=
89github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb h1:GT/STWThMsrOfYQnhnIPb165e/g1waAp0gNMFvEO6WI=
88github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb/go.mod h1:929X+xY3LeoOZrDWIBVZcx/zyS0CYtyLiUIvE4VbKC0=
90github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb/go.mod h1:929X+xY3LeoOZrDWIBVZcx/zyS0CYtyLiUIvE4VbKC0=
89github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
91github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
internal/exp/diffview/diffview.go+106−12
@@ -1,11 +1,18 @@
1package diffview
1package diffview
2
2
3import (
3import (
4+ "os"
5+ "strings"
6+
4 "github.com/aymanbagabas/go-udiff"
7 "github.com/aymanbagabas/go-udiff"
5 "github.com/aymanbagabas/go-udiff/myers"
8 "github.com/aymanbagabas/go-udiff/myers"
6 "github.com/charmbracelet/lipgloss/v2"
9 "github.com/charmbracelet/lipgloss/v2"
10+ "github.com/charmbracelet/x/ansi"
11+ "github.com/charmbracelet/x/exp/charmtone"
7)
12)
8
13
14+const leadingSymbolsSize = 2
15+
9type file struct {
16type file struct {
10 path string
17 path string
11 content string
18 content string
@@ -18,16 +25,60 @@ const (
18 layoutSplit
25 layoutSplit
19)
26)
20
27
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+
21// DiffView represents a view for displaying differences between two files.
72// DiffView represents a view for displaying differences between two files.
22type DiffView struct {
73type DiffView struct {
23 layout layout
74 layout layout
24 before file
75 before file
25 after file
76 after file
26 contextLines int
77 contextLines int
27− baseStyle lipgloss.Style
28 highlight bool
78 highlight bool
29 height int
79 height int
30 width int
80 width int
81+ style Style
31
82
32 isComputed bool
83 isComputed bool
33 err error
84 err error
@@ -37,10 +88,16 @@ type DiffView struct {
37
88
38// New creates a new DiffView with default settings.
89// New creates a new DiffView with default settings.
39func New() *DiffView {
90func New() *DiffView {
40− return &DiffView{
91+ dv := &DiffView{
41 layout: layoutUnified,
92 layout: layoutUnified,
42 contextLines: udiff.DefaultContextLines,
93 contextLines: udiff.DefaultContextLines,
43 }
94 }
95+ if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
96+ dv.style = DefaultDarkStyle
97+ } else {
98+ dv.style = DefaultLightStyle
99+ }
100+ return dv
44}
101}
45
102
46// Unified sets the layout of the DiffView to unified.
103// Unified sets the layout of the DiffView to unified.
@@ -73,10 +130,9 @@ func (dv *DiffView) ContextLines(contextLines int) *DiffView {
73 return dv
130 return dv
74}
131}
75
132
76−// BaseStyle sets the base style for the DiffView.
133+// Style sets the 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 {
134+func (dv *DiffView) Style(style Style) *DiffView {
79− dv.baseStyle = baseStyle
135+ dv.style = style
80 return dv
136 return dv
81}
137}
82
138
@@ -100,16 +156,39 @@ func (dv *DiffView) Width(width int) *DiffView {
100
156
101// String returns the string representation of the DiffView.
157// String returns the string representation of the DiffView.
102func (dv *DiffView) String() string {
158func (dv *DiffView) String() string {
103− if !dv.isComputed {
159+ if err := dv.computeDiff(); err != nil {
104− dv.compute()
160+ return err.Error()
105 }
161 }
106− if dv.err != nil {
162+ dv.detectWidth()
107− return dv.err.Error()
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+ }
108 }
183 }
184+
109− return dv.unified.String()
185+ return b.String()
110}
186}
111
187
112−func (dv *DiffView) compute() {
188+func (dv *DiffView) computeDiff() error {
189+ if dv.isComputed {
190+ return dv.err
191+ }
113 dv.isComputed = true
192 dv.isComputed = true
114 dv.edits = myers.ComputeEdits(
193 dv.edits = myers.ComputeEdits(
115 dv.before.content,
194 dv.before.content,
@@ -122,4 +201,19 @@ func (dv *DiffView) compute() {
122 dv.edits,
201 dv.edits,
123 dv.contextLines,
202 dv.contextLines,
124 )
203 )
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+ }
125}
219}
internal/exp/diffview/diffview_test.go+19−3
@@ -1,15 +1,31 @@
1package diffview_test
1package diffview_test
2
2
3import (
3import (
4+ _ "embed"
4 "testing"
5 "testing"
5
6
6 "github.com/charmbracelet/x/exp/golden"
7 "github.com/charmbracelet/x/exp/golden"
7 "github.com/opencode-ai/opencode/internal/exp/diffview"
8 "github.com/opencode-ai/opencode/internal/exp/diffview"
8)
9)
9
10
11+//go:embed testdata/TestDefault.before
12+var TestDefaultBefore string
13+
14+//go:embed testdata/TestDefault.after
15+var TestDefaultAfter string
16+
10func TestDefault(t *testing.T) {
17func TestDefault(t *testing.T) {
11 dv := diffview.New().
18 dv := diffview.New().
12− Before("test.txt", "This is the original content.").
19+ Before("main.go", TestDefaultBefore).
13− After("test.txt", "This is the modified content.")
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)
14− golden.RequireEqual(t, []byte(dv.String()))
29+ golden.RequireEqual(t, []byte(dv.String()))
30+ })
15}
31}
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+ }