authorgravatar for 57975070+LetZico@users.noreply.github.comZico <57975070+LetZico@users.noreply.github.com> 2026-09-01 18:06:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-01 18:06:04-07:00
logfeb3ea7ebf83de2e301adb1f20300b262773ec54
treef5d921eb3b7560792e9fe7722d7458eb3e884766
parentea71a19d4181ce13b0e8f374f45ba9237ee4a9ce
signature Signed by PGP key B5690EEEBB952194

fix(web): stop highlighter freezes and worker spin by using the Oniguruma WASM engine (#8360)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

7 files changed, 23 insertions(+), 2 deletions(-)

apps/web/src/components/DiffPanel.tsx+2
......@@ -37,6 +37,7 @@ import {
3737 resolveDiffThemeName,
3838 resolveFileDiffPath,
3939} from "../lib/diffRendering";
40import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting";
4041import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse";
4142import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
4243import { useWorkspaceMutationRefresh } from "../hooks/useWorkspaceMutationRefresh";
......@@ -948,6 +949,7 @@ export default function DiffPanel({
948949 lineDiffType: "none",
949950 overflow: wordWrap ? "wrap" : "scroll",
950951 theme: resolveDiffThemeName(resolvedTheme),
952 preferredHighlighter: PREFERRED_HIGHLIGHTER,
951953 themeType: resolvedTheme as DiffThemeType,
952954 stickyHeaders: true,
953955 ...(loadDiffFiles ? { loadDiffFiles } : {}),
apps/web/src/components/DiffWorkerPoolProvider.tsx+2
......@@ -4,6 +4,7 @@ import * as Schema from "effect/Schema";
44import { useEffect, useMemo, type ReactNode } from "react";
55import { useTheme } from "../hooks/useTheme";
66import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";
7import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting";
78
89export class DiffWorkerError extends Schema.TaggedErrorClass<DiffWorkerError>()("DiffWorkerError", {
910 operation: Schema.Literals(["create-worker", "get-render-options", "set-render-options"]),
......@@ -73,6 +74,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
7374 }}
7475 highlighterOptions={{
7576 theme: diffThemeName,
77 preferredHighlighter: PREFERRED_HIGHLIGHTER,
7678 tokenizeMaxLineLength: 1_000,
7779 useTokenTransformer: true,
7880 }}
apps/web/src/components/chat/MessagesTimeline.tsx+2
......@@ -59,6 +59,7 @@ import {
5959 resolveDiffThemeName,
6060 resolveFileDiffPath,
6161} from "../../lib/diffRendering";
62import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
6263import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown";
6364import {
6465 BotIcon,
......@@ -2060,6 +2061,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte
20602061 collapsed: false,
20612062 diffStyle: "unified",
20622063 theme: resolveDiffThemeName(ctx.resolvedTheme),
2064 preferredHighlighter: PREFERRED_HIGHLIGHTER,
20632065 }}
20642066 />
20652067 ))}
apps/web/src/components/files/FilePreviewPanel.tsx+3
......@@ -31,6 +31,7 @@ import { useTheme } from "~/hooks/useTheme";
3131import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "~/hooks/useLocalStorage";
3232import { useWorkspaceMutationRefresh } from "~/hooks/useWorkspaceMutationRefresh";
3333import { DIFF_SURFACE_THEME_UNSAFE_CSS, resolveDiffThemeName } from "~/lib/diffRendering";
34import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting";
3435import { cn } from "~/lib/utils";
3536import { isPreviewSupportedInRuntime } from "~/previewStateStore";
3637import { resolvePathLinkTarget } from "~/terminal-links";
......@@ -755,6 +756,7 @@ function EditableFileSurface({
755756 onLineSelectionEnd: handleLineSelectionEnd,
756757 overflow: wordWrap ? "wrap" : "scroll",
757758 theme: resolveDiffThemeName(resolvedTheme),
759 preferredHighlighter: PREFERRED_HIGHLIGHTER,
758760 themeType: resolvedTheme,
759761 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
760762 onPostRender: handlePostRender,
......@@ -1149,6 +1151,7 @@ export default function FilePreviewPanel({
11491151 disableFileHeader: true,
11501152 overflow: wordWrap ? "wrap" : "scroll",
11511153 theme: resolveDiffThemeName(resolvedTheme),
1154 preferredHighlighter: PREFERRED_HIGHLIGHTER,
11521155 themeType: resolvedTheme,
11531156 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
11541157 onPostRender: onFilePostRender,
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+2
......@@ -42,6 +42,7 @@ import {
4242 resolveFileDiffPreviousPath,
4343 type RenderablePatch,
4444} from "~/lib/diffRendering";
45import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting";
4546import { cn } from "~/lib/utils";
4647import { createPullRequestDiffFileContentsLoader } from "~/lib/diffFileContents";
4748import {
......@@ -743,6 +744,7 @@ export function PullRequestCodeTab({
743744 lineDiffType: "none" as const,
744745 overflow: wordWrap ? ("wrap" as const) : ("scroll" as const),
745746 theme: resolveDiffThemeName(resolvedTheme),
747 preferredHighlighter: PREFERRED_HIGHLIGHTER,
746748 themeType: resolvedTheme,
747749 stickyHeaders: true,
748750 loadDiffFiles,
apps/web/src/components/settings/SettingsFontPreviews.tsx+2-1
......@@ -5,6 +5,7 @@ import { terminalThemeFromApp } from "../ThreadTerminalDrawer";
55import { useTheme } from "../../hooks/useTheme";
66import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder";
77import { resolveDiffThemeName, type DiffThemeName } from "../../lib/diffRendering";
8import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
89import { GhosttyTerminalSurface } from "~/terminal/ghostty/surface";
910
1011// The font previews are the real surfaces, not lookalikes: the composer's
......@@ -79,7 +80,7 @@ function loadDiffPreviewHtml(theme: DiffThemeName): Promise<readonly string[]> {
7980 if (promise === undefined) {
8081 promise = preloadPatchFile({
8182 patch: DIFF_PREVIEW_PATCH,
82 options: { diffStyle: "unified", theme },
83 options: { diffStyle: "unified", theme, preferredHighlighter: PREFERRED_HIGHLIGHTER },
8384 }).then((results) => results.map((result) => result.prerenderedHTML));
8485 diffPreviewHtmlByTheme.set(theme, promise);
8586 }
apps/web/src/lib/syntaxHighlighting.ts+10-1
......@@ -1,11 +1,20 @@
11import {
22 getSharedHighlighter,
33 type DiffsHighlighter,
4 type HighlighterTypes,
45 type SupportedLanguages,
56} from "@pierre/diffs";
67
78import { resolveDiffThemeName } from "./diffRendering";
89
10/**
11 * Always highlight with the Oniguruma WASM engine — the JS regex engine can
12 * backtrack catastrophically and hang the tokenizing thread. The shared
13 * highlighter is a first-caller-wins singleton, so every creation site must
14 * pass this value.
15 */
16export const PREFERRED_HIGHLIGHTER: HighlighterTypes = "shiki-wasm";
17
918const highlighterPromiseCache = new Map<string, Promise<DiffsHighlighter>>();
1019
1120export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHighlighter> {
......@@ -15,7 +24,7 @@ export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHigh
1524 const promise = getSharedHighlighter({
1625 themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")],
1726 langs: [language as SupportedLanguages],
18 preferredHighlighter: "shiki-js",
27 preferredHighlighter: PREFERRED_HIGHLIGHTER,
1928 }).catch((error) => {
2029 if (language === "text") {
2130 highlighterPromiseCache.delete(language);