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