diffview.go

  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}