@@ -1,11 +1,18 @@
11package diffview
22
33import (
4+ "os"
5+ "strings"
6+
47 "github.com/aymanbagabas/go-udiff"
58 "github.com/aymanbagabas/go-udiff/myers"
69 "github.com/charmbracelet/lipgloss/v2"
10+ "github.com/charmbracelet/x/ansi"
11+ "github.com/charmbracelet/x/exp/charmtone"
712)
813
14+const leadingSymbolsSize = 2
15+
916type file struct {
1017 path string
1118 content string
@@ -18,16 +25,60 @@ const (
1825 layoutSplit
1926)
2027
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+
2172// DiffView represents a view for displaying differences between two files.
2273type DiffView struct {
2374 layout layout
2475 before file
2576 after file
2677 contextLines int
27− baseStyle lipgloss.Style
2878 highlight bool
2979 height int
3080 width int
81+ style Style
3182
3283 isComputed bool
3384 err error
@@ -37,10 +88,16 @@ type DiffView struct {
3788
3889// New creates a new DiffView with default settings.
3990func New() *DiffView {
40− return &DiffView{
91+ dv := &DiffView{
4192 layout: layoutUnified,
4293 contextLines: udiff.DefaultContextLines,
4394 }
95+ if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
96+ dv.style = DefaultDarkStyle
97+ } else {
98+ dv.style = DefaultLightStyle
99+ }
100+ return dv
44101}
45102
46103// Unified sets the layout of the DiffView to unified.
@@ -73,10 +130,9 @@ func (dv *DiffView) ContextLines(contextLines int) *DiffView {
73130 return dv
74131}
75132
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
133+// Style sets the style for the DiffView.
134+func (dv *DiffView) Style(style Style) *DiffView {
135+ dv.style = style
80136 return dv
81137}
82138
@@ -100,16 +156,39 @@ func (dv *DiffView) Width(width int) *DiffView {
100156
101157// String returns the string representation of the DiffView.
102158func (dv *DiffView) String() string {
103− if !dv.isComputed {
104− dv.compute()
159+ if err := dv.computeDiff(); err != nil {
160+ return err.Error()
105161 }
106− if dv.err != nil {
107− return dv.err.Error()
162+ dv.detectWidth()
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+ }
108183 }
109− return dv.unified.String()
184+
185+ return b.String()
110186}
111187
112−func (dv *DiffView) compute() {
188+func (dv *DiffView) computeDiff() error {
189+ if dv.isComputed {
190+ return dv.err
191+ }
113192 dv.isComputed = true
114193 dv.edits = myers.ComputeEdits(
115194 dv.before.content,
@@ -122,4 +201,19 @@ func (dv *DiffView) compute() {
122201 dv.edits,
123202 dv.contextLines,
124203 )
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+ }
125219}