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.
49 unified udiff.UnifiedDiff
52 splitHunks []splitHunk
55 fullCodeWidth int // with leading symbols
56 extraColOnAfter bool // add extra column on after panel
61// New creates a new DiffView with default settings.
64 layout: layoutUnified,
65 contextLines: udiff.DefaultContextLines,
69 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
70 dv.style = DefaultDarkStyle
72 dv.style = DefaultLightStyle
77// Unified sets the layout of the DiffView to unified.
78func (dv *DiffView) Unified() *DiffView {
79 dv.layout = layoutUnified
83// Split sets the layout of the DiffView to split (side-by-side).
84func (dv *DiffView) Split() *DiffView {
85 dv.layout = layoutSplit
89// Before sets the "before" file for the DiffView.
90func (dv *DiffView) Before(path, content string) *DiffView {
91 dv.before = file{path: path, content: content}
95// After sets the "after" file for the DiffView.
96func (dv *DiffView) After(path, content string) *DiffView {
97 dv.after = file{path: path, content: content}
101// ContextLines sets the number of context lines for the DiffView.
102func (dv *DiffView) ContextLines(contextLines int) *DiffView {
103 dv.contextLines = contextLines
107// Style sets the style for the DiffView.
108func (dv *DiffView) Style(style Style) *DiffView {
113// LineNumbers sets whether to display line numbers in the DiffView.
114func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
115 dv.lineNumbers = lineNumbers
119// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
120func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
121 dv.highlight = highlight
125// Height sets the height of the DiffView.
126func (dv *DiffView) Height(height int) *DiffView {
131// Width sets the width of the DiffView.
132func (dv *DiffView) Width(width int) *DiffView {
137// XOffset sets the horizontal offset for the DiffView.
138func (dv *DiffView) XOffset(xOffset int) *DiffView {
143// YOffset sets the vertical offset for the DiffView.
144func (dv *DiffView) YOffset(yOffset int) *DiffView {
149// TabWidth sets the tab width. Only relevant for code that contains tabs, like
151func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
152 dv.tabWidth = tabWidth
156// String returns the string representation of the DiffView.
157func (dv *DiffView) String() string {
159 if err := dv.computeDiff(); err != nil {
162 dv.convertDiffToSplit()
172 style := lipgloss.NewStyle()
174 style = style.MaxWidth(dv.width)
177 style = style.MaxHeight(dv.height)
182 return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
184 return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
186 panic("unknown diffview layout")
190// replaceTabs replaces tabs in the before and after file contents with spaces
191// according to the specified tab width.
192func (dv *DiffView) replaceTabs() {
193 spaces := strings.Repeat(" ", dv.tabWidth)
194 dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
195 dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
198// computeDiff computes the differences between the "before" and "after" files.
199func (dv *DiffView) computeDiff() error {
204 dv.edits = myers.ComputeEdits( //nolint:staticcheck
208 dv.unified, dv.err = udiff.ToUnifiedDiff(
218// convertDiffToSplit converts the unified diff to a split diff if the layout is
220func (dv *DiffView) convertDiffToSplit() {
221 if dv.layout != layoutSplit {
225 dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
226 for i, h := range dv.unified.Hunks {
227 dv.splitHunks[i] = hunkToSplit(h)
231// adjustStyles adjusts adds padding and alignment to the styles.
232func (dv *DiffView) adjustStyles() {
233 setPadding := func(s lipgloss.Style) lipgloss.Style {
234 return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
236 dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
237 dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
238 dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
239 dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
240 dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
243// detectNumDigits calculates the maximum number of digits needed for before and
244// after line numbers.
245func (dv *DiffView) detectNumDigits() {
246 dv.beforeNumDigits = 0
247 dv.afterNumDigits = 0
249 for _, h := range dv.unified.Hunks {
250 dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
251 dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
255// detectCodeWidth calculates the maximum width of code lines in the diff view.
256func (dv *DiffView) detectCodeWidth() {
259 dv.detectUnifiedCodeWidth()
261 dv.detectSplitCodeWidth()
263 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
266// detectUnifiedCodeWidth calculates the maximum width of code lines in a
268func (dv *DiffView) detectUnifiedCodeWidth() {
271 for _, h := range dv.unified.Hunks {
272 shownLines := ansi.StringWidth(dv.hunkLineFor(h))
274 for _, l := range h.Lines {
275 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
276 dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
281// detectSplitCodeWidth calculates the maximum width of code lines in a
283func (dv *DiffView) detectSplitCodeWidth() {
286 for i, h := range dv.splitHunks {
287 shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
289 for _, l := range h.lines {
291 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
292 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
295 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
296 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
302// resizeCodeWidth resizes the code width to fit within the specified width.
303func (dv *DiffView) resizeCodeWidth() {
304 fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
305 fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
309 dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
311 remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
312 dv.codeWidth = remainingWidth / 2
313 dv.extraColOnAfter = isOdd(remainingWidth)
316 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
319// renderUnified renders the unified diff view as a string.
320func (dv *DiffView) renderUnified() string {
321 var b strings.Builder
323 fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
324 printedLines := -dv.yOffset
326 write := func(s string) {
327 if printedLines >= 0 {
333 for i, h := range dv.unified.Hunks {
335 write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
336 write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
338 content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
339 write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
343 beforeLine := h.FromLine
344 afterLine := h.ToLine
346 for j, l := range h.Lines {
347 // print ellipis if we don't have enough space to print the rest of the diff
348 hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
349 isLastHunk := i+1 == len(dv.unified.Hunks)
350 isLastLine := j+1 == len(h.Lines)
351 if hasReachedHeight && (!isLastHunk || !isLastLine) {
352 lineStyle := dv.lineStyleForType(l.Kind)
354 write(lineStyle.LineNumber.Render(pad("…", dv.beforeNumDigits)))
355 write(lineStyle.LineNumber.Render(pad("…", dv.afterNumDigits)))
357 write(fullContentStyle.Render(
358 lineStyle.Code.Width(dv.fullCodeWidth).Render(" …"),
364 content := strings.TrimSuffix(l.Content, "\n")
365 content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
366 content = ansi.Truncate(content, dv.codeWidth, "…")
368 leadingEllipsis := dv.xOffset > 0 && strings.TrimSpace(content) != ""
373 write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
374 write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
376 write(fullContentStyle.Render(
377 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
383 write(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
384 write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
386 write(fullContentStyle.Render(
387 dv.style.InsertLine.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
388 dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content),
393 write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
394 write(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
396 write(fullContentStyle.Render(
397 dv.style.DeleteLine.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
398 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
408 for printedLines < dv.height {
410 write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
411 write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
413 write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
421// renderSplit renders the split (side-by-side) diff view as a string.
422func (dv *DiffView) renderSplit() string {
423 var b strings.Builder
425 beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
426 afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
427 printedLines := -dv.yOffset
429 write := func(s string) {
430 if printedLines >= 0 {
436 for i, h := range dv.splitHunks {
438 write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
440 content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
441 write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
443 write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
445 write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
449 beforeLine := h.fromLine
450 afterLine := h.toLine
452 for j, l := range h.lines {
453 // print ellipis if we don't have enough space to print the rest of the diff
454 hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
455 isLastHunk := i+1 == len(dv.unified.Hunks)
456 isLastLine := j+1 == len(h.lines)
457 if hasReachedHeight && (!isLastHunk || !isLastLine) {
458 lineStyle := dv.style.MissingLine
460 lineStyle = dv.lineStyleForType(l.before.Kind)
463 write(lineStyle.LineNumber.Render(pad("…", dv.beforeNumDigits)))
465 write(beforeFullContentStyle.Render(
466 lineStyle.Code.Width(dv.fullCodeWidth).Render(" …"),
468 lineStyle = dv.style.MissingLine
470 lineStyle = dv.lineStyleForType(l.after.Kind)
473 write(lineStyle.LineNumber.Render(pad("…", dv.afterNumDigits)))
475 write(afterFullContentStyle.Render(
476 lineStyle.Code.Width(dv.fullCodeWidth).Render(" …"),
482 var beforeContent string
483 var afterContent string
485 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
486 beforeContent = ansi.GraphemeWidth.Cut(beforeContent, dv.xOffset, len(beforeContent))
487 beforeContent = ansi.Truncate(beforeContent, dv.codeWidth, "…")
490 afterContent = strings.TrimSuffix(l.after.Content, "\n")
491 afterContent = ansi.GraphemeWidth.Cut(afterContent, dv.xOffset, len(afterContent))
492 afterContent = ansi.Truncate(afterContent, dv.codeWidth+btoi(dv.extraColOnAfter), "…")
495 leadingBeforeEllipsis := dv.xOffset > 0 && strings.TrimSpace(beforeContent) != ""
496 leadingAfterEllipsis := dv.xOffset > 0 && strings.TrimSpace(afterContent) != ""
499 case l.before == nil:
501 write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
503 write(beforeFullContentStyle.Render(
504 dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "),
506 case l.before.Kind == udiff.Equal:
508 write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
510 write(beforeFullContentStyle.Render(
511 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingBeforeEllipsis, " …", " ") + beforeContent),
514 case l.before.Kind == udiff.Delete:
516 write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
518 write(beforeFullContentStyle.Render(
519 dv.style.DeleteLine.Symbol.Render(ternary(leadingBeforeEllipsis, "-…", "- ")) +
520 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent),
528 write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
530 write(afterFullContentStyle.Render(
531 dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
533 case l.after.Kind == udiff.Equal:
535 write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
537 write(afterFullContentStyle.Render(
538 dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingAfterEllipsis, " …", " ") + afterContent),
541 case l.after.Kind == udiff.Insert:
543 write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
545 write(afterFullContentStyle.Render(
546 dv.style.InsertLine.Symbol.Render(ternary(leadingAfterEllipsis, "+…", "+ ")) +
547 dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(afterContent),
558 for printedLines < dv.height {
560 write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
562 write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
564 write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
566 write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
574// hunkLineFor formats the header line for a hunk in the unified diff view.
575func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
576 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
579 " @@ -%d,%d +%d,%d @@ ",
587// hunkShownLines calculates the number of lines shown in a hunk for both before
588// and after versions.
589func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
590 for _, l := range h.Lines {
604func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
607 return dv.style.EqualLine
609 return dv.style.InsertLine
611 return dv.style.DeleteLine
613 return dv.style.MissingLine
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