diffview.go

  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}