From b9a387406a4e04fb60d9c8f76c8bce6e0e796380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 6 Apr 2026 22:17:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20rewrite=20CodeViewer=20with=20prism-rea?= =?UTF-8?q?ct-renderer=20=E2=80=94=20pure=20React=20rendering,=20GitHub=20?= =?UTF-8?q?theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src/components/chat/CodeViewer.tsx | 116 +++++++++------------ 1 file changed, 48 insertions(+), 68 deletions(-) diff --git a/desktop/src/components/chat/CodeViewer.tsx b/desktop/src/components/chat/CodeViewer.tsx index 4af18121..57abd7ed 100644 --- a/desktop/src/components/chat/CodeViewer.tsx +++ b/desktop/src/components/chat/CodeViewer.tsx @@ -1,5 +1,5 @@ -import { useMemo, useState } from 'react' -import hljs from 'highlight.js' +import { useState } from 'react' +import { Highlight, themes } from 'prism-react-renderer' import { CopyButton } from '../shared/CopyButton' type Props = { @@ -12,37 +12,14 @@ type Props = { export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = true }: Props) { const [expanded, setExpanded] = useState(false) - const lines = code.split('\n') - const isTruncated = !expanded && lines.length > maxLines - const visibleCode = isTruncated ? lines.slice(0, maxLines).join('\n') : code - - // Only highlight when language is explicitly known. - // Do NOT use highlightAuto — it misdetects plain text as code. - const hasLanguage = !!language && !!hljs.getLanguage(language) - const effectiveShowLineNumbers = showLineNumbers && hasLanguage - - const highlightedLines = useMemo(() => { - return visibleCode.split('\n').map((line) => { - try { - if (hasLanguage) { - return hljs.highlight(line || ' ', { language: language!, ignoreIllegals: true }).value - } - return escapeHtml(line || ' ') - } catch { - return escapeHtml(line) - } - }) - }, [hasLanguage, language, visibleCode]) - - const startLine = 1 + const allLines = code.split('\n') + const isTruncated = !expanded && allLines.length > maxLines + const visibleCode = isTruncated ? allLines.slice(0, maxLines).join('\n') : code + const effectiveShowLineNumbers = showLineNumbers && !!language && language !== 'text' const languageLabel = language || 'code' - - const visibleLines = visibleCode.split('\n') - - const lineCountLabel = `${lines.length} ${lines.length === 1 ? 'line' : 'lines'}` - - const showExpandToggle = lines.length > maxLines + const lineCountLabel = `${allLines.length} ${allLines.length === 1 ? 'line' : 'lines'}` + const showExpandToggle = allLines.length > maxLines return (
@@ -58,34 +35,45 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = tr
-
- {visibleLines.map((line, index) => ( - effectiveShowLineNumbers ? ( -
- - {startLine + index} - - -
- ) : ( -
- -
- ) - ))} -
+ + {({ tokens, getLineProps, getTokenProps }) => ( +
+              {tokens.map((line, i) => {
+                const lineProps = getLineProps({ line })
+                return effectiveShowLineNumbers ? (
+                  
+ + {i + 1} + + + {line.map((token, key) => ( + + ))} + +
+ ) : ( +
+ + {line.map((token, key) => ( + + ))} + +
+ ) + })} +
+ )} +
{showExpandToggle && ( @@ -93,17 +81,9 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = tr onClick={() => setExpanded((value) => !value)} className="w-full border-t border-[#d0d7de] bg-[#f6f8fa] py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-[#57606a] transition-colors hover:bg-[#eaeef2] hover:text-[#24292f]" > - {expanded ? 'Collapse' : `Show ${lines.length - maxLines} more lines`} + {expanded ? 'Collapse' : `Show ${allLines.length - maxLines} more lines`} )} ) } - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -}