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 LineStyle struct {
29 Symbol lipgloss.Style
30 Code lipgloss.Style
31}
32
33type Style struct {
34 EqualLine LineStyle
35 InsertLine LineStyle
36 DeleteLine LineStyle
37}
38
39var DefaultLightStyle = Style{
40 EqualLine: LineStyle{
41 Code: lipgloss.NewStyle().
42 Foreground(charmtone.Pepper).
43 Background(charmtone.Salt),
44 },
45 InsertLine: LineStyle{
46 Symbol: lipgloss.NewStyle().
47 Foreground(charmtone.Turtle).
48 Background(lipgloss.Color("#e8f5e9")),
49 Code: lipgloss.NewStyle().
50 Foreground(charmtone.Pepper).
51 Background(lipgloss.Color("#e8f5e9")),
52 },
53 DeleteLine: LineStyle{
54 Symbol: lipgloss.NewStyle().
55 Foreground(charmtone.Cherry).
56 Background(lipgloss.Color("#ffebee")),
57 Code: lipgloss.NewStyle().
58 Foreground(charmtone.Pepper).
59 Background(lipgloss.Color("#ffebee")),
60 },
61}
62
63var DefaultDarkStyle = Style{
64 EqualLine: LineStyle{
65 Code: lipgloss.NewStyle().
66 Foreground(charmtone.Salt).
67 Background(charmtone.Pepper),
68 },
69 InsertLine: LineStyle{
70 Symbol: lipgloss.NewStyle().
71 Foreground(charmtone.Turtle).
72 Background(lipgloss.Color("#303a30")),
73 Code: lipgloss.NewStyle().
74 Foreground(charmtone.Salt).
75 Background(lipgloss.Color("#303a30")),
76 },
77 DeleteLine: LineStyle{
78 Symbol: lipgloss.NewStyle().
79 Foreground(charmtone.Cherry).
80 Background(lipgloss.Color("#3a3030")),
81 Code: lipgloss.NewStyle().
82 Foreground(charmtone.Salt).
83 Background(lipgloss.Color("#3a3030")),
84 },
85}
86
87// DiffView represents a view for displaying differences between two files.
88type DiffView struct {
89 layout layout
90 before file
91 after file
92 contextLines int
93 highlight bool
94 height int
95 width int
96 style Style
97
98 isComputed bool
99 err error
100 unified udiff.UnifiedDiff
101 edits []udiff.Edit
102}
103
104// New creates a new DiffView with default settings.
105func New() *DiffView {
106 dv := &DiffView{
107 layout: layoutUnified,
108 contextLines: udiff.DefaultContextLines,
109 }
110 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
111 dv.style = DefaultDarkStyle
112 } else {
113 dv.style = DefaultLightStyle
114 }
115 return dv
116}
117
118// Unified sets the layout of the DiffView to unified.
119func (dv *DiffView) Unified() *DiffView {
120 dv.layout = layoutUnified
121 return dv
122}
123
124// Split sets the layout of the DiffView to split (side-by-side).
125func (dv *DiffView) Split() *DiffView {
126 dv.layout = layoutSplit
127 return dv
128}
129
130// Before sets the "before" file for the DiffView.
131func (dv *DiffView) Before(path, content string) *DiffView {
132 dv.before = file{path: path, content: content}
133 return dv
134}
135
136// After sets the "after" file for the DiffView.
137func (dv *DiffView) After(path, content string) *DiffView {
138 dv.after = file{path: path, content: content}
139 return dv
140}
141
142// ContextLines sets the number of context lines for the DiffView.
143func (dv *DiffView) ContextLines(contextLines int) *DiffView {
144 dv.contextLines = contextLines
145 return dv
146}
147
148// Style sets the style for the DiffView.
149func (dv *DiffView) Style(style Style) *DiffView {
150 dv.style = style
151 return dv
152}
153
154// SyntaxHighlight sets whether to enable syntax highlighting in the DiffView.
155func (dv *DiffView) SyntaxHighlight(highlight bool) *DiffView {
156 dv.highlight = highlight
157 return dv
158}
159
160// Height sets the height of the DiffView.
161func (dv *DiffView) Height(height int) *DiffView {
162 dv.height = height
163 return dv
164}
165
166// Width sets the width of the DiffView.
167func (dv *DiffView) Width(width int) *DiffView {
168 dv.width = width
169 return dv
170}
171
172// String returns the string representation of the DiffView.
173func (dv *DiffView) String() string {
174 if err := dv.computeDiff(); err != nil {
175 return err.Error()
176 }
177 dv.detectWidth()
178
179 var b strings.Builder
180
181 for _, h := range dv.unified.Hunks {
182 for _, l := range h.Lines {
183 content := strings.TrimSuffix(l.Content, "\n")
184 width := dv.width - leadingSymbolsSize
185
186 switch l.Kind {
187 case udiff.Insert:
188 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
189 b.WriteString(dv.style.InsertLine.Code.Width(width).Render(content))
190 case udiff.Delete:
191 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
192 b.WriteString(dv.style.DeleteLine.Code.Width(width).Render(content))
193 case udiff.Equal:
194 b.WriteString(dv.style.EqualLine.Code.Width(width + leadingSymbolsSize).Render(" " + content))
195 }
196 b.WriteRune('\n')
197 }
198 }
199
200 return b.String()
201}
202
203func (dv *DiffView) computeDiff() error {
204 if dv.isComputed {
205 return dv.err
206 }
207 dv.isComputed = true
208 dv.edits = myers.ComputeEdits(
209 dv.before.content,
210 dv.after.content,
211 )
212 dv.unified, dv.err = udiff.ToUnifiedDiff(
213 dv.before.path,
214 dv.after.path,
215 dv.before.content,
216 dv.edits,
217 dv.contextLines,
218 )
219 return dv.err
220}
221
222func (dv *DiffView) detectWidth() {
223 if dv.width > 0 {
224 return
225 }
226
227 for _, h := range dv.unified.Hunks {
228 for _, l := range h.Lines {
229 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
230 lineWidth += leadingSymbolsSize
231 dv.width = max(dv.width, lineWidth)
232 }
233 }
234}