import type {
Chunk,
Highlight
} from "#lib/api/lexicons/types/org/tangled/temp/search/searchCode.ts";
export interface Span {
text: string;
match?: boolean;
color?: string;
}
export interface ThemedToken {
content: string;
color?: string;
}
export interface Line {
num: number;
spans: Span[];
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function mergeRanges(highlights: Highlight[]): Highlight[] {
const sorted = highlights.filter((h) => h.end > h.start).sort((a, b) => a.start - b.start);
const out: Highlight[] = [];
for (const h of sorted) {
const last = out.at(-1);
if (last && h.start <= last.end) {
last.end = Math.max(last.end, h.end);
} else {
out.push({ ...h });
}
}
return out;
}
function lineAt(
bytes: Uint8Array,
start: number,
end: number,
num: number,
ranges: Highlight[]
): Line {
const slice = (from: number, to: number) => decoder.decode(bytes.subarray(from, to));
const spans: Span[] = [];
let pos = start;
for (const r of ranges) {
const s = Math.max(r.start, start);
const e = Math.min(r.end, end);
if (s >= e) continue;
if (s > pos) spans.push({ text: slice(pos, s) });
spans.push({ text: slice(s, e), match: true });
pos = e;
}
if (pos < end) spans.push({ text: slice(pos, end) });
return { num, spans };
}
export function mergeSpansWithTokens(spans: Span[], tokens?: ThemedToken[]): Span[] {
if (!tokens || tokens.length === 0) return spans;
const result: Span[] = [];
let spanIdx = 0;
let spanOffset = 0;
let tokenIdx = 0;
let tokenOffset = 0;
while (spanIdx < spans.length && tokenIdx < tokens.length) {
const span = spans[spanIdx];
const token = tokens[tokenIdx];
const spanRemaining = span.text.length - spanOffset;
const tokenRemaining = token.content.length - tokenOffset;
const take = Math.min(spanRemaining, tokenRemaining);
if (take > 0) {
const entry: Span = { text: span.text.slice(spanOffset, spanOffset + take) };
if (span.match) entry.match = true;
if (token.color) entry.color = token.color;
result.push(entry);
spanOffset += take;
tokenOffset += take;
}
if (spanOffset >= span.text.length) {
spanIdx++;
spanOffset = 0;
}
if (tokenOffset >= token.content.length) {
tokenIdx++;
tokenOffset = 0;
}
}
while (spanIdx < spans.length) {
const span = spans[spanIdx];
if (spanOffset < span.text.length) {
const entry: Span = { text: span.text.slice(spanOffset) };
if (span.match) entry.match = true;
result.push(entry);
}
spanIdx++;
spanOffset = 0;
}
return result;
}
export function chunkLines(chunk: Chunk, tokens?: ThemedToken[][]): Line[] {
const ranges = mergeRanges(chunk.highlights ?? []);
const bytes = encoder.encode(chunk.content.replace(/\n$/, ""));
const firstNum = Math.max(1, chunk.lineStart);
const lines: Line[] = [];
let start = 0;
for (let i = 0; i <= bytes.length; i++) {
if (i < bytes.length && bytes[i] !== 0x0a) continue;
const lineIdx = lines.length;
const line = lineAt(bytes, start, i, firstNum + lineIdx, ranges);
if (tokens && tokens[lineIdx]) {
line.spans = mergeSpansWithTokens(line.spans, tokens[lineIdx]);
}
lines.push(line);
start = i + 1;
}
return lines;
}
export const matchCount = (chunks: Chunk[]) =>
chunks.reduce((n, c) => n + (c.highlights?.length ?? 0), 0);