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
53 extraColOnAfter bool // add extra column on after panel
58// New creates a new DiffView with default settings.
61 layout: layoutUnified,
62 contextLines: udiff.DefaultContextLines,
65 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
66 dv.style = DefaultDarkStyle
68 dv.style = DefaultLightStyle
73// Unified sets the layout of the DiffView to unified.
74func (dv *DiffView) Unified() *DiffView {
75 dv.layout = layoutUnified
79// Split sets the layout of the DiffView to split (side-by-side).
80func (dv *DiffView) Split() *DiffView {
81 dv.layout = layoutSplit
85// Before sets the "before" file for the DiffView.
86func (dv *DiffView) Before(path, content string) *DiffView {
87 dv.before = file{path: path, content: content}
91// After sets the "after" file for the DiffView.
92func (dv *DiffView) After(path, content string) *DiffView {
93 dv.after = file{path: path, content: content}
97// ContextLines sets the number of context lines for the DiffView.
98func (dv *DiffView) ContextLines(contextLines int) *DiffView {
99 dv.contextLines = contextLines
103// Style sets the style for the DiffView.
104func (dv *DiffView) Style(style Style) *DiffView {
109// LineNumbers sets whether to display line numbers in the DiffView.
110func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
111 dv.lineNumbers = lineNumbers
115// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
116func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
117 dv.highlight = highlight
121// Height sets the height of the DiffView.
122func (dv *DiffView) Height(height int) *DiffView {
127// Width sets the width of the DiffView.
128func (dv *DiffView) Width(width int) *DiffView {
133// String returns the string representation of the DiffView.
134func (dv *DiffView) String() string {
135 if err := dv.computeDiff(); err != nil {
138 dv.convertDiffToSplit()
148 style := lipgloss.NewStyle()
150 style = style.MaxWidth(dv.width)
153 style = style.MaxHeight(dv.height)
158 return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
160 return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
162 panic("unknown diffview layout")
166// computeDiff computes the differences between the "before" and "after" files.
167func (dv *DiffView) computeDiff() error {
172 dv.edits = myers.ComputeEdits( //nolint:staticcheck
176 dv.unified, dv.err = udiff.ToUnifiedDiff(
186// convertDiffToSplit converts the unified diff to a split diff if the layout is
188func (dv *DiffView) convertDiffToSplit() {
189 if dv.layout != layoutSplit {
193 dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
194 for i, h := range dv.unified.Hunks {
195 dv.splitHunks[i] = hunkToSplit(h)
199// adjustStyles adjusts adds padding and alignment to the styles.
200func (dv *DiffView) adjustStyles() {
201 dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
202 dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
203 dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
204 dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
205 dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
208// detectNumDigits calculates the maximum number of digits needed for before and
209// after line numbers.
210func (dv *DiffView) detectNumDigits() {
211 dv.beforeNumDigits = 0
212 dv.afterNumDigits = 0
214 for _, h := range dv.unified.Hunks {
215 dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
216 dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
220func setPadding(s lipgloss.Style) lipgloss.Style {
221 return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
224// detectCodeWidth calculates the maximum width of code lines in the diff view.
225func (dv *DiffView) detectCodeWidth() {
228 dv.detectUnifiedCodeWidth()
230 dv.detectSplitCodeWidth()
232 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
235// detectUnifiedCodeWidth calculates the maximum width of code lines in a
237func (dv *DiffView) detectUnifiedCodeWidth() {
240 for _, h := range dv.unified.Hunks {
241 shownLines := ansi.StringWidth(dv.hunkLineFor(h))
243 for _, l := range h.Lines {
244 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
245 dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
250// detectSplitCodeWidth calculates the maximum width of code lines in a
252func (dv *DiffView) detectSplitCodeWidth() {
255 for i, h := range dv.splitHunks {
256 shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
258 for _, l := range h.lines {
260 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
261 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
264 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
265 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
271// resizeCodeWidth resizes the code width to fit within the specified width.
272func (dv *DiffView) resizeCodeWidth() {
273 fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
274 fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
278 dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
280 remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
281 dv.codeWidth = remainingWidth / 2
282 dv.extraColOnAfter = isOdd(remainingWidth)
285 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
288// renderUnified renders the unified diff view as a string.
289func (dv *DiffView) renderUnified() string {
290 var b strings.Builder
292 fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
295 for _, h := range dv.unified.Hunks {
297 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
298 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
300 content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
301 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
305 beforeLine := h.FromLine
306 afterLine := h.ToLine
308 for _, l := range h.Lines {
309 content := strings.TrimSuffix(l.Content, "\n")
310 content = ansi.Truncate(content, dv.codeWidth, "…")
315 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
316 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
318 b.WriteString(fullContentStyle.Render(
319 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + content),
325 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
326 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
328 b.WriteString(fullContentStyle.Render(
329 dv.style.InsertLine.Symbol.Render("+ ") +
330 dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content),
335 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
336 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
338 b.WriteString(fullContentStyle.Render(
339 dv.style.DeleteLine.Symbol.Render("- ") +
340 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
350 for printedLines < dv.height {
352 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
353 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
355 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
363// renderSplit renders the split (side-by-side) diff view as a string.
364func (dv *DiffView) renderSplit() string {
365 var b strings.Builder
367 beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
368 afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
371 for i, h := range dv.splitHunks {
373 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
375 content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
376 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
378 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
380 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
384 beforeLine := h.fromLine
385 afterLine := h.toLine
387 for _, l := range h.lines {
388 var beforeContent string
389 var afterContent string
391 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
392 beforeContent = ansi.Truncate(beforeContent, dv.codeWidth, "…")
395 afterContent = strings.TrimSuffix(l.after.Content, "\n")
396 afterContent = ansi.Truncate(afterContent, dv.codeWidth+btoi(dv.extraColOnAfter), "…")
400 case l.before == nil:
402 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
404 b.WriteString(beforeFullContentStyle.Render(
405 dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "),
407 case l.before.Kind == udiff.Equal:
409 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
411 b.WriteString(beforeFullContentStyle.Render(
412 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + beforeContent),
415 case l.before.Kind == udiff.Delete:
417 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
419 b.WriteString(beforeFullContentStyle.Render(
420 dv.style.DeleteLine.Symbol.Render("- ") +
421 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent),
429 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
431 b.WriteString(afterFullContentStyle.Render(
432 dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
434 case l.after.Kind == udiff.Equal:
436 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
438 b.WriteString(afterFullContentStyle.Render(
439 dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" " + afterContent),
442 case l.after.Kind == udiff.Insert:
444 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
446 b.WriteString(afterFullContentStyle.Render(
447 dv.style.InsertLine.Symbol.Render("+ ") +
448 dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(afterContent),
459 for printedLines < dv.height {
461 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
463 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
465 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
467 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
475// hunkLineFor formats the header line for a hunk in the unified diff view.
476func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
477 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
480 " @@ -%d,%d +%d,%d @@ ",
488// hunkShownLines calculates the number of lines shown in a hunk for both before
489// and after versions.
490func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
491 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