1import { toHtml } from "hast-util-to-html";
2import { getSharedHighlighter } from "@pierre/diffs";
3import { describe, expect, it } from "vite-plus/test";
4
5import { createIncrementalHighlightedDocument } from "./incrementalHighlighting";
6
7const samples = {
8 typescript: "/* multi\nline comment */\nconst x = `template\n${1 + 2}`;\nconst re = /abc/;\n",
9 python: '#!/usr/bin/python\nx = """multi\nline"""\nprint(x)\n',
10 bash: "#!/bin/bash\ncat <<EOF\nhello\nEOF\necho hi\n",
11 html: "<script>\nconst x = 1;\n</script>\n<style>\np {color:red;}\n</style>\n",
12 markdown: "# heading\n\n```ts\nconst a = 1;\n```\n\ntext\n",
13 rust: 'fn main() {\n let x = r#"multi\nline"#;\n}\n',
14 tsx: 'const element = <div\n className="x">\n{value}\n</div>;\n',
15 json: '{\n "value": [1,\n 2, 3]\n}\n',
16 yaml: "key: |\n multiline\n value\nnext: true\n",
17 css: '/* comment\n continued */\np::before {\n content: "text";\n}\n',
18 sql: "SELECT 'multi\nline'\nFROM table_name;\n",
19} as const;
20
21const highlighterPromise = getSharedHighlighter({
22 langs: Object.keys(samples) as Array<keyof typeof samples>,
23 themes: ["pierre-dark", "pierre-light"],
24 preferredHighlighter: "shiki-wasm",
25});
26
27describe("incremental code highlighting", () => {
28 it.each(Object.entries(samples))(
29 "matches full HTML at every streaming prefix in %s",
30 async (language, code) => {
31 const highlighter = await highlighterPromise;
32 for (const theme of ["pierre-dark", "pierre-light"] as const) {
33 const highlight = createIncrementalHighlightedDocument(highlighter, language, theme);
34 for (let end = 0; end <= code.length; end++) {
35 const text = code.slice(0, end);
36 expect(toHtml(highlight(text)), `${theme}, prefix ${end}`).toBe(
37 highlighter.codeToHtml(text, { lang: language, theme }),
38 );
39 }
40 }
41 },
42 );
43
44 it("resets after edits and truncation, including edits to a completed line", async () => {
45 const highlighter = await highlighterPromise;
46 const highlight = createIncrementalHighlightedDocument(
47 highlighter,
48 "typescript",
49 "pierre-dark",
50 );
51 const inputs = [
52 "/* open\ncomment\n",
53 "/* open\ncomment\n*/\nconst x = 1;",
54 "const edited = 2;\nconst x = 1;",
55 "const edited = 2;\nconst x = 10;",
56 "const edited = 2;\n",
57 "",
58 "\n\n\nconst fresh = true;\n",
59 ];
60 for (const text of inputs) {
61 expect(toHtml(highlight(text))).toBe(
62 highlighter.codeToHtml(text, { lang: "typescript", theme: "pierre-dark" }),
63 );
64 }
65 });
66
67 it.each(["text", "plaintext", "plain", "txt", "ansi"])(
68 "preserves %s without requesting grammar state",
69 async (language) => {
70 const highlighter = await highlighterPromise;
71 const highlight = createIncrementalHighlightedDocument(highlighter, language, "pierre-dark");
72 for (const text of ["plain\ntext", "\u001b[31mred\ncontinued", "\n"]) {
73 expect(toHtml(highlight(text))).toBe(
74 highlighter.codeToHtml(text, { lang: language, theme: "pierre-dark" }),
75 );
76 }
77 },
78 );
79
80 it("preserves partial CRLF and CR line endings", async () => {
81 const highlighter = await highlighterPromise;
82 const highlight = createIncrementalHighlightedDocument(
83 highlighter,
84 "typescript",
85 "pierre-dark",
86 );
87 const code = "/* multi\r\nline */\r\nconst x = 1;\r\n";
88 for (let end = 0; end <= code.length; end++) {
89 const text = code.slice(0, end);
90 expect(toHtml(highlight(text))).toBe(
91 highlighter.codeToHtml(text, { lang: "typescript", theme: "pierre-dark" }),
92 );
93 }
94 });
95});