From b7ab1a8091421b7ea05a47f36ec31e1e7b8765d7 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 4 Jun 2025 14:23:34 -0300 Subject: [PATCH] feat(diffview): basic working functionality --- go.mod | 2 + go.sum | 2 + internal/exp/diffview/diffview.go | 118 ++++++++++++++++-- internal/exp/diffview/diffview_test.go | 22 +++- .../exp/diffview/testdata/TestDefault.after | 10 ++ .../exp/diffview/testdata/TestDefault.before | 9 ++ .../exp/diffview/testdata/TestDefault.golden | 7 -- .../testdata/TestDefault/DarkMode.golden | 7 ++ .../testdata/TestDefault/LightMode.golden | 7 ++ 9 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 internal/exp/diffview/testdata/TestDefault.after create mode 100644 internal/exp/diffview/testdata/TestDefault.before delete mode 100644 internal/exp/diffview/testdata/TestDefault.golden create mode 100644 internal/exp/diffview/testdata/TestDefault/DarkMode.golden create mode 100644 internal/exp/diffview/testdata/TestDefault/LightMode.golden diff --git a/go.mod b/go.mod index 1db1275fadeeac9c8cc2033b1c689a843e41d8a5..bc13eef3e2a293e915c44042f5577be4bf51ac2b 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,8 @@ require ( github.com/stretchr/testify v1.10.0 ) +require github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 + require ( cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.13.0 // indirect diff --git a/go.sum b/go.sum index 3aff4dea3db17367f337812636de7f5431e1ff3f..f3108274b2726806d9f1548f1b9a22374422e320 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 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/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= 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= diff --git a/internal/exp/diffview/diffview.go b/internal/exp/diffview/diffview.go index b4949c1be2ea2d15ba0829e4a686e020cd9c5a45..12a764b26784325999ed80fd5f53d08a45857794 100644 --- a/internal/exp/diffview/diffview.go +++ b/internal/exp/diffview/diffview.go @@ -1,11 +1,18 @@ package diffview import ( + "os" + "strings" + "github.com/aymanbagabas/go-udiff" "github.com/aymanbagabas/go-udiff/myers" "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/exp/charmtone" ) +const leadingSymbolsSize = 2 + type file struct { path string content string @@ -18,16 +25,60 @@ const ( layoutSplit ) +type Style struct { + Base lipgloss.Style + InsertLine lipgloss.Style + InsertSymbols lipgloss.Style + DeleteLine lipgloss.Style + DeleteSymbols lipgloss.Style +} + +var DefaultLightStyle = Style{ + Base: lipgloss.NewStyle(). + Foreground(charmtone.Pepper). + Background(charmtone.Salt), + InsertLine: lipgloss.NewStyle(). + Foreground(charmtone.Pepper). + Background(lipgloss.Color("#e8f5e9")), + InsertSymbols: lipgloss.NewStyle(). + Foreground(charmtone.Turtle). + Background(lipgloss.Color("#e8f5e9")), + DeleteLine: lipgloss.NewStyle(). + Foreground(charmtone.Pepper). + Background(lipgloss.Color("#ffebee")), + DeleteSymbols: lipgloss.NewStyle(). + Foreground(charmtone.Cherry). + Background(lipgloss.Color("#ffebee")), +} + +var DefaultDarkStyle = Style{ + Base: lipgloss.NewStyle(). + Foreground(charmtone.Salt). + Background(charmtone.Pepper), + InsertLine: lipgloss.NewStyle(). + Foreground(charmtone.Salt). + Background(lipgloss.Color("#303a30")), + InsertSymbols: lipgloss.NewStyle(). + Foreground(charmtone.Turtle). + Background(lipgloss.Color("#303a30")), + DeleteLine: lipgloss.NewStyle(). + Foreground(charmtone.Salt). + Background(lipgloss.Color("#3a3030")), + DeleteSymbols: lipgloss.NewStyle(). + Foreground(charmtone.Cherry). + Background(lipgloss.Color("#3a3030")), +} + // 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 + style Style isComputed bool err error @@ -37,10 +88,16 @@ type DiffView struct { // New creates a new DiffView with default settings. func New() *DiffView { - return &DiffView{ + dv := &DiffView{ layout: layoutUnified, contextLines: udiff.DefaultContextLines, } + if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) { + dv.style = DefaultDarkStyle + } else { + dv.style = DefaultLightStyle + } + return dv } // Unified sets the layout of the DiffView to unified. @@ -73,10 +130,9 @@ func (dv *DiffView) ContextLines(contextLines int) *DiffView { 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 +// Style sets the style for the DiffView. +func (dv *DiffView) Style(style Style) *DiffView { + dv.style = style return dv } @@ -100,16 +156,39 @@ func (dv *DiffView) Width(width int) *DiffView { // String returns the string representation of the DiffView. func (dv *DiffView) String() string { - if !dv.isComputed { - dv.compute() + if err := dv.computeDiff(); err != nil { + return err.Error() } - if dv.err != nil { - return dv.err.Error() + dv.detectWidth() + + var b strings.Builder + + for _, h := range dv.unified.Hunks { + for _, l := range h.Lines { + content := strings.TrimSuffix(l.Content, "\n") + width := dv.width - leadingSymbolsSize + + switch l.Kind { + case udiff.Insert: + b.WriteString(dv.style.InsertSymbols.Render("+ ")) + b.WriteString(dv.style.InsertLine.Width(width).Render(content)) + case udiff.Delete: + b.WriteString(dv.style.DeleteSymbols.Render("- ")) + b.WriteString(dv.style.DeleteLine.Width(width).Render(content)) + case udiff.Equal: + b.WriteString(dv.style.Base.Width(width + leadingSymbolsSize).Render(" " + content)) + } + b.WriteRune('\n') + } } - return dv.unified.String() + + return b.String() } -func (dv *DiffView) compute() { +func (dv *DiffView) computeDiff() error { + if dv.isComputed { + return dv.err + } dv.isComputed = true dv.edits = myers.ComputeEdits( dv.before.content, @@ -122,4 +201,19 @@ func (dv *DiffView) compute() { dv.edits, dv.contextLines, ) + return dv.err +} + +func (dv *DiffView) detectWidth() { + if dv.width > 0 { + return + } + + for _, h := range dv.unified.Hunks { + for _, l := range h.Lines { + lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + lineWidth += leadingSymbolsSize + dv.width = max(dv.width, lineWidth) + } + } } diff --git a/internal/exp/diffview/diffview_test.go b/internal/exp/diffview/diffview_test.go index b04adacccc9e78461377dacc2c8c39a763c70c27..bd70048a4c81ce260d2d569a3ee1e8fe8aa10e19 100644 --- a/internal/exp/diffview/diffview_test.go +++ b/internal/exp/diffview/diffview_test.go @@ -1,15 +1,31 @@ package diffview_test import ( + _ "embed" "testing" "github.com/charmbracelet/x/exp/golden" "github.com/opencode-ai/opencode/internal/exp/diffview" ) +//go:embed testdata/TestDefault.before +var TestDefaultBefore string + +//go:embed testdata/TestDefault.after +var TestDefaultAfter string + 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())) + Before("main.go", TestDefaultBefore). + After("main.go", TestDefaultAfter) + + t.Run("LightMode", func(t *testing.T) { + dv = dv.Style(diffview.DefaultLightStyle) + golden.RequireEqual(t, []byte(dv.String())) + }) + + t.Run("DarkMode", func(t *testing.T) { + dv = dv.Style(diffview.DefaultDarkStyle) + golden.RequireEqual(t, []byte(dv.String())) + }) } diff --git a/internal/exp/diffview/testdata/TestDefault.after b/internal/exp/diffview/testdata/TestDefault.after new file mode 100644 index 0000000000000000000000000000000000000000..f5cedbfb45f13c7e15835782ddfdc511adb769c0 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDefault.after @@ -0,0 +1,10 @@ +package main + +import ( + "fmt" +) + +func main() { + content := "Hello, world!" + fmt.Println(content) +} diff --git a/internal/exp/diffview/testdata/TestDefault.before b/internal/exp/diffview/testdata/TestDefault.before new file mode 100644 index 0000000000000000000000000000000000000000..5dc991b75fcbbc9a47076de46349322816027e86 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDefault.before @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") +} diff --git a/internal/exp/diffview/testdata/TestDefault.golden b/internal/exp/diffview/testdata/TestDefault.golden deleted file mode 100644 index 2ef961a2191b5195f5dadbea21350e370ba5c426..0000000000000000000000000000000000000000 --- a/internal/exp/diffview/testdata/TestDefault.golden +++ /dev/null @@ -1,7 +0,0 @@ ---- test.txt -+++ test.txt -@@ -1 +1 @@ --This is the original content. -\ No newline at end of file -+This is the modified content. -\ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDefault/DarkMode.golden b/internal/exp/diffview/testdata/TestDefault/DarkMode.golden new file mode 100644 index 0000000000000000000000000000000000000000..1cefd40f62dbe898b5d215a7d9b3e88fea8cb477 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDefault/DarkMode.golden @@ -0,0 +1,7 @@ + )  +   + func main() {  +-  fmt.Println("Hello, world!") ++  content := "Hello, world!"  ++  fmt.Println(content)  + }  diff --git a/internal/exp/diffview/testdata/TestDefault/LightMode.golden b/internal/exp/diffview/testdata/TestDefault/LightMode.golden new file mode 100644 index 0000000000000000000000000000000000000000..dffbc6071dea4f70e5534473aefdb02af7f0a389 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDefault/LightMode.golden @@ -0,0 +1,7 @@ + )  +   + func main() {  +-  fmt.Println("Hello, world!") ++  content := "Hello, world!"  ++  fmt.Println(content)  + }