feat(diffview): getting started with the api design

Andrey Nering created

Change summary

go.mod                                            |   5 
go.sum                                            |  10 
internal/exp/diffview/diffview.go                 | 124 +++++++++++++++++
internal/exp/diffview/diffview_test.go            |  15 ++
internal/exp/diffview/testdata/TestDefault.golden |   7 
5 files changed, 155 insertions(+), 6 deletions(-)

Detailed changes

go.mod 🔗

@@ -15,6 +15,7 @@ require (
 	github.com/charmbracelet/bubbletea v1.3.5
 	github.com/charmbracelet/glamour v0.9.1
 	github.com/charmbracelet/lipgloss v1.1.0
+	github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1
 	github.com/charmbracelet/x/ansi v0.8.0
 	github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb
 	github.com/fsnotify/fsnotify v1.8.0
@@ -59,8 +60,8 @@ require (
 	github.com/aws/smithy-go v1.20.3 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
-	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
-	github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+	github.com/charmbracelet/colorprofile v0.3.0 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
 	github.com/charmbracelet/x/term v0.2.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/disintegration/imaging v1.6.2

go.sum 🔗

@@ -72,16 +72,18 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
 github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
 github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
 github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
-github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
-github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ=
+github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0=
 github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
 github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
 github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
 github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
 github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
 github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
-github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
-github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
 github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb h1:GT/STWThMsrOfYQnhnIPb165e/g1waAp0gNMFvEO6WI=
 github.com/charmbracelet/x/exp/golden v0.0.0-20250602192518-9e722df69bbb/go.mod h1:929X+xY3LeoOZrDWIBVZcx/zyS0CYtyLiUIvE4VbKC0=
 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=

internal/exp/diffview/diffview.go 🔗

@@ -1 +1,125 @@
 package diffview
+
+import (
+	"github.com/aymanbagabas/go-udiff"
+	"github.com/aymanbagabas/go-udiff/myers"
+	"github.com/charmbracelet/lipgloss/v2"
+)
+
+type file struct {
+	path    string
+	content string
+}
+
+type layout int
+
+const (
+	layoutUnified layout = iota + 1
+	layoutSplit
+)
+
+// DiffView represents a view for displaying differences between two files.
+type DiffView struct {
+	layout       layout
+	before       file
+	after        file
+	contextLines int
+	baseStyle    lipgloss.Style
+	highlight    bool
+	height       int
+	width        int
+
+	isComputed bool
+	err        error
+	unified    udiff.UnifiedDiff
+	edits      []udiff.Edit
+}
+
+// New creates a new DiffView with default settings.
+func New() *DiffView {
+	return &DiffView{
+		layout:       layoutUnified,
+		contextLines: udiff.DefaultContextLines,
+	}
+}
+
+// Unified sets the layout of the DiffView to unified.
+func (dv *DiffView) Unified() *DiffView {
+	dv.layout = layoutUnified
+	return dv
+}
+
+// Split sets the layout of the DiffView to split (side-by-side).
+func (dv *DiffView) Split() *DiffView {
+	dv.layout = layoutSplit
+	return dv
+}
+
+// Before sets the "before" file for the DiffView.
+func (dv *DiffView) Before(path, content string) *DiffView {
+	dv.before = file{path: path, content: content}
+	return dv
+}
+
+// After sets the "after" file for the DiffView.
+func (dv *DiffView) After(path, content string) *DiffView {
+	dv.after = file{path: path, content: content}
+	return dv
+}
+
+// ContextLines sets the number of context lines for the DiffView.
+func (dv *DiffView) ContextLines(contextLines int) *DiffView {
+	dv.contextLines = contextLines
+	return dv
+}
+
+// BaseStyle sets the base style for the DiffView.
+// This is useful for setting a custom background color, for example.
+func (dv *DiffView) BaseStyle(baseStyle lipgloss.Style) *DiffView {
+	dv.baseStyle = baseStyle
+	return dv
+}
+
+// SyntaxHighlight sets whether to enable syntax highlighting in the DiffView.
+func (dv *DiffView) SyntaxHighlight(highlight bool) *DiffView {
+	dv.highlight = highlight
+	return dv
+}
+
+// Height sets the height of the DiffView.
+func (dv *DiffView) Height(height int) *DiffView {
+	dv.height = height
+	return dv
+}
+
+// Width sets the width of the DiffView.
+func (dv *DiffView) Width(width int) *DiffView {
+	dv.width = width
+	return dv
+}
+
+// String returns the string representation of the DiffView.
+func (dv *DiffView) String() string {
+	if !dv.isComputed {
+		dv.compute()
+	}
+	if dv.err != nil {
+		return dv.err.Error()
+	}
+	return dv.unified.String()
+}
+
+func (dv *DiffView) compute() {
+	dv.isComputed = true
+	dv.edits = myers.ComputeEdits(
+		dv.before.content,
+		dv.after.content,
+	)
+	dv.unified, dv.err = udiff.ToUnifiedDiff(
+		dv.before.path,
+		dv.after.path,
+		dv.before.content,
+		dv.edits,
+		dv.contextLines,
+	)
+}

internal/exp/diffview/diffview_test.go 🔗

@@ -0,0 +1,15 @@
+package diffview_test
+
+import (
+	"testing"
+
+	"github.com/charmbracelet/x/exp/golden"
+	"github.com/opencode-ai/opencode/internal/exp/diffview"
+)
+
+func TestDefault(t *testing.T) {
+	dv := diffview.New().
+		Before("test.txt", "This is the original content.").
+		After("test.txt", "This is the modified content.")
+	golden.RequireEqual(t, []byte(dv.String()))
+}