From f03d2655c0fbb08451c42b4bab47477f18e81b95 Mon Sep 17 00:00:00 2001 From: Steve Shreeve Date: Wed, 19 Aug 2026 14:07:31 -0600 Subject: [PATCH] Don't let a menu with no suggestions swallow Enter The completion menu stays active while the user types past it, and any Enter/Submit/SubmitOrNewline while a menu is active is routed to the menu. A menu whose filtered suggestions are empty has nothing to accept, so the keypress simply died: the line did not run and nothing visibly happened, leaving Enter to work only on the second press. The guard now ignores active menus with no values, so the event falls through to the normal submit path, and submit_buffer closes any menu that let a submit through so it cannot stay active into the next line's editing. --- src/engine.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 6f58a594..66d04a2d 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1634,10 +1634,21 @@ impl Reedline { self.painter.clear_scrollback()?; Ok(EventStatus::Handled) } + // A menu with no suggestions has nothing to accept: swallowing the + // keypress would make Enter appear dead, so the guard ignores it + // and the event falls through to submit as if no menu were open + // (submit_buffer closes any straggler menus). ReedlineEvent::Enter | ReedlineEvent::Submit | ReedlineEvent::SubmitOrNewline - if self.menus.iter().any(|menu| menu.is_active()) => + if self + .menus + .iter() + .any(|menu| menu.is_active() && !menu.get_values().is_empty()) => { - if let Some(menu) = self.menus.iter_mut().find(|menu| menu.is_active()) { + if let Some(menu) = self + .menus + .iter_mut() + .find(|menu| menu.is_active() && !menu.get_values().is_empty()) + { menu.replace_in_buffer(&mut self.editor); menu.menu_event(MenuEvent::Deactivate); Ok(EventStatus::Handled) @@ -2803,6 +2814,9 @@ impl Reedline { } fn submit_buffer(&mut self, prompt: &dyn Prompt) -> io::Result { + // A menu that let the submit through (no suggestions to accept) must + // not stay active into the next line's editing. + self.deactivate_menus(); let buffer = self.editor.get_buffer().to_string(); self.hide_hints = true; // Additional repaint to show the content without hints etc. @@ -5448,6 +5462,82 @@ mod tests { reedline } + /// The menu maintenance the paint cycle runs between keystrokes — value + /// refresh only, no layout (the headless test painter has no width) — so + /// an assertion after a typed character sees what a user would on screen. + fn apply_menu_maintenance(reedline: &mut Reedline) { + for menu in reedline.menus.iter_mut() { + if menu.is_active() { + menu.update_values( + &mut reedline.editor, + reedline.completer.as_mut(), + reedline.history.as_ref(), + ); + } + } + } + + // --- a stale menu must not intercept Enter --- + // + // The completion menu stays active while the user types past it, and any + // Enter while a menu is active is routed to the menu. Left unguarded that + // turns Enter into a dead key (empty menu) or an insertion of whatever the + // menu last highlighted (non-empty menu) at the end of a finished line. + + /// A menu whose filtered suggestions are empty has nothing to accept: + /// Enter must close it and submit the line, not be swallowed. + #[test] + fn enter_with_an_empty_menu_submits_the_line() { + let mut reedline = engine_with_active_menu(false, false); + reedline.painter.force_prompt_anchored_for_test(0); + let prompt = DefaultPrompt::default(); + // "thz" matches nothing; the menu refilters to empty but stays active. + reedline + .handle_event( + &prompt, + ReedlineEvent::Edit(vec![EditCommand::InsertChar('z')]), + ) + .unwrap(); + apply_menu_maintenance(&mut reedline); + assert!(menu_is_active(&reedline)); + + let status = reedline + .handle_event(&prompt, ReedlineEvent::Enter) + .unwrap(); + assert!( + matches!(status, EventStatus::Exits(Signal::Success(ref s)) if s == "thz"), + "Enter was swallowed by an empty menu" + ); + assert!(!menu_is_active(&reedline)); + } + + /// The full shape of the report this fixes: Tab opens the menu mid-line, + /// the user types the rest of the statement and hits Enter. Before the + /// fix the Enter was consumed by the stale menu instead of submitting. + #[test] + fn typing_past_a_completion_then_enter_runs_the_line() { + let mut reedline = engine_with_active_menu(false, false); + reedline.painter.force_prompt_anchored_for_test(0); + let prompt = DefaultPrompt::default(); + for c in " more words".chars() { + reedline + .handle_event( + &prompt, + ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)]), + ) + .unwrap(); + } + apply_menu_maintenance(&mut reedline); + + let status = reedline + .handle_event(&prompt, ReedlineEvent::Enter) + .unwrap(); + assert!( + matches!(status, EventStatus::Exits(Signal::Success(ref s)) if s == "th more words"), + "the line was not submitted intact" + ); + } + /// Engine with a completion menu open over "th" and partial completions on, so /// `MenuNext` reaches the completer through `can_partially_complete`. fn engine_with_partial_completion_menu() -> Reedline {