| 1 | import { toHtml } from "hast-util-to-html"; |
| 2 | import { getSharedHighlighter } from "@pierre/diffs"; |
| 3 | import { describe, expect, it } from "vite-plus/test"; |
| 4 | |
| 5 | import { createIncrementalHighlightedDocument } from "./incrementalHighlighting"; |
| 6 | |
| 7 | const 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 | |
| 21 | const highlighterPromise = getSharedHighlighter({ |
| 22 | langs: Object.keys(samples) as Array<keyof typeof samples>, |
| 23 | themes: ["pierre-dark", "pierre-light"], |
| 24 | preferredHighlighter: "shiki-wasm", |
| 25 | }); |
| 26 | |
| 27 | describe("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 | }); |