Update handling of 'pending' keys

Conrad Irwin created

Before this change if you had a matching binding and a pending key,
the matching binding happened unconditionally.

Now we will wait a second before triggering that binding to give you
time to complete the action.

Change summary

crates/gpui/src/key_dispatch.rs         |  14 ++-
crates/gpui/src/keymap/matcher.rs       |  21 +++--
crates/gpui/src/platform.rs             |   9 ++
crates/gpui/src/platform/keystroke.rs   |  26 +++---
crates/gpui/src/platform/mac/window.rs  |   4 
crates/gpui/src/platform/test/window.rs |   5 
crates/gpui/src/window.rs               | 102 +++++++++++++++++++++++---
7 files changed, 137 insertions(+), 44 deletions(-)

Detailed changes

crates/gpui/src/key_dispatch.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, KeyMatch,
-    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+    Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap,
+    KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext,
 };
 use collections::FxHashMap;
 use parking_lot::Mutex;
@@ -276,8 +276,9 @@ impl DispatchTree {
         &mut self,
         keystroke: &Keystroke,
         dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
-    ) -> SmallVec<[KeyBinding; 1]> {
+    ) -> KeymatchResult {
         let mut actions = SmallVec::new();
+        let mut pending = false;
 
         let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
         for node_id in dispatch_path {
@@ -294,12 +295,13 @@ impl DispatchTree {
                 .entry(context_stack.clone())
                 .or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
 
-            let mut matches = keystroke_matcher.match_keystroke(keystroke, &context_stack);
-            actions.append(&mut matches);
+            let mut result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
+            pending = result.pending || pending;
+            actions.append(&mut result.actions);
             context_stack.pop();
         }
 
-        actions
+        KeymatchResult { actions, pending }
     }
 
     pub fn has_pending_keystrokes(&self) -> bool {

crates/gpui/src/keymap/matcher.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Action, KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke};
+use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
 use parking_lot::Mutex;
 use smallvec::SmallVec;
 use std::sync::Arc;
@@ -9,6 +9,11 @@ pub struct KeystrokeMatcher {
     keymap_version: KeymapVersion,
 }
 
+pub struct KeymatchResult {
+    pub actions: SmallVec<[Box<dyn Action>; 1]>,
+    pub pending: bool,
+}
+
 impl KeystrokeMatcher {
     pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
         let keymap_version = keymap.lock().version();
@@ -40,7 +45,7 @@ impl KeystrokeMatcher {
         &mut self,
         keystroke: &Keystroke,
         context_stack: &[KeyContext],
-    ) -> SmallVec<[KeyBinding; 1]> {
+    ) -> KeymatchResult {
         let keymap = self.keymap.lock();
         // Clear pending keystrokes if the keymap has changed since the last matched keystroke.
         if keymap.version() != self.keymap_version {
@@ -49,7 +54,7 @@ impl KeystrokeMatcher {
         }
 
         let mut pending_key = None;
-        let mut found = SmallVec::new();
+        let mut actions = SmallVec::new();
 
         for binding in keymap.bindings().rev() {
             if !keymap.binding_enabled(binding, context_stack) {
@@ -60,7 +65,7 @@ impl KeystrokeMatcher {
                 self.pending_keystrokes.push(candidate.clone());
                 match binding.match_keystrokes(&self.pending_keystrokes) {
                     KeyMatch::Matched => {
-                        found.push(binding.clone());
+                        actions.push(binding.action.boxed_clone());
                     }
                     KeyMatch::Pending => {
                         pending_key.get_or_insert(candidate);
@@ -71,15 +76,15 @@ impl KeystrokeMatcher {
             }
         }
 
-        if !found.is_empty() {
-            self.pending_keystrokes.clear();
-        } else if let Some(pending_key) = pending_key {
+        let pending = if let Some(pending_key) = pending_key {
             self.pending_keystrokes.push(pending_key);
+            true
         } else {
             self.pending_keystrokes.clear();
+            false
         };
 
-        found
+        KeymatchResult { actions, pending }
     }
 }
 

crates/gpui/src/platform.rs 🔗

@@ -359,7 +359,7 @@ impl PlatformInputHandler {
         self.cx
             .update(|cx| {
                 self.handler
-                    .replace_text_in_range(replacement_range, text, cx)
+                    .replace_text_in_range(replacement_range, text, cx);
             })
             .ok();
     }
@@ -392,6 +392,13 @@ impl PlatformInputHandler {
             .ok()
             .flatten()
     }
+
+    pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) {
+        let Some(range) = self.handler.selected_text_range(cx) else {
+            return;
+        };
+        self.handler.replace_text_in_range(Some(range), &input, cx);
+    }
 }
 
 /// Zed's interface for handling text input from the platform's IME system

crates/gpui/src/platform/keystroke.rs 🔗

@@ -30,24 +30,26 @@ impl Keystroke {
     pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
         let mut possibilities = SmallVec::new();
         match self.ime_key.as_ref() {
-            None => possibilities.push(self.clone()),
             Some(ime_key) => {
-                possibilities.push(Keystroke {
-                    modifiers: Modifiers {
-                        control: self.modifiers.control,
-                        alt: false,
-                        shift: false,
-                        command: false,
-                        function: false,
-                    },
-                    key: ime_key.to_string(),
-                    ime_key: None,
-                });
+                if ime_key != &self.key {
+                    possibilities.push(Keystroke {
+                        modifiers: Modifiers {
+                            control: self.modifiers.control,
+                            alt: false,
+                            shift: false,
+                            command: false,
+                            function: false,
+                        },
+                        key: ime_key.to_string(),
+                        ime_key: None,
+                    });
+                }
                 possibilities.push(Keystroke {
                     ime_key: None,
                     ..self.clone()
                 });
             }
+            None => possibilities.push(self.clone()),
         }
         possibilities
     }

crates/gpui/src/platform/mac/window.rs 🔗

@@ -1542,9 +1542,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
                 replacement_range,
                 text: text.to_string(),
             });
-            if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key {
-                pending_key_down.0.keystroke.ime_key = Some(text.to_string());
-            }
+            pending_key_down.0.keystroke.ime_key = Some(text.to_string());
             window_state.lock().pending_key_down = Some(pending_key_down);
         }
     }

crates/gpui/src/platform/test/window.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
     px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
-    Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
-    Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
+    Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
+    PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds,
+    WindowOptions,
 };
 use collections::HashMap;
 use parking_lot::Mutex;

