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}