1import {
2 getSharedHighlighter,
3 type DiffsHighlighter,
4 type HighlighterTypes,
5 type SupportedLanguages,
6} from "@pierre/diffs";
7
8import { resolveDiffThemeName } from "./diffRendering";
9
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
18const highlighterPromiseCache = new Map<string, Promise<DiffsHighlighter>>();
19
20export function getSyntaxHighlighterPromise(language: string): Promise<DiffsHighlighter> {
21 const cached = highlighterPromiseCache.get(language);
22 if (cached) return cached;
23
24 const promise = getSharedHighlighter({
25 themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")],
26 langs: [language as SupportedLanguages],
27 preferredHighlighter: PREFERRED_HIGHLIGHTER,
28 })
29 .then(async (highlighter) => {
30 for (const name of [resolveDiffThemeName("dark"), resolveDiffThemeName("light")]) {
31 const theme = highlighter.getTheme(name);
32 const neutralColors = Object.fromEntries(
33 theme.settings.flatMap(({ scope, settings }) => {
34 const scopes = new Set([scope].flat());
35 if (!settings.foreground) return [];
36 if (scopes.has("comment")) {
37 return [
38 [
39 settings.foreground,
40 "color-mix(in srgb, var(--code-foreground) 80%, var(--code-background))",
41 ],
42 ];
43 }
44 if (scopes.has("punctuation")) {
45 return [[settings.foreground, "var(--code-foreground)"]];
46 }
47 return [];
48 }),
49 );
50 if (
51 Object.entries(neutralColors).every(
52 ([color, replacement]) => theme.colorReplacements?.[color] === replacement,
53 )
54 )
55 continue;
56 await highlighter.loadTheme({
57 ...theme,
58 colorReplacements: { ...theme.colorReplacements, ...neutralColors },
59 });
60 }
61 return highlighter;
62 })
63 .catch((error) => {
64 if (language === "text") {
65 highlighterPromiseCache.delete(language);
66 // "text" itself failed — Shiki cannot initialize at all, surface the error
67 throw error;
68 }
69 // Language not supported by Shiki — fall back to "text"
70 return getSyntaxHighlighterPromise("text");
71 });
72 highlighterPromiseCache.set(language, promise);
73 return promise;
74}