diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index a4c8dc0e5b7e5eb01f099c11f29a5d651da09303..fcda8688d427f3e6b937f00edc7c3586dfdbef36 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6551,12 +6551,12 @@ async fn test_pane_split_left(cx: &mut TestAppContext) { assert!(workspace.items(cx).collect::>().len() == 2); }); cx.simulate_keystrokes("cmd-k"); - // sleep for longer than the timeout in keyboard shortcut handling - // to verify that it doesn't fire in this case. + // Sleep past the historical timeout to ensure the multi-stroke binding + // still fires now that unambiguous prefixes no longer auto-expire. cx.executor().advance_clock(Duration::from_secs(2)); cx.simulate_keystrokes("left"); workspace.update(cx, |workspace, cx| { - assert!(workspace.items(cx).collect::>().len() == 2); + assert!(workspace.items(cx).collect::>().len() == 3); }); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e06ba62ce94c69828b3ab08465b4375b4c862343..c3da066cfd73cab5b2de610bbf1bc653f7e6d874 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22165,13 +22165,7 @@ impl Editor { .pending_input_keystrokes() .into_iter() .flatten() - .filter_map(|keystroke| { - if keystroke.modifiers.is_subset_of(&Modifiers::shift()) { - keystroke.key_char.clone() - } else { - None - } - }) + .filter_map(|keystroke| keystroke.key_char.clone()) .collect(); if !self.input_enabled || self.read_only || !self.focus_handle.is_focused(window) { diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index f0c857abd6f3c353105b4272b51ca519f1906078..ae4553408fa8d0dc7ed640319ae0b0a178465b74 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -121,6 +121,7 @@ pub(crate) struct Replay { #[derive(Default, Debug)] pub(crate) struct DispatchResult { pub(crate) pending: SmallVec<[Keystroke; 1]>, + pub(crate) pending_has_binding: bool, pub(crate) bindings: SmallVec<[KeyBinding; 1]>, pub(crate) to_replay: SmallVec<[Replay; 1]>, pub(crate) context_stack: Vec, @@ -480,6 +481,7 @@ impl DispatchTree { if pending { return DispatchResult { pending: input, + pending_has_binding: !bindings.is_empty(), context_stack, ..Default::default() }; @@ -608,9 +610,11 @@ impl DispatchTree { #[cfg(test)] mod tests { use crate::{ - self as gpui, Element, ElementId, GlobalElementId, InspectorElementId, LayoutId, Style, + self as gpui, DispatchResult, Element, ElementId, GlobalElementId, InspectorElementId, + Keystroke, LayoutId, Style, }; use core::panic; + use smallvec::SmallVec; use std::{cell::RefCell, ops::Range, rc::Rc}; use crate::{ @@ -676,6 +680,49 @@ mod tests { assert!(keybinding[0].action.partial_eq(&TestAction)) } + #[test] + fn test_pending_has_binding_state() { + let bindings = vec![ + KeyBinding::new("ctrl-b h", TestAction, None), + KeyBinding::new("space", TestAction, Some("ContextA")), + KeyBinding::new("space f g", TestAction, Some("ContextB")), + ]; + let keymap = Rc::new(RefCell::new(Keymap::new(bindings))); + let mut registry = ActionRegistry::default(); + registry.load_action::(); + let mut tree = DispatchTree::new(keymap, Rc::new(registry)); + + type DispatchPath = SmallVec<[super::DispatchNodeId; 32]>; + fn dispatch( + tree: &mut DispatchTree, + pending: SmallVec<[Keystroke; 1]>, + key: &str, + path: &DispatchPath, + ) -> DispatchResult { + tree.dispatch_key(pending, Keystroke::parse(key).unwrap(), path) + } + + let dispatch_path: DispatchPath = SmallVec::new(); + let result = dispatch(&mut tree, SmallVec::new(), "ctrl-b", &dispatch_path); + assert_eq!(result.pending.len(), 1); + assert!(!result.pending_has_binding); + + let result = dispatch(&mut tree, result.pending, "h", &dispatch_path); + assert_eq!(result.pending.len(), 0); + assert_eq!(result.bindings.len(), 1); + assert!(!result.pending_has_binding); + + let node_id = tree.push_node(); + tree.set_key_context(KeyContext::parse("ContextB").unwrap()); + tree.pop_node(); + + let dispatch_path = tree.dispatch_path(node_id); + let result = dispatch(&mut tree, SmallVec::new(), "space", &dispatch_path); + + assert_eq!(result.pending.len(), 1); + assert!(!result.pending_has_binding); + } + #[crate::test] fn test_input_handler_pending(cx: &mut TestAppContext) { #[derive(Clone)] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 3505da3e7d85ed3dca5e9050787d11902941f364..215a9423482925ee093a93af896e6cd00872aba6 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -909,6 +909,7 @@ struct PendingInput { keystrokes: SmallVec<[Keystroke; 1]>, focus: Option, timer: Option>, + needs_timeout: bool, } pub(crate) struct ElementStateBox { @@ -3896,32 +3897,52 @@ impl Window { } if !match_result.pending.is_empty() { + currently_pending.timer.take(); currently_pending.keystrokes = match_result.pending; currently_pending.focus = self.focus; - currently_pending.timer = Some(self.spawn(cx, async move |cx| { - cx.background_executor.timer(Duration::from_secs(1)).await; - cx.update(move |window, cx| { - let Some(currently_pending) = window - .pending_input - .take() - .filter(|pending| pending.focus == window.focus) - else { - return; - }; - let node_id = window.focus_node_id_in_rendered_frame(window.focus); - let dispatch_path = window.rendered_frame.dispatch_tree.dispatch_path(node_id); - - let to_replay = window - .rendered_frame - .dispatch_tree - .flush_dispatch(currently_pending.keystrokes, &dispatch_path); + let text_input_requires_timeout = event + .downcast_ref::() + .filter(|key_down| key_down.keystroke.key_char.is_some()) + .and_then(|_| self.platform_window.take_input_handler()) + .map_or(false, |mut input_handler| { + let accepts = input_handler.accepts_text_input(self, cx); + self.platform_window.set_input_handler(input_handler); + accepts + }); - window.pending_input_changed(cx); - window.replay_pending_input(to_replay, cx) - }) - .log_err(); - })); + currently_pending.needs_timeout |= + match_result.pending_has_binding || text_input_requires_timeout; + + if currently_pending.needs_timeout { + currently_pending.timer = Some(self.spawn(cx, async move |cx| { + cx.background_executor.timer(Duration::from_secs(1)).await; + cx.update(move |window, cx| { + let Some(currently_pending) = window + .pending_input + .take() + .filter(|pending| pending.focus == window.focus) + else { + return; + }; + + let node_id = window.focus_node_id_in_rendered_frame(window.focus); + let dispatch_path = + window.rendered_frame.dispatch_tree.dispatch_path(node_id); + + let to_replay = window + .rendered_frame + .dispatch_tree + .flush_dispatch(currently_pending.keystrokes, &dispatch_path); + + window.pending_input_changed(cx); + window.replay_pending_input(to_replay, cx) + }) + .log_err(); + })); + } else { + currently_pending.timer = None; + } self.pending_input = Some(currently_pending); self.pending_input_changed(cx); cx.propagate_event = false;