diffview.go

  1package diffview
  2
  3import (
  4	"fmt"
  5	"image/color"
  6	"strconv"
  7	"strings"
  8
  9	"github.com/alecthomas/chroma/v2"
 10	"github.com/alecthomas/chroma/v2/lexers"
 11	"github.com/aymanbagabas/go-udiff"
 12	"github.com/aymanbagabas/go-udiff/myers"
 13	"github.com/charmbracelet/lipgloss/v2"
 14	"github.com/charmbracelet/x/ansi"
 15)
 16
 17const (
 18	leadingSymbolsSize = 2
 19	lineNumPadding     = 1
 20)
 21
 22type file struct {
 23	path    string
 24	content string
 25}
 26
 27type layout int
 28
 29const (
 30	layoutUnified layout = iota + 1
 31	layoutSplit
 32)
 33
 34// DiffView represents a view for displaying differences between two files.
 35type DiffView struct {
 36	layout          layout
 37	before          file
 38	after           file
 39	contextLines    int
 40	lineNumbers     bool
 41	height          int
 42	width           int
 43	xOffset         int
 44	yOffset         int
 45	infiniteYScroll bool
 46	style           Style
 47	tabWidth        int
 48	chromaStyle     *chroma.Style
 49
 50	isComputed bool
 51	err        error
 52	unified    udiff.UnifiedDiff
 53	edits      []udiff.Edit
 54
 55	splitHunks []splitHunk
 56
 57	totalLines      int
 58	codeWidth       int
 59	fullCodeWidth   int  // with leading symbols
 60	extraColOnAfter bool // add extra column on after panel
 61	beforeNumDigits int
 62	afterNumDigits  int
 63}
 64
 65// New creates a new DiffView with default settings.
 66func New() *DiffView {
 67	dv := &DiffView{
 68		layout:       layoutUnified,
 69		contextLines: udiff.DefaultContextLines,
 70		lineNumbers:  true,
 71		tabWidth:     8,
 72	}
 73	dv.style = DefaultDarkStyle()
 74	return dv
 75}
 76
 77// Unified sets the layout of the DiffView to unified.
 78func (dv *DiffView) Unified() *DiffView {
 79	dv.layout = layoutUnified
 80	return dv
 81}
 82
 83// Split sets the layout of the DiffView to split (side-by-side).
 84func (dv *DiffView) Split() *DiffView {
 85	dv.layout = layoutSplit
 86	return dv
 87}
 88
 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}
 92	return dv
 93}
 94
 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}
 98	return dv
 99}
