1import { createHighlighterCore } from "@shikijs/core";
2import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript";
3import typescript from "@shikijs/langs/typescript";
4import dark from "@shikijs/themes/github-dark-default";
5import { describe, expect, it } from "vite-plus/test";
6
7import { createIncrementalSnippet } from "./incrementalSnippet";
8import { highlightCodeSnippet } from "./shikiReviewHighlighter";
9
10const highlighter = await createHighlighterCore({
11 langs: [typescript],
12 themes: [dark],
13 engine: createJavaScriptRegexEngine(),
14});
15const rendered = (lines: ReturnType<typeof highlighter.codeToTokensBase>) =>
16 lines.map((line) => line.map(({ content, color, fontStyle }) => ({ content, color, fontStyle })));
17const options = { lang: "typescript", theme: "github-dark-default" };
18
19describe("incremental snippet highlighting", () => {
20 it("matches full highlighting through partial lines, multiline syntax, edits and truncation", async () => {
21 const highlight = createIncrementalSnippet(highlighter, options.lang, options.theme);
22 const source = "/* comment\nstill comment */\nconst text = `first\nsecond ${42}`;\n";
23 for (let end = 1; end <= source.length; end++) {
24 const code = source.slice(0, end);
25 expect(rendered(highlight.read(code) ?? (await highlight(code)))).toEqual(
26 rendered(highlighter.codeToTokensBase(code, options)),
27 );
28 }
29 for (const code of ['const other = "new";\n', "const other", source, source.slice(0, 20)]) {
30 expect(rendered(highlight.read(code) ?? (await highlight(code)))).toEqual(
31 rendered(highlighter.codeToTokensBase(code, options)),
32 );
33 }
34 });
35
36 it("carries grammar state across batches and overlapping updates", async () => {
37 const highlight = createIncrementalSnippet(highlighter, options.lang, options.theme);
38 const code = "/*\n" + "still a comment\n".repeat(410) + "*/\nconst value = 42;";
39 const first = highlight(code);
40 const next = highlight("const changed = true;\n");
41 expect(rendered(await first)).toEqual(rendered(highlighter.codeToTokensBase(code, options)));
42 expect(rendered(await next)).toEqual(
43 rendered(highlighter.codeToTokensBase("const changed = true;\n", options)),
44 );
45 const last = 'const changed = true;\nconst tail = "ok";';
46 expect(rendered(await highlight(last))).toEqual(
47 rendered(highlighter.codeToTokensBase(last, options)),
48 );
49 });
50
51 it("reuses completed token rows on warm reads and falls back for large updates", async () => {
52 const session = {};
53 const input = { language: "ts", theme: "dark" as const, session };
54 const code = "const a = 1;\nconst b = 2;";
55 expect(highlightCodeSnippet.read({ ...input, code })).toBeUndefined();
56 const first = await highlightCodeSnippet({ ...input, code });
57 const nextCode = code + "\nconst c = 3;";
58 const next = highlightCodeSnippet.read({ ...input, code: nextCode })!;
59 expect(next[0]).toBe(first[0]);
60 expect(next).toEqual(
61 await highlightCodeSnippet({ code: nextCode, language: "ts", theme: "dark" }),
62 );
63 expect(
64 highlightCodeSnippet.read({ ...input, code: nextCode + "\n" + "const x = 1;\n".repeat(210) }),
65 ).toBeUndefined();
66 expect(highlightCodeSnippet.read({ ...input, code: nextCode, theme: "light" })).toBeUndefined();
67 });
68
69 it("resets session on language, theme, CRLF, long-line and empty input changes", async () => {
70 const session = {};
71 const cases = [
72 { code: "/* hello\nworld */", language: "ts", theme: "dark" as const },
73 { code: "/* hello\nworld */\nconst n = 3;", language: "ts", theme: "light" as const },
74 { code: "hello\nworld", language: "text", theme: "light" as const },
75 { code: "const n = 2;\r\nconst m = 3;", language: "ts", theme: "dark" as const },
76 { code: "a".repeat(1100), language: "ts", theme: "dark" as const },
77 { code: "", language: "ts", theme: "dark" as const },
78 { code: "const fresh = true;\n", language: "ts", theme: "dark" as const },
79 ];
80 for (const input of cases)
81 expect(await highlightCodeSnippet({ ...input, session })).toEqual(
82 await highlightCodeSnippet(input),
83 );
84 });
85});