1package diffview
2
3import (
4 "os"
5 "strconv"
6 "strings"
7
8 "github.com/aymanbagabas/go-udiff"
9 "github.com/aymanbagabas/go-udiff/myers"
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/x/ansi"
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
28// DiffView represents a view for displaying differences between two files.
29type DiffView struct {
30 layout layout
31 before file
32 after file
33 contextLines int
34 lineNumbers bool
35 highlight bool
36 height int
37 width int
38 style Style
39
40 isComputed bool
41 err error
42 unified udiff.UnifiedDiff
43 edits []udiff.Edit
44}
45
46// New creates a new DiffView with default settings.
47func New() *DiffView {
48 dv := &DiffView{
49 layout: layoutUnified,
50 contextLines: udiff.DefaultContextLines,
51 lineNumbers: true,
52 }
53 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
54 dv.style = DefaultDarkStyle
55 } else {
56 dv.style = DefaultLightStyle
57 }
58 return dv
59}
60
61// Unified sets the layout of the DiffView to unified.
62func (dv *DiffView) Unified() *DiffView {
63 dv.layout = layoutUnified
64 return dv
65}
66
67// Split sets the layout of the DiffView to split (side-by-side).
68func (dv *DiffView) Split() *DiffView {
69 dv.layout = layoutSplit
70 return dv
71}
72
73// Before sets the "before" file for the DiffView.
74func (dv *DiffView) Before(path, content string) *DiffView {
75 dv.before = file{path: path, content: content}
76 return dv
77}
78
79// After sets the "after" file for the DiffView.
80func (dv *DiffView) After(path, content string) *DiffView {
81 dv.after = file{path: path, content: content}
82 return dv
83}
84
85// ContextLines sets the number of context lines for the DiffView.
86func (dv *DiffView) ContextLines(contextLines int) *DiffView {
87 dv.contextLines = contextLines
88 return dv
89}
90
91// Style sets the style for the DiffView.
92func (dv *DiffView) Style(style Style) *DiffView {
93 dv.style = style
94 return dv
95}
96
97// LineNumbers sets whether to display line numbers in the DiffView.
98func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
99 dv.lineNumbers = lineNumbers
100 return dv
101}
102
103// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
104func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
105 dv.highlight = highlight
106 return dv
107}
108
109// Height sets the height of the DiffView.
110func (dv *DiffView) Height(height int) *DiffView {
111 dv.height = height
112 return dv
113}
114
115// Width sets the width of the DiffView.
116func (dv *DiffView) Width(width int) *DiffView {
117 dv.width = width
118 return dv
119}
120
121// String returns the string representation of the DiffView.
122func (dv *DiffView) String() string {
123 if err := dv.computeDiff(); err != nil {
124 return err.Error()
125 }
126 dv.detectWidth()
127
128 lineNumberWidth := func(start, num int) int {
129 return len(strconv.Itoa(start + num))
130 }
131
132 var b strings.Builder
133
134 for _, h := range dv.unified.Hunks {
135 beforeLine := h.FromLine
136 afterLine := h.ToLine
137
138 beforeNumDigits := lineNumberWidth(h.FromLine, len(h.Lines))
139 afterNumDigits := lineNumberWidth(h.ToLine, len(h.Lines))
140
141 for _, l := range h.Lines {
142 content := strings.TrimSuffix(l.Content, "\n")
143 codeWidth := dv.width - leadingSymbolsSize
144
145 switch l.Kind {
146 case udiff.Equal:
147 if dv.lineNumbers {
148 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
149 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
150 }
151 b.WriteString(dv.style.EqualLine.Code.Width(codeWidth + leadingSymbolsSize).Render(" " + content))
152 beforeLine++
153 afterLine++
154 case udiff.Insert:
155 if dv.lineNumbers {
156 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
157 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
158 }
159 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
160 b.WriteString(dv.style.InsertLine.Code.Width(codeWidth).Render(content))
161 afterLine++
162 case udiff.Delete:
163 if dv.lineNumbers {
164 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
165 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
166 }
167 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
168 b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
169 beforeLine++
170 }
171 b.WriteRune('\n')
172 }
173 }
174
175 return b.String()
176}
177
178func (dv *DiffView) computeDiff() error {
179 if dv.isComputed {
180 return dv.err
181 }
182 dv.isComputed = true
183 dv.edits = myers.ComputeEdits(
184 dv.before.content,
185 dv.after.content,
186 )
187 dv.unified, dv.err = udiff.ToUnifiedDiff(
188 dv.before.path,
189 dv.after.path,
190 dv.before.content,
191 dv.edits,
192 dv.contextLines,
193 )
194 return dv.err
195}
196
197func (dv *DiffView) detectWidth() {
198 if dv.width > 0 {
199 return
200 }
201
202 for _, h := range dv.unified.Hunks {
203 for _, l := range h.Lines {
204 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
205 lineWidth += leadingSymbolsSize
206 dv.width = max(dv.width, lineWidth)
207 }
208 }
209}