diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index f8a7a9417d..146ba20867 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/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 { diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs index ab25a0387d..ef875bce38 100644 --- a/crates/gpui/src/keymap/matcher.rs +++ b/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; 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, } - diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 173a64b37b..c03384aadf 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/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); } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index aa0df7d757..9b3d04929b 100644 --- a/crates/gpui/src/window.rs +++ b/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; 1]>, + bindings: SmallVec<[KeyBinding; 1]>, focus: Option, timer: Option>, } @@ -1796,7 +1796,7 @@ impl<'a> WindowContext<'a> { .dispatch_path(node_id); if let Some(key_down_event) = event.downcast_ref::() { - 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; } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 63c0c2a192..01d8bec569 100644 --- a/crates/vim/src/motion.rs +++ b/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)] diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index fa2dcb45cd..da990c530c 100644 --- a/crates/vim/src/test.rs +++ b/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 ").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; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 7753a4e850..977d6aa7c6 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/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>>, - neovim: NeovimConnection, + pub(crate) neovim: NeovimConnection, last_set_state: Option, recent_keystrokes: Vec, diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index a2daf7499d..4de0943321 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -42,6 +42,7 @@ pub enum NeovimData { Key(String), Get { state: String, mode: Option }, 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() { diff --git a/crates/vim/test_data/test_comma_w.json b/crates/vim/test_data/test_comma_w.json new file mode 100644 index 0000000000..ac7a91c80c --- /dev/null +++ b/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"}} diff --git a/crates/vim/test_data/test_jk.json b/crates/vim/test_data/test_jk.json new file mode 100644 index 0000000000..bc1a6a4ba5 --- /dev/null +++ b/crates/vim/test_data/test_jk.json @@ -0,0 +1,8 @@ +{"Exec":{"command":"imap jk "}} +{"Put":{"state":"ˇhello"}} +{"Key":"i"} +{"Key":"j"} +{"Key":"o"} +{"Key":"j"} +{"Key":"k"} +{"Get":{"state":"jˇohello","mode":"Normal"}}