on e00a70a help sign in
803 lines 23 kB history raw
1package diffview
3import (
4 "fmt"
5 "image/color"
6 "strconv"
7 "strings"
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 "github.com/zeebo/xxh3"
17const (
18 leadingSymbolsSize = 2
19 lineNumPadding = 1
22type file struct {
23 path string
24 content string
27type layout int
29const (
30 layoutUnified layout = iota + 1
31 layoutSplit
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
50 isComputed bool
51 err error
52 unified udiff.UnifiedDiff
53 edits []udiff.Edit
55 splitHunks []splitHunk
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
64 // Cache lexer to avoid expensive file pattern matching on every line
65 cachedLexer chroma.Lexer
67 // Cache highlighted lines to avoid re-highlighting the same content
68 // Key: hash of (content + background color), Value: highlighted string
69 syntaxCache map[string]string
72// New creates a new DiffView with default settings.
73func New() *DiffView {
74 dv := &DiffView{
75 layout: layoutUnified,
76 contextLines: udiff.DefaultContextLines,
77 lineNumbers: true,
78 tabWidth: 8,
79 syntaxCache: make(map[string]string),
80 }
81 dv.style = DefaultDarkStyle()
82 return dv
85// Unified sets the layout of the DiffView to unified.
86func (dv *DiffView) Unified() *DiffView {
87 dv.layout = layoutUnified
88 return dv
91// Split sets the layout of the DiffView to split (side-by-side).
92func (dv *DiffView) Split() *DiffView {
93 dv.layout = layoutSplit
94 return dv
97// Before sets the "before" file for the DiffView.
98func (dv *DiffView) Before(path, content string) *DiffView {
99 dv.before = file{path: path, content: content}
100 // Clear caches when content changes
101 dv.clearCaches()
102 return dv
105// After sets the "after" file for the DiffView.
106func (dv *DiffView) After(path, content string) *DiffView {
107 dv.after = file{path: path, content: content}
108 // Clear caches when content changes
109 dv.clearCaches()
110 return dv
113// clearCaches clears all caches when content or major settings change.
114func (dv *DiffView) clearCaches() {
115 dv.cachedLexer = nil
116 dv.clearSyntaxCache()
117 dv.isComputed = false
120// ContextLines sets the number of context lines for the DiffView.
121func (dv *DiffView) ContextLines(contextLines int) *DiffView {
122 dv.contextLines = contextLines
123 return dv
126// Style sets the style for the DiffView.
127func (dv *DiffView) Style(style Style) *DiffView {
128 dv.style = style
129 return dv
132// LineNumbers sets whether to display line numbers in the DiffView.
133func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
134 dv.lineNumbers = lineNumbers
135 return dv
138// Height sets the height of the DiffView.
139func (dv *DiffView) Height(height int) *DiffView {
140 dv.height = height
141 return dv
144// Width sets the width of the DiffView.
145func (dv *DiffView) Width(width int) *DiffView {
146 dv.width = width
147 return dv
150// XOffset sets the horizontal offset for the DiffView.
151func (dv *DiffView) XOffset(xOffset int) *DiffView {
152 dv.xOffset = xOffset
153 return dv
156// YOffset sets the vertical offset for the DiffView.
157func (dv *DiffView) YOffset(yOffset int) *DiffView {
158 dv.yOffset = yOffset
159 return dv
162// InfiniteYScroll allows the YOffset to scroll beyond the last line.
163func (dv *DiffView) InfiniteYScroll(infiniteYScroll bool) *DiffView {
164 dv.infiniteYScroll = infiniteYScroll
165 return dv
168// TabWidth sets the tab width. Only relevant for code that contains tabs, like
169// Go code.
170func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
171 dv.tabWidth = tabWidth
172 return dv
175// ChromaStyle sets the chroma style for syntax highlighting.
176// If nil, no syntax highlighting will be applied.
177func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
178 dv.chromaStyle = style
179 // Clear syntax cache when style changes since highlighting will be different
180 dv.clearSyntaxCache()
181 return dv
184// clearSyntaxCache clears the syntax highlighting cache.
185func (dv *DiffView) clearSyntaxCache() {
186 if dv.syntaxCache != nil {
187 // Clear the map but keep it allocated
188 for k := range dv.syntaxCache {
189 delete(dv.syntaxCache, k)
190 }
191 }
194// String returns the string representation of the DiffView.
195func (dv *DiffView) String() string {
196 dv.normalizeLineEndings()
197 dv.replaceTabs()
198 if err := dv.computeDiff(); err != nil {
199 return err.Error()
200 }
201 dv.convertDiffToSplit()
202 dv.adjustStyles()
203 dv.detectNumDigits()
204 dv.detectTotalLines()
205 dv.preventInfiniteYScroll()
207 if dv.width <= 0 {
208 dv.detectCodeWidth()
209 } else {
210 dv.resizeCodeWidth()
211 }
213 style := lipgloss.NewStyle()
214 if dv.width > 0 {
215 style = style.MaxWidth(dv.width)
216 }
217 if dv.height > 0 {
218 style = style.MaxHeight(dv.height)
219 }
221 switch dv.layout {
222 case layoutUnified:
223 return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
224 case layoutSplit:
225 return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
226 default:
227 panic("unknown diffview layout")
228 }
231// normalizeLineEndings ensures the file contents use Unix-style line endings.
232func (dv *DiffView) normalizeLineEndings() {
233 dv.before.content = strings.ReplaceAll(dv.before.content, "\r\n", "\n")
234 dv.after.content = strings.ReplaceAll(dv.after.content, "\r\n", "\n")
237// replaceTabs replaces tabs in the before and after file contents with spaces
238// according to the specified tab width.
239func (dv *DiffView) replaceTabs() {
240 spaces := strings.Repeat(" ", dv.tabWidth)
241 dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
242 dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
245// computeDiff computes the differences between the "before" and "after" files.
246func (dv *DiffView) computeDiff() error {
247 if dv.isComputed {
248 return dv.err
249 }
250 dv.isComputed = true
251 dv.edits = udiff.Strings(
252 dv.before.content,
253 dv.after.content,
254 )
255 dv.unified, dv.err = udiff.ToUnifiedDiff(
256 dv.before.path,
257 dv.after.path,
258 dv.before.content,
259 dv.edits,
260 dv.contextLines,
261 )
262 return dv.err
265// convertDiffToSplit converts the unified diff to a split diff if the layout is
266// set to split.
267func (dv *DiffView) convertDiffToSplit() {
268 if dv.layout != layoutSplit {
269 return
270 }
272 dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
273 for i, h := range dv.unified.Hunks {
274 dv.splitHunks[i] = hunkToSplit(h)
275 }
278// adjustStyles adjusts adds padding and alignment to the styles.
279func (dv *DiffView) adjustStyles() {
280 setPadding := func(s lipgloss.Style) lipgloss.Style {
281 return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
282 }
283 dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
284 dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
285 dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
286 dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
287 dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
290// detectNumDigits calculates the maximum number of digits needed for before and
291// after line numbers.
292func (dv *DiffView) detectNumDigits() {
293 dv.beforeNumDigits = 0
294 dv.afterNumDigits = 0
296 for _, h := range dv.unified.Hunks {
297 dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
298 dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
299 }
302func (dv *DiffView) detectTotalLines() {
303 dv.totalLines = 0
305 switch dv.layout {
306 case layoutUnified:
307 for _, h := range dv.unified.Hunks {
308 dv.totalLines += 1 + len(h.Lines)
309 }
310 case layoutSplit:
311 for _, h := range dv.splitHunks {
312 dv.totalLines += 1 + len(h.lines)
313 }
314 }
317func (dv *DiffView) preventInfiniteYScroll() {
318 if dv.infiniteYScroll {
319 return
320 }
322 // clamp yOffset to prevent scrolling beyond the last line
323 if dv.height > 0 {
324 maxYOffset := max(0, dv.totalLines-dv.height)
325 dv.yOffset = min(dv.yOffset, maxYOffset)
326 } else {
327 // if no height limit, ensure yOffset doesn't exceed total lines
328 dv.yOffset = min(dv.yOffset, max(0, dv.totalLines-1))
329 }
330 dv.yOffset = max(0, dv.yOffset) // ensure yOffset is not negative
333// detectCodeWidth calculates the maximum width of code lines in the diff view.
334func (dv *DiffView) detectCodeWidth() {
335 switch dv.layout {
336 case layoutUnified:
337 dv.detectUnifiedCodeWidth()
338 case layoutSplit:
339 dv.detectSplitCodeWidth()
340 }
341 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
344// detectUnifiedCodeWidth calculates the maximum width of code lines in a
345// unified diff.
346func (dv *DiffView) detectUnifiedCodeWidth() {
347 dv.codeWidth = 0
349 for _, h := range dv.unified.Hunks {
350 shownLines := ansi.StringWidth(dv.hunkLineFor(h))
352 for _, l := range h.Lines {
353 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
354 dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
355 }
356 }
359// detectSplitCodeWidth calculates the maximum width of code lines in a
360// split diff.
361func (dv *DiffView) detectSplitCodeWidth() {
362 dv.codeWidth = 0
364 for i, h := range dv.splitHunks {
365 shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
367 for _, l := range h.lines {
368 if l.before != nil {
369 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
370 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
371 }
372 if l.after != nil {
373 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
374 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
375 }
376 }
377 }
380// resizeCodeWidth resizes the code width to fit within the specified width.
381func (dv *DiffView) resizeCodeWidth() {
382 fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
383 fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
385 switch dv.layout {
386 case layoutUnified:
387 dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
388 case layoutSplit:
389 remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
390 dv.codeWidth = remainingWidth / 2
391 dv.extraColOnAfter = isOdd(remainingWidth)
392 }
394 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
397// renderUnified renders the unified diff view as a string.
398func (dv *DiffView) renderUnified() string {
399 var b strings.Builder
401 fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
402 printedLines := -dv.yOffset
403 shouldWrite := func() bool { return printedLines >= 0 }
405 getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
406 content = strings.TrimSuffix(in, "\n")
407 content = dv.hightlightCode(content, ls.Code.GetBackground())
408 content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
409 content = ansi.Truncate(content, dv.codeWidth, "…")
410 leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
411 return
412 }
414outer:
415 for i, h := range dv.unified.Hunks {
416 if shouldWrite() {
417 ls := dv.style.DividerLine
418 if dv.lineNumbers {
419 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
420 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
421 }
422 content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
423 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
424 b.WriteString("\n")
425 }
426 printedLines++
428 beforeLine := h.FromLine
429 afterLine := h.ToLine
431 for j, l := range h.Lines {
432 // print ellipis if we don't have enough space to print the rest of the diff
433 hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
434 isLastHunk := i+1 == len(dv.unified.Hunks)
435 isLastLine := j+1 == len(h.Lines)
436 if hasReachedHeight && (!isLastHunk || !isLastLine) {
437 if shouldWrite() {
438 ls := dv.lineStyleForType(l.Kind)
439 if dv.lineNumbers {
440 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
441 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
442 }
443 b.WriteString(fullContentStyle.Render(
444 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
445 ))
446 b.WriteRune('\n')
447 }
448 break outer
449 }
451 switch l.Kind {
452 case udiff.Equal:
453 if shouldWrite() {
454 ls := dv.style.EqualLine
455 content, leadingEllipsis := getContent(l.Content, ls)
456 if dv.lineNumbers {
457 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
458 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
459 }
460 b.WriteString(fullContentStyle.Render(
461 ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
462 ))
463 }
464 beforeLine++
465 afterLine++
466 case udiff.Insert:
467 if shouldWrite() {
468 ls := dv.style.InsertLine
469 content, leadingEllipsis := getContent(l.Content, ls)
470 if dv.lineNumbers {
471 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
472 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
473 }
474 b.WriteString(fullContentStyle.Render(
475 ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
476 ls.Code.Width(dv.codeWidth).Render(content),
477 ))
478 }
479 afterLine++
480 case udiff.Delete:
481 if shouldWrite() {
482 ls := dv.style.DeleteLine
483 content, leadingEllipsis := getContent(l.Content, ls)
484 if dv.lineNumbers {
485 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
486 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
487 }
488 b.WriteString(fullContentStyle.Render(
489 ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
490 ls.Code.Width(dv.codeWidth).Render(content),
491 ))
492 }
493 beforeLine++
494 }
495 if shouldWrite() {
496 b.WriteRune('\n')
497 }
499 printedLines++
500 }
501 }
503 for printedLines < dv.height {
504 if shouldWrite() {
505 ls := dv.style.MissingLine
506 if dv.lineNumbers {
507 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
508 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
509 }
510 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
511 b.WriteRune('\n')
512 }
513 printedLines++
514 }
516 return b.String()
519// renderSplit renders the split (side-by-side) diff view as a string.
520func (dv *DiffView) renderSplit() string {
521 var b strings.Builder
523 beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
524 afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
525 printedLines := -dv.yOffset
526 shouldWrite := func() bool { return printedLines >= 0 }
528 getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
529 content = strings.TrimSuffix(in, "\n")
530 content = dv.hightlightCode(content, ls.Code.GetBackground())
531 content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
532 content = ansi.Truncate(content, dv.codeWidth, "…")
533 leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
534 return
535 }
537outer:
538 for i, h := range dv.splitHunks {
539 if shouldWrite() {
540 ls := dv.style.DividerLine
541 if dv.lineNumbers {
542 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
543 }
544 content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
545 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
546 if dv.lineNumbers {
547 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
548 }
549 b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
550 b.WriteRune('\n')
551 }
552 printedLines++
554 beforeLine := h.fromLine
555 afterLine := h.toLine
557 for j, l := range h.lines {
558 // print ellipis if we don't have enough space to print the rest of the diff
559 hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
560 isLastHunk := i+1 == len(dv.unified.Hunks)
561 isLastLine := j+1 == len(h.lines)
562 if hasReachedHeight && (!isLastHunk || !isLastLine) {
563 if shouldWrite() {
564 ls := dv.style.MissingLine
565 if l.before != nil {
566 ls = dv.lineStyleForType(l.before.Kind)
567 }
568 if dv.lineNumbers {
569 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
570 }
571 b.WriteString(beforeFullContentStyle.Render(
572 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
573 ))
574 ls = dv.style.MissingLine
575 if l.after != nil {
576 ls = dv.lineStyleForType(l.after.Kind)
577 }
578 if dv.lineNumbers {
579 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
580 }
581 b.WriteString(afterFullContentStyle.Render(
582 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
583 ))
584 b.WriteRune('\n')
585 }
586 break outer
587 }
589 switch {
590 case l.before == nil:
591 if shouldWrite() {
592 ls := dv.style.MissingLine
593 if dv.lineNumbers {
594 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
595 }
596 b.WriteString(beforeFullContentStyle.Render(
597 ls.Code.Width(dv.fullCodeWidth).Render(" "),
598 ))
599 }
600 case l.before.Kind == udiff.Equal:
601 if shouldWrite() {
602 ls := dv.style.EqualLine
603 content, leadingEllipsis := getContent(l.before.Content, ls)
604 if dv.lineNumbers {
605 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
606 }
607 b.WriteString(beforeFullContentStyle.Render(
608 ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
609 ))
610 }
611 beforeLine++
612 case l.before.Kind == udiff.Delete:
613 if shouldWrite() {
614 ls := dv.style.DeleteLine
615 content, leadingEllipsis := getContent(l.before.Content, ls)
616 if dv.lineNumbers {
617 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
618 }
619 b.WriteString(beforeFullContentStyle.Render(
620 ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
621 ls.Code.Width(dv.codeWidth).Render(content),
622 ))
623 }
624 beforeLine++
625 }
627 switch {
628 case l.after == nil:
629 if shouldWrite() {
630 ls := dv.style.MissingLine
631 if dv.lineNumbers {
632 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
633 }
634 b.WriteString(afterFullContentStyle.Render(
635 ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
636 ))
637 }
638 case l.after.Kind == udiff.Equal:
639 if shouldWrite() {
640 ls := dv.style.EqualLine
641 content, leadingEllipsis := getContent(l.after.Content, ls)
642 if dv.lineNumbers {
643 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
644 }
645 b.WriteString(afterFullContentStyle.Render(
646 ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content),
647 ))
648 }
649 afterLine++
650 case l.after.Kind == udiff.Insert:
651 if shouldWrite() {
652 ls := dv.style.InsertLine
653 content, leadingEllipsis := getContent(l.after.Content, ls)
654 if dv.lineNumbers {
655 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
656 }
657 b.WriteString(afterFullContentStyle.Render(
658 ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
659 ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
660 ))
661 }
662 afterLine++
663 }
665 if shouldWrite() {
666 b.WriteRune('\n')
667 }
669 printedLines++
670 }
671 }
673 for printedLines < dv.height {
674 if shouldWrite() {
675 ls := dv.style.MissingLine
676 if dv.lineNumbers {
677 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
678 }
679 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
680 if dv.lineNumbers {
681 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
682 }
683 b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
684 b.WriteRune('\n')
685 }
686 printedLines++
687 }
689 return b.String()
692// hunkLineFor formats the header line for a hunk in the unified diff view.
693func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
694 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
696 return fmt.Sprintf(
697 " @@ -%d,%d +%d,%d @@ ",
698 h.FromLine,
699 beforeShownLines,
700 h.ToLine,
701 afterShownLines,
702 )
705// hunkShownLines calculates the number of lines shown in a hunk for both before
706// and after versions.
707func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
708 for _, l := range h.Lines {
709 switch l.Kind {
710 case udiff.Equal:
711 before++
712 after++
713 case udiff.Insert:
714 after++
715 case udiff.Delete:
716 before++
717 }
718 }
719 return
722func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
723 switch t {
724 case udiff.Equal:
725 return dv.style.EqualLine
726 case udiff.Insert:
727 return dv.style.InsertLine
728 case udiff.Delete:
729 return dv.style.DeleteLine
730 default:
731 return dv.style.MissingLine
732 }
735func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
736 if dv.chromaStyle == nil {
737 return source
738 }
740 // Create cache key from content and background color
741 cacheKey := dv.createSyntaxCacheKey(source, bgColor)
743 // Check if we already have this highlighted
744 if cached, exists := dv.syntaxCache[cacheKey]; exists {
745 return cached
746 }
748 l := dv.getChromaLexer()
749 f := dv.getChromaFormatter(bgColor)
751 it, err := l.Tokenise(nil, source)
752 if err != nil {
753 return source
754 }
756 var b strings.Builder
757 if err := f.Format(&b, dv.chromaStyle, it); err != nil {
758 return source
759 }
761 result := b.String()
763 // Cache the result for future use
764 dv.syntaxCache[cacheKey] = result
766 return result
769// createSyntaxCacheKey creates a cache key from source content and background color.
770// We use a simple hash to keep memory usage reasonable.
771func (dv *DiffView) createSyntaxCacheKey(source string, bgColor color.Color) string {
772 // Convert color to string representation
773 r, g, b, a := bgColor.RGBA()
774 colorStr := fmt.Sprintf("%d,%d,%d,%d", r, g, b, a)
776 // Create a hash of the content + color to use as cache key
777 h := xxh3.New()
778 h.Write([]byte(source))
779 h.Write([]byte(colorStr))
780 return fmt.Sprintf("%x", h.Sum(nil))
783func (dv *DiffView) getChromaLexer() chroma.Lexer {
784 if dv.cachedLexer != nil {
785 return dv.cachedLexer
786 }
788 l := lexers.Match(dv.before.path)
789 if l == nil {
790 l = lexers.Analyse(dv.before.content)
791 }
792 if l == nil {
793 l = lexers.Fallback
794 }
795 dv.cachedLexer = chroma.Coalesce(l)
796 return dv.cachedLexer
799func (dv *DiffView) getChromaFormatter(bgColor color.Color) chroma.Formatter {
800 return chromaFormatter{
801 bgColor: bgColor,
802 }