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);