search.ts
1 import type { SelectItem } from "@mariozechner/pi-tui"; 2 import type { Snippet } from "./snippets"; 3 import { getSnippetPreview } from "./snippets"; 4 5 export type SearchIndexItem = { 6 item: SelectItem; 7 idx: number; 8 raw: string; 9 normalized: string; 10 }; 11 12 export function normalizeForSearch(value: string): string { 13 return value 14 .toLowerCase() 15 .replace(/[^a-z0-9]+/g, " ") 16 .replace(/\s+/g, " ") 17 .trim(); 18 } 19 20 export function buildSearchIndex(snippets: Snippet[], items: SelectItem[]): SearchIndexItem[] { 21 return snippets.map((snippet, idx) => { 22 const preview = getSnippetPreview(snippet); 23 const type = snippet.type; 24 const lang = snippet.language ?? ""; 25 const raw = `${preview} ${type} ${lang} ${snippet.sourceLabel}`.toLowerCase(); 26 return { 27 item: items[idx]!, 28 idx, 29 raw, 30 normalized: normalizeForSearch(raw), 31 }; 32 }); 33 } 34 35 export function rankedFilterItems( 36 filter: string, 37 items: SelectItem[], 38 searchIndex: SearchIndexItem[], 39 ): SelectItem[] { 40 const lower = filter.toLowerCase(); 41 if (lower.length === 0) return items; 42 43 const norm = normalizeForSearch(lower); 44 const tokens = norm.length > 0 ? norm.split(" ") : []; 45 const scored: Array<{ item: SelectItem; idx: number; score: number }> = []; 46 47 for (const entry of searchIndex) { 48 let score = 0; 49 50 const rawIndex = entry.raw.indexOf(lower); 51 if (rawIndex !== -1) { 52 score = 1000 - rawIndex; 53 } else if (tokens.length > 0) { 54 let allMatch = true; 55 let firstPos = Number.MAX_SAFE_INTEGER; 56 for (const token of tokens) { 57 const pos = entry.normalized.indexOf(token); 58 if (pos === -1) { 59 allMatch = false; 60 break; 61 } 62 firstPos = Math.min(firstPos, pos); 63 } 64 if (!allMatch) continue; 65 score = 500 - firstPos; 66 } else { 67 continue; 68 } 69 70 scored.push({ item: entry.item, idx: entry.idx, score }); 71 } 72 73 scored.sort((a, b) => { 74 if (b.score !== a.score) return b.score - a.score; 75 return a.idx - b.idx; 76 }); 77 78 return scored.map((entry) => entry.item); 79 }