import { describe, expect, it } from "vitest"; import { chunkLines, matchCount, mergeSpansWithTokens, type Span, type ThemedToken } from "#lib/components/search/chunks.ts"; /** compact view of a line: line number + its spans as "plain" / "[match]" */ const shape = (chunk: Parameters[0], tokens?: ThemedToken[][]) => chunkLines(chunk, tokens).map((l) => [ l.num, l.spans.map((s) => (s.match ? `[${s.text}]` : s.text)) ]); describe("chunkLines", () => { it("numbers lines from lineStart and splits each into plain/matched spans", () => { // "main" at bytes 5-9, "main" again at 21-25 const content = "func main() {\n\tcallMain()\n}\n"; expect(shape({ content, lineStart: 12, highlights: [{ start: 5, end: 9 }] })).toEqual([ [12, ["func ", "[main]", "() {"]], [13, ["\tcallMain()"]], [14, ["}"]] ]); }); it("clips a highlight that spans a newline to each line it covers", () => { // bytes 2-8 cover "c\nde" — "b\n" is at 1-3 const content = "abc\ndef"; expect(shape({ content, lineStart: 1, highlights: [{ start: 2, end: 6 }] })).toEqual([ [1, ["ab", "[c]"]], [2, ["[de]", "f"]] ]); }); it("uses byte offsets, not UTF-16 indices", () => { // "héllo wörld": h(1) é(2) l l o space w -> "wörld" starts at byte 7 const content = "héllo wörld"; expect(shape({ content, lineStart: 1, highlights: [{ start: 7, end: 13 }] })).toEqual([ [1, ["héllo ", "[wörld]"]] ]); }); it("emits an empty span list for a blank line and no spurious trailing line", () => { expect(shape({ content: "a\n\nb\n", lineStart: 3 })).toEqual([ [3, ["a"]], [4, []], [5, ["b"]] ]); }); it("merges overlapping and adjacent highlights", () => { const content = "abcdef"; expect( shape({ content, lineStart: 1, highlights: [ { start: 3, end: 5 }, { start: 1, end: 3 }, { start: 4, end: 6 } ] }) ).toEqual([[1, ["a", "[bcdef]"]]]); }); it("applies syntax tokens to line spans", () => { const content = "const x = 1;"; const tokens: ThemedToken[][] = [ [ { content: "const", color: "kw" }, { content: " ", color: "plain" }, { content: "x", color: "ident" }, { content: " = ", color: "punct" }, { content: "1;", color: "num" } ] ]; const lines = chunkLines( { content, lineStart: 1, highlights: [{ start: 6, end: 7 }] }, tokens ); expect(lines[0].spans).toEqual([ { text: "const", color: "kw" }, { text: " ", color: "plain" }, { text: "x", match: true, color: "ident" }, { text: " = ", color: "punct" }, { text: "1;", color: "num" } ]); }); }); describe("mergeSpansWithTokens", () => { it("splits tokens across search match boundaries", () => { const spans: Span[] = [{ text: "func " }, { text: "main", match: true }, { text: "()" }]; const tokens: ThemedToken[] = [ { content: "func", color: "kw" }, { content: " ", color: "plain" }, { content: "main", color: "func" }, { content: "()", color: "punct" } ]; expect(mergeSpansWithTokens(spans, tokens)).toEqual([ { text: "func", color: "kw" }, { text: " ", color: "plain" }, { text: "main", match: true, color: "func" }, { text: "()", color: "punct" } ]); }); it("splits a single token when a match falls inside it", () => { const spans: Span[] = [ { text: 'println("hello ' }, { text: "world", match: true }, { text: '")' } ]; const tokens: ThemedToken[] = [ { content: "println", color: "func" }, { content: "(", color: "punct" }, { content: '"hello world"', color: "str" }, { content: ")", color: "punct" } ]; expect(mergeSpansWithTokens(spans, tokens)).toEqual([ { text: "println", color: "func" }, { text: "(", color: "punct" }, { text: '"hello ', color: "str" }, { text: "world", match: true, color: "str" }, { text: '"', color: "str" }, { text: ")", color: "punct" } ]); }); it("returns original spans if tokens are missing or empty", () => { const spans: Span[] = [{ text: "hello", match: true }]; expect(mergeSpansWithTokens(spans, undefined)).toBe(spans); expect(mergeSpansWithTokens(spans, [])).toBe(spans); }); }); describe("matchCount", () => { it("sums highlights across chunks, counting a missing list as zero", () => { expect( matchCount([ { content: "a", lineStart: 1, highlights: [{ start: 0, end: 1 }] }, { content: "b", lineStart: 2 } ]) ).toBe(1); }); });