1- import { DiffsHighlighter , getSharedHighlighter , SupportedLanguages } from "@pierre/diffs" ;
21import { CheckIcon , CopyIcon } from "lucide-react" ;
32import React , {
43 Children ,
@@ -16,60 +15,29 @@ import React, {
1615import type { Components } from "react-markdown" ;
1716import ReactMarkdown from "react-markdown" ;
1817import remarkGfm from "remark-gfm" ;
18+ import { CodeHighlightErrorBoundary } from "./CodeHighlightErrorBoundary" ;
1919import { useAppSettings } from "../appSettings" ;
2020import { openFileReference } from "../fileOpen" ;
2121import { useFileViewNavigation } from "~/hooks/useFileViewNavigation" ;
2222import { resolveDiffThemeName , type DiffThemeName } from "../lib/diffRendering" ;
23- import { fnv1a32 } from "../lib/diffRendering" ;
24- import { LRUCache } from "../lib/lruCache" ;
23+ import {
24+ extractFenceLanguage ,
25+ getCachedHighlightedHtml ,
26+ getHighlighterPromise ,
27+ renderHighlightedCodeHtml ,
28+ setCachedHighlightedHtml ,
29+ } from "../lib/syntaxHighlighting" ;
2530import { useTheme } from "../hooks/useTheme" ;
2631import { resolveMarkdownFileLinkTarget } from "../markdown-links" ;
2732import { readNativeApi } from "../nativeApi" ;
2833import { toastManager } from "./ui/toast" ;
2934
30- class CodeHighlightErrorBoundary extends React . Component <
31- { fallback : ReactNode ; children : ReactNode } ,
32- { hasError : boolean }
33- > {
34- constructor ( props : { fallback : ReactNode ; children : ReactNode } ) {
35- super ( props ) ;
36- this . state = { hasError : false } ;
37- }
38-
39- static getDerivedStateFromError ( ) {
40- return { hasError : true } ;
41- }
42-
43- override render ( ) {
44- if ( this . state . hasError ) {
45- return this . props . fallback ;
46- }
47- return this . props . children ;
48- }
49- }
50-
5135interface ChatMarkdownProps {
5236 text : string ;
5337 cwd : string | undefined ;
5438 isStreaming ?: boolean ;
5539}
5640
57- const CODE_FENCE_LANGUAGE_REGEX = / (?: ^ | \s ) l a n g u a g e - ( [ ^ \s ] + ) / ;
58- const MAX_HIGHLIGHT_CACHE_ENTRIES = 500 ;
59- const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024 ;
60- const highlightedCodeCache = new LRUCache < string > (
61- MAX_HIGHLIGHT_CACHE_ENTRIES ,
62- MAX_HIGHLIGHT_CACHE_MEMORY_BYTES ,
63- ) ;
64- const highlighterPromiseCache = new Map < string , Promise < DiffsHighlighter > > ( ) ;
65-
66- function extractFenceLanguage ( className : string | undefined ) : string {
67- const match = className ?. match ( CODE_FENCE_LANGUAGE_REGEX ) ;
68- const raw = match ?. [ 1 ] ?? "text" ;
69- // Shiki doesn't bundle a gitignore grammar; ini is a close match (#685)
70- return raw === "gitignore" ? "ini" : raw ;
71- }
72-
7341function nodeToPlainText ( node : ReactNode ) : string {
7442 if ( typeof node === "string" || typeof node === "number" ) {
7543 return String ( node ) ;
@@ -105,35 +73,6 @@ function extractCodeBlock(
10573 } ;
10674}
10775
108- function createHighlightCacheKey ( code : string , language : string , themeName : DiffThemeName ) : string {
109- return `${ fnv1a32 ( code ) . toString ( 36 ) } :${ code . length } :${ language } :${ themeName } ` ;
110- }
111-
112- function estimateHighlightedSize ( html : string , code : string ) : number {
113- return Math . max ( html . length * 2 , code . length * 3 ) ;
114- }
115-
116- function getHighlighterPromise ( language : string ) : Promise < DiffsHighlighter > {
117- const cached = highlighterPromiseCache . get ( language ) ;
118- if ( cached ) return cached ;
119-
120- const promise = getSharedHighlighter ( {
121- themes : [ resolveDiffThemeName ( "dark" ) , resolveDiffThemeName ( "light" ) ] ,
122- langs : [ language as SupportedLanguages ] ,
123- preferredHighlighter : "shiki-js" ,
124- } ) . catch ( ( err ) => {
125- highlighterPromiseCache . delete ( language ) ;
126- if ( language === "text" ) {
127- // "text" itself failed — Shiki cannot initialize at all, surface the error
128- throw err ;
129- }
130- // Language not supported by Shiki — fall back to "text"
131- return getHighlighterPromise ( "text" ) ;
132- } ) ;
133- highlighterPromiseCache . set ( language , promise ) ;
134- return promise ;
135- }
136-
13776function MarkdownCodeBlock ( { code, children } : { code : string ; children : ReactNode } ) {
13877 const [ copied , setCopied ] = useState ( false ) ;
13978 const copiedTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
@@ -196,8 +135,9 @@ function SuspenseShikiCodeBlock({
196135 isStreaming,
197136} : SuspenseShikiCodeBlockProps ) {
198137 const language = extractFenceLanguage ( className ) ;
199- const cacheKey = createHighlightCacheKey ( code , language , themeName ) ;
200- const cachedHighlightedHtml = ! isStreaming ? highlightedCodeCache . get ( cacheKey ) : null ;
138+ const cachedHighlightedHtml = ! isStreaming
139+ ? getCachedHighlightedHtml ( code , language , themeName , "chat-markdown" )
140+ : null ;
201141
202142 if ( cachedHighlightedHtml != null ) {
203143 return (
@@ -210,28 +150,14 @@ function SuspenseShikiCodeBlock({
210150
211151 const highlighter = use ( getHighlighterPromise ( language ) ) ;
212152 const highlightedHtml = useMemo ( ( ) => {
213- try {
214- return highlighter . codeToHtml ( code , { lang : language , theme : themeName } ) ;
215- } catch ( error ) {
216- // Log highlighting failures for debugging while falling back to plain text
217- console . warn (
218- `Code highlighting failed for language "${ language } ", falling back to plain text.` ,
219- error instanceof Error ? error . message : error ,
220- ) ;
221- // If highlighting fails for this language, render as plain text
222- return highlighter . codeToHtml ( code , { lang : "text" , theme : themeName } ) ;
223- }
153+ return renderHighlightedCodeHtml ( highlighter , code , language , themeName ) ;
224154 } , [ code , highlighter , language , themeName ] ) ;
225155
226156 useEffect ( ( ) => {
227157 if ( ! isStreaming ) {
228- highlightedCodeCache . set (
229- cacheKey ,
230- highlightedHtml ,
231- estimateHighlightedSize ( highlightedHtml , code ) ,
232- ) ;
158+ setCachedHighlightedHtml ( code , language , themeName , "chat-markdown" , highlightedHtml ) ;
233159 }
234- } , [ cacheKey , code , highlightedHtml , isStreaming ] ) ;
160+ } , [ code , highlightedHtml , isStreaming , language , themeName ] ) ;
235161
236162 return (
237163 < div className = "chat-markdown-shiki" dangerouslySetInnerHTML = { { __html : highlightedHtml } } />
0 commit comments