on main help sign in
Merge pull request #271 from charmbracelet/diffview-improvements

chore: improve diff view performance

commitd41c0525f4322833538e8479b29988f39eabeb79 authorRaphael Amorim <rapha850@gmail.com> dateTue 22 Jul 2025, 13:41 (1 year ago) parentbb4420f4db80f0
1 file changed+72−7
internal/tui/exp/diffview/diffview.go+72−7
internal/tui/exp/diffview/diffview.go+72−7
@@ -1,6 +1,7 @@
11package diffview
22
33import (
4+ "crypto/sha256"
45 "fmt"
56 "image/color"
67 "strconv"
@@ -59,6 +60,13 @@ type DiffView struct {
5960 extraColOnAfter bool // add extra column on after panel
6061 beforeNumDigits int
6162 afterNumDigits int
63+
64+ // Cache lexer to avoid expensive file pattern matching on every line
65+ cachedLexer chroma.Lexer
66+
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
6270}
6371
6472// New creates a new DiffView with default settings.
@@ -68,6 +76,7 @@ func New() *DiffView {
6876 contextLines: udiff.DefaultContextLines,
6977 lineNumbers: true,
7078 tabWidth: 8,
79+ syntaxCache: make(map[string]string),
7180 }
7281 dv.style = DefaultDarkStyle()
7382 return dv
@@ -88,15 +97,26 @@ func (dv *DiffView) Split() *DiffView {
8897// Before sets the "before" file for the DiffView.
8998func (dv *DiffView) Before(path, content string) *DiffView {
9099 dv.before = file{path: path, content: content}
100+ // Clear caches when content changes
101+ dv.clearCaches()
91102 return dv
92103}
93104
94105// After sets the "after" file for the DiffView.
95106func (dv *DiffView) After(path, content string) *DiffView {
96107 dv.after = file{path: path, content: content}
108+ // Clear caches when content changes
109+ dv.clearCaches()
97110 return dv
98111}
99112
113+// clearCaches clears all caches when content or major settings change.
114+func (dv *DiffView) clearCaches() {
115+ dv.cachedLexer = nil
116+ dv.clearSyntaxCache()
117+ dv.isComputed = false
118+}
119+
100120// ContextLines sets the number of context lines for the DiffView.
101121func (dv *DiffView) ContextLines(contextLines int) *DiffView {
102122 dv.contextLines = contextLines
@@ -156,9 +176,21 @@ func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
156176// If nil, no syntax highlighting will be applied.
157177func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
158178 dv.chromaStyle = style
179+ // Clear syntax cache when style changes since highlighting will be different
180+ dv.clearSyntaxCache()
159181 return dv
160182}
161183
184+// clearSyntaxCache clears the syntax highlighting cache.
185+func (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+ }
192+}
193+
162194// String returns the string representation of the DiffView.
163195func (dv *DiffView) String() string {
164196 dv.replaceTabs()
@@ -700,7 +732,15 @@ func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
700732 return source
701733 }
702734
703− l := dv.getChromaLexer(source)
735+ // Create cache key from content and background color
736+ cacheKey := dv.createSyntaxCacheKey(source, bgColor)
737+
738+ // Check if we already have this highlighted
739+ if cached, exists := dv.syntaxCache[cacheKey]; exists {
740+ return cached
741+ }
742+
743+ l := dv.getChromaLexer()
704744 f := dv.getChromaFormatter(bgColor)
705745
706746 it, err := l.Tokenise(nil, source)
@@ -712,22 +752,47 @@ func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
712752 if err := f.Format(&b, dv.chromaStyle, it); err != nil {
713753 return source
714754 }
715− return b.String()
755+
756+ result := b.String()
757+
758+ // Cache the result for future use
759+ dv.syntaxCache[cacheKey] = result
760+
761+ return result
716762}
717763
718−func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
764+// createSyntaxCacheKey creates a cache key from source content and background color.
765+// We use a simple hash to keep memory usage reasonable.
766+func (dv *DiffView) createSyntaxCacheKey(source string, bgColor color.Color) string {
767+ // Convert color to string representation
768+ r, g, b, a := bgColor.RGBA()
769+ colorStr := fmt.Sprintf("%d,%d,%d,%d", r, g, b, a)
770+
771+ // Create a hash of the content + color to use as cache key
772+ h := sha256.New()
773+ h.Write([]byte(source))
774+ h.Write([]byte(colorStr))
775+ return fmt.Sprintf("%x", h.Sum(nil))
776+}
777+
778+func (dv *DiffView) getChromaLexer() chroma.Lexer {
779+ if dv.cachedLexer != nil {
780+ return dv.cachedLexer
781+ }
782+
719783 l := lexers.Match(dv.before.path)
720784 if l == nil {
721− l = lexers.Analyse(source)
785+ l = lexers.Analyse(dv.before.content)
722786 }
723787 if l == nil {
724788 l = lexers.Fallback
725789 }
726− return chroma.Coalesce(l)
790+ dv.cachedLexer = chroma.Coalesce(l)
791+ return dv.cachedLexer
727792}
728793
729−func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
794+func (dv *DiffView) getChromaFormatter(bgColor color.Color) chroma.Formatter {
730795 return chromaFormatter{
731− bgColor: gbColor,
796+ bgColor: bgColor,
732797 }
733798}