100
101// ContextLines sets the number of context lines for the DiffView.
102func (dv *DiffView) ContextLines(contextLines int) *DiffView {
103	dv.contextLines = contextLines
104	return dv
105}
106
107// Style sets the style for the DiffView.
108func (dv *DiffView) Style(style Style) *DiffView {
109	dv.style = style
110	return dv
111}
112
113// LineNumbers sets whether to display line numbers in the DiffView.
114func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
115	dv.lineNumbers = lineNumbers
116	return dv
117}
118
119// Height sets the height of the DiffView.
120func (dv *DiffView) Height(height int) *DiffView {
121	dv.height = height
122	return dv
123}
124
125// Width sets the width of the DiffView.
126func (dv *DiffView) Width(width int) *DiffView {
127	dv.width = width
128	return dv
129}
130
131// XOffset sets the horizontal offset for the DiffView.
132func (dv *DiffView) XOffset(xOffset int) *DiffView {
133	dv.xOffset = xOffset
134	return dv
135}
136
137// YOffset sets the vertical offset for the DiffView.
138func (dv *DiffView) YOffset(yOffset int) *DiffView {
139	dv.yOffset = yOffset
140	return dv
141}
142
143// InfiniteYScroll allows the YOffset to scroll beyond the last line.
144func (dv *DiffView) InfiniteYScroll(infiniteYScroll bool) *DiffView {
145	dv.infiniteYScroll = infiniteYScroll
146	return dv
147}
148
149// TabWidth sets the tab width. Only relevant for code that contains tabs, like
150// Go code.
151func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
152	dv.tabWidth = tabWidth
153	return dv
154}
155
156// ChromaStyle sets the chroma style for syntax highlighting.
157// If nil, no syntax highlighting will be applied.
158func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
159	dv.chromaStyle = style
160	return dv
161}
162
163// String returns the string representation of the DiffView.
164func (dv *DiffView) String() string {
165	dv.replaceTabs()
166	if err := dv.computeDiff(); err != nil {
167		return err.Error()
168	}
169	dv.convertDiffToSplit()
170	dv.adjustStyles()
171	dv.detectNumDigits()
172	dv.detectTotalLines()
173	dv.preventInfiniteYScroll()
174
175	if dv.width <= 0 {
176		dv.detectCodeWidth()
177	} else {
178		dv.resizeCodeWidth()
179	}
180
181	style := lipgloss.NewStyle()
182	if dv.width > 0 {
183		style = style.MaxWidth(dv.width)
184	}
185	if dv.height > 0 {
186		style = style.MaxHeight(dv.height)
187	}
188
189	switch dv.layout {
190	case layoutUnified:
191		return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
192	case layoutSplit:
193		return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
194	default:
195		panic("unknown diffview layout")
196	}
197}
198
199// replaceTabs replaces tabs in the before and after file contents with spaces
200// according to the specified tab width.
201func (dv *DiffView) replaceTabs() {
202	spaces := strings.Repeat(" ", dv.tabWidth)
203	dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
204	dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
205}
206
207// computeDiff computes the differences between the "before" and "after" files.
208func (dv *DiffView) computeDiff() error {
209	if dv.isComputed {
210		return dv.err
211	}
212	dv.isComputed = true
213	dv.edits = myers.ComputeEdits( //nolint:staticcheck
214		dv.before.content,
215		dv.after.content,
216	)
217	dv.unified, dv.err = udiff.ToUnifiedDiff(
218		dv.before.path,
219		dv.after.path,
220		dv.before.content,
221		dv.edits,
222		dv.contextLines,
223	)
224	return dv.err
225}
226
227// convertDiffToSplit converts the unified diff to a split diff if the layout is
228// set to split.
229func (dv *DiffView) convertDiffToSplit() {
230	if dv.layout != layoutSplit {
231		return
232	}
233
234	dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
235	for i, h := range dv.unified.Hunks {
236		dv.splitHunks[i] = hunkToSplit(h)
237	}
238}
239
240// adjustStyles adjusts adds padding and alignment to the styles.
241func (dv *DiffView) adjustStyles() {
242	setPadding := func(s lipgloss.Style) lipgloss.Style {
243		return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
244	}
245	dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
246	dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
247	dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
248	dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
249	dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
250}
251
252// detectNumDigits calculates the maximum number of digits needed for before and
253// after line numbers.
254func (dv *DiffView) detectNumDigits() {
255	dv.beforeNumDigits = 0
256	dv.afterNumDigits = 0
257
258	for _, h := range dv.unified.Hunks {
259		dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
260		dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
261	}
262}
263
264func (dv *DiffView) detectTotalLines() {
265	dv.totalLines = 0
266
267	switch dv.layout {
268	case layoutUnified:
269		for _, h := range dv.unified.Hunks {
270			dv.totalLines += 1 + len(h.Lines)
271		}
272	case layoutSplit:
273		for _, h := range dv.splitHunks {
274			dv.totalLines += 1 + len(h.lines)
275		}
276	}
277}
278
279func (dv *DiffView) preventInfiniteYScroll() {
280	if dv.infiniteYScroll {
281		return
282	}
283
284	// clamp yOffset to prevent scrolling beyond the last line
285	if dv.height > 0 {
286		maxYOffset := max(0, dv.totalLines-dv.height)
287		dv.yOffset = min(dv.yOffset, maxYOffset)
288	} else {
289		// if no height limit, ensure yOffset doesn't exceed total lines
290		dv.yOffset = min(dv.yOffset, max(0, dv.totalLines-1))
291	}
292	dv.yOffset = max(0, dv.yOffset) // ensure yOffset is not negative
293}
294
295// detectCodeWidth calculates the maximum width of code lines in the diff view.
296func (dv *DiffView) detectCodeWidth() {
297	switch dv.layout {
298	case layoutUnified:
299		dv.detectUnifiedCodeWidth()
300	case layoutSplit:
301		dv.detectSplitCodeWidth()
302	}
303	dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
304}
305
306// detectUnifiedCodeWidth calculates the maximum width of code lines in a
307// unified diff.
308func (dv *DiffView) detectUnifiedCodeWidth() {
309	dv.codeWidth = 0
310
311	for _, h := range dv.unified.Hunks {
312		shownLines := ansi.StringWidth(dv.hunkLineFor(h))
313
314		for _, l := range h.Lines {
315			lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
316			dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
317		}
318	}
319}
320
321// detectSplitCodeWidth calculates the maximum width of code lines in a
322// split diff.
323func (dv *DiffView) detectSplitCodeWidth() {
324	dv.codeWidth = 0
325
326	for i, h := range dv.splitHunks {
327		shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
328
329		for _, l := range h.lines {
330			if l.before != nil {
331				codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
332				dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
333			}
334			if l.after != nil {
335				codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
336				dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
337			}
338		}
339	}
340}
341
342// resizeCodeWidth resizes the code width to fit within the specified width.
343func (dv *DiffView) resizeCodeWidth() {
344	fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
345	fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
346
347	switch dv.layout {
348	case layoutUnified:
349		dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
350	case layoutSplit:
351		remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
352		dv.codeWidth = remainingWidth / 2
353		dv.extraColOnAfter = isOdd(remainingWidth)
354	}
355
356	dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
357}
358
359// renderUnified renders the unified diff view as a string.
360func (dv *DiffView) renderUnified() string {
361	var b strings.Builder
362
363	fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
364	printedLines := -dv.yOffset
365	shouldWrite := func() bool { return printedLines >= 0 }
366
367	getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
368		content = strings.ReplaceAll(in, "\r\n", "\n")
369		content = strings.TrimSuffix(content, "\n")
370		content = dv.hightlightCode(content, ls.Code.GetBackground())
371		content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
372		content = ansi.Truncate(content, dv.codeWidth, "…")
373		leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
374		return
375	}
376
377outer:
378	for i, h := range dv.unified.Hunks {
379		if shouldWrite() {
380			ls := dv.style.DividerLine
381			if dv.lineNumbers {
382				b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
383				b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
384			}
385			content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
386			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
387			b.WriteString("\n")
388		}
389		printedLines++
390
391		beforeLine := h.FromLine
392		afterLine := h.ToLine
393
394		for j, l := range h.Lines {
395			// print ellipis if we don't have enough space to print the rest of the diff
396			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
397			isLastHunk := i+1 == len(dv.unified.Hunks)
398			isLastLine := j+1 == len(h.Lines)
399			if hasReachedHeight && (!isLastHunk || !isLastLine) {
400				if shouldWrite() {
401					ls := dv.lineStyleForType(l.Kind)
402					if dv.lineNumbers {
403						b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
404						b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
405					}
406					b.WriteString(fullContentStyle.Render(
407						ls.Code.Width(dv.fullCodeWidth).Render("  …"),
408					))
409					b.WriteRune('\n')
410				}
411				break outer
412			}
413
414			switch l.Kind {
415			case udiff.Equal:
416				if shouldWrite() {
417					ls := dv.style.EqualLine
418					content, leadingEllipsis := getContent(l.Content, ls)
419					if dv.lineNumbers {
420						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
421						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
422					}
423					b.WriteString(fullContentStyle.Render(
424						ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
425					))
426				}
427				beforeLine++
428				afterLine++
429			case udiff.Insert:
430				if shouldWrite() {
431					ls := dv.style.InsertLine
432					content, leadingEllipsis := getContent(l.Content, ls)
433					if dv.lineNumbers {
434						b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
435						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
436					}
437					b.WriteString(fullContentStyle.Render(
438						ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
439							ls.Code.Width(dv.codeWidth).Render(content),
440					))
441				}
442				afterLine++
443			case udiff.Delete:
444				if shouldWrite() {
445					ls := dv.style.DeleteLine
446					content, leadingEllipsis := getContent(l.Content, ls)
447					if dv.lineNumbers {
448						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
449						b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
450					}
451					b.WriteString(fullContentStyle.Render(
452						ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
453							ls.Code.Width(dv.codeWidth).Render(content),
454					))
455				}
456				beforeLine++
457			}
458			if shouldWrite() {
459				b.WriteRune('\n')
460			}
461
462			printedLines++
463		}
464	}
465
466	for printedLines < dv.height {
467		if shouldWrite() {
468			ls := dv.style.MissingLine
469			if dv.lineNumbers {
470				b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
471				b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
472			}
473			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render("  "))
474			b.WriteRune('\n')
475		}
476		printedLines++
477	}
478
479	return b.String()
480}
481
482// renderSplit renders the split (side-by-side) diff view as a string.
483func (dv *DiffView) renderSplit() string {
484	var b strings.Builder
485
486	beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
487	afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
488	printedLines := -dv.yOffset
489	shouldWrite := func() bool { return printedLines >= 0 }
490
491	getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
492		content = strings.ReplaceAll(in, "\r\n", "\n")
493		content = strings.TrimSuffix(content, "\n")
494		content = dv.hightlightCode(content, ls.Code.GetBackground())
495		content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
496		content = ansi.Truncate(content, dv.codeWidth, "…")
497		leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
498		return
499	}
500
501outer:
502	for i, h := range dv.splitHunks {
503		if shouldWrite() {
504			ls := dv.style.DividerLine
505			if dv.lineNumbers {
506				b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
507			}
508			content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
509			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
510			if dv.lineNumbers {
511				b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
512			}
513			b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
514			b.WriteRune('\n')
515		}
516		printedLines++
517
518		beforeLine := h.fromLine
519		afterLine := h.toLine
520
521		for j, l := range h.lines {
522			// print ellipis if we don't have enough space to print the rest of the diff
523			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
524			isLastHunk := i+1 == len(dv.unified.Hunks)
525			isLastLine := j+1 == len(h.lines)
526			if hasReachedHeight && (!isLastHunk || !isLastLine) {
527				if shouldWrite() {
528					ls := dv.style.MissingLine
529					if l.before != nil {
530						ls = dv.lineStyleForType(l.before.Kind)
531					}
532					if dv.lineNumbers {
533						b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
534					}
535					b.WriteString(beforeFullContentStyle.Render(
536						ls.Code.Width(dv.fullCodeWidth).Render("  …"),
537					))
538					ls = dv.style.MissingLine
539					if l.after != nil {
540						ls = dv.lineStyleForType(l.after.Kind)
541					}
542					if dv.lineNumbers {
543						b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
544					}
545					b.WriteString(afterFullContentStyle.Render(
546						ls.Code.Width(dv.fullCodeWidth).Render("  …"),
547					))
548					b.WriteRune('\n')
549				}
550				break outer
551			}
552
553			switch {
554			case l.before == nil:
555				if shouldWrite() {
556					ls := dv.style.MissingLine
557					if dv.lineNumbers {
558						b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
559					}
560					b.WriteString(beforeFullContentStyle.Render(
561						ls.Code.Width(dv.fullCodeWidth).Render("  "),
562					))
563				}
564			case l.before.Kind == udiff.Equal:
565				if shouldWrite() {
566					ls := dv.style.EqualLine
567					content, leadingEllipsis := getContent(l.before.Content, ls)
568					if dv.lineNumbers {
569						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
570					}
571					b.WriteString(beforeFullContentStyle.Render(
572						ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
573					))
574				}
575				beforeLine++
576			case l.before.Kind == udiff.Delete:
577				if shouldWrite() {
578					ls := dv.style.DeleteLine
579					content, leadingEllipsis := getContent(l.before.Content, ls)
580					if dv.lineNumbers {
581						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
582					}
583					b.WriteString(beforeFullContentStyle.Render(
584						ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
585							ls.Code.Width(dv.codeWidth).Render(content),
586					))
587				}
588				beforeLine++
589			}
590
591			switch {
592			case l.after == nil:
593				if shouldWrite() {
594					ls := dv.style.MissingLine
595					if dv.lineNumbers {
596						b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
597					}
598					b.WriteString(afterFullContentStyle.Render(
599						ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render("  "),
600					))
601				}
602			case l.after.Kind == udiff.Equal:
603				if shouldWrite() {
604					ls := dv.style.EqualLine
605					content, leadingEllipsis := getContent(l.after.Content, ls)
606					if dv.lineNumbers {
607						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
608					}
609					b.WriteString(afterFullContentStyle.Render(
610						ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", "  ") + content),
611					))
612				}
613				afterLine++
614			case l.after.Kind == udiff.Insert:
615				if shouldWrite() {
616					ls := dv.style.InsertLine
617					content, leadingEllipsis := getContent(l.after.Content, ls)
618					if dv.lineNumbers {
619						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
620					}
621					b.WriteString(afterFullContentStyle.Render(
622						ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
623							ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
624					))
625				}
626				afterLine++
627			}
628
629			if shouldWrite() {
630				b.WriteRune('\n')
631			}
632
633			printedLines++
634		}
635	}
636
637	for printedLines < dv.height {
638		if shouldWrite() {
639			ls := dv.style.MissingLine
640			if dv.lineNumbers {
641				b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
642			}
643			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
644			if dv.lineNumbers {
645				b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
646			}
647			b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
648			b.WriteRune('\n')
649		}
650		printedLines++
651	}
652
653	return b.String()
654}
655
656// hunkLineFor formats the header line for a hunk in the unified diff view.
657func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
658	beforeShownLines, afterShownLines := dv.hunkShownLines(h)
659
660	return fmt.Sprintf(
661		"  @@ -%d,%d +%d,%d @@ ",
662		h.FromLine,
663		beforeShownLines,
664		h.ToLine,
665		afterShownLines,
666	)
667}
668
669// hunkShownLines calculates the number of lines shown in a hunk for both before
670// and after versions.
671func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
672	for _, l := range h.Lines {
673		switch l.Kind {
674		case udiff.Equal:
675			before++
676			after++
677		case udiff.Insert:
678			after++
679		case udiff.Delete:
680			before++
681		}
682	}
683	return
684}
685
686func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
687	switch t {
688	case udiff.Equal:
689		return dv.style.EqualLine
690	case udiff.Insert:
691		return dv.style.InsertLine
692	case udiff.Delete:
693		return dv.style.DeleteLine
694	default:
695		return dv.style.MissingLine
696	}
697}
698
699func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
700	if dv.chromaStyle == nil {
701		return source
702	}
703
704	l := dv.getChromaLexer(source)
705	f := dv.getChromaFormatter(bgColor)
706
707	it, err := l.Tokenise(nil, source)
708	if err != nil {
709		return source
710	}
711
712	var b strings.Builder
713	if err := f.Format(&b, dv.chromaStyle, it); err != nil {
714		return source
715	}
716	return b.String()
717}
718
719func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
720	l := lexers.Match(dv.before.path)
721	if l == nil {
722		l = lexers.Analyse(source)
723	}
724	if l == nil {
725		l = lexers.Fallback
726	}
727	return chroma.Coalesce(l)
728}
729
730func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
731	return chromaFormatter{
732		bgColor: gbColor,
733	}
734}