dbx/apps/desktop/src/lib/sidebarSearch.ts

76 lines
2.5 KiB
TypeScript

import { parseSlashDelimitedRegexQuery } from "@/lib/searchPattern";
export type SidebarSearchMatchKind =
| "exact"
| "prefix"
| "word-prefix"
| "substring"
| "abbreviation"
| "fuzzy"
| "regex";
export interface SidebarSearchMatch {
kind: SidebarSearchMatchKind;
score: number;
}
export type SidebarLabelMatcher = (label: string) => SidebarSearchMatch | null;
function isWordBoundary(text: string, index: number): boolean {
if (index === 0) return true;
const prev = text[index - 1];
return prev === "_" || prev === "-" || prev === "." || prev === " " || prev === "/" || prev === "\\";
}
function matchesWordPrefix(text: string, query: string): boolean {
for (let i = 0; i < text.length; i++) {
if (isWordBoundary(text, i) && text.startsWith(query, i)) return true;
}
return false;
}
function matchesAbbreviation(text: string, query: string): boolean {
let j = 0;
for (let i = 0; i < text.length && j < query.length; i++) {
if (isWordBoundary(text, i) && text[i] === query[j]) j++;
}
return j === query.length;
}
function matchesSubsequence(text: string, query: string): boolean {
if (query.length < 2 || query.length > text.length) return false;
let j = 0;
for (let i = 0; i < text.length && j < query.length; i++) {
if (isWordBoundary(text, i) && i > 0) {
j = 0;
}
if (text[i] === query[j]) j++;
}
return j === query.length;
}
function matchSidebarLabelWithRegex(label: string, query: string, regex: RegExp | null): SidebarSearchMatch | null {
if (!query) return null;
if (regex) return regex.test(label) ? { kind: "regex", score: 95 } : null;
if (label === query) return { kind: "exact", score: 100 };
if (label.startsWith(query)) return { kind: "prefix", score: 90 };
if (matchesWordPrefix(label, query)) return { kind: "word-prefix", score: 80 };
if (label.includes(query)) return { kind: "substring", score: 70 };
if (query.length >= 2 && matchesAbbreviation(label, query)) return { kind: "abbreviation", score: 60 };
if (matchesSubsequence(label, query)) return { kind: "fuzzy", score: 40 };
return null;
}
export function createSidebarLabelMatcher(query: string): SidebarLabelMatcher {
const regex = parseSlashDelimitedRegexQuery(query);
return (label) => matchSidebarLabelWithRegex(label, query, regex);
}
export function matchSidebarLabel(label: string, query: string): SidebarSearchMatch | null {
return matchSidebarLabelWithRegex(label, query, parseSlashDelimitedRegexQuery(query));
}