80b5516199
* fix(web): improve command palette alias matching * docs(web): explain command priority tie-breaker * fix(web): preserve command palette selection
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { defaultFilter } from "cmdk";
|
|
import type { CommandItem } from "./types";
|
|
|
|
const MAX_COMBINED_TERMS_SCORE = 0.99;
|
|
const UNORDERED_TERMS_SCORE = 0.98;
|
|
|
|
export function getCommandSearchTerms(command: CommandItem): string[] {
|
|
return [command.label, ...(command.keywords ?? [])];
|
|
}
|
|
|
|
function tokenize(value: string): string[] {
|
|
return value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
}
|
|
|
|
function scoreUnorderedTerms(terms: string[], search: string): number {
|
|
const queryTokens = tokenize(search);
|
|
if (queryTokens.length < 2) return 0;
|
|
|
|
const termTokens = terms.flatMap(tokenize);
|
|
const allTokensMatch = queryTokens.every((queryToken) =>
|
|
termTokens.some((termToken) => termToken.startsWith(queryToken)),
|
|
);
|
|
return allTokensMatch ? UNORDERED_TERMS_SCORE : 0;
|
|
}
|
|
|
|
export function scoreCommandSearch(_value: string, search: string, searchTerms?: string[]): number {
|
|
const normalizedSearch = search.trim();
|
|
const terms = searchTerms ?? [];
|
|
const bestTermScore = terms.reduce(
|
|
(bestScore, term) => Math.max(bestScore, defaultFilter(term, normalizedSearch, [])),
|
|
0,
|
|
);
|
|
const combinedTermsScore = defaultFilter(terms.join(" "), normalizedSearch, []);
|
|
return Math.max(
|
|
bestTermScore,
|
|
Math.min(combinedTermsScore, MAX_COMBINED_TERMS_SCORE),
|
|
scoreUnorderedTerms(terms, normalizedSearch),
|
|
);
|
|
}
|
|
|
|
function commandSearchScore(command: CommandItem, search: string): number {
|
|
return scoreCommandSearch(command.id, search, getCommandSearchTerms(command));
|
|
}
|
|
|
|
function compareCommandPriority(a: CommandItem, b: CommandItem): number {
|
|
return (a.priority ?? 100) - (b.priority ?? 100);
|
|
}
|
|
|
|
export function sortCommandsForSearch(commands: CommandItem[], search: string): CommandItem[] {
|
|
return [...commands].sort((a, b) => {
|
|
const scoreDifference = commandSearchScore(b, search) - commandSearchScore(a, search);
|
|
return scoreDifference || compareCommandPriority(a, b);
|
|
});
|
|
}
|
|
|
|
export function findFirstMatchingCommand(
|
|
commands: CommandItem[],
|
|
search: string,
|
|
preferredCommandId?: string,
|
|
): CommandItem | undefined {
|
|
const preferredCommand = preferredCommandId
|
|
? commands.find((command) => command.id === preferredCommandId)
|
|
: undefined;
|
|
if (preferredCommand && commandSearchScore(preferredCommand, search) > 0) {
|
|
return preferredCommand;
|
|
}
|
|
return sortCommandsForSearch(commands, search).find(
|
|
(command) => commandSearchScore(command, search) > 0,
|
|
);
|
|
}
|
|
|
|
export function selectCommandSearchResult(
|
|
commands: CommandItem[],
|
|
search: string,
|
|
leadingResultValues: string[],
|
|
preferredValue?: string,
|
|
): string {
|
|
const normalizedSearch = search.trim();
|
|
if (preferredValue) {
|
|
if (leadingResultValues.includes(preferredValue)) return preferredValue;
|
|
const preferredCommand = commands.find((command) => command.id === preferredValue);
|
|
const preferredCommandStillVisible =
|
|
preferredCommand &&
|
|
(!normalizedSearch ||
|
|
findFirstMatchingCommand(commands, normalizedSearch, preferredValue)?.id ===
|
|
preferredValue);
|
|
if (preferredCommandStillVisible) return preferredValue;
|
|
}
|
|
|
|
const firstLeadingResult = leadingResultValues[0];
|
|
if (firstLeadingResult) return firstLeadingResult;
|
|
return normalizedSearch ? (findFirstMatchingCommand(commands, normalizedSearch)?.id ?? "") : "";
|
|
}
|