1package diffview
2
3import (
4 "fmt"
5 "os"
6 "strconv"
7 "strings"
8
9 "github.com/aymanbagabas/go-udiff"
10 "github.com/aymanbagabas/go-udiff/myers"
11 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/x/ansi"
13)
14
15const leadingSymbolsSize = 2
16
17type file struct {
18 path string
19 content string
20}
21
22type layout int
23
24const (
25 layoutUnified layout = iota + 1
26 layoutSplit
27)
28
29// DiffView represents a view for displaying differences between two files.
30type DiffView struct {
31 layout layout
32 before file
33 after file
34 contextLines int
35 lineNumbers bool
36 highlight bool
37 height int
38 width int
39 style Style
40
41 isComputed bool
42 err error
43 unified udiff.UnifiedDiff
44 edits []udiff.Edit
45}
46
47// New creates a new DiffView with default settings.
48func New() *DiffView {
49 dv := &DiffView{
50 layout: layoutUnified,
51 contextLines: udiff.DefaultContextLines,
52 lineNumbers: true,
53 }
54 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
55 dv.style = DefaultDarkStyle
56 } else {
57 dv.style = DefaultLightStyle
58 }
59 return dv
60}
61
62// Unified sets the layout of the DiffView to unified.
63func (dv *DiffView) Unified() *DiffView {
64 dv.layout = layoutUnified
65 return dv
66}
67
68// Split sets the layout of the DiffView to split (side-by-side).
69func (dv *DiffView) Split() *DiffView {
70 dv.layout = layoutSplit
71 return dv
72}
73
74// Before sets the "before" file for the DiffView.
75func (dv *DiffView) Before(path, content string) *DiffView {
76 dv.before = file{path: path, content: content}
77 return dv
78}
79
80// After sets the "after" file for the DiffView.
81func (dv *DiffView) After(path, content string) *DiffView {
82 dv.after = file{path: path, content: content}
83 return dv
84}
85
86// ContextLines sets the number of context lines for the DiffView.
87func (dv *DiffView) ContextLines(contextLines int) *DiffView {
88 dv.contextLines = contextLines
89 return dv
90}
91
92// Style sets the style for the DiffView.
93func (dv *DiffView) Style(style Style) *DiffView {
94 dv.style = style
95 return dv
96}
97
98// LineNumbers sets whether to display line numbers in the DiffView.
99func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
100 dv.lineNumbers = lineNumbers
101 return dv
102}
103
104// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
105func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
106 dv.highlight = highlight
107 return dv
108}
109
110// Height sets the height of the DiffView.
111func (dv *DiffView) Height(height int) *DiffView {
112 dv.height = height
113 return dv
114}
115
116// Width sets the width of the DiffView.
117func (dv *DiffView) Width(width int) *DiffView {
118 dv.width = width
119 return dv
120}
121
122// String returns the string representation of the DiffView.
123func (dv *DiffView) String() string {
124 if err := dv.computeDiff(); err != nil {
125 return err.Error()
126 }
127 dv.detectWidth()
128
129 codeWidth := dv.width - leadingSymbolsSize
130 beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
131
132 var b strings.Builder
133
134 for i, h := range dv.unified.Hunks {
135 if dv.lineNumbers {
136 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
137 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
138 }
139 b.WriteString(dv.style.DividerLine.Code.Width(codeWidth + leadingSymbolsSize).Render(dv.hunkLineFor(i)))
140 b.WriteRune('\n')
141
142 beforeLine := h.FromLine
143 afterLine := h.ToLine
144
145 for _, l := range h.Lines {
146 content := strings.TrimSuffix(l.Content, "\n")
147
148 switch l.Kind {
149 case udiff.Equal:
150 if dv.lineNumbers {
151 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
152 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
153 }
154 b.WriteString(dv.style.EqualLine.Code.Width(codeWidth + leadingSymbolsSize).Render(" " + content))
155 beforeLine++
156 afterLine++
157 case udiff.Insert:
158 if dv.lineNumbers {
159 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
160 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
161 }
162 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
163 b.WriteString(dv.style.InsertLine.Code.Width(codeWidth).Render(content))
164 afterLine++
165 case udiff.Delete:
166 if dv.lineNumbers {
167 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
168 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
169 }
170 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
171 b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
172 beforeLine++
173 }
174 b.WriteRune('\n')
175 }
176 }
177
178 return b.String()
179}
180
181func (dv *DiffView) computeDiff() error {
182 if dv.isComputed {
183 return dv.err
184 }
185 dv.isComputed = true
186 dv.edits = myers.ComputeEdits(
187 dv.before.content,
188 dv.after.content,
189 )
190 dv.unified, dv.err = udiff.ToUnifiedDiff(
191 dv.before.path,
192 dv.after.path,
193 dv.before.content,
194 dv.edits,
195 dv.contextLines,
196 )
197 return dv.err
198}
199
200// lineNumberDigits calculates the maximum number of digits needed for before and
201// after line numbers.
202func (dv *DiffView) lineNumberDigits() (maxBefore, maxAfter int) {
203 for _, h := range dv.unified.Hunks {
204 maxBefore = max(maxBefore, len(strconv.Itoa(h.FromLine+len(h.Lines))))
205 maxAfter = max(maxAfter, len(strconv.Itoa(h.ToLine+len(h.Lines))))
206 }
207 return
208}
209
210func (dv *DiffView) hunkLineFor(i int) string {
211 h := dv.unified.Hunks[i]
212 beforeShownLines, afterShownLines := dv.hunkShownLines(i)
213
214 return fmt.Sprintf(
215 " @@ -%d,%d +%d,%d @@ ",
216 h.FromLine,
217 beforeShownLines,
218 h.ToLine,
219 afterShownLines,
220 )
221}
222
223// hunkShownLines calculates the number of lines shown in a hunk for both before
224// and after versions.
225func (dv *DiffView) hunkShownLines(i int) (before, after int) {
226 for _, l := range dv.unified.Hunks[i].Lines {
227 switch l.Kind {
228 case udiff.Equal:
229 before++
230 after++
231 case udiff.Insert:
232 after++
233 case udiff.Delete:
234 before++
235 }
236 }
237 return
238}
239
240func (dv *DiffView) detectWidth() {
241 if dv.width > 0 {
242 return
243 }
244
245 for i, h := range dv.unified.Hunks {
246 shownLines := ansi.StringWidth(dv.hunkLineFor(i))
247
248 for _, l := range h.Lines {
249 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
250 lineWidth += leadingSymbolsSize
251 dv.width = max(dv.width, lineWidth, shownLines)
252 }
253 }
254}