Fix cmd-k left

Conrad Irwin created

Change summary

crates/collab/src/tests/integration_tests.rs | 24 +++++++++++++++++++
crates/collab/src/tests/test_server.rs       |  5 ++++
crates/gpui/src/key_dispatch.rs              | 21 ++++++++++++++--
crates/gpui/src/window.rs                    | 27 ++++++++++++---------
4 files changed, 62 insertions(+), 15 deletions(-)

Detailed changes

crates/collab/src/tests/integration_tests.rs 🔗

@@ -34,6 +34,7 @@ use std::{
         atomic::{AtomicBool, Ordering::SeqCst},
         Arc,
     },
+    time::Duration,
 };
 use unindent::Unindent as _;
 
@@ -5945,3 +5946,26 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
     });
     assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
 }
+
+#[gpui::test]
+async fn test_cmd_k_left(cx: &mut TestAppContext) {
+    let client = TestServer::start1(cx).await;
+    let (workspace, cx) = client.build_test_workspace(cx).await;
+
+    cx.simulate_keystrokes("cmd-n");
+    workspace.update(cx, |workspace, cx| {
+        assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
+    });
+    cx.simulate_keystrokes("cmd-k left");
+    workspace.update(cx, |workspace, cx| {
+        assert!(workspace.items(cx).collect::<Vec<_>>().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.
+    cx.executor().advance_clock(Duration::from_secs(2));
+    cx.simulate_keystrokes("left");
+    workspace.update(cx, |workspace, cx| {
+        assert!(workspace.items(cx).collect::<Vec<_>>().len() == 3);
+    });
+}

crates/collab/src/tests/test_server.rs 🔗

@@ -127,6 +127,11 @@ impl TestServer {
         (client_a, client_b, channel_id)
     }
 
+    pub async fn start1<'a>(cx: &'a mut TestAppContext) -> TestClient {
+        let mut server = Self::start(cx.executor().clone()).await;
+        server.create_client(cx, "user_a").await
+    }
+
     pub async fn reset(&self) {
         self.app_state.db.reset();
         let epoch = self

crates/gpui/src/key_dispatch.rs 🔗

@@ -272,12 +272,17 @@ impl DispatchTree {
             .collect()
     }
 
+    // dispatch_key pushses the next keystroke into any key binding matchers.
+    // any matching bindings are returned in the order that they should be dispatched:
+    // * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first)
+    // * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a
+    // binding for "b", the Editor action fires first).
     pub fn dispatch_key(
         &mut self,
         keystroke: &Keystroke,
         dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
     ) -> KeymatchResult {
-        let mut bindings = SmallVec::new();
+        let mut bindings = SmallVec::<[KeyBinding; 1]>::new();
         let mut pending = false;
 
         let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
@@ -295,9 +300,19 @@ impl DispatchTree {
                 .entry(context_stack.clone())
                 .or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
 
-            let mut result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
+            let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
             pending = result.pending || pending;
-            bindings.append(&mut result.bindings);
+            for new_binding in result.bindings {
+                match bindings
+                    .iter()
+                    .position(|el| el.keystrokes.len() < new_binding.keystrokes.len())
+                {
+                    Some(idx) => {
+                        bindings.insert(idx, new_binding);
+                    }
+                    None => bindings.push(new_binding),
+                }
+            }
             context_stack.pop();
         }
 

crates/gpui/src/window.rs 🔗

@@ -1186,18 +1186,21 @@ impl<'a> WindowContext<'a> {
                     currently_pending.bindings.push(binding);
                 }
 
-                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);
+                // for vim compatibility, we also shoul check "is input handler enabled"
+                if !currently_pending.text.is_empty() || !currently_pending.bindings.is_empty() {
+                    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;