on main help sign in
feat(diffview): getting started with the api design
1f98b24Andrey Nering
go.mod+3−2
@@ -15,6 +15,7 @@ require (
15 github.com/charmbracelet/bubbletea v1.3.5
15 github.com/charmbracelet/bubbletea v1.3.5
16 github.com/charmbracelet/glamour v0.9.1
16 github.com/charmbracelet/glamour v0.9.1
17 github.com/charmbracelet/lipgloss v1.1.0
17 github.com/charmbracelet/lipgloss v1.1.0
18+ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1
18 github.com/charmbracelet/x/ansi v0.8.0
19 github.com/charmbracelet/x/ansi v0.8.0
19 github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb
20 github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb
20 github.com/fsnotify/fsnotify v1.8.0
21 github.com/fsnotify/fsnotify v1.8.0
@@ -59,8 +60,8 @@ require (
59 github.com/aws/smithy-go v1.20.3 // indirect
60 github.com/aws/smithy-go v1.20.3 // indirect
60 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
61 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
61 github.com/aymerick/douceur v0.2.0 // indirect
62 github.com/aymerick/douceur v0.2.0 // indirect
62− github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
63+ github.com/charmbracelet/colorprofile v0.3.0 // indirect
63− github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
64+ github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
64 github.com/charmbracelet/x/term v0.2.1 // indirect
65 github.com/charmbracelet/x/term v0.2.1 // indirect
65 github.com/davecgh/go-spew v1.1.1 // indirect
66 github.com/davecgh/go-spew v1.1.1 // indirect
66 github.com/disintegration/imaging v1.6.2
67 github.com/disintegration/imaging v1.6.2
go.sum+6−4
@@ -72,16 +72,18 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
72github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
72github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
73github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
73github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
74github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
74github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
75−github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
75+github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ=
76−github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
76+github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0=
77github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
77github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
78github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
78github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
79github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
79github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
80github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
80github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
81+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU=
82+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
81github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
83github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
82github.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=
83−github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
85+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
84−github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
86+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
85github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb h1:GT/STWThMsrOfYQnhnIPb165e/g1waAp0gNMFvEO6WI=
87github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb h1:GT/STWThMsrOfYQnhnIPb165e/g1waAp0gNMFvEO6WI=
86github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb/go.mod h1:929X+xY3LeoOZrDWIBVZcx/zyS0CYtyLiUIvE4VbKC0=
88github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb/go.mod h1:929X+xY3LeoOZrDWIBVZcx/zyS0CYtyLiUIvE4VbKC0=
87github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
89github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
internal/exp/diffview/diffview.go+124
@@ -1 +1,125 @@
1package diffview
1package diffview
2+
3+import (
4+ "github.com/aymanbagabas/go-udiff"
5+ "github.com/aymanbagabas/go-udiff/myers"
6+ "github.com/charmbracelet/lipgloss/v2"
7+)
8+
9+type file struct {
10+ path string
11+ content string
12+}
13+
14+type layout int
15+
16+const (
17+ layoutUnified layout = iota + 1
18+ layoutSplit
19+)
20+
21+// DiffView represents a view for displaying differences between two files.
22+type DiffView struct {
23+ layout layout
24+ before file
25+ after file
26+ contextLines int
27+ baseStyle lipgloss.Style
28+ highlight bool
29+ height int
30+ width int
31+
32+ isComputed bool
33+ err error
34+ unified udiff.UnifiedDiff
35+ edits []udiff.Edit
36+}
37+
38+// New creates a new DiffView with default settings.
39+func New() *DiffView {
40+ return &DiffView{
41+ layout: layoutUnified,
42+ contextLines: udiff.DefaultContextLines,
43+ }
44+}
45+
46+// Unified sets the layout of the DiffView to unified.
47+func (dv *DiffView) Unified() *DiffView {
48+ dv.layout = layoutUnified
49+ return dv
50+}
51+
52+// Split sets the layout of the DiffView to split (side-by-side).
53+func (dv *DiffView) Split() *DiffView {
54+ dv.layout = layoutSplit
55+ return dv
56+}
57+
58+// Before sets the "before" file for the DiffView.
59+func (dv *DiffView) Before(path, content string) *DiffView {
60+ dv.before = file{path: path, content: content}
61+ return dv
62+}
63+
64+// After sets the "after" file for the DiffView.
65+func (dv *DiffView) After(path, content string) *DiffView {
66+ dv.after = file{path: path, content: content}
67+ return dv
68+}
69+
70+// ContextLines sets the number of context lines for the DiffView.
71+func (dv *DiffView) ContextLines(contextLines int) *DiffView {
72+ dv.contextLines = contextLines
73+ return dv
74+}
75+
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
80+ return dv
81+}
82+
83+// SyntaxHighlight sets whether to enable syntax highlighting in the DiffView.
84+func (dv *DiffView) SyntaxHighlight(highlight bool) *DiffView {
85+ dv.highlight = highlight
86+ return dv
87+}
88+
89+// Height sets the height of the DiffView.
90+func (dv *DiffView) Height(height int) *DiffView {
91+ dv.height = height
92+ return dv
93+}
94+
95+// Width sets the width of the DiffView.
96+func (dv *DiffView) Width(width int) *DiffView {
97+ dv.width = width
98+ return dv
99+}
100+
101+// String returns the string representation of the DiffView.
102+func (dv *DiffView) String() string {
103+ if !dv.isComputed {
104+ dv.compute()
105+ }
106+ if dv.err != nil {
107+ return dv.err.Error()
108+ }
109+ return dv.unified.String()
110+}
111+
112+func (dv *DiffView) compute() {
113+ dv.isComputed = true
114+ dv.edits = myers.ComputeEdits(
115+ dv.before.content,
116+ dv.after.content,
117+ )
118+ dv.unified, dv.err = udiff.ToUnifiedDiff(
119+ dv.before.path,
120+ dv.after.path,
121+ dv.before.content,
122+ dv.edits,
123+ dv.contextLines,
124+ )
125+}
internal/exp/diffview/diffview_test.gonew file+15
@@ -0,0 +1,15 @@
1+package diffview_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/charmbracelet/x/exp/golden"
7+ "github.com/opencode-ai/opencode/internal/exp/diffview"
8+)
9+
10+func TestDefault(t *testing.T) {
11+ 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()))
15+}
internal/exp/diffview/testdata/TestDefault.goldennew file+7
@@ -0,0 +1,7 @@
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