import type { SelectItem } from "@earendil-works/pi-tui"; import type { Snippet } from "./snippets.js"; import { getSnippetPreview } from "./snippets.js"; export type SearchIndexItem = { item: SelectItem; idx: number; raw: string; normalized: string; }; export function normalizeForSearch(value: string): string { return value .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .replace(/\s+/g, " ") .trim(); } export function buildSearchIndex(snippets: Snippet[], items: SelectItem[]): SearchIndexItem[] { return snippets.flatMap((snippet, idx) => { const item = items[idx]; if (!item) return []; const preview = getSnippetPreview(snippet); const type = snippet.type; const lang = snippet.language ?? ""; const raw = `${preview} ${type} ${lang} ${snippet.sourceLabel}`.toLowerCase(); return [ { item, idx, raw, normalized: normalizeForSearch(raw), }, ]; }); } export function rankedFilterItems(filter: string, items: SelectItem[], searchIndex: SearchIndexItem[]): SelectItem[] { const lower = filter.toLowerCase(); if (lower.length === 0) return items; const norm = normalizeForSearch(lower); const tokens = norm.length > 0 ? norm.split(" ") : []; const scored: Array<{ item: SelectItem; idx: number; score: number }> = []; for (const entry of searchIndex) { let score = 0; const rawIndex = entry.raw.indexOf(lower); if (rawIndex !== -1) { score = 1000 - rawIndex; } else if (tokens.length > 0) { let allMatch = true; let firstPos = Number.MAX_SAFE_INTEGER; for (const token of tokens) { const pos = entry.normalized.indexOf(token); if (pos === -1) { allMatch = false; break; } firstPos = Math.min(firstPos, pos); } if (!allMatch) continue; score = 500 - firstPos; } else { continue; } scored.push({ item: entry.item, idx: entry.idx, score }); } scored.sort((a, b) => { if (b.score !== a.score) return b.score - a.score; return a.idx - b.idx; }); return scored.map((entry) => entry.item); }