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 {...@@ -37,6 +37,7 @@ import {
37 resolveDiffThemeName,37 resolveDiffThemeName,
38 resolveFileDiffPath,38 resolveFileDiffPath,
39} from "../lib/diffRendering";39} from "../lib/diffRendering";
40import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting";
40import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse";41import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse";
41import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";42import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
42import { useWorkspaceMutationRefresh } from "../hooks/useWorkspaceMutationRefresh";43import { useWorkspaceMutationRefresh } from "../hooks/useWorkspaceMutationRefresh";
...@@ -948,6 +949,7 @@ export default function DiffPanel({...@@ -948,6 +949,7 @@ export default function DiffPanel({
948 lineDiffType: "none",949 lineDiffType: "none",
949 overflow: wordWrap ? "wrap" : "scroll",950 overflow: wordWrap ? "wrap" : "scroll",
950 theme: resolveDiffThemeName(resolvedTheme),951 theme: resolveDiffThemeName(resolvedTheme),
952 preferredHighlighter: PREFERRED_HIGHLIGHTER,
951 themeType: resolvedTheme as DiffThemeType,953 themeType: resolvedTheme as DiffThemeType,
952 stickyHeaders: true,954 stickyHeaders: true,
953 ...(loadDiffFiles ? { loadDiffFiles } : {}),955 ...(loadDiffFiles ? { loadDiffFiles } : {}),
apps/web/src/components/DiffWorkerPoolProvider.tsx+2
...@@ -4,6 +4,7 @@ import * as Schema from "effect/Schema";...@@ -4,6 +4,7 @@ import * as Schema from "effect/Schema";
4import { useEffect, useMemo, type ReactNode } from "react";4import { useEffect, useMemo, type ReactNode } from "react";
5import { useTheme } from "../hooks/useTheme";5import { useTheme } from "../hooks/useTheme";
6import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";6import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";
7import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting";
78
8export class DiffWorkerError extends Schema.TaggedErrorClass<DiffWorkerError>()("DiffWorkerError", {9export class DiffWorkerError extends Schema.TaggedErrorClass<DiffWorkerError>()("DiffWorkerError", {
9 operation: Schema.Literals(["create-worker", "get-render-options", "set-render-options"]),10 operation: Schema.Literals(["create-worker", "get-render-options", "set-render-options"]),
...@@ -73,6 +74,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {...@@ -73,6 +74,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
73 }}74 }}
74 highlighterOptions={{75 highlighterOptions={{
75 theme: diffThemeName,76 theme: diffThemeName,
77 preferredHighlighter: PREFERRED_HIGHLIGHTER,
76 tokenizeMaxLineLength: 1_000,78 tokenizeMaxLineLength: 1_000,
77 useTokenTransformer: true,79 useTokenTransformer: true,
78 }}80 }}
apps/web/src/components/chat/MessagesTimeline.tsx+2
...@@ -59,6 +59,7 @@ import {...@@ -59,6 +59,7 @@ import {
59 resolveDiffThemeName,59 resolveDiffThemeName,
60 resolveFileDiffPath,60 resolveFileDiffPath,
61} from "../../lib/diffRendering";61} from "../../lib/diffRendering";
62import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
62import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown";63import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown";
63import {64import {
64 BotIcon,65 BotIcon,
...@@ -2060,6 +2061,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte...@@ -2060,6 +2061,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte
2060 collapsed: false,2061 collapsed: false,
2061 diffStyle: "unified",2062 diffStyle: "unified",
2062 theme: resolveDiffThemeName(ctx.resolvedTheme),2063 theme: resolveDiffThemeName(ctx.resolvedTheme),
2064 preferredHighlighter: PREFERRED_HIGHLIGHTER,
2063 }}2065 }}
2064 />2066 />
2065 ))}2067 ))}
apps/web/src/components/files/FilePreviewPanel.tsx+3
...@@ -31,6 +31,7 @@ import { useTheme } from "~/hooks/useTheme";...@@ -31,6 +31,7 @@ import { useTheme } from "~/hooks/useTheme";
31import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "~/hooks/useLocalStorage";31import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "~/hooks/useLocalStorage";
32import { useWorkspaceMutationRefresh } from "~/hooks/useWorkspaceMutationRefresh";32import { useWorkspaceMutationRefresh } from "~/hooks/useWorkspaceMutationRefresh";
33import { DIFF_SURFACE_THEME_UNSAFE_CSS, resolveDiffThemeName } from "~/lib/diffRendering";33import { DIFF_SURFACE_THEME_UNSAFE_CSS, resolveDiffThemeName } from "~/lib/diffRendering";
34import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting";
34import { cn } from "~/lib/utils";35import { cn } from "~/lib/utils";
35import { isPreviewSupportedInRuntime } from "~/previewStateStore";36import { isPreviewSupportedInRuntime } from "~/previewStateStore";
36import { resolvePathLinkTarget } from "~/terminal-links";37import { resolvePathLinkTarget } from "~/terminal-links";
...@@ -755,6 +756,7 @@ function EditableFileSurface({...@@ -755,6 +756,7 @@ function EditableFileSurface({
755 onLineSelectionEnd: handleLineSelectionEnd,756 onLineSelectionEnd: handleLineSelectionEnd,
756 overflow: wordWrap ? "wrap" : "scroll",757 overflow: wordWrap ? "wrap" : "scroll",
757 theme: resolveDiffThemeName(resolvedTheme),758 theme: resolveDiffThemeName(resolvedTheme),
759 preferredHighlighter: PREFERRED_HIGHLIGHTER,
758 themeType: resolvedTheme,760 themeType: resolvedTheme,
759 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,761 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
760 onPostRender: handlePostRender,762 onPostRender: handlePostRender,
...@@ -1149,6 +1151,7 @@ export default function FilePreviewPanel({...@@ -1149,6 +1151,7 @@ export default function FilePreviewPanel({
1149 disableFileHeader: true,1151 disableFileHeader: true,
1150 overflow: wordWrap ? "wrap" : "scroll",1152 overflow: wordWrap ? "wrap" : "scroll",
1151 theme: resolveDiffThemeName(resolvedTheme),1153 theme: resolveDiffThemeName(resolvedTheme),
1154 preferredHighlighter: PREFERRED_HIGHLIGHTER,
1152 themeType: resolvedTheme,1155 themeType: resolvedTheme,
1153 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,1156 unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS,
1154 onPostRender: onFilePostRender,1157 onPostRender: onFilePostRender,
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+2
...@@ -42,6 +42,7 @@ import {...@@ -42,6 +42,7 @@ import {
42 resolveFileDiffPreviousPath,42 resolveFileDiffPreviousPath,
43 type RenderablePatch,43 type RenderablePatch,
44} from "~/lib/diffRendering";44} from "~/lib/diffRendering";
45import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting";
45import { cn } from "~/lib/utils";46import { cn } from "~/lib/utils";
46import { createPullRequestDiffFileContentsLoader } from "~/lib/diffFileContents";47import { createPullRequestDiffFileContentsLoader } from "~/lib/diffFileContents";
47import {48import {
...@@ -743,6 +744,7 @@ export function PullRequestCodeTab({...@@ -743,6 +744,7 @@ export function PullRequestCodeTab({
743 lineDiffType: "none" as const,744 lineDiffType: "none" as const,
744 overflow: wordWrap ? ("wrap" as const) : ("scroll" as const),745 overflow: wordWrap ? ("wrap" as const) : ("scroll" as const),
745 theme: resolveDiffThemeName(resolvedTheme),746 theme: resolveDiffThemeName(resolvedTheme),
747 preferredHighlighter: PREFERRED_HIGHLIGHTER,
746 themeType: resolvedTheme,748 themeType: resolvedTheme,
747 stickyHeaders: true,749 stickyHeaders: true,
748 loadDiffFiles,750 loadDiffFiles,
apps/web/src/components/settings/SettingsFontPreviews.tsx+2-1
...@@ -5,6 +5,7 @@ import { terminalThemeFromApp } from "../ThreadTerminalDrawer";...@@ -5,6 +5,7 @@ import { terminalThemeFromApp } from "../ThreadTerminalDrawer";
5import { useTheme } from "../../hooks/useTheme";5import { useTheme } from "../../hooks/useTheme";
6import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder";6import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder";
7import { resolveDiffThemeName, type DiffThemeName } from "../../lib/diffRendering";7import { resolveDiffThemeName, type DiffThemeName } from "../../lib/diffRendering";
8import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
8import { GhosttyTerminalSurface } from "~/terminal/ghostty/surface";9import { GhosttyTerminalSurface } from "~/terminal/ghostty/surface";
910
10// The font previews are the real surfaces, not lookalikes: the composer's11// The font previews are the real surfaces, not lookalikes: the composer's
...@@ -79,7 +80,7 @@ function loadDiffPreviewHtml(theme: DiffThemeName): Promise<readonly string[]> {...@@ -79,7 +80,7 @@ function loadDiffPreviewHtml(theme: DiffThemeName): Promise<readonly string[]> {
79 if (promise === undefined) {80 if (promise === undefined) {
80 promise = preloadPatchFile({81 promise = preloadPatchFile({
81 patch: DIFF_PREVIEW_PATCH,82 patch: DIFF_PREVIEW_PATCH,
82 options: { diffStyle: "unified", theme },83 options: { diffStyle: "unified", theme, preferredHighlighter: PREFERRED_HIGHLIGHTER },
83 }).then((results) => results.map((result) => result.prerenderedHTML));84 }).then((results) => results.map((result) => result.prerenderedHTML));
84 diffPreviewHtmlByTheme.set(theme, promise);85 diffPreviewHtmlByTheme.set(theme, promise);
85 }86 }
apps/web/src/lib/syntaxHighlighting.ts+10-1
...@@ -1,11 +1,20 @@...@@ -1,11 +1,20 @@
1import {1import {
2 getSharedHighlighter,2 getSharedHighlighter,
3 type DiffsHighlighter,3 type DiffsHighlighter,
4 type HighlighterTypes,
4 type SupportedLanguages,5 type SupportedLanguages,
5} from "@pierre/diffs";6} from "@pierre/diffs";
67
7import { resolveDiffThemeName } from "./diffRendering";8import { 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
9const highlighterPromiseCache = new Map<string, Promise<DiffsHighlighter>>();18const highlighterPromiseCache = new Map<string, Promise<DiffsHighlighter>>();
1019
11export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHighlighter> {20export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHighlighter> {
...@@ -15,7 +24,7 @@ export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHigh...@@ -15,7 +24,7 @@ export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHigh
15 const promise = getSharedHighlighter({24 const promise = getSharedHighlighter({
16 themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")],25 themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")],
17 langs: [language as SupportedLanguages],26 langs: [language as SupportedLanguages],
18 preferredHighlighter: "shiki-js",27 preferredHighlighter: PREFERRED_HIGHLIGHTER,
19 }).catch((error) => {28 }).catch((error) => {
20 if (language === "text") {29 if (language === "text") {
21 highlighterPromiseCache.delete(language);30 highlighterPromiseCache.delete(language);