perf(code): replace react-syntax-highlighter and prismjs with the Shiki worker

Route all non-markdown code highlighting through the off-main-thread Shiki
worker, removing react-syntax-highlighter and prismjs entirely.

- Extend the worker with highlightLines: tokenize a whole block once and return
  per-line inner HTML, so per-line layouts (diffs, gutters, virtualization) make
  one worker call instead of one highlighter per line.
- Add shared WorkerHighlightedCode (whole-block) and useWorkerHighlightedLines
  (per-line) primitives. Colors resolve via the --md-syntax-* CSS variables, so
  theme changes never re-highlight.
- Migrate all 12 react-syntax-highlighter call sites: PermissionCard,
  ToolPart, ContextSidebarTab, ToolOutputDialog (whole block) and
  DiffPreview/WritePreview (per line).
- Migrate VirtualizedCodeBlock off prismjs to the worker, keeping virtua
  virtualization; whole-block tokenization also restores cross-line syntax
  context that per-line highlighting lost.
- Drop react-syntax-highlighter (+types) from ui and web, prismjs (+types) from
  ui, and the orphaned create-element type shim.
This commit is contained in:
Bohdan Triapitsyn
2026-06-16 01:07:43 +03:00
parent 464c4ac0ca
commit e41e5bac91
15 changed files with 398 additions and 406 deletions
@@ -7,9 +7,11 @@ import type { MarkdownWorkerRequest, MarkdownWorkerResponse } from './markdown-w
// tokenization error) the promise resolves to `null` and the caller keeps the
// escaped plain-text code — highlighting never falls back onto the main thread.
type PendingResolver = (response: MarkdownWorkerResponse | null) => void;
let worker: Worker | undefined;
let nextId = 0;
const pending = new Map<number, (html: string | null) => void>();
const pending = new Map<number, PendingResolver>();
const failAll = (): void => {
pending.forEach((resolve) => resolve(null));
@@ -30,7 +32,7 @@ const getWorker = (): Worker | undefined => {
const resolve = pending.get(event.data.id);
if (!resolve) return;
pending.delete(event.data.id);
resolve(event.data.type === 'highlight' ? event.data.html : null);
resolve(event.data);
};
worker.onerror = failAll;
worker.onmessageerror = failAll;
@@ -38,16 +40,31 @@ const getWorker = (): Worker | undefined => {
return worker;
};
const request = (payload: (id: number) => MarkdownWorkerRequest): Promise<MarkdownWorkerResponse | null> => {
const instance = getWorker();
if (!instance) return Promise.resolve(null);
const id = ++nextId;
return new Promise<MarkdownWorkerResponse | null>((resolve) => {
pending.set(id, resolve);
instance.postMessage(payload(id));
});
};
/**
* Highlight a complete code block in the worker. Resolves to Shiki `<pre>` HTML,
* or `null` if highlighting is unavailable or failed (caller keeps plain code).
*/
export const highlightCodeInWorker = (code: string, lang: string): Promise<string | null> => {
const instance = getWorker();
if (!instance) return Promise.resolve(null);
const id = ++nextId;
return new Promise<string | null>((resolve) => {
pending.set(id, resolve);
instance.postMessage({ type: 'highlight', id, code, lang } satisfies MarkdownWorkerRequest);
});
export const highlightCodeInWorker = async (code: string, lang: string): Promise<string | null> => {
const response = await request((id) => ({ type: 'highlight', id, code, lang }));
return response?.type === 'highlight' ? response.html : null;
};
/**
* Highlight a whole block and return per-line inner HTML (one entry per source
* line). For per-line layouts (diffs, gutters, virtualization) — one worker
* round-trip instead of one per line. Resolves to `null` on failure.
*/
export const highlightLinesInWorker = async (code: string, lang: string): Promise<string[] | null> => {
const response = await request((id) => ({ type: 'highlightLines', id, code, lang }));
return response?.type === 'highlightLines' ? response.lines : null;
};