1package diffview
2
3import (
4 "os"
5 "strings"
6
7 "github.com/aymanbagabas/go-udiff"
8 "github.com/aymanbagabas/go-udiff/myers"
9 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/charmbracelet/x/ansi"
11 "github.com/charmbracelet/x/exp/charmtone"
12)
13
14const leadingSymbolsSize = 2
15
16type file struct {
17 path string
18 content string
19}
20
21type layout int
22
23const (
24 layoutUnified layout = iota + 1
25 layoutSplit
26)
27
28type Style struct {
29 Base lipgloss.Style
30 InsertLine lipgloss.Style
31 InsertSymbols lipgloss.Style
32 DeleteLine lipgloss.Style
33 DeleteSymbols lipgloss.Style
34}
35
36var 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
54var 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
72// DiffView represents a view for displaying differences between two files.
73type DiffView struct {
74 layout layout
75 before file
76 after file
77 contextLines int
78 highlight bool
79 height int
80 width int
81 style Style
82
83 isComputed bool
84 err error
85 unified udiff.UnifiedDiff
86 edits []udiff.Edit
87}
88
89// New creates a new DiffView with default settings.
90func New() *DiffView {
91 dv := &DiffView{
92 layout: layoutUnified,
93 contextLines: udiff.DefaultContextLines,
94 }
95 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
96 dv.style = DefaultDarkStyle
97 } else {
98 dv.style = DefaultLightStyle
99 }
100 return dv
101}
102
103// Unified sets the layout of the DiffView to unified.
104func (dv *DiffView) Unified() *DiffView {
105 dv.layout = layoutUnified
106 return dv
107}
108
109// Split sets the layout of the DiffView to split (side-by-side).
110func (dv *DiffView) Split() *DiffView {
111 dv.layout = layoutSplit
112 return dv
113}
114
115// Before sets the "before" file for the DiffView.
116func (dv *DiffView) Before(path, content string) *DiffView {
117 dv.before = file{path: path, content: content}
118 return dv
119}
120
121// After sets the "after" file for the DiffView.
122func (dv *DiffView) After(path, content string) *DiffView {
123 dv.after = file{path: path, content: content}
124 return dv
125}
126
127// ContextLines sets the number of context lines for the DiffView.
128func (dv *DiffView) ContextLines(contextLines int) *DiffView {
129 dv.contextLines = contextLines
130 return dv
131}
132
133// Style sets the style for the DiffView.
134func (dv *DiffView) Style(style Style) *DiffView {
135 dv.style = style
136 return dv
137}
138
139// SyntaxHighlight sets whether to enable syntax highlighting in the DiffView.
140func (dv *DiffView) SyntaxHighlight(highlight bool) *DiffView {
141 dv.highlight = highlight
142 return dv
143}
144
145// Height sets the height of the DiffView.
146func (dv *DiffView) Height(height int) *DiffView {
147 dv.height = height
148 return dv
149}
150
151// Width sets the width of the DiffView.
152func (dv *DiffView) Width(width int) *DiffView {
153 dv.width = width
154 return dv
155}
156
157// String returns the string representation of the DiffView.
158func (dv *DiffView) String() string {
159 if err := dv.computeDiff(); err != nil {
160 return err.Error()
161 }
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 }
183 }
184
185 return b.String()
186}
187
188func (dv *DiffView) computeDiff() error {
189 if dv.isComputed {
190 return dv.err
191 }
192 dv.isComputed = true
193 dv.edits = myers.ComputeEdits(
194 dv.before.content,
195 dv.after.content,
196 )
197 dv.unified, dv.err = udiff.ToUnifiedDiff(
198 dv.before.path,
199 dv.after.path,
200 dv.before.content,
201 dv.edits,
202 dv.contextLines,
203 )
204 return dv.err
205}
206
207func (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 }
219}