import { describe, test, expect, beforeAll } from "bun:test";
import { detectLang, hasBinaryContent, highlightStartup } from "../src/services/highlight.ts";
import { parseDiff, highlightFile } from "../src/services/diffHighlight.ts";
beforeAll(async () => {
await highlightStartup();
});
describe("detectLang", () => {
// Extensions
test("detects TypeScript", () => expect(detectLang("foo.ts")).toBe("typescript"));
test("detects TSX", () => expect(detectLang("foo.tsx")).toBe("tsx"));
test("detects JavaScript", () => expect(detectLang("foo.js")).toBe("javascript"));
test("detects Python", () => expect(detectLang("foo.py")).toBe("python"));
test("detects Rust", () => expect(detectLang("foo.rs")).toBe("rust"));
test("detects Go", () => expect(detectLang("foo.go")).toBe("go"));
test("detects C", () => expect(detectLang("foo.c")).toBe("c"));
test("detects C++", () => expect(detectLang("foo.cpp")).toBe("cpp"));
test("detects C header", () => expect(detectLang("foo.h")).toBe("c"));
test("detects YAML", () => expect(detectLang("foo.yml")).toBe("yaml"));
test("detects TOML", () => expect(detectLang("foo.toml")).toBe("toml"));
test("detects JSON", () => expect(detectLang("foo.json")).toBe("json"));
test("detects Markdown", () => expect(detectLang("foo.md")).toBe("markdown"));
test("detects Shell", () => expect(detectLang("foo.sh")).toBe("shellscript"));
test("detects diff/patch", () => expect(detectLang("foo.patch")).toBe("diff"));
// Specific filenames
test("detects Dockerfile by filename", () => expect(detectLang("Dockerfile")).toBe("docker"));
test("detects Makefile by filename", () => expect(detectLang("Makefile")).toBe("make"));
// Path stripping
test("strips path before detecting", () => expect(detectLang("src/foo/bar.ts")).toBe("typescript"));
test("strips path for Dockerfile", () => expect(detectLang("infra/Dockerfile")).toBe("docker"));
// Fallback
test("falls back to text for unknown extension", () => expect(detectLang("foo.xyz")).toBe("text"));
test("falls back to text for no extension", () => expect(detectLang("LICENSE")).toBe("text"));
});
describe("hasBinaryContent", () => {
test("detects NUL byte as binary", () => {
expect(hasBinaryContent(Buffer.from([0x68, 0x65, 0x00, 0x6c, 0x6f]))).toBe(true);
});
test("detects NUL-only buffer as binary", () => {
expect(hasBinaryContent(Buffer.alloc(10, 0))).toBe(true);
});
test("plain text is not binary", () => {
expect(hasBinaryContent(Buffer.from("hello world\n"))).toBe(false);
});
test("CR LF text is not binary", () => {
expect(hasBinaryContent(Buffer.from("line1\r\nline2\r\n"))).toBe(false);
});
test("other control chars without NUL are not binary", () => {
// \x01, \x07 (BEL), \x1b (ESC) — git treats these as text
expect(hasBinaryContent(Buffer.from([0x01, 0x07, 0x1b, 0x41]))).toBe(false);
});
test("only checks first 8000 bytes", () => {
// NUL appears after the 8000-byte sample window — should not be detected
const buf = Buffer.alloc(8001, 0x41);
buf[8000] = 0x00;
expect(hasBinaryContent(buf)).toBe(false);
});
test("detects NUL within the 8000-byte sample", () => {
const buf = Buffer.alloc(8001, 0x41);
buf[7999] = 0x00;
expect(hasBinaryContent(buf)).toBe(true);
});
});
// Build a minimal unified diff string with the given added line(s).
function makeDiff(...lines: string[]): string {
return [
"diff --git a/test.txt b/test.txt",
"index 0000000..1111111 100644",
"--- a/test.txt",
"+++ b/test.txt",
`@@ -0,0 +${lines.length} @@`,
...lines.map((l) => `+${l}`),
].join("\n");
}
describe("diff control character rendering", () => {
test("CR renders as ^M with diff-ctrl span", async () => {
const files = await parseDiff(makeDiff("hello\rworld"));
const rendered = await highlightFile(files[0]!);
const html = rendered.hunks[0]!.rows[0]!.html;
expect(html).toContain('^M');
});
test("trailing CR renders as ^M on every line, not just the last", async () => {
// Simulates a CRLF line-ending change where every line ends with \r.
// Shiki normalises \r\n → \n and would silently drop the \r from all
// but the last line without the pre-strip fix.
const files = await parseDiff(makeDiff("alpha\r", "beta\r", "gamma\r"));
const rendered = await highlightFile(files[0]!);
const rows = rendered.hunks[0]!.rows;
for (const row of rows) {
expect(row.html).toContain('^M');
}
});
test("BEL (\\x07) renders as ^G", async () => {
const files = await parseDiff(makeDiff("ring\x07bell"));
const rendered = await highlightFile(files[0]!);
const html = rendered.hunks[0]!.rows[0]!.html;
expect(html).toContain('^G');
});
test("ESC (\\x1b) renders as ^[", async () => {
const files = await parseDiff(makeDiff("\x1b[31mred\x1b[0m"));
const rendered = await highlightFile(files[0]!);
const html = rendered.hunks[0]!.rows[0]!.html;
expect(html).toContain('^[');
});
test("TAB does not render as a control char span", async () => {
const files = await parseDiff(makeDiff("\thello"));
const rendered = await highlightFile(files[0]!);
const html = rendered.hunks[0]!.rows[0]!.html;
expect(html).not.toContain("diff-ctrl");
});
test("normal text has no control char spans", async () => {
const files = await parseDiff(makeDiff("hello world"));
const rendered = await highlightFile(files[0]!);
const html = rendered.hunks[0]!.rows[0]!.html;
expect(html).not.toContain("diff-ctrl");
});
});
describe("lazy grammar loading", () => {
// codeToHtml emits `color:...;--shiki-dark:...`, codeToTokensWithThemes
// emits `--shiki-light:...;--shiki-dark:...`. Both carry --shiki-dark.
const shikiSpan = / {
// A fresh highlighter carries only Shiki's built-in special langs.
const { getHighlighter } = await import("../src/services/highlight.ts");
expect(getHighlighter().getLoadedLanguages().length).toBeLessThan(20);
});
test("ensureLang loads a real grammar and reports it", async () => {
const { ensureLang, getHighlighter } = await import(
"../src/services/highlight.ts"
);
expect(await ensureLang("rust")).toBe("rust");
expect(getHighlighter().getLoadedLanguages()).toContain("rust");
});
test("ensureLang falls back to text for a language outside the bundle", async () => {
const { ensureLang } = await import("../src/services/highlight.ts");
expect(await ensureLang("definitely-not-a-language")).toBe("text");
});
test("ensureLang is idempotent", async () => {
const { ensureLang } = await import("../src/services/highlight.ts");
expect(await ensureLang("python")).toBe("python");
expect(await ensureLang("python")).toBe("python");
});
test("serveFile actually highlights, for a grammar loaded on demand", async () => {
const { serveFile } = await import("../src/services/highlight.ts");
const view = await serveFile(
Buffer.from("def greet():\n return 42\n"),
"sample.py",
"",
);
expect(view.type).toBe("inline");
if (view.type !== "inline") throw new Error("expected inline");
// Regression guard: without ensureLang this silently degrades to the
// plain-text table, which has no per-token colour spans.
expect(view.html).toMatch(shikiSpan);
});
test("serveFile still renders an unknown extension as plain text", async () => {
const { serveFile } = await import("../src/services/highlight.ts");
const view = await serveFile(
Buffer.from("just some words\n"),
"notes.xyz",
"",
);
expect(view.type).toBe("inline");
if (view.type !== "inline") throw new Error("expected inline");
expect(view.html).toContain("just some words");
});
test("highlightFile colours diff hunks for an on-demand grammar", async () => {
const diff = [
"diff --git a/x.go b/x.go",
"index 111..222 100644",
"--- a/x.go",
"+++ b/x.go",
"@@ -1,2 +1,2 @@",
" package main",
"-const a = 1",
"+const a = 2",
].join("\n");
const [file] = await parseDiff(diff);
const rendered = await highlightFile(file!);
const html = rendered.hunks.flatMap((h) => h.rows.map((r) => r.html)).join("");
expect(html).toMatch(shikiSpan);
});
});