crates/gpui/src/window.rs 🔗

@@ -4,13 +4,13 @@ use crate::{
     DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect,
     Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
     ImageData, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent,
-    KeystrokeEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton,
-    MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay,
-    PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel,
-    Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
-    Shadow, SharedString, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine,
-    Task, Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions,
-    SUBPIXEL_VARIANTS,
+    KeymatchResult, KeystrokeEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite,
+    MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas,
+    PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite,
+    PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
+    Scene, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, Surface,
+    TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
+    WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::{FxHashMap, FxHashSet};
@@ -38,6 +38,7 @@ use std::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
     },
+    time::Duration,
 };
 use util::{post_inc, ResultExt};
 
@@ -282,11 +283,20 @@ pub struct Window {
     activation_observers: SubscriberSet<(), AnyObserver>,
     pub(crate) focus: Option<FocusId>,
     focus_enabled: bool,
+    pending_input: Option<PendingInput>,
 
     #[cfg(any(test, feature = "test-support"))]
     pub(crate) focus_invalidated: bool,
 }
 
+#[derive(Default)]
+struct PendingInput {
+    text: String,
+    actions: SmallVec<[Box<dyn Action>; 1]>,
+    focus: Option<FocusId>,
+    timer: Option<Task<()>>,
+}
+
 pub(crate) struct ElementStateBox {
     inner: Box<dyn Any>,
     parent_view_id: EntityId,
@@ -506,6 +516,7 @@ impl Window {
             activation_observers: SubscriberSet::new(),
             focus: None,
             focus_enabled: true,
+            pending_input: None,
 
             #[cfg(any(test, feature = "test-support"))]
             focus_invalidated: false,
@@ -1785,21 +1796,56 @@ impl<'a> WindowContext<'a> {
             .dispatch_path(node_id);
 
         if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
-            let bindings = self
+            let KeymatchResult { actions, pending } = self
                 .window
                 .rendered_frame
                 .dispatch_tree
                 .dispatch_key(&key_down_event.keystroke, &dispatch_path);
 
-            if !bindings.is_empty() {
+            if pending {
+                let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
+                if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
+                {
+                    currently_pending = PendingInput::default();
+                }
+                currently_pending.focus = self.window.focus;
+                if let Some(new_text) = &key_down_event.keystroke.ime_key.as_ref() {
+                    currently_pending.text += new_text
+                }
+                for action in actions {
+                    currently_pending.actions.push(action);
+                }
+
+                currently_pending.timer = Some(self.spawn(|mut cx| async move {
+                    cx.background_executor.timer(Duration::from_secs(1)).await;
+                    cx.update(move |cx| {
+                        cx.clear_pending_keystrokes();
+                        let Some(currently_pending) = cx.window.pending_input.take() else {
+                            return;
+                        };
+                        cx.replay_pending_input(currently_pending)
+                    })
+                    .log_err();
+                }));
+                self.window.pending_input = Some(currently_pending);
+
+                self.propagate_event = false;
+                return;
+            } else if let Some(currently_pending) = self.window.pending_input.take() {
+                if actions.is_empty() {
+                    self.replay_pending_input(currently_pending)
+                }
+            }
+
+            if !actions.is_empty() {
                 self.clear_pending_keystrokes();
             }
 
             self.propagate_event = true;
-            for binding in bindings {
-                self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
+            for action in actions {
+                self.dispatch_action_on_node(node_id, action.boxed_clone());
                 if !self.propagate_event {
-                    self.dispatch_keystroke_observers(event, Some(binding.action));
+                    self.dispatch_keystroke_observers(event, Some(action));
                     return;
                 }
             }
@@ -1840,6 +1886,38 @@ impl<'a> WindowContext<'a> {
             .has_pending_keystrokes()
     }
 
+    fn replay_pending_input(&mut self, currently_pending: PendingInput) {
+        let node_id = self
+            .window
+            .focus
+            .and_then(|focus_id| {
+                self.window
+                    .rendered_frame
+                    .dispatch_tree
+                    .focusable_node_id(focus_id)
+            })
+            .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
+
+        if self.window.focus != currently_pending.focus {
+            return;
+        }
+
+        self.propagate_event = true;
+        for action in currently_pending.actions {
+            self.dispatch_action_on_node(node_id, action);
+            if !self.propagate_event {
+                return;
+            }
+        }
+
+        if !currently_pending.text.is_empty() {
+            if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
+                input_handler.flush_pending_input(&currently_pending.text, self);
+                self.window.platform_window.set_input_handler(input_handler)
+            }
+        }
+    }
+
     fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
         let dispatch_path = self
             .window