import { CodeToTokenTransformStream, type RecallToken } from '@shikijs/stream'; import { ShikiStreamRenderer } from '@shikijs/stream/react'; import type { ThemedToken } from 'shiki/core'; import { useEffect, useRef, useState } from 'react'; import { codeThemeName, getCodeHighlighter, normalizeCodeLanguage, type CodeTheme, } from '../../lib/code-highlighter'; interface StreamingCodeBlockProps { code: string; language: string; theme: CodeTheme; complete: boolean; } export function StreamingCodeBlock({ code, language, theme, complete, }: StreamingCodeBlockProps) { const normalizedLanguage = normalizeCodeLanguage(language); const [revision, setRevision] = useState(0); const [tokenStream, setTokenStream] = useState | null>(null); const [highlightFailed, setHighlightFailed] = useState(false); const writerRef = useRef | null>(null); const writtenRef = useRef(''); const closedRef = useRef(false); const codeRef = useRef(code); const completeRef = useRef(complete); const queueRef = useRef>(Promise.resolve()); codeRef.current = code; completeRef.current = complete; const queueWrite = ( writer: WritableStreamDefaultWriter, chunk: string, closeAfter: boolean, ) => { queueRef.current = queueRef.current .then(async () => { if (chunk) await writer.write(chunk); if (closeAfter && !closedRef.current) { closedRef.current = true; await writer.close(); } }) .catch(() => { if (writerRef.current === writer) { writerRef.current = null; setHighlightFailed(true); } }); }; useEffect(() => { let disposed = false; let writer: WritableStreamDefaultWriter | null = null; setTokenStream(null); setHighlightFailed(false); writtenRef.current = ''; closedRef.current = false; queueRef.current = Promise.resolve(); void getCodeHighlighter(normalizedLanguage) .then((highlighter) => { if (disposed) return; const transform = new CodeToTokenTransformStream({ highlighter, lang: normalizedLanguage, theme: codeThemeName(theme), allowRecalls: true, }); writer = transform.writable.getWriter(); writerRef.current = writer; const currentCode = codeRef.current; writtenRef.current = currentCode; setTokenStream(transform.readable); queueWrite(writer, currentCode, completeRef.current); }) .catch(() => { if (!disposed) setHighlightFailed(true); }); return () => { disposed = true; if (writer && writerRef.current === writer) writerRef.current = null; if (writer && !closedRef.current) { closedRef.current = true; void writer.close().catch(() => undefined); } }; }, [normalizedLanguage, revision, theme]); useEffect(() => { const writer = writerRef.current; if (!writer || closedRef.current) return; const written = writtenRef.current; if (!code.startsWith(written)) { writerRef.current = null; closedRef.current = true; void writer.close().catch(() => undefined); setRevision((current) => current + 1); return; } const delta = code.slice(written.length); writtenRef.current = code; if (delta || complete) queueWrite(writer, delta, complete); }, [code, complete]); const label = language.trim() || 'plain text'; const useFallback = !tokenStream || highlightFailed; return (
{label} {!complete && streaming}
{useFallback ?
{code}
: }
); }