authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 22:09:50-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 01:50:33-07:00
log2b462d10dda0898b5f553701b46982d68bfc8661
tree13a86b761214cfe7afe0d3280bfa36d75c71fab0
parentd92401a36cb3a706f3f80bd0b6aeb1f5cb789375
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: Enter in the palette never runs another command than the one typed

Typing Spelling and Enter toggled Hide Spelling Errors: Spelling was disabled, so the highlight skipped to the next match. A query that is an item's whole text now ranks it first, and a disabled best match is left unhighlighted, so Enter runs nothing rather than something else. Assisted-by: claude-opus-5.5

2 files changed, 38 insertions(+), 4 deletions(-)

crates/ui/src/popup.rs+6-4
......@@ -502,12 +502,13 @@ fn choose(
502502 }
503503 let typed = &query[prefix.len()..];
504504 let matches = Matches::new(items, typed, style.rule_band);
505 // Unfiltered, the current item starts highlighted; filtered, the best match.
505 // Unfiltered, the current item starts highlighted; filtered, the best match, unless it
506 // is disabled, so Enter never runs a worse one.
506507 highlight = highlight.or_else(|| {
507508 let first = if typed.is_empty() {
508509 matches.order.iter().position(|index| items[*index].current)
509510 } else {
510 (0..matches.count()).find(|row| matches.selectable(*row))
511 Some(0).filter(|row| *row < matches.count() && matches.selectable(*row))
511512 };
512513 first.map(|row| matches.key(row))
513514 });
......@@ -1532,8 +1533,9 @@ impl<'a> Matches<'a> {
15321533 })
15331534 .collect()
15341535 });
1535 // Stable, so equal matches keep the items' order.
1536 ranked.sort_by_key(|(score, _)| std::cmp::Reverse(*score));
1536 // Stable, so equal matches keep the items' order; the text itself typed leads.
1537 let typed = |index: usize| items[index].text.eq_ignore_ascii_case(query.trim());
1538 ranked.sort_by_key(|(score, index)| (!typed(*index), std::cmp::Reverse(*score)));
15371539 ranked.into_iter().map(|(_, index)| index).collect()
15381540 };
15391541 if order.is_empty() && !pattern.atoms.is_empty() {
crates/ui/src/tests.rs+32
......@@ -1591,6 +1591,38 @@ fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {
15911591 );
15921592}
15931593
1594#[test]
1595fn enter_runs_no_worse_match_past_a_disabled_best_one() {
1596 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1597 let palette = Id::ROOT.child("palette");
1598 let commands = [
1599 popup::Item {
1600 text: "Hide Spelling Errors",
1601 ..popup::Item::default()
1602 },
1603 popup::Item {
1604 text: "Spelling",
1605 disabled: true,
1606 ..popup::Item::default()
1607 },
1608 ];
1609 let build = |ui: &mut Ui| {
1610 let mut chosen = None;
1611 frame(ui, |ui| {
1612 chosen = popup::palette(ui, palette, &[("", &commands)], "Search commands")
1613 });
1614 chosen
1615 };
1616 build(&mut ui);
1617 ui.open_popup(palette);
1618 build(&mut ui);
1619 ui.event(typed("Spelling"));
1620 build(&mut ui);
1621 ui.event(key(NamedKey::Enter));
1622 assert_eq!(build(&mut ui), None);
1623 assert!(ui.popup_open(palette));
1624}
1625
15941626#[test]
15951627fn a_prefix_typed_in_the_palette_switches_what_it_lists() {
15961628 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);