Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 } : {}),
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/DiffWorkerPoolProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>()("DiffWorkerError", {
operation: Schema.Literals(["create-worker", "get-render-options", "set-render-options"]),
Expand Down Expand Up @@ -73,6 +74,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
}}
highlighterOptions={{
theme: diffThemeName,
preferredHighlighter: PREFERRED_HIGHLIGHTER,
tokenizeMaxLineLength: 1_000,
useTokenTransformer: true,
}}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
resolveDiffThemeName,
resolveFileDiffPath,
} from "../../lib/diffRendering";
import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
import ChatMarkdown from "../ChatMarkdown";
import {
BotIcon,
Expand Down Expand Up @@ -2078,6 +2079,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte
collapsed: false,
diffStyle: "unified",
theme: resolveDiffThemeName(ctx.resolvedTheme),
preferredHighlighter: PREFERRED_HIGHLIGHTER,
}}
/>
))}
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/settings/SettingsFontPreviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,7 +80,7 @@ function loadDiffPreviewHtml(theme: DiffThemeName): Promise<readonly string[]> {
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);
}
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/lib/syntaxHighlighting.ts
Original file line number Diff line number Diff line change
@@ -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";
Comment on lines +10 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invariant documented here ("first-caller-wins singleton, so every creation site must pass this value") is not applied everywhere: apps/web/src/components/settings/SettingsFontPreviews.tsx:80-83 still calls preloadPatchFile({ patch: DIFF_PREVIEW_PATCH, options: { diffStyle: "unified", theme } }), and its own comment above states that pipeline "always awaits the shared highlighter". Appearance settings can easily be opened before any diff/file surface mounts, in which case that call wins the race and pins the JS regex engine for the whole session — exactly the hang this PR fixes.

Suggested smallest fix: add preferredHighlighter: PREFERRED_HIGHLIGHTER to the options passed to preloadPatchFile. If the SSR options type does not accept it, please note in this comment that the SSR path builds its own highlighter rather than the shared singleton, so the invariant stated here is accurate.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 5e5651cpreloadPatchFile reaches DiffHunksRenderer.initializeHighlighter(), which does create the shared singleton with the shiki-js default, so this was a real missed site. preferredHighlighter: PREFERRED_HIGHLIGHTER is now passed there too (the SSR options type accepts it). Typecheck and the 2,859 apps/web unit tests pass.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.


const highlighterPromiseCache = new Map<string, Promise<DiffsHighlighter>>();

export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHighlighter> {
Expand All @@ -15,7 +24,7 @@ export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHigh
const promise = getSharedHighlighter({
themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")],
langs: [language as SupportedLanguages],
preferredHighlighter: "shiki-js",
preferredHighlighter: PREFERRED_HIGHLIGHTER,
}).catch((error) => {
if (language === "text") {
highlighterPromiseCache.delete(language);
Expand Down
Loading