Detailed changes
@@ -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 {
@@ -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,
}
-
@@ -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);
}
@@ -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;
}
@@ -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)]
@@ -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;
+}
@@ -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>,
@@ -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() {
@@ -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"}}
@@ -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"}}