/ extensions / code-actions / search.ts
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  }