| 1 | import { createHighlighterCore } from "@shikijs/core"; |
| 2 | import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript"; |
| 3 | import typescript from "@shikijs/langs/typescript"; |
| 4 | import dark from "@shikijs/themes/github-dark-default"; |
| 5 | import { describe, expect, it } from "vite-plus/test"; |
| 6 | |
| 7 | import { createIncrementalSnippet } from "./incrementalSnippet"; |
| 8 | import { highlightCodeSnippet } from "./shikiReviewHighlighter"; |
| 9 | |
| 10 | const highlighter = await createHighlighterCore({ |
| 11 | langs: [typescript], |
| 12 | themes: [dark], |
| 13 | engine: createJavaScriptRegexEngine(), |
| 14 | }); |
| 15 | const rendered = (lines: ReturnType<typeof highlighter.codeToTokensBase>) => |
| 16 | lines.map((line) => line.map(({ content, color, fontStyle }) => ({ content, color, fontStyle }))); |
| 17 | const options = { lang: "typescript", theme: "github-dark-default" }; |
| 18 | |
| 19 | describe("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 | }); |