Unverified 提交 1ce39958 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Use specific languages for shiki (#1938)

<!-- CURSOR_SUMMARY --> > [!NOTE] > Refactors code highlighting to use react-shiki/core with a singleton highlighter and preloaded langs/themes, adds a fallback renderer, bumps react-shiki, and adds tests for message summarization. > > - **Frontend** > - **CodeHighlight** (`src/components/chat/CodeHighlight.tsx`): Replace `useShikiHighlighter` with `ShikiHighlighter` from `react-shiki/core` using a singleton `createHighlighterCore` and JS regex engine; preload common languages and GitHub light/dark themes; add `<pre><code>` fallback while loading. > - **Tests** > - Add/expand Vitest suite for `formatMessagesForSummary` (`src/__tests__/formatMessagesForSummary.test.ts`): covers truncation, ordering, special chars, undefined content, and edge cases. > - **Dependencies** > - Upgrade `react-shiki` to `^0.9.0` in `package.json`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b32d224cd21d3c76e77799f2995905e523406bf9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Preloaded a specific set of Shiki languages and themes using react-shiki core, and updated CodeHighlight to use a singleton highlighter. This reduces bundle size and stabilizes code rendering with a simple fallback while loading. - **Refactors** - Switched to react-shiki/core with ShikiHighlighter and a singleton highlighter. - Preloaded common languages (js/ts/jsx/tsx/html/css/json/markdown/python/etc.) and GitHub light/dark themes. - Used the JavaScript regex engine and added a plain <pre><code> fallback until the highlighter is ready. - **Dependencies** - Upgraded react-shiki to ^0.9.0. <sup>Written for commit b32d224cd21d3c76e77799f2995905e523406bf9. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
上级 8d88460f
...@@ -765,3 +765,4 @@ ...@@ -765,3 +765,4 @@
"indexes": {} "indexes": {}
} }
} }
...@@ -2,3 +2,4 @@ Here is a simple response to test the context limit banner functionality. ...@@ -2,3 +2,4 @@ Here is a simple response to test the context limit banner functionality.
This message simulates being close to the model's context window limit. This message simulates being close to the model's context window limit.
差异被折叠。
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"react-shiki": "^0.5.2", "react-shiki": "^0.9.0",
"recast": "^0.23.11", "recast": "^0.23.11",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"shell-env": "^4.0.1", "shell-env": "^4.0.1",
......
import { formatMessagesForSummary } from "../ipc/handlers/chat_stream_handlers"; import { formatMessagesForSummary } from "../ipc/handlers/chat_stream_handlers";
import { describe, it, expect } from "vitest";
describe("formatMessagesForSummary", () => { describe("formatMessagesForSummary", () => {
it("should return all messages when there are 8 or fewer messages", () => { it("should return all messages when there are 8 or fewer messages", () => {
......
import React, { import React, { useState, useEffect, memo, type ReactNode } from "react";
useState, import ShikiHighlighter, {
useEffect, isInlineCode,
useRef, createHighlighterCore,
memo, createJavaScriptRegexEngine,
type ReactNode, } from "react-shiki/core";
} from "react";
import { isInlineCode, useShikiHighlighter } from "react-shiki";
import github from "@shikijs/themes/github-light-default";
import githubDark from "@shikijs/themes/github-dark-default";
import type { Element as HastElement } from "hast"; import type { Element as HastElement } from "hast";
import { useTheme } from "../../contexts/ThemeContext"; import { useTheme } from "../../contexts/ThemeContext";
import { Copy, Check } from "lucide-react"; import { Copy, Check } from "lucide-react";
import github from "@shikijs/themes/github-light-default";
import githubDark from "@shikijs/themes/github-dark-default";
// common languages
import astro from "@shikijs/langs/astro";
import css from "@shikijs/langs/css";
import graphql from "@shikijs/langs/graphql";
import html from "@shikijs/langs/html";
import java from "@shikijs/langs/java";
import javascript from "@shikijs/langs/javascript";
import json from "@shikijs/langs/json";
import jsx from "@shikijs/langs/jsx";
import less from "@shikijs/langs/less";
import markdown from "@shikijs/langs/markdown";
import python from "@shikijs/langs/python";
import sass from "@shikijs/langs/sass";
import scss from "@shikijs/langs/scss";
import shell from "@shikijs/langs/shell";
import sql from "@shikijs/langs/sql";
import tsx from "@shikijs/langs/tsx";
import typescript from "@shikijs/langs/typescript";
import vue from "@shikijs/langs/vue";
type HighlighterCore = Awaited<ReturnType<typeof createHighlighterCore>>;
// Create a singleton highlighter instance
let highlighterPromise: Promise<HighlighterCore> | null = null;
function getHighlighter(): Promise<HighlighterCore> {
if (!highlighterPromise) {
highlighterPromise = createHighlighterCore({
themes: [github, githubDark],
langs: [
astro,
css,
graphql,
html,
java,
javascript,
json,
jsx,
less,
markdown,
python,
sass,
scss,
shell,
sql,
tsx,
typescript,
vue,
],
engine: createJavaScriptRegexEngine(),
});
}
return highlighterPromise as Promise<HighlighterCore>;
}
function useHighlighter() {
const [highlighter, setHighlighter] = useState<HighlighterCore>();
useEffect(() => {
getHighlighter().then(setHighlighter);
}, []);
return highlighter;
}
interface CodeHighlightProps { interface CodeHighlightProps {
className?: string | undefined; className?: string | undefined;
...@@ -32,29 +94,8 @@ export const CodeHighlight = memo( ...@@ -32,29 +94,8 @@ export const CodeHighlight = memo(
}; };
const { isDarkMode } = useTheme(); const { isDarkMode } = useTheme();
const highlighter = useHighlighter();
// Cache for the highlighted code
const highlightedCodeCache = useRef<ReactNode | null>(null);
// Only update the highlighted code if the inputs change
const highlightedCode = useShikiHighlighter(
code,
language,
isDarkMode ? githubDark : github,
{
delay: 150,
},
);
// Update the cache whenever we get a new highlighted code
useEffect(() => {
if (highlightedCode) {
highlightedCodeCache.current = highlightedCode;
}
}, [highlightedCode]);
// Use the cached version during transitions to prevent flickering
const displayedCode = highlightedCode || highlightedCodeCache.current;
return !isInline ? ( return !isInline ? (
<div <div
className="shiki not-prose relative [&_pre]:overflow-auto className="shiki not-prose relative [&_pre]:overflow-auto
...@@ -77,7 +118,20 @@ export const CodeHighlight = memo( ...@@ -77,7 +118,20 @@ export const CodeHighlight = memo(
)} )}
</div> </div>
) : null} ) : null}
{displayedCode} {highlighter ? (
<ShikiHighlighter
highlighter={highlighter}
language={language}
theme={isDarkMode ? "github-dark-default" : "github-light-default"}
delay={150}
>
{code}
</ShikiHighlighter>
) : (
<pre>
<code>{code}</code>
</pre>
)}
</div> </div>
) : ( ) : (
<code className={className} {...props}> <code className={className} {...props}>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论