Work on tests

Conrad Irwin created

Change summary

crates/gpui/src/key_dispatch.rs                   |  6 
crates/gpui/src/keymap/matcher.rs                 | 21 ++--
crates/gpui/src/platform/test/window.rs           | 24 ++++-
crates/gpui/src/window.rs                         | 34 ++++--
crates/vim/src/motion.rs                          |  4 
crates/vim/src/test.rs                            | 75 ++++++++++++++++
crates/vim/src/test/neovim_backed_test_context.rs |  2 
crates/vim/src/test/neovim_connection.rs          | 27 ++++++
crates/vim/test_data/test_comma_w.json            | 15 +++
crates/vim/test_data/test_jk.json                 |  8 +
10 files changed, 181 insertions(+), 35 deletions(-)

Detailed changes

crates/gpui/src/key_dispatch.rs 🔗

@@ -277,7 +277,7 @@ impl DispatchTree {
         keystroke: &Keystroke,
         dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
     ) -> KeymatchResult {
-        let mut actions = SmallVec::new();
+        let mut bindings = SmallVec::new();
         let mut pending = false;
 
         let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
@@ -297,11 +297,11 @@ impl DispatchTree {
 
             let mut result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
             pending = result.pending || pending;
-            actions.append(&mut result.actions);
+            bindings.append(&mut result.bindings);
             context_stack.pop();
         }
 
-        KeymatchResult { actions, pending }
+        KeymatchResult { bindings, pending }
     }
 
     pub fn has_pending_keystrokes(&self) -> bool {

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

@@ -1,4 +1,4 @@
-use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
+use crate::{KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke};
 use parking_lot::Mutex;
 use smallvec::SmallVec;
 use std::sync::Arc;
@@ -10,7 +10,7 @@ pub(crate) struct KeystrokeMatcher {
 }
 
 pub struct KeymatchResult {
-    pub actions: SmallVec<[Box<dyn Action>; 1]>,
+    pub bindings: SmallVec<[KeyBinding; 1]>,
     pub pending: bool,
 }
 
@@ -24,10 +24,6 @@ impl KeystrokeMatcher {
         }
     }
 
-    pub fn clear_pending(&mut self) {
-        self.pending_keystrokes.clear();
-    }
-
     pub fn has_pending_keystrokes(&self) -> bool {
         !self.pending_keystrokes.is_empty()
     }
@@ -54,7 +50,7 @@ impl KeystrokeMatcher {
         }
 
         let mut pending_key = None;
