Merge pull request #271 from charmbracelet/diffview-improvements

Raphael Amorim created

chore: improve diff view performance

Change summary

internal/tui/exp/diffview/diffview.go | 79 ++++++++++++++++++++++++++--
1 file changed, 72 insertions(+), 7 deletions(-)

Detailed changes

internal/tui/exp/diffview/diffview.go 🔗

@@ -1,6 +1,7 @@
 package diffview
 
 import (
+	"crypto/sha256"
 	"fmt"
 	"image/color"
 	"strconv"
@@ -59,6 +60,13 @@ type DiffView struct {
 	extraColOnAfter bool // add extra column on after panel
 	beforeNumDigits int
 	afterNumDigits  int
+
+	// Cache lexer to avoid expensive file pattern matching on every line
+	cachedLexer chroma.Lexer
+
+	// Cache highlighted lines to avoid re-highlighting the same content
+	// Key: hash of (content + background color), Value: highlighted string
+	syntaxCache map[string]string
 }
 
 // New creates a new DiffView with default settings.
@@ -68,6 +76,7 @@ func New() *DiffView {
 		contextLines: udiff.DefaultContextLines,
 		lineNumbers:  true,
 		tabWidth:     8,
+		syntaxCache:  make(map[string]string),
 	}
 	dv.style = DefaultDarkStyle()
 	return dv
@@ -88,15 +97,26 @@ func (dv *DiffView) Split() *DiffView {
 // Before sets the "before" file for the DiffView.
 func (dv *DiffView) Before(path, content string) *DiffView {
 	dv.before = file{path: path, content: content}
+	// Clear caches when content changes
+	dv.clearCaches()
 	return dv
 }
 
 // After sets the "after" file for the DiffView.
 func (dv *DiffView) After(path, content string) *DiffView {
 	dv.after = file{path: path, content: content}
+	// Clear caches when content changes
+	dv.clearCaches()
 	return dv
 }
 
+// clearCaches clears all caches when content or major settings change.
+func (dv *DiffView) clearCaches() {
+	dv.cachedLexer = nil
+	dv.clearSyntaxCache()
+	dv.isComputed = false
+}
+
 // ContextLines sets the number of context lines for the DiffView.
 func (dv *DiffView) ContextLines(contextLines int) *DiffView {
 	dv.contextLines = contextLines
@@ -156,9 +176,21 @@ func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
 // If nil, no syntax highlighting will be applied.
 func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
 	dv.chromaStyle = style
+	// Clear syntax cache when style changes since highlighting will be different
+	dv.clearSyntaxCache()
 	return dv
 }
 
+// clearSyntaxCache clears the syntax highlighting cache.
+func (dv *DiffView) clearSyntaxCache() {
+	if dv.syntaxCache != nil {
+		// Clear the map but keep it allocated
+		for k := range dv.syntaxCache {
+			delete(dv.syntaxCache, k)
+		}
+	}
+}
+
 // String returns the string representation of the DiffView.
 func (dv *DiffView) String() string {
 	dv.replaceTabs()
@@ -700,7 +732,15 @@ func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
 		return source
 	}
 
-	l := dv.getChromaLexer(source)
+	// Create cache key from content and background color
+	cacheKey := dv.createSyntaxCacheKey(source, bgColor)
+
+	// Check if we already have this highlighted
+	if cached, exists := dv.syntaxCache[cacheKey]; exists {
+		return cached
+	}
+
+	l := dv.getChromaLexer()
 	f := dv.getChromaFormatter(bgColor)
 
 	it, err := l.Tokenise(nil, source)
@@ -712,22 +752,47 @@ func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
 	if err := f.Format(&b, dv.chromaStyle, it); err != nil {
 		return source
 	}
-	return b.String()
+
+	result := b.String()
+
+	// Cache the result for future use
+	dv.syntaxCache[cacheKey] = result
+
+	return result
 }
 
-func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
+// createSyntaxCacheKey creates a cache key from source content and background color.
+// We use a simple hash to keep memory usage reasonable.
+func (dv *DiffView) createSyntaxCacheKey(source string, bgColor color.Color) string {
+	// Convert color to string representation
+	r, g, b, a := bgColor.RGBA()
+	colorStr := fmt.Sprintf("%d,%d,%d,%d", r, g, b, a)
+
+	// Create a hash of the content + color to use as cache key
+	h := sha256.New()
+	h.Write([]byte(source))
+	h.Write([]byte(colorStr))
+	return fmt.Sprintf("%x", h.Sum(nil))
+}
+
+func (dv *DiffView) getChromaLexer() chroma.Lexer {
+	if dv.cachedLexer != nil {
+		return dv.cachedLexer
+	}
+
 	l := lexers.Match(dv.before.path)
 	if l == nil {
-		l = lexers.Analyse(source)
+		l = lexers.Analyse(dv.before.content)
 	}
 	if l == nil {
 		l = lexers.Fallback
 	}
-	return chroma.Coalesce(l)
+	dv.cachedLexer = chroma.Coalesce(l)
+	return dv.cachedLexer
 }
 
-func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
+func (dv *DiffView) getChromaFormatter(bgColor color.Color) chroma.Formatter {
 	return chromaFormatter{
-		bgColor: gbColor,
+		bgColor: bgColor,
 	}
 }