import type { CodeSearchLine } from "./code-intel"; export const CODE_PAGE_SIZE = 20; /** Deep pages re-rank every match, so paging stops here; narrow the query to see further. */ export const CODE_MAX_PAGES = 50; const MAX_LINE_CHARS = 240; const LEAD_CHARS = 60; /** Case-insensitive [start, end) spans of every term in `text`, merged where they overlap. */ export function matchRanges(text: string, terms: string[]): Array<[number, number]> { const lower = text.toLowerCase(); const spans: Array<[number, number]> = []; for (const term of terms) { const needle = term.toLowerCase(); if (!needle) continue; for (let at = lower.indexOf(needle); at !== -1; at = lower.indexOf(needle, at + needle.length)) { spans.push([at, at + needle.length]); } } spans.sort((left, right) => left[0] - right[0]); const merged: Array<[number, number]> = []; for (const span of spans) { const last = merged.at(-1); if (last && span[0] <= last[1]) last[1] = Math.max(last[1], span[1]); else merged.push([span[0], span[1]]); } return merged; } /** Long lines are cut to a window that starts a little before the first match. */ function clip(text: string, ranges: Array<[number, number]>): { text: string; offset: number } { if (text.length <= MAX_LINE_CHARS) return { text, offset: 0 }; const offset = Math.max(0, Math.min((ranges[0]?.[0] ?? 0) - LEAD_CHARS, text.length - MAX_LINE_CHARS)); return { text: text.slice(offset, offset + MAX_LINE_CHARS), offset }; } /** * The first `limit` lines of `content` containing any term, with 1-based line numbers and highlight * spans, plus how many lines matched in total. */ export function matchingLines( content: string, terms: string[], limit: number, ): { lines: CodeSearchLine[]; matchCount: number } { const lines: CodeSearchLine[] = []; let matchCount = 0; if (terms.length === 0) return { lines, matchCount }; const all = content.split("\n"); for (const [index, raw] of all.entries()) { const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw; const ranges = matchRanges(line, terms); if (ranges.length === 0) continue; matchCount++; if (lines.length >= limit) continue; const clipped = clip(line, ranges); const end = clipped.offset + clipped.text.length; lines.push({ number: index + 1, text: clipped.text, ranges: ranges .filter(([start, stop]) => stop > clipped.offset && start < end) .map(([start, stop]) => ({ start: Math.max(0, start - clipped.offset), end: Math.min(clipped.text.length, stop - clipped.offset), })), clippedStart: clipped.offset > 0, clippedEnd: end < line.length, }); } return { lines, matchCount }; }