diff --git a/web/src/highlight/highlighter.test.ts b/web/src/highlight/highlighter.test.ts index f181304..f754c48 100644 --- a/web/src/highlight/highlighter.test.ts +++ b/web/src/highlight/highlighter.test.ts @@ -58,3 +58,20 @@ it("colours a window inside a multiline comment like the complete syntax stream" expect(tokenize.range!(lines, 100, 110)).toEqual(complete.slice(100, 110)); expect(tokenize.range!(lines, 178, 184)).toEqual(complete.slice(178, 184)); }); + +it("colours an anonymous Go struct literal without locking up the page", async () => { + // Shiki's JavaScript regex engine backtracked forever on this shape, and a + // regex running on the main thread cannot be interrupted: the review froze + // the moment the window holding it scrolled near. + await load("go"); + const tokenize = tokenizerFor("go")!; + const lines = [ + "func handler(w http.ResponseWriter) {", + "\trequire.NoError(t, json.NewEncoder(w).Encode(struct {", + '\t\tItems []contracts.Fact `json:"items"`', + '\t}{Items: []contracts.Fact{{Candidate: contracts.Candidate{Reference: reference, ChatID: "conversation", BusinessObjectID: "appointment", SourceFlow: "reminder"}, Eligible: true, Revision: "rev"}}}))', + "}", + ]; + + expect(tokenize(lines.join("\n"))).toHaveLength(5); +}, 2_000); diff --git a/web/src/highlight/highlighter.ts b/web/src/highlight/highlighter.ts index da9b451..e4e9703 100644 --- a/web/src/highlight/highlighter.ts +++ b/web/src/highlight/highlighter.ts @@ -3,18 +3,20 @@ import { type GrammarState, type HighlighterCore, } from "shiki/core"; -import { createJavaScriptRegexEngine } from "shiki/engine/javascript"; +import { createOnigurumaEngine } from "shiki/engine/oniguruma"; import { bundledLanguages } from "shiki/langs"; import { dark, light } from "./theme"; import type { Token, Tokenize } from "./tokens"; /** Shiki, loaded a grammar at a time. * - * The regex engine in JavaScript rather than the WebAssembly one: it costs a - * few exotic grammars and saves half a megabyte that every reader would carry - * to read one language. Every grammar Shiki ships is reachable, and each is its - * own file — the page starts with none of them and fetches the one the file in - * front of you needs. */ + * The WebAssembly regex engine, not the JavaScript one. The JavaScript engine + * saved half a megabyte, but its translation of the Go grammar backtracked + * forever on an anonymous struct literal — and a regex on the main thread + * cannot be interrupted, so the whole review froze. The WASM is fetched once, + * with the first grammar. Every grammar Shiki ships is reachable, and each is + * its own file — the page starts with none of them and fetches the one the + * file in front of you needs. */ let core: Promise | null = null; let built: HighlighterCore | null = null; @@ -31,7 +33,7 @@ function highlighter(): Promise { core ??= createHighlighterCore({ langs: [], themes: [light, dark], - engine: createJavaScriptRegexEngine(), + engine: createOnigurumaEngine(import("shiki/wasm")), }).then((made) => (built = made)); return core; }