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.TrimSuffix(in, "\n")
369		content = dv.hightlightCode(content, ls.Code.GetBackground())
370		content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
371		content = ansi.Truncate(content, dv.codeWidth, "…")
372		leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
373		return
374	}
375
376outer:
377	for i, h := range dv.unified.Hunks {
378		if shouldWrite() {
379			ls := dv.style.DividerLine
380			if dv.lineNumbers {
381				b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
382				b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
383			}
384			content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
385			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
386			b.WriteString("\n")
387		}
388		printedLines++
389
390		beforeLine := h.FromLine
391		afterLine := h.ToLine
392
393		for j, l := range h.Lines {
394			// print ellipis if we don't have enough space to print the rest of the diff
395			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
396			isLastHunk := i+1 == len(dv.unified.Hunks)
397			isLastLine := j+1 == len(h.Lines)
398			if hasReachedHeight && (!isLastHunk || !isLastLine) {
399				if shouldWrite() {
400					ls := dv.lineStyleForType(l.Kind)
401					if dv.lineNumbers {
402						b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
403						b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
404					}
405					b.WriteString(fullContentStyle.Render(
406						ls.Code.Width(dv.fullCodeWidth).Render("  …"),
407					))
408					b.WriteRune('\n')
409				}
410				break outer
411			}
412
413			switch l.Kind {
414			case udiff.Equal:
415				if shouldWrite() {
416					ls := dv.style.EqualLine
417					content, leadingEllipsis := getContent(l.Content, ls)
418					if dv.lineNumbers {
419						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
420						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
421					}
422					b.WriteString(fullContentStyle.Render(
423						ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
424					))
425				}
426				beforeLine++
427				afterLine++
428			case udiff.Insert:
429				if shouldWrite() {
430					ls := dv.style.InsertLine
431					content, leadingEllipsis := getContent(l.Content, ls)
432					if dv.lineNumbers {
433						b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
434						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
435					}
436					b.WriteString(fullContentStyle.Render(
437						ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
438							ls.Code.Width(dv.codeWidth).Render(content),
439					))
440				}
441				afterLine++
442			case udiff.Delete:
443				if shouldWrite() {
444					ls := dv.style.DeleteLine
445					content, leadingEllipsis := getContent(l.Content, ls)
446					if dv.lineNumbers {
447						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
448						b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
449					}
450					b.WriteString(fullContentStyle.Render(
451						ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
452							ls.Code.Width(dv.codeWidth).Render(content),
453					))
454				}
455				beforeLine++
456			}
457			if shouldWrite() {
458				b.WriteRune('\n')
459			}
460
461			printedLines++
462		}
463	}
464
465	for printedLines < dv.height {
466		if shouldWrite() {
467			ls := dv.style.MissingLine
468			if dv.lineNumbers {
469				b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
470				b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
471			}
472			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render("  "))
473			b.WriteRune('\n')
474		}
475		printedLines++
476	}
477
478	return b.String()
479}
480
481// renderSplit renders the split (side-by-side) diff view as a string.
482func (dv *DiffView) renderSplit() string {
483	var b strings.Builder
484
485	beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
486	afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
487	printedLines := -dv.yOffset
488	shouldWrite := func() bool { return printedLines >= 0 }
489
490	getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
491		content = strings.TrimSuffix(in, "\n")
492		content = dv.hightlightCode(content, ls.Code.GetBackground())
493		content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
494		content = ansi.Truncate(content, dv.codeWidth, "…")
495		leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
496		return
497	}
498
499outer:
500	for i, h := range dv.splitHunks {
501		if shouldWrite() {
502			ls := dv.style.DividerLine
503			if dv.lineNumbers {
504				b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
505			}
506			content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
507			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
508			if dv.lineNumbers {
509				b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
510			}
511			b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
512			b.WriteRune('\n')
513		}
514		printedLines++
515
516		beforeLine := h.fromLine
517		afterLine := h.toLine
518
519		for j, l := range h.lines {
520			// print ellipis if we don't have enough space to print the rest of the diff
521			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
522			isLastHunk := i+1 == len(dv.unified.Hunks)
523			isLastLine := j+1 == len(h.lines)
524			if hasReachedHeight && (!isLastHunk || !isLastLine) {
525				if shouldWrite() {
526					ls := dv.style.MissingLine
527					if l.before != nil {
528						ls = dv.lineStyleForType(l.before.Kind)
529					}
530					if dv.lineNumbers {
531						b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
532					}
533					b.WriteString(beforeFullContentStyle.Render(
534						ls.Code.Width(dv.fullCodeWidth).Render("  …"),
535					))
536					ls = dv.style.MissingLine
537					if l.after != nil {
538						ls = dv.lineStyleForType(l.after.Kind)
539					}
540					if dv.lineNumbers {
541						b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
542					}
543					b.WriteString(afterFullContentStyle.Render(
544						ls.Code.Width(dv.fullCodeWidth).Render("  …"),
545					))
546					b.WriteRune('\n')
547				}
548				break outer
549			}
550
551			switch {
552			case l.before == nil:
553				if shouldWrite() {
554					ls := dv.style.MissingLine
555					if dv.lineNumbers {
556						b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
557					}
558					b.WriteString(beforeFullContentStyle.Render(
559						ls.Code.Width(dv.fullCodeWidth).Render("  "),
560					))
561				}
562			case l.before.Kind == udiff.Equal:
563				if shouldWrite() {
564					ls := dv.style.EqualLine
565					content, leadingEllipsis := getContent(l.before.Content, ls)
566					if dv.lineNumbers {
567						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
568					}
569					b.WriteString(beforeFullContentStyle.Render(
570						ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
571					))
572				}
573				beforeLine++
574			case l.before.Kind == udiff.Delete:
575				if shouldWrite() {
576					ls := dv.style.DeleteLine
577					content, leadingEllipsis := getContent(l.before.Content, ls)
578					if dv.lineNumbers {
579						b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
580					}
581					b.WriteString(beforeFullContentStyle.Render(
582						ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
583							ls.Code.Width(dv.codeWidth).Render(content),
584					))
585				}
586				beforeLine++
587			}
588
589			switch {
590			case l.after == nil:
591				if shouldWrite() {
592					ls := dv.style.MissingLine
593					if dv.lineNumbers {
594						b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
595					}
596					b.WriteString(afterFullContentStyle.Render(
597						ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render("  "),
598					))
599				}
600			case l.after.Kind == udiff.Equal:
601				if shouldWrite() {
602					ls := dv.style.EqualLine
603					content, leadingEllipsis := getContent(l.after.Content, ls)
604					if dv.lineNumbers {
605						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
606					}
607					b.WriteString(afterFullContentStyle.Render(
608						ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", "  ") + content),
609					))
610				}
611				afterLine++
612			case l.after.Kind == udiff.Insert:
613				if shouldWrite() {
614					ls := dv.style.InsertLine
615					content, leadingEllipsis := getContent(l.after.Content, ls)
616					if dv.lineNumbers {
617						b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
618					}
619					b.WriteString(afterFullContentStyle.Render(
620						ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
621							ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
622					))
623				}
624				afterLine++
625			}
626
627			if shouldWrite() {
628				b.WriteRune('\n')
629			}
630
631			printedLines++
632		}
633	}
634
635	for printedLines < dv.height {
636		if shouldWrite() {
637			ls := dv.style.MissingLine
638			if dv.lineNumbers {
639				b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
640			}
641			b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
642			if dv.lineNumbers {
643				b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
644			}
645			b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
646			b.WriteRune('\n')
647		}
648		printedLines++
649	}
650
651	return b.String()
652}
653
654// hunkLineFor formats the header line for a hunk in the unified diff view.
655func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
656	beforeShownLines, afterShownLines := dv.hunkShownLines(h)
657
658	return fmt.Sprintf(
659		"  @@ -%d,%d +%d,%d @@ ",
660		h.FromLine,
661		beforeShownLines,
662		h.ToLine,
663		afterShownLines,
664	)
665}
666
667// hunkShownLines calculates the number of lines shown in a hunk for both before
668// and after versions.
669func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
670	for _, l := range h.Lines {
671		switch l.Kind {
672		case udiff.Equal:
673			before++
674			after++
675		case udiff.Insert:
676			after++
677		case udiff.Delete:
678			before++
679		}
680	}
681	return
682}
683
684func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
685	switch t {
686	case udiff.Equal:
687		return dv.style.EqualLine
688	case udiff.Insert:
689		return dv.style.InsertLine
690	case udiff.Delete:
691		return dv.style.DeleteLine
692	default:
693		return dv.style.MissingLine
694	}
695}
696
697func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
698	if dv.chromaStyle == nil {
699		return source
700	}
701
702	l := dv.getChromaLexer(source)
703	f := dv.getChromaFormatter(bgColor)
704
705	it, err := l.Tokenise(nil, source)
706	if err != nil {
707		return source
708	}
709
710	var b strings.Builder
711	if err := f.Format(&b, dv.chromaStyle, it); err != nil {
712		return source
713	}
714	return b.String()
715}
716
717func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
718	l := lexers.Match(dv.before.path)
719	if l == nil {
720		l = lexers.Analyse(source)
721	}
722	if l == nil {
723		l = lexers.Fallback
724	}
725	return chroma.Coalesce(l)
726}
727
728func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
729	return chromaFormatter{
730		bgColor: gbColor,
731	}
732}