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"
16 leadingSymbolsSize = 2
28 layoutUnified layout = iota + 1
32// DiffView represents a view for displaying differences between two files.
46 unified udiff.UnifiedDiff
49 splitHunks []splitHunk
52 fullCodeWidth int // with leading symbols
57// New creates a new DiffView with default settings.
60 layout: layoutUnified,
61 contextLines: udiff.DefaultContextLines,
64 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
65 dv.style = DefaultDarkStyle
67 dv.style = DefaultLightStyle
72// Unified sets the layout of the DiffView to unified.
73func (dv *DiffView) Unified() *DiffView {
74 dv.layout = layoutUnified
78// Split sets the layout of the DiffView to split (side-by-side).
79func (dv *DiffView) Split() *DiffView {
80 dv.layout = layoutSplit
84// Before sets the "before" file for the DiffView.
85func (dv *DiffView) Before(path, content string) *DiffView {
86 dv.before = file{path: path, content: content}
90// After sets the "after" file for the DiffView.
91func (dv *DiffView) After(path, content string) *DiffView {
92 dv.after = file{path: path, content: content}
96// ContextLines sets the number of context lines for the DiffView.
97func (dv *DiffView) ContextLines(contextLines int) *DiffView {
98 dv.contextLines = contextLines
102// Style sets the style for the DiffView.
103func (dv *DiffView) Style(style Style) *DiffView {
108// LineNumbers sets whether to display line numbers in the DiffView.
109func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
110 dv.lineNumbers = lineNumbers
114// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
115func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
116 dv.highlight = highlight
120// Height sets the height of the DiffView.
121func (dv *DiffView) Height(height int) *DiffView {
126// Width sets the width of the DiffView.
127func (dv *DiffView) Width(width int) *DiffView {
132// String returns the string representation of the DiffView.
133func (dv *DiffView) String() string {
134 if err := dv.computeDiff(); err != nil {
137 dv.convertDiffToSplit()
144 return dv.renderUnified()
146 return dv.renderSplit()
148 panic("unknown diffview layout")
152// computeDiff computes the differences between the "before" and "after" files.
153func (dv *DiffView) computeDiff() error {
158 dv.edits = myers.ComputeEdits( //nolint:staticcheck
162 dv.unified, dv.err = udiff.ToUnifiedDiff(
172// convertDiffToSplit converts the unified diff to a split diff if the layout is
174func (dv *DiffView) convertDiffToSplit() {
175 if dv.layout != layoutSplit {
179 dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
180 for i, h := range dv.unified.Hunks {
181 dv.splitHunks[i] = hunkToSplit(h)
185// adjustStyles adjusts adds padding and alignment to the styles.
186func (dv *DiffView) adjustStyles() {
187 dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
188 dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
189 dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
190 dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
191 dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
194// detectNumDigits calculates the maximum number of digits needed for before and
195// after line numbers.
196func (dv *DiffView) detectNumDigits() {
197 dv.beforeNumDigits = 0
198 dv.afterNumDigits = 0
200 for _, h := range dv.unified.Hunks {
201 dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
202 dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
206func setPadding(s lipgloss.Style) lipgloss.Style {
207 return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
210// detectCodeWidth calculates the maximum width of code lines in the diff view.
211func (dv *DiffView) detectCodeWidth() {
214 dv.detectUnifiedCodeWidth()
216 dv.detectSplitCodeWidth()
218 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
221// detectUnifiedCodeWidth calculates the maximum width of code lines in a
223func (dv *DiffView) detectUnifiedCodeWidth() {
226 for _, h := range dv.unified.Hunks {
227 shownLines := ansi.StringWidth(dv.hunkLineFor(h))
229 for _, l := range h.Lines {
230 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
231 dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
236// detectSplitCodeWidth calculates the maximum width of code lines in a
238func (dv *DiffView) detectSplitCodeWidth() {
241 for i, h := range dv.splitHunks {
242 shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
244 for _, l := range h.lines {
246 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
247 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
250 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
251 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
257// renderUnified renders the unified diff view as a string.
258func (dv *DiffView) renderUnified() string {
259 var b strings.Builder
261 for _, h := range dv.unified.Hunks {
263 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
264 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
266 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(h)))
269 beforeLine := h.FromLine
270 afterLine := h.ToLine
272 for _, l := range h.Lines {
273 content := strings.TrimSuffix(l.Content, "\n")
278 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
279 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
281 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + content))
286 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
287 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
289 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
290 b.WriteString(dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content))
294 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
295 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
297 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
298 b.WriteString(dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content))
308// renderSplit renders the split (side-by-side) diff view as a string.
309func (dv *DiffView) renderSplit() string {
310 var b strings.Builder
312 for i, h := range dv.splitHunks {
314 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
316 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(dv.unified.Hunks[i])))
318 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
320 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(" "))
323 beforeLine := h.fromLine
324 afterLine := h.toLine
326 for _, l := range h.lines {
327 var beforeContent string
328 var afterContent string
330 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
333 afterContent = strings.TrimSuffix(l.after.Content, "\n")
337 case l.before == nil:
339 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
341 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
342 case l.before.Kind == udiff.Equal:
344 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
346 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + beforeContent))
348 case l.before.Kind == udiff.Delete:
350 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
352 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
353 b.WriteString(dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent))
360 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
362 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
363 case l.after.Kind == udiff.Equal:
365 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
367 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + afterContent))
369 case l.after.Kind == udiff.Insert:
371 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
373 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
374 b.WriteString(dv.style.InsertLine.Code.Width(dv.codeWidth).Render(afterContent))
385// hunkLineFor formats the header line for a hunk in the unified diff view.
386func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
387 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
390 " @@ -%d,%d +%d,%d @@ ",
398// hunkShownLines calculates the number of lines shown in a hunk for both before
399// and after versions.
400func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
401 for _, l := range h.Lines {
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list