-        let mut actions = SmallVec::new();
+        let mut bindings = SmallVec::new();
 
         for binding in keymap.bindings().rev() {
             if !keymap.binding_enabled(binding, context_stack) {
@@ -65,7 +61,7 @@ impl KeystrokeMatcher {
                 self.pending_keystrokes.push(candidate.clone());
                 match binding.match_keystrokes(&self.pending_keystrokes) {
                     KeyMatch::Matched => {
-                        actions.push(binding.action.boxed_clone());
+                        bindings.push(binding.clone());
                     }
                     KeyMatch::Pending => {
                         pending_key.get_or_insert(candidate);
@@ -76,6 +72,12 @@ impl KeystrokeMatcher {
             }
         }
 
+        if bindings.len() == 0 && pending_key.is_none() && self.pending_keystrokes.len() > 0 {
+            drop(keymap);
+            self.pending_keystrokes.remove(0);
+            return self.match_keystroke(keystroke, context_stack);
+        }
+
         let pending = if let Some(pending_key) = pending_key {
             self.pending_keystrokes.push(pending_key);
             true
@@ -84,7 +86,7 @@ impl KeystrokeMatcher {
             false
         };
 
-        KeymatchResult { actions, pending }
+        KeymatchResult { bindings, pending }
     }
 }
 
@@ -98,4 +100,3 @@ pub enum KeyMatch {
     Pending,
     Matched,
 }
-

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

@@ -1,8 +1,7 @@
 use crate::{
     px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
-    Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
-    PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds,
-    WindowOptions,
+    Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
+    Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -97,7 +96,19 @@ impl TestWindow {
         result
     }
 
-    pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) {
+    pub fn simulate_keystroke(&mut self, mut keystroke: Keystroke, is_held: bool) {
+        if keystroke.ime_key.is_none()
+            && !keystroke.modifiers.command
+            && !keystroke.modifiers.control
+            && !keystroke.modifiers.function
+        {
+            keystroke.ime_key = Some(if keystroke.modifiers.shift {
+                keystroke.key.to_ascii_uppercase().clone()
+            } else {
+                keystroke.key.clone()
+            })
+        }
+
         if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
             keystroke: keystroke.clone(),
             is_held,
@@ -113,8 +124,9 @@ impl TestWindow {
             );
         };
         drop(lock);
-        let text = keystroke.ime_key.unwrap_or(keystroke.key);
-        input_handler.replace_text_in_range(None, &text);
+        if let Some(text) = keystroke.ime_key.as_ref() {
+            input_handler.replace_text_in_range(None, &text);
+        }
 
         self.0.lock().input_handler = Some(input_handler);
     }

crates/gpui/src/window.rs 🔗

@@ -289,10 +289,10 @@ pub struct Window {
     pub(crate) focus_invalidated: bool,
 }
 
-#[derive(Default)]
+#[derive(Default, Debug)]
 struct PendingInput {
     text: String,
-    actions: SmallVec<[Box<dyn Action>; 1]>,
+    bindings: SmallVec<[KeyBinding; 1]>,
     focus: Option<FocusId>,
     timer: Option<Task<()>>,
 }
@@ -1796,7 +1796,7 @@ impl<'a> WindowContext<'a> {
             .dispatch_path(node_id);
 
         if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
-            let KeymatchResult { actions, pending } = self
+            let KeymatchResult { bindings, pending } = self
                 .window
                 .rendered_frame
                 .dispatch_tree
@@ -1812,8 +1812,8 @@ impl<'a> WindowContext<'a> {
                 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);
+                for binding in bindings {
+                    currently_pending.bindings.push(binding);
                 }
 
                 currently_pending.timer = Some(self.spawn(|mut cx| async move {
@@ -1832,20 +1832,30 @@ impl<'a> WindowContext<'a> {
                 self.propagate_event = false;
                 return;
             } else if let Some(currently_pending) = self.window.pending_input.take() {
-                if actions.is_empty() {
+                // if you have bound , to one thing, and ,w to another.
+                // then typing ,i should trigger the comma actions, then the i actions.
+                // in that scenario "binding.keystrokes" is "i" and "pending.keystrokes" is ",".
+                // on the other hand if you type ,, it should not trigger the , action.
+                // in that scenario "binding.keystrokes" is ",w" and "pending.keystrokes" is ",".
+
+                if bindings.iter().all(|binding| {
+                    currently_pending.bindings.iter().all(|pending| {
+                        dbg!(!dbg!(binding.keystrokes()).starts_with(dbg!(&pending.keystrokes)))
+                    })
+                }) {
                     self.replay_pending_input(currently_pending)
                 }
             }
 
-            if !actions.is_empty() {
+            if !bindings.is_empty() {
                 self.clear_pending_keystrokes();
             }
 
             self.propagate_event = true;
-            for action in actions {
-                self.dispatch_action_on_node(node_id, action.boxed_clone());
+            for binding in bindings {
+                self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
                 if !self.propagate_event {
-                    self.dispatch_keystroke_observers(event, Some(action));
+                    self.dispatch_keystroke_observers(event, Some(binding.action));
                     return;
                 }
             }
@@ -1903,8 +1913,8 @@ impl<'a> WindowContext<'a> {
         }
 
         self.propagate_event = true;
-        for action in currently_pending.actions {
-            self.dispatch_action_on_node(node_id, action);
+        for binding in currently_pending.bindings {
+            self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
             if !self.propagate_event {
                 return;
             }

crates/vim/src/motion.rs 🔗

@@ -73,9 +73,9 @@ pub(crate) struct Up {
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
-struct Down {
+pub(crate) struct Down {
     #[serde(default)]
-    display_lines: bool,
+    pub(crate) display_lines: bool,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]

crates/vim/src/test.rs 🔗

@@ -3,8 +3,11 @@ mod neovim_backed_test_context;
 mod neovim_connection;
 mod vim_test_context;
 
+use std::time::Duration;
+
 use command_palette::CommandPalette;
 use editor::DisplayPoint;
+use gpui::{Action, KeyBinding};
 pub use neovim_backed_binding_test_context::*;
 pub use neovim_backed_test_context::*;
 pub use vim_test_context::*;
@@ -12,7 +15,7 @@ pub use vim_test_context::*;
 use indoc::indoc;
 use search::BufferSearchBar;
 
-use crate::{state::Mode, ModeIndicator};
+use crate::{insert::NormalBefore, motion, normal::InsertLineBelow, state::Mode, ModeIndicator};
 
 #[gpui::test]
 async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
@@ -774,3 +777,73 @@ async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) {
         Mode::Visual,
     );
 }
+
+#[gpui::test]
+async fn test_jk(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.update(|cx| {
+        cx.bind_keys([KeyBinding::new(
+            "j k",
+            NormalBefore,
+            Some("vim_mode == insert"),
+        )])
+    });
+    cx.neovim.exec("imap jk <esc>").await;
+
+    cx.set_shared_state("ˇhello").await;
+    cx.simulate_shared_keystrokes(["i", "j", "o", "j", "k"])
+        .await;
+    cx.assert_shared_state("jˇohello").await;
+}
+
+#[gpui::test]
+async fn test_jk_delay(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.update(|cx| {
+        cx.bind_keys([KeyBinding::new(
+            "j k",
+            NormalBefore,
+            Some("vim_mode == insert"),
+        )])
+    });
+
+    cx.set_state("ˇhello", Mode::Normal);
+    cx.simulate_keystrokes(["i", "j"]);
+    cx.executor().advance_clock(Duration::from_millis(500));
+    cx.run_until_parked();
+    cx.assert_state("ˇhello", Mode::Insert);
+    cx.executor().advance_clock(Duration::from_millis(500));
+    cx.run_until_parked();
+    cx.assert_state("jˇhello", Mode::Insert);
+    cx.simulate_keystrokes(["k", "j", "k"]);
+    cx.assert_state("jˇkhello", Mode::Normal);
+}
+
+#[gpui::test]
+async fn test_comma_w(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.update(|cx| {
+        cx.bind_keys([KeyBinding::new(
+            ", w",
+            motion::Down {
+                display_lines: false,
+            },
+            Some("vim_mode == normal"),
+        )])
+    });
+    cx.neovim.exec("map ,w j").await;
+
+    cx.set_shared_state("ˇhello hello\nhello hello").await;
+    cx.simulate_shared_keystrokes(["f", "o", ";", ",", "w"])
+        .await;
+    cx.assert_shared_state("hello hello\nhello hellˇo").await;
+
+    cx.set_shared_state("ˇhello hello\nhello hello").await;
+    cx.simulate_shared_keystrokes(["f", "o", ";", ",", "i"])
+        .await;
+    cx.assert_shared_state("hellˇo hello\nhello hello").await;
+    cx.assert_shared_mode(Mode::Insert).await;
+}

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -52,7 +52,7 @@ pub struct NeovimBackedTestContext {
     // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
     // bindings are exempted. If None, all bindings are ignored for that insertion text.
     exemptions: HashMap<String, Option<HashSet<String>>>,
-    neovim: NeovimConnection,
+    pub(crate) neovim: NeovimConnection,
 
     last_set_state: Option<String>,
     recent_keystrokes: Vec<String>,

crates/vim/src/test/neovim_connection.rs 🔗

@@ -42,6 +42,7 @@ pub enum NeovimData {
     Key(String),
     Get { state: String, mode: Option<Mode> },
     ReadRegister { name: char, value: String },
+    Exec { command: String },
     SetOption { value: String },
 }
 
@@ -269,6 +270,32 @@ impl NeovimConnection {
         );
     }
 
+    #[cfg(feature = "neovim")]
+    pub async fn exec(&mut self, value: &str) {
+        self.nvim
+            .command_output(format!("{}", value).as_str())
+            .await
+            .unwrap();
+
+        self.data.push_back(NeovimData::Exec {
+            command: value.to_string(),
+        })
+    }
+
+    #[cfg(not(feature = "neovim"))]
+    pub async fn exec(&mut self, value: &str) {
+        if let Some(NeovimData::Get { .. }) = self.data.front() {
+            self.data.pop_front();
+        };
+        assert_eq!(
+            self.data.pop_front(),
+            Some(NeovimData::Exec {
+                command: value.to_string(),
+            }),
+            "operation does not match recorded script. re-record with --features=neovim"
+        );
+    }
+
     #[cfg(not(feature = "neovim"))]
     pub async fn read_register(&mut self, register: char) -> String {
         if let Some(NeovimData::Get { .. }) = self.data.front() {

crates/vim/test_data/test_comma_w.json 🔗

@@ -0,0 +1,15 @@
+{"Exec":{"command":"map ,w j"}}
+{"Put":{"state":"ˇhello hello\nhello hello"}}
+{"Key":"f"}
+{"Key":"o"}
+{"Key":";"}
+{"Key":","}
+{"Key":"w"}
+{"Get":{"state":"hello hello\nhello hellˇo","mode":"Normal"}}
+{"Put":{"state":"ˇhello hello\nhello hello"}}
+{"Key":"f"}
+{"Key":"o"}
+{"Key":";"}
+{"Key":","}
+{"Key":"i"}
+{"Get":{"state":"hellˇo hello\nhello hello","mode":"Insert"}}

crates/vim/test_data/test_jk.json 🔗

@@ -0,0 +1,8 @@
+{"Exec":{"command":"imap jk <esc>"}}
+{"Put":{"state":"ˇhello"}}
+{"Key":"i"}
+{"Key":"j"}
+{"Key":"o"}
+{"Key":"j"}
+{"Key":"k"}
+{"Get":{"state":"jˇohello","mode":"Normal"}}