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 _, 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(h)))
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(h *udiff.Hunk) string {
211 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
212
213 return fmt.Sprintf(
214 " @@ -%d,%d +%d,%d @@ ",
215 h.FromLine,
216 beforeShownLines,
217 h.ToLine,
218 afterShownLines,
219 )
220}
221
222// hunkShownLines calculates the number of lines shown in a hunk for both before
223// and after versions.
224func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
225 for _, l := range h.Lines {
226 switch l.Kind {
227 case udiff.Equal:
228 before++
229 after++
230 case udiff.Insert:
231 after++
232 case udiff.Delete:
233 before++
234 }
235 }
236 return
237}
238
239func (dv *DiffView) detectWidth() {
240 if dv.width > 0 {
241 return
242 }
243
244 for _, h := range dv.unified.Hunks {
245 shownLines := ansi.StringWidth(dv.hunkLineFor(h))
246
247 for _, l := range h.Lines {
248 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
249 lineWidth += leadingSymbolsSize
250 dv.width = max(dv.width, lineWidth, shownLines)
251 }
252 }
253}