~james/core

core/web/src/lib/components/search/chunks.test.ts -rw-r--r-- 4.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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<typeof chunkLines>[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);
	});
});