| 1 | import { |
| 2 | getSharedHighlighter, |
| 3 | type DiffsHighlighter, |
| 4 | type HighlighterTypes, |
| 5 | type SupportedLanguages, |
| 6 | } from "@pierre/diffs"; |
| 7 | |
| 8 | import { 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 | */ |
| 16 | export const PREFERRED_HIGHLIGHTER: HighlighterTypes = "shiki-wasm"; |
| 17 | |
| 18 | const highlighterPromiseCache = new Map<string, Promise<DiffsHighlighter>>(); |
| 19 | |
| 20 | export 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 | } |