feat(app): rank combobox options by word-boundary match quality
Introduce a shared scoreMatch utility that tiers matches by word-boundary vs mid-word offset, and use it to rank combobox options so e.g. "py" prefers "a/py" over "happy".
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { compareMatchScores, type MatchScore, scoreMatch } from "../../utils/score-match";
|
||||
|
||||
export type ComboboxOptionKind = "directory" | "file";
|
||||
|
||||
export interface ComboboxOptionModel {
|
||||
@@ -7,6 +9,23 @@ export interface ComboboxOptionModel {
|
||||
kind?: ComboboxOptionKind;
|
||||
}
|
||||
|
||||
const DESCRIPTION_FALLBACK_TIER = 99;
|
||||
|
||||
function scoreOption(opt: ComboboxOptionModel, search: string): MatchScore | null {
|
||||
const labelScore = scoreMatch(search, opt.label);
|
||||
const idScore = scoreMatch(search, opt.id);
|
||||
let best: MatchScore | null = null;
|
||||
if (labelScore) best = labelScore;
|
||||
if (idScore && (!best || compareMatchScores(idScore, best) < 0)) {
|
||||
best = idScore;
|
||||
}
|
||||
if (best) return best;
|
||||
if (opt.description && opt.description.toLowerCase().includes(search)) {
|
||||
return { tier: DESCRIPTION_FALLBACK_TIER, offset: 0 };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface BuildVisibleComboboxOptionsInput {
|
||||
options: ComboboxOptionModel[];
|
||||
searchQuery: string;
|
||||
@@ -40,21 +59,17 @@ export function filterAndRankComboboxOptions(
|
||||
search: string,
|
||||
): ComboboxOptionModel[] {
|
||||
if (!search) return options;
|
||||
return options
|
||||
.filter(
|
||||
(opt) =>
|
||||
opt.label.toLowerCase().includes(search) ||
|
||||
opt.id.toLowerCase().includes(search) ||
|
||||
opt.description?.toLowerCase().includes(search),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aPrefix =
|
||||
a.label.toLowerCase().startsWith(search) || a.id.toLowerCase().startsWith(search);
|
||||
const bPrefix =
|
||||
b.label.toLowerCase().startsWith(search) || b.id.toLowerCase().startsWith(search);
|
||||
if (aPrefix !== bPrefix) return aPrefix ? -1 : 1;
|
||||
return 0;
|
||||
});
|
||||
const scored: { opt: ComboboxOptionModel; score: MatchScore }[] = [];
|
||||
for (const opt of options) {
|
||||
const score = scoreOption(opt, search);
|
||||
if (score) scored.push({ opt, score });
|
||||
}
|
||||
scored.sort((a, b) => {
|
||||
const cmp = compareMatchScores(a.score, b.score);
|
||||
if (cmp !== 0) return cmp;
|
||||
return a.opt.label.localeCompare(b.opt.label);
|
||||
});
|
||||
return scored.map((entry) => entry.opt);
|
||||
}
|
||||
|
||||
export function buildVisibleComboboxOptions(
|
||||
|
||||
Reference in New Issue
Block a user