From 4d649468d539369e825cafae8e0790b3f6fc2ac3 Mon Sep 17 00:00:00 2001 From: Jeff Corcoran Date: Mon, 23 Mar 2026 09:24:51 -0400 Subject: [PATCH] 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. --- quickshell/Common/fzf.js | 15 ++++++++------- quickshell/Widgets/DankDropdown.qml | 20 +++++++++++++++++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/quickshell/Common/fzf.js b/quickshell/Common/fzf.js index 995a093c..94946e66 100644 --- a/quickshell/Common/fzf.js +++ b/quickshell/Common/fzf.js @@ -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; diff --git a/quickshell/Widgets/DankDropdown.qml b/quickshell/Widgets/DankDropdown.qml index 5dd76671..05b962a0 100644 --- a/quickshell/Widgets/DankDropdown.qml +++ b/quickshell/Widgets/DankDropdown.qml @@ -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