Syntax-highlight Edit, Write, and Read tool calls (#1218)

Highlighting reuses the existing code-block tokenizer and LRU cache, runs
lazily on expand, and falls back to plain text above a size cap so a large
Read can't stall the main thread. The diff highlighter reconstructs the old
and new file text by position rather than from @@ line ranges, so diffs that
arrive without real hunk headers still highlight.

Read content is normalized server-side: the cat -n line-number gutter some
providers emit is stripped so content is uniformly raw source, with the first
line number surfaced as offset for the client to rebuild the gutter.
This commit is contained in:
Mohamed Boudra
2026-05-29 14:51:14 +08:00
committed by GitHub
parent 25d6fc8515
commit 38f0560f9b
12 changed files with 661 additions and 77 deletions
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import {
extensionFromPath,
highlightToKeyedLines,
MAX_HIGHLIGHT_CHARS,
tokenizeToLines,
} from "./highlight-cache";
describe("extensionFromPath", () => {
it("extracts a lowercased extension regardless of absolute/relative path", () => {
expect(extensionFromPath("/repo/src/Index.TS")).toBe("ts");
expect(extensionFromPath("src/index.ts")).toBe("ts");
expect(extensionFromPath("a.b/c.tsx")).toBe("tsx");
});
it("returns null for paths without a usable extension", () => {
expect(extensionFromPath(null)).toBeNull();
expect(extensionFromPath(undefined)).toBeNull();
expect(extensionFromPath("Makefile")).toBeNull();
expect(extensionFromPath(".gitignore")).toBeNull();
expect(extensionFromPath("trailingdot.")).toBeNull();
});
});
describe("tokenizeToLines", () => {
it("returns one token array per line for a supported language", () => {
const lines = tokenizeToLines("const a = 1;\nconst b = 2;", "ts");
expect(lines).not.toBeNull();
expect(lines).toHaveLength(2);
expect(lines?.[0].some((token) => token.style === "keyword")).toBe(true);
});
it("returns null when there is no extension", () => {
expect(tokenizeToLines("whatever", null)).toBeNull();
});
it("falls back to style-less per-line tokens for an unknown extension", () => {
const lines = tokenizeToLines("line one\nline two", "unknownext");
expect(lines).toHaveLength(2);
expect(lines?.[0]).toEqual([{ text: "line one", style: null }]);
});
it("returns null above the size cap so callers fall back to plain text", () => {
const huge = "x".repeat(MAX_HIGHLIGHT_CHARS + 1);
expect(tokenizeToLines(huge, "ts")).toBeNull();
});
it("serves a cached result on repeat calls (identity-stable)", () => {
const first = tokenizeToLines("const cached = true;", "ts");
const second = tokenizeToLines("const cached = true;", "ts");
expect(first).toBe(second);
});
});
describe("highlightToKeyedLines", () => {
it("produces stable keys for lines and tokens", () => {
const keyed = highlightToKeyedLines("const a = 1;", "ts");
expect(keyed?.[0].key).toBe("line-0");
expect(keyed?.[0].tokens[0].key).toBe("0-0");
});
it("returns null when highlighting is unavailable", () => {
expect(highlightToKeyedLines("text", null)).toBeNull();
});
});