fix(dropdown): sort fuzzy search results by score and fix empty results on reopen (#2051)

fzf.js relied on stable Array.sort to preserve score ordering, which is
not guaranteed in QML's JS engine. Results appeared in arbitrary order
with low-relevance matches above exact matches. The sort comparator now
explicitly sorts by score descending, with a length-based tiebreaker so
shorter matches rank first when scores are tied.

Also fixed Object.assign mutating the shared defaultOpts object, which
could cause options to leak between Finder instances.

DankDropdown's onOpened handler now reinitializes the search when previous
search text exists, fixing the empty results shown on reopen.

Added resetSearch() for consumers to clear search state externally.
This commit is contained in:
Jeff Corcoran authored and GitHub committed 2026-03-23 09:24:51 -04:00
1 parent c5f145be36
commit 4d649468d5
2 files changed
+25 -10

No files matched your search

+8 -7
View File
@@ -1249,7 +1249,7 @@ const defaultOpts = {
};
class Finder {
constructor(list, ...optionsTuple) {
this.opts = Object.assign(defaultOpts, optionsTuple[0]);
this.opts = Object.assign({}, defaultOpts, optionsTuple[0]);
this.items = list;
this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));
this.algoFn = exactMatchNaive;
@@ -1283,12 +1283,13 @@ function postProcessResultItems(result, opts) {
if (opts.sort) {
const { selector } = opts;
result.sort((a, b) => {
if (a.score === b.score) {
for (const tiebreaker of opts.tiebreakers) {
const diff = tiebreaker(a, b, selector);
if (diff !== 0) {
return diff;
}
if (a.score !== b.score) {
return b.score - a.score;
}
for (const tiebreaker of opts.tiebreakers) {
const diff = tiebreaker(a, b, selector);
if (diff !== 0) {
return diff;
}
}
return 0;
+17 -3
View File
@@ -58,6 +58,13 @@ Item {
dropdownMenu.close();
}
function resetSearch() {
searchField.text = "";
dropdownMenu.fzfFinder = null;
dropdownMenu.searchQuery = "";
dropdownMenu.selectedIndex = -1;
}
width: compactMode ? dropdownWidth : parent.width
implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
@@ -206,7 +213,9 @@ Item {
fzfFinder = new Fzf.Finder(root.options, {
"selector": option => option,
"limit": 50,
"casing": "case-insensitive"
"casing": "case-insensitive",
"sort": true,
"tiebreakers": [(a, b, selector) => selector(a.item).length - selector(b.item).length]
});
}
@@ -233,9 +242,14 @@ Item {
}
onOpened: {
fzfFinder = null;
searchQuery = "";
selectedIndex = -1;
if (searchField.text.length > 0) {
initFinder();
searchQuery = searchField.text;
} else {
fzfFinder = null;
searchQuery = "";
}
}
parent: Overlay.overlay