@@ -173,10 +173,14 @@ func ResetStyles() {
173173 globalStylesOnce = sync.Once {}
174174 globalStylesMu .Unlock ()
175175
176- // Also clear chroma syntax highlighting cache
176+ // Also clear chroma syntax highlighting caches
177177 chromaStyleCacheMu .Lock ()
178178 chromaStyleCache = make (map [chroma.TokenType ]ansiStyle )
179179 chromaStyleCacheMu .Unlock ()
180+
181+ syntaxHighlightCacheMu .Lock ()
182+ syntaxHighlightCache .clear ()
183+ syntaxHighlightCacheMu .Unlock ()
180184}
181185
182186func getGlobalStyles () * cachedStyles {
@@ -2158,41 +2162,56 @@ type token struct {
21582162 style ansiStyle
21592163}
21602164
2165+ // syntaxCacheKey builds a cache key for syntax highlighting results.
2166+ type syntaxCacheKey struct {
2167+ lang string
2168+ code string
2169+ }
2170+
21612171var (
21622172 lexerCache = make (map [string ]chroma.Lexer )
21632173 lexerCacheMu sync.RWMutex
21642174
21652175 // Cache for chroma token type to ansiStyle conversion (with code bg)
21662176 chromaStyleCache = make (map [chroma.TokenType ]ansiStyle )
21672177 chromaStyleCacheMu sync.RWMutex
2178+
2179+ // Cache for syntax highlighting results to avoid re-tokenizing unchanged code blocks.
2180+ // Uses an LRU cache bounded to 128 entries to prevent unbounded memory growth
2181+ // in long-running TUI sessions with many unique code blocks.
2182+ syntaxHighlightCache = newLRUCache [syntaxCacheKey , []token ](syntaxHighlightCacheSize )
2183+ syntaxHighlightCacheMu sync.RWMutex
2184+ )
2185+
2186+ const (
2187+ // syntaxHighlightCacheSize is the maximum number of syntax-highlighted code blocks
2188+ // to keep in cache. This bounds memory usage while retaining recently viewed blocks.
2189+ syntaxHighlightCacheSize = 128
21682190)
21692191
21702192func (p * parser ) syntaxHighlight (code , lang string ) []token {
2171- var lexer chroma.Lexer
2172-
2173- if lang != "" {
2174- // Try cache first
2175- lexerCacheMu .RLock ()
2176- lexer = lexerCache [lang ]
2177- lexerCacheMu .RUnlock ()
2178-
2179- if lexer == nil {
2180- lexer = lexers .Get (lang )
2181- if lexer == nil {
2182- // Try with file extension
2183- lexer = lexers .Match ("file." + lang )
2184- }
2185- if lexer != nil {
2186- lexer = chroma .Coalesce (lexer )
2187- lexerCacheMu .Lock ()
2188- lexerCache [lang ] = lexer
2189- lexerCacheMu .Unlock ()
2190- }
2191- }
2193+ cacheKey := syntaxCacheKey {lang : lang , code : code }
2194+
2195+ syntaxHighlightCacheMu .RLock ()
2196+ if cached , ok := syntaxHighlightCache .get (cacheKey ); ok {
2197+ syntaxHighlightCacheMu .RUnlock ()
2198+ return cached
21922199 }
2200+ syntaxHighlightCacheMu .RUnlock ()
2201+
2202+ tokens := p .doSyntaxHighlight (code , lang )
2203+
2204+ syntaxHighlightCacheMu .Lock ()
2205+ syntaxHighlightCache .put (cacheKey , tokens )
2206+ syntaxHighlightCacheMu .Unlock ()
2207+
2208+ return tokens
2209+ }
21932210
2211+ // doSyntaxHighlight performs the actual syntax highlighting without caching.
2212+ func (p * parser ) doSyntaxHighlight (code , lang string ) []token {
2213+ lexer := p .getLexer (lang )
21942214 if lexer == nil {
2195- // No highlighting - return plain text with code background
21962215 return []token {{text : code , style : p .getCodeStyle (chroma .None )}}
21972216 }
21982217
@@ -2212,10 +2231,37 @@ func (p *parser) syntaxHighlight(code, lang string) []token {
22122231 style : p .getCodeStyle (tok .Type ),
22132232 })
22142233 }
2215-
22162234 return tokens
22172235}
22182236
2237+ // getLexer returns a cached chroma lexer for the given language, or nil if unknown.
2238+ func (p * parser ) getLexer (lang string ) chroma.Lexer {
2239+ if lang == "" {
2240+ return nil
2241+ }
2242+
2243+ lexerCacheMu .RLock ()
2244+ lexer := lexerCache [lang ]
2245+ lexerCacheMu .RUnlock ()
2246+ if lexer != nil {
2247+ return lexer
2248+ }
2249+
2250+ lexer = lexers .Get (lang )
2251+ if lexer == nil {
2252+ lexer = lexers .Match ("file." + lang )
2253+ }
2254+ if lexer == nil {
2255+ return nil
2256+ }
2257+
2258+ lexer = chroma .Coalesce (lexer )
2259+ lexerCacheMu .Lock ()
2260+ lexerCache [lang ] = lexer
2261+ lexerCacheMu .Unlock ()
2262+ return lexer
2263+ }
2264+
22192265func (p * parser ) getCodeStyle (tokenType chroma.TokenType ) ansiStyle {
22202266 chromaStyleCacheMu .RLock ()
22212267 style , ok := chromaStyleCache [tokenType ]
0 commit comments