/** * Grep Compressor - Compress search results while preserving essential matches * * Strategies: * - Group by file * - Show first N matches per file + count * - Dedupe similar/adjacent matches * - Prioritize exact matches */ function parseGrepOutput(output) { const matches = []; const lines = output.split('\n').filter(l => l.trim()); for (const line of lines) { // Parse format: file:line:content or file:line-content const match = line.match(/^(.+?):(\d+)[:-](.*)$/); if (match) { matches.push({ file: match[1], line: parseInt(match[2]), content: match[3], }); } } return matches; } function groupByFile(matches) { const grouped = new Map(); for (const match of matches) { const existing = grouped.get(match.file) || []; existing.push(match); grouped.set(match.file, existing); } return grouped; } function dedupeAdjacent(matches, threshold = 3) { if (matches.length <= 1) return matches; const result = [matches[0]]; let skipped = 0; for (let i = 1; i < matches.length; i++) { const prev = result[result.length - 1]; const curr = matches[i]; // Skip if within threshold lines of previous match if (curr.line - prev.line <= threshold) { skipped++; continue; } result.push(curr); } // Add note about skipped adjacent matches if (skipped > 0 && result.length > 0) { const last = result[result.length - 1]; result.push({ file: last.file, line: -1, content: `[${skipped} adjacent matches omitted]`, }); } return result; } export function compressGrep(output, options = {}) { const { maxMatchesPerFile = 3, maxTotalMatches = 20, dedupeAdjacent: shouldDedupe = true, showCounts = true, } = options; const matches = parseGrepOutput(output); const originalMatches = matches.length; if (originalMatches === 0) { return { content: 'No matches found.', originalMatches: 0, compressedMatches: 0, filesMatched: 0, savings: '0%', }; } const grouped = groupByFile(matches); const filesMatched = grouped.size; const result = []; let totalShown = 0; // Sort files by match count (most matches first) const sortedFiles = Array.from(grouped.entries()).sort((a, b) => b[1].length - a[1].length); for (const [file, fileMatches] of sortedFiles) { if (totalShown >= maxTotalMatches) { const remaining = sortedFiles.length - result.filter(l => l.startsWith('## ')).length; if (remaining > 0) { result.push(`\n... [${remaining} more files with matches]`); } break; } // Dedupe adjacent matches if configured let processed = shouldDedupe ? dedupeAdjacent(fileMatches) : fileMatches; // Limit matches per file const totalInFile = fileMatches.length; const shown = processed.slice(0, maxMatchesPerFile); const omitted = totalInFile - shown.length; result.push(`## ${file}`); if (showCounts && totalInFile > maxMatchesPerFile) { result.push(`(${totalInFile} matches, showing ${shown.length})`); } for (const match of shown) { if (match.line === -1) { result.push(` ${match.content}`); } else { result.push(` ${match.line}: ${match.content.trim()}`); totalShown++; } } if (omitted > 0) { result.push(` ... [${omitted} more matches in this file]`); } result.push(''); } const compressedMatches = totalShown; const savings = ((1 - compressedMatches / originalMatches) * 100).toFixed(1); return { content: result.join('\n').trim(), originalMatches, compressedMatches, filesMatched, savings: `${savings}%`, }; }