refactor(search): unify dropdown filtering on the shared ranked matcher
Branch, project, agent, model, provider, stash, SSH-host, skill-catalog and archive filters each had their own toLowerCase().includes (or no ordering at all); the git branch and gitmoji pickers also let cmdk re-filter and reorder on top of the manual filter, silently dropping rows. All of them now go through rankByQuery/matchesRankQuery: results are relevance-ordered, multi-word queries match in any order, matching ignores punctuation, and cmdk filtering is disabled where the ranked list is already final. rankBranchesForQuery keeps relevance order instead of re-sorting matches alphabetically; the model picker now also matches model ids.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { rankBranchesForQuery } from './branchSearch';
|
||||
|
||||
describe('rankBranchesForQuery', () => {
|
||||
test('empty query keeps everything in the other groups', () => {
|
||||
const result = rankBranchesForQuery({ localBranches: ['main'], remoteBranches: ['origin/dev'], query: ' ' });
|
||||
expect(result.matching).toEqual([]);
|
||||
expect(result.otherLocal).toEqual(['main']);
|
||||
expect(result.otherRemote).toEqual(['origin/dev']);
|
||||
});
|
||||
|
||||
test('orders matches by relevance, not alphabetically', () => {
|
||||
const result = rankBranchesForQuery({
|
||||
localBranches: ['aaa-fix-scroll', 'fix/scroll', 'main'],
|
||||
remoteBranches: ['origin/fix/scroll-old'],
|
||||
query: 'fix',
|
||||
});
|
||||
expect(result.matching[0]).toEqual({ label: 'fix/scroll', value: 'fix/scroll', source: 'local' });
|
||||
expect(result.matching.map((entry) => entry.label)).toEqual([
|
||||
'fix/scroll',
|
||||
'aaa-fix-scroll',
|
||||
'origin/fix/scroll-old',
|
||||
]);
|
||||
expect(result.otherLocal).toEqual(['main']);
|
||||
expect(result.otherRemote).toEqual([]);
|
||||
});
|
||||
|
||||
test('remote matches carry the remotes/ checkout value', () => {
|
||||
const result = rankBranchesForQuery({ localBranches: [], remoteBranches: ['origin/feat/x'], query: 'feat' });
|
||||
expect(result.matching[0].value).toBe('remotes/origin/feat/x');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { partitionByFuzzyQuery } from "@/lib/search/fuzzySearch";
|
||||
import { rankByQuery } from "@/lib/search/fuzzySearch";
|
||||
|
||||
export interface RankedBranchGroups {
|
||||
matching: Array<{
|
||||
@@ -26,42 +26,19 @@ export function rankBranchesForQuery(args: {
|
||||
};
|
||||
}
|
||||
|
||||
const localPartition = partitionByFuzzyQuery(localBranches, normalizedQuery, (branch) => branch);
|
||||
const remotePartition = partitionByFuzzyQuery(remoteBranches, normalizedQuery, (branch) => branch);
|
||||
const matching: RankedBranchGroups['matching'] = [];
|
||||
const otherLocal = localPartition.other;
|
||||
const otherRemote = remotePartition.other;
|
||||
|
||||
for (const branch of localPartition.matching) {
|
||||
matching.push({
|
||||
label: branch,
|
||||
value: branch,
|
||||
source: 'local',
|
||||
});
|
||||
}
|
||||
|
||||
for (const branch of remotePartition.matching) {
|
||||
matching.push({
|
||||
label: branch,
|
||||
value: `remotes/${branch}`,
|
||||
source: 'remote',
|
||||
});
|
||||
}
|
||||
|
||||
matching.sort((a, b) => {
|
||||
const byLabel = a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
|
||||
if (byLabel !== 0) {
|
||||
return byLabel;
|
||||
}
|
||||
if (a.source !== b.source) {
|
||||
return a.source.localeCompare(b.source);
|
||||
}
|
||||
return a.value.localeCompare(b.value);
|
||||
});
|
||||
// Rank local and remote branches together so the order reflects match
|
||||
// quality (an exact or prefix match lands first), not the source group or
|
||||
// the alphabet.
|
||||
const candidates: RankedBranchGroups['matching'] = [
|
||||
...localBranches.map((branch) => ({ label: branch, value: branch, source: 'local' as const })),
|
||||
...remoteBranches.map((branch) => ({ label: branch, value: `remotes/${branch}`, source: 'remote' as const })),
|
||||
];
|
||||
const matching = rankByQuery(candidates, normalizedQuery, (branch) => [branch.label]);
|
||||
const matched = new Set(matching);
|
||||
|
||||
return {
|
||||
matching,
|
||||
otherLocal,
|
||||
otherRemote,
|
||||
otherLocal: candidates.filter((entry) => entry.source === 'local' && !matched.has(entry)).map((entry) => entry.label),
|
||||
otherRemote: candidates.filter((entry) => entry.source === 'remote' && !matched.has(entry)).map((entry) => entry.label),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user