Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
gioui.org/x v0.9.0 // indirect
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/zodimo/go-lazy v0.1.1 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.41.0 // indirect
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ git.sr.ht/~schnwalter/gio-mw v0.0.0-20250713180710-9d8d98474447 h1:HYmUhTNys/xHf
git.sr.ht/~schnwalter/gio-mw v0.0.0-20250713180710-9d8d98474447/go.mod h1:2delIHRFXOUBnmXbltTSUCnnbpzWawIGwQGxqw2K7p0=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/zodimo/go-lazy v0.1.1 h1:lYsYK6eH1rbQ6VkVqPJntjiGIIoFe0Vsu4wH50j3uWE=
github.com/zodimo/go-lazy v0.1.1/go.mod h1:+GKplxvAWyHv/XirM6KZwdHDvXBQDGGjCRpbUGlrZyg=
github.com/zodimo/go-maybe v0.1.6 h1:Htq0qrJn/1OcMsKkoMSnjRBwEXyqWNLVuHAY/abSzu4=
Expand All @@ -43,3 +53,7 @@ golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
151 changes: 151 additions & 0 deletions pkg/x/diff/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package diff

import (
"strings"

"github.com/sergi/go-diff/diffmatchpatch"

"github.com/zodimo/go-compose/compose/foundation/layout/column"
"github.com/zodimo/go-compose/compose/foundation/layout/row"
fText "github.com/zodimo/go-compose/compose/foundation/text"
iconbutton "github.com/zodimo/go-compose/compose/material3/iconbutton"
m3Text "github.com/zodimo/go-compose/compose/material3/text"
"github.com/zodimo/go-compose/compose/ui/graphics"
"github.com/zodimo/go-compose/modifiers/background"
"github.com/zodimo/go-compose/modifiers/weight"
"github.com/zodimo/go-compose/pkg/api"
"github.com/zodimo/go-compose/compose/ui"
"golang.org/x/exp/shiny/materialdesign/icons"
)

type ChangeType int

const (
ChangeEqual ChangeType = iota
ChangeDiff
)

type DiffSection struct {
Type ChangeType
Original string
Modified string
}

func CalculateDiff(original, modified string) []DiffSection {
dmp := diffmatchpatch.New()
a, b, c := dmp.DiffLinesToChars(original, modified)
diffs := dmp.DiffMain(a, b, false)
diffs = dmp.DiffCharsToLines(diffs, c)

var sections []DiffSection
for i := 0; i < len(diffs); {
if diffs[i].Type == diffmatchpatch.DiffEqual {
sections = append(sections, DiffSection{
Type: ChangeEqual,
Original: diffs[i].Text,
Modified: diffs[i].Text,
})
i++
} else {
// Combine adjacent deletes and inserts into a single section
var orig, mod strings.Builder
for i < len(diffs) && diffs[i].Type != diffmatchpatch.DiffEqual {
if diffs[i].Type == diffmatchpatch.DiffDelete {
orig.WriteString(diffs[i].Text)
} else if diffs[i].Type == diffmatchpatch.DiffInsert {
mod.WriteString(diffs[i].Text)
}
i++
}
sections = append(sections, DiffSection{
Type: ChangeDiff,
Original: orig.String(),
Modified: mod.String(),
})
}
}
return sections
}

// DiffViewer renders the diff between original and modified strings
func DiffViewer(original, modified string, onApplyLeftToRight, onApplyRightToLeft func(sectionIndex int, section DiffSection)) api.Composable {
sections := CalculateDiff(original, modified)

return func(c api.Composer) api.Composer {
c.StartBlock("DiffViewer")
defer c.EndBlock()

column.Column(
c.Sequence(
c.Range(len(sections), func(i int) api.Composable {
return row.Row(
c.Sequence(
// Left Side
row.Row(
c.Sequence(renderSection(i, sections[i], true, onApplyLeftToRight)),
row.WithModifier(weight.Weight(1)),
),
// Right Side
row.Row(
c.Sequence(renderSection(i, sections[i], false, onApplyRightToLeft)),
row.WithModifier(weight.Weight(1)),
),
),
)
}),
),
)(c)

return c
}
}

func renderSection(index int, section DiffSection, isLeft bool, onApply func(int, DiffSection)) api.Composable {
return func(c api.Composer) api.Composer {
c.StartBlock("DiffSection")
defer c.EndBlock()

return column.Column(
c.Sequence(func(c api.Composer) api.Composer {
textStr := section.Original
if !isLeft {
textStr = section.Modified
}

if textStr == "" && section.Type == ChangeDiff {
// Placeholder for deleted/empty blocks
m3Text.Text("---", fText.WithModifier(
background.Background(graphics.Color(0xFFC8C8C8)),
))(c)
return c
}

lines := strings.Split(strings.TrimSuffix(textStr, "\n"), "\n")
var mod ui.Modifier = ui.EmptyModifier

if section.Type == ChangeDiff {
if isLeft {
mod = background.Background(graphics.Color(0xFFFFC8C8)) // Faint red
} else {
mod = background.Background(graphics.Color(0xFFC8FFC8)) // Faint green
}
}

c.Range(len(lines), func(j int) api.Composable {
return m3Text.Text(lines[j], fText.WithModifier(mod))
})(c)

// Inline button for changes
if section.Type == ChangeDiff {
if isLeft {
iconbutton.Standard(func() { onApply(index, section) }, icons.HardwareKeyboardArrowRight, "Apply Left to Right")(c)
} else {
iconbutton.Standard(func() { onApply(index, section) }, icons.HardwareKeyboardArrowLeft, "Apply Right to Left")(c)
}
}

return c
}),
)(c)
}
}
60 changes: 60 additions & 0 deletions pkg/x/diff/diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package diff

import (
"reflect"
"testing"
)

func TestCalculateDiff(t *testing.T) {
tests := []struct {
name string
original string
modified string
want []DiffSection
}{
{
name: "identical strings",
original: "line1\nline2\n",
modified: "line1\nline2\n",
want: []DiffSection{
{Type: ChangeEqual, Original: "line1\nline2\n", Modified: "line1\nline2\n"},
},
},
{
name: "one line changed",
original: "line1\nline2\nline3\n",
modified: "line1\nline2 modified\nline3\n",
want: []DiffSection{
{Type: ChangeEqual, Original: "line1\n", Modified: "line1\n"},
{Type: ChangeDiff, Original: "line2\n", Modified: "line2 modified\n"},
{Type: ChangeEqual, Original: "line3\n", Modified: "line3\n"},
},
},
{
name: "line added",
original: "line1\n",
modified: "line1\nline2\n",
want: []DiffSection{
{Type: ChangeEqual, Original: "line1\n", Modified: "line1\n"},
{Type: ChangeDiff, Original: "", Modified: "line2\n"},
},
},
{
name: "line removed",
original: "line1\nline2\n",
modified: "line1\n",
want: []DiffSection{
{Type: ChangeEqual, Original: "line1\n", Modified: "line1\n"},
{Type: ChangeDiff, Original: "line2\n", Modified: ""},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CalculateDiff(tt.original, tt.modified); !reflect.DeepEqual(got, tt.want) {
t.Errorf("CalculateDiff() = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 2 additions & 0 deletions run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
go test ./...