diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 53de0fdb8dd2..7a7e274999bf 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -37,6 +37,7 @@ import { resolveDiffThemeName, resolveFileDiffPath, } from "../lib/diffRendering"; +import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting"; import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useProject, useThread } from "../state/entities"; @@ -960,6 +961,7 @@ export default function DiffPanel({ lineDiffType: "none", overflow: wordWrap ? "wrap" : "scroll", theme: resolveDiffThemeName(resolvedTheme), + preferredHighlighter: PREFERRED_HIGHLIGHTER, themeType: resolvedTheme as DiffThemeType, stickyHeaders: true, ...(loadDiffFiles ? { loadDiffFiles } : {}), diff --git a/apps/web/src/components/DiffWorkerPoolProvider.tsx b/apps/web/src/components/DiffWorkerPoolProvider.tsx index 3ec748c6bcb2..bcf90b118410 100644 --- a/apps/web/src/components/DiffWorkerPoolProvider.tsx +++ b/apps/web/src/components/DiffWorkerPoolProvider.tsx @@ -4,6 +4,7 @@ import * as Schema from "effect/Schema"; import { useEffect, useMemo, type ReactNode } from "react"; import { useTheme } from "../hooks/useTheme"; import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering"; +import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting"; export class DiffWorkerError extends Schema.TaggedErrorClass()("DiffWorkerError", { operation: Schema.Literals(["create-worker", "get-render-options", "set-render-options"]), @@ -73,6 +74,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { }} highlighterOptions={{ theme: diffThemeName, + preferredHighlighter: PREFERRED_HIGHLIGHTER, tokenizeMaxLineLength: 1_000, useTokenTransformer: true, }} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index af920c0d6156..de509a43363a 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -43,6 +43,7 @@ import { resolveDiffThemeName, resolveFileDiffPath, } from "../../lib/diffRendering"; +import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting"; import ChatMarkdown from "../ChatMarkdown"; import { BotIcon, @@ -2078,6 +2079,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte collapsed: false, diffStyle: "unified", theme: resolveDiffThemeName(ctx.resolvedTheme), + preferredHighlighter: PREFERRED_HIGHLIGHTER, }} /> ))} diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index a8c364763c28..f98179027315 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -25,6 +25,7 @@ import { useClientSettings } from "~/hooks/useSettings"; import { useTheme } from "~/hooks/useTheme"; import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "~/hooks/useLocalStorage"; import { DIFF_SURFACE_THEME_UNSAFE_CSS, resolveDiffThemeName } from "~/lib/diffRendering"; +import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting"; import { cn } from "~/lib/utils"; import { isPreviewSupportedInRuntime } from "~/previewStateStore"; import { resolvePathLinkTarget } from "~/terminal-links"; @@ -670,6 +671,7 @@ function EditableFileSurface({ onLineSelectionEnd: handleLineSelectionEnd, overflow: wordWrap ? "wrap" : "scroll", theme: resolveDiffThemeName(resolvedTheme), + preferredHighlighter: PREFERRED_HIGHLIGHTER, themeType: resolvedTheme, unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS, onPostRender: handlePostRender, @@ -1039,6 +1041,7 @@ export default function FilePreviewPanel({ disableFileHeader: true, overflow: wordWrap ? "wrap" : "scroll", theme: resolveDiffThemeName(resolvedTheme), + preferredHighlighter: PREFERRED_HIGHLIGHTER, themeType: resolvedTheme, unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS, onPostRender: onFilePostRender, diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx index b0e00d57cc61..d9f1e7af716d 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx @@ -42,6 +42,7 @@ import { resolveFileDiffPreviousPath, type RenderablePatch, } from "~/lib/diffRendering"; +import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting"; import { cn } from "~/lib/utils"; import { createPullRequestDiffFileContentsLoader } from "~/lib/diffFileContents"; import { @@ -743,6 +744,7 @@ export function PullRequestCodeTab({ lineDiffType: "none" as const, overflow: wordWrap ? ("wrap" as const) : ("scroll" as const), theme: resolveDiffThemeName(resolvedTheme), + preferredHighlighter: PREFERRED_HIGHLIGHTER, themeType: resolvedTheme, stickyHeaders: true, loadDiffFiles, diff --git a/apps/web/src/components/settings/SettingsFontPreviews.tsx b/apps/web/src/components/settings/SettingsFontPreviews.tsx index 57d714d2042d..856d85fb098a 100644 --- a/apps/web/src/components/settings/SettingsFontPreviews.tsx +++ b/apps/web/src/components/settings/SettingsFontPreviews.tsx @@ -5,6 +5,7 @@ import { terminalThemeFromApp } from "../ThreadTerminalDrawer"; import { useTheme } from "../../hooks/useTheme"; import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder"; import { resolveDiffThemeName, type DiffThemeName } from "../../lib/diffRendering"; +import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting"; import { GhosttyTerminalSurface } from "~/terminal/ghostty/surface"; // The font previews are the real surfaces, not lookalikes: the composer's @@ -79,7 +80,7 @@ function loadDiffPreviewHtml(theme: DiffThemeName): Promise { if (promise === undefined) { promise = preloadPatchFile({ patch: DIFF_PREVIEW_PATCH, - options: { diffStyle: "unified", theme }, + options: { diffStyle: "unified", theme, preferredHighlighter: PREFERRED_HIGHLIGHTER }, }).then((results) => results.map((result) => result.prerenderedHTML)); diffPreviewHtmlByTheme.set(theme, promise); } diff --git a/apps/web/src/lib/syntaxHighlighting.ts b/apps/web/src/lib/syntaxHighlighting.ts index 171725617e80..3be008ac2b79 100644 --- a/apps/web/src/lib/syntaxHighlighting.ts +++ b/apps/web/src/lib/syntaxHighlighting.ts @@ -1,11 +1,20 @@ import { getSharedHighlighter, type DiffsHighlighter, + type HighlighterTypes, type SupportedLanguages, } from "@pierre/diffs"; import { resolveDiffThemeName } from "./diffRendering"; +/** + * Always highlight with the Oniguruma WASM engine — the JS regex engine can + * backtrack catastrophically and hang the tokenizing thread. The shared + * highlighter is a first-caller-wins singleton, so every creation site must + * pass this value. + */ +export const PREFERRED_HIGHLIGHTER: HighlighterTypes = "shiki-wasm"; + const highlighterPromiseCache = new Map>(); export function getSyntaxHighlighterPromise(language: string): Promise { @@ -15,7 +24,7 @@ export function getSyntaxHighlighterPromise(language: string): Promise { if (language === "text") { highlighterPromiseCache.delete(language);