export interface LineMatchOptions { needles: string[]; contextLines?: number; caseSensitive?: boolean; maxChars?: number; } export interface LineMatchResult { text: string; matchCount: number; truncated: boolean; } export function filterLineMatches( text: string, options: LineMatchOptions, ): LineMatchResult { const needles = options.needles.filter((x) => x.length > 0); if (needles.length === 0) return { text: "", matchCount: 0, truncated: false }; const lines = text.split(/\r?\n/u); const haystack = options.caseSensitive ? lines : lines.map((x) => x.toLowerCase()); const wanted = options.caseSensitive ? needles : needles.map((x) => x.toLowerCase()); const context = Math.max(0, options.contextLines ?? 0); const ranges: Array<[number, number]> = []; let matchCount = 0; for (let i = 0; i < haystack.length; i++) { if (!wanted.some((needle) => haystack[i].includes(needle))) continue; matchCount++; const start = Math.max(0, i - context); const end = Math.min(lines.length - 1, i + context); const last = ranges[ranges.length - 1]; if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end); else ranges.push([start, end]); } if (matchCount === 0) return { text: "", matchCount: 0, truncated: false }; const out: string[] = []; for (const [start, end] of ranges) { if (out.length > 0) out.push("--"); for (let i = start; i <= end; i++) out.push(`${i + 1}: ${lines[i]}`); } const full = out.join("\n"); const max = options.maxChars ?? 8_000; if (full.length <= max) return { text: full, matchCount, truncated: false }; return { text: `${full.slice(0, max)}\n… (matches truncated)`, matchCount, truncated: true, }; }