diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index d21eeaee78..a48d55c19f 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -117,7 +117,7 @@ pub(crate) struct Replay { } #[derive(Default, Debug)] -pub(crate) struct MatchResult { +pub(crate) struct DispatchResult { pub(crate) pending: SmallVec<[Keystroke; 1]>, pub(crate) bindings: SmallVec<[KeyBinding; 1]>, pub(crate) to_replay: SmallVec<[Replay; 1]>, @@ -402,164 +402,95 @@ impl DispatchTree { keymap .bindings_for_action(action) .filter(|binding| { - for i in 0..context_stack.len() { - let context = &context_stack[0..=i]; - if keymap.binding_enabled(binding, context) { - return true; - } - } - false + let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, &context_stack); + bindings + .iter() + .next() + .is_some_and(|b| b.action.partial_eq(action)) }) .cloned() .collect() } - fn match_input( + fn bindings_for_input( &self, input: &[Keystroke], dispatch_path: &SmallVec<[DispatchNodeId; 32]>, ) -> (SmallVec<[KeyBinding; 1]>, bool) { - let keymap = self.keymap.borrow(); + let context_stack: SmallVec<[KeyContext; 4]> = dispatch_path + .iter() + .filter_map(|node_id| self.node(*node_id).context.clone()) + .collect(); - // We want the following behaviour: - // - If there are multiple bindings matching completely, - // return them in order of depth in the context. - // - If there are some bindings that are pending, - // they take priority over bindings defined earlier in the keymap. - - let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new(); - for node_id in dispatch_path { - let node = self.node(*node_id); - - if let Some(context) = node.context.clone() { - context_stack.push(context); - } - } - - let possibilities = keymap.bindings().rev().filter_map(|binding| { - binding - .match_keystrokes(input) - .map(|pending| (binding, pending)) - }); - - let mut bindings: SmallVec<[(KeyBinding, usize); 1]> = SmallVec::new(); - let mut first_pending = None; - - for (binding, pending) in possibilities { - for depth in (0..=context_stack.len()).rev() { - if keymap.binding_enabled(binding, &context_stack[0..depth]) { - if first_pending.is_none() { - first_pending = Some(pending); - } - if !pending { - bindings.push((binding.clone(), depth)); - } - } - } - } - bindings.sort_by(|a, b| a.1.cmp(&b.1).reverse()); - let bindings = bindings.into_iter().map(|(binding, _)| binding).collect(); - - // Precedence: - // - if the user has defined a longer keystroke (e.g. ,=), we should say 'pending' for the builtin ',' - // - if the user has defined a shorter keystroke (e.g. ctrl-w), we should say 'not pending' for the builtin `ctrl-w q` - // - hypoethetically, if they have defined *both*, then what? - - // - ,: vim::Next - // - ,=: custom wev - // - ,: custom wev - - // -ctrl-w q: vim::Close - // -ctrl-w: terminal::Close - // -ctrl-w q: vim::Close - - return (bindings, first_pending.unwrap_or_default()); + self.keymap + .borrow() + .bindings_for_input(&input, &context_stack) } /// dispatch_key processes the keystroke /// input should be set to the value of `pending` from the previous call to dispatch_key. - /// In the common case, input will be empty, and the MatchResult will return just actions to do. - /// If input is not empty, then either: - /// - input + keystroke matches a set of bindings, which will be returned. - /// - input + keystroke is still a prefix, so only pending - /// - input + keystroke did not match anything. In that case, the caller will need to replay - /// what the original input should have done before handling any of the returned actions. + /// This returns three instructions to the input handler: + /// - bindings: any bindings to execute before processing this keystroke + /// - pending: the new set of pending keystrokes to store + /// - to_replay: any keystroke that had been pushed to pending, but are no-longer matched, + /// these should be replayed first. pub fn dispatch_key( &mut self, mut input: SmallVec<[Keystroke; 1]>, keystroke: Keystroke, dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> MatchResult { - println!("---------- {:?} {:?} ---------------", input, keystroke); + ) -> DispatchResult { input.push(keystroke.clone()); - let (bindings, pending) = self.match_input(&input, dispatch_path); + let (bindings, pending) = self.bindings_for_input(&input, dispatch_path); if pending { - return MatchResult { + return DispatchResult { pending: input, ..Default::default() }; - } - - if !bindings.is_empty() { - return MatchResult { + } else if !bindings.is_empty() { + return DispatchResult { bindings, ..Default::default() }; + } else if input.len() == 1 { + return DispatchResult::default(); } - input.pop(); - if input.len() == 0 { - return MatchResult::default(); - } - let mut to_replay: SmallVec<[Replay; 1]> = Default::default(); + let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); - // At this point we know that we have multiple keys in flight, and the - // full set matches nothing. - // Try matching with prefixes of the full set, and if that doesn't work, - // replay the first input key, and recurse. - - for last in (0..input.len()).rev() { - let (bindings, _) = self.match_input(&input[0..=last], dispatch_path); - if !bindings.is_empty() { - to_replay.push(Replay { - keystroke: input.drain(0..=last).last().unwrap(), - bindings, - }); - break; - } - if last == 0 { - to_replay.push(Replay { - keystroke: input.remove(0), - ..Default::default() - }); - break; - } - } - - let mut result = self.dispatch_key(input, keystroke, dispatch_path); + let mut result = self.dispatch_key(suffix, keystroke, dispatch_path); to_replay.extend(result.to_replay); result.to_replay = to_replay; return result; } - /// flush converts any remaining pending input to replay events. - /// This is called when a user hits the timeout mid-way through typing a key. - pub fn flush( + /// If the user types a matching prefix of a binding and then waits for a timeout + /// flush_dispatch() converts any previously pending input to replay events. + pub fn flush_dispatch( + &mut self, + input: SmallVec<[Keystroke; 1]>, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> SmallVec<[Replay; 1]> { + let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); + + if suffix.len() > 0 { + to_replay.extend(self.flush_dispatch(suffix, dispatch_path)) + } + + to_replay + } + + /// Converts the longest prefix of input to a replay event and returns the rest. + fn replay_prefix( &mut self, mut input: SmallVec<[Keystroke; 1]>, dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> SmallVec<[Replay; 1]> { + ) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) { let mut to_replay: SmallVec<[Replay; 1]> = Default::default(); - - // At this point we know that we have multiple keys in flight, and the - // full set matches nothing. - // Try matching with prefixes of the full set, and if that doesn't work, - // replay the first input key, and recurse. - for last in (0..input.len()).rev() { - let (bindings, _) = self.match_input(&input[0..=last], dispatch_path); + let (bindings, _) = self.bindings_for_input(&input[0..=last], dispatch_path); if !bindings.is_empty() { to_replay.push(Replay { keystroke: input.drain(0..=last).last().unwrap(), @@ -567,20 +498,14 @@ impl DispatchTree { }); break; } - if last == 0 { - to_replay.push(Replay { - keystroke: input.remove(0), - ..Default::default() - }); - break; - } } - - if input.len() > 0 { - to_replay.extend(self.flush(input, dispatch_path)) + if to_replay.is_empty() { + to_replay.push(Replay { + keystroke: input.remove(0), + ..Default::default() + }); } - - to_replay + (input, to_replay) } pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index cf47b70387..f17300d9e7 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -5,7 +5,7 @@ pub use binding::*; pub use context::*; use crate::{Action, Keystroke, NoAction}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use smallvec::SmallVec; use std::any::{Any, TypeId}; @@ -19,8 +19,6 @@ pub struct KeymapVersion(usize); pub struct Keymap { bindings: Vec, binding_indices_by_action_id: HashMap>, - disabled_keystrokes: - HashMap, HashSet>>, version: KeymapVersion, } @@ -39,22 +37,13 @@ impl Keymap { /// Add more bindings to the keymap. pub fn add_bindings>(&mut self, bindings: T) { - let no_action_id = (NoAction {}).type_id(); - for binding in bindings { let action_id = binding.action().as_any().type_id(); - if action_id == no_action_id { - self.disabled_keystrokes - .entry(binding.keystrokes) - .or_default() - .insert(binding.context_predicate); - } else { - self.binding_indices_by_action_id - .entry(action_id) - .or_default() - .push(self.bindings.len()); - self.bindings.push(binding); - } + self.binding_indices_by_action_id + .entry(action_id) + .or_default() + .push(self.bindings.len()); + self.bindings.push(binding); } self.version.0 += 1; @@ -64,7 +53,6 @@ impl Keymap { pub fn clear(&mut self) { self.bindings.clear(); self.binding_indices_by_action_id.clear(); - self.disabled_keystrokes.clear(); self.version.0 += 1; } @@ -87,8 +75,66 @@ impl Keymap { .filter(move |binding| binding.action().partial_eq(action)) } + /// bindings_for_input returns a list of bindings that match the given input, + /// and a boolean indicating whether or not more bindings might match if + /// the input was longer. + /// + /// Precedence is defined by the depth in the tree (matches on the Editor take + /// precedence over matches on the Pane, then the Workspace, etc.). Bindings with + /// no context are treated as the same as the deepest context. + /// + /// In the case of multiple bindings at the same depth, the ones defined later in the + /// keymap take precedence (so user bindings take precedence over built-in bindings). + /// + /// If a user has disabled a binding with `"x": null` it will not be returned. Disabled + /// bindings are evaluated with the same precedence rules so you can disable a rule in + /// a given context only. + /// + /// In the case of multi-key bindings, the + pub fn bindings_for_input( + &self, + input: &[Keystroke], + context_stack: &[KeyContext], + ) -> (SmallVec<[KeyBinding; 1]>, bool) { + let possibilities = self.bindings().rev().filter_map(|binding| { + binding + .match_keystrokes(input) + .map(|pending| (binding, pending)) + }); + + let mut bindings: SmallVec<[(KeyBinding, usize); 1]> = SmallVec::new(); + let mut is_pending = None; + + 'outer: for (binding, pending) in possibilities { + for depth in (0..=context_stack.len()).rev() { + if self.binding_enabled(binding, &context_stack[0..depth]) { + if is_pending.is_none() { + is_pending = Some(pending); + } + if !pending { + bindings.push((binding.clone(), depth)); + continue 'outer; + } + } + } + } + bindings.sort_by(|a, b| a.1.cmp(&b.1).reverse()); + let bindings = bindings + .into_iter() + .map_while(|(binding, _)| { + if binding.action.as_any().type_id() == (NoAction {}).type_id() { + None + } else { + Some(binding) + } + }) + .collect(); + + return (bindings, is_pending.unwrap_or_default()); + } + /// Check if the given binding is enabled, given a certain key context. - pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool { + fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool { // If binding has a context predicate, it must match the current context, if let Some(predicate) = &binding.context_predicate { if !predicate.eval(context) { @@ -96,22 +142,6 @@ impl Keymap { } } - if let Some(disabled_predicates) = self.disabled_keystrokes.get(&binding.keystrokes) { - for disabled_predicate in disabled_predicates { - match disabled_predicate { - // The binding must not be globally disabled. - None => return false, - - // The binding must not be disabled in the current context. - Some(predicate) => { - if predicate.eval(context) { - return false; - } - } - } - } - } - true } } @@ -166,16 +196,37 @@ mod tests { keymap.add_bindings(bindings.clone()); // binding is only enabled in a specific context - assert!(!keymap.binding_enabled(&bindings[0], &[KeyContext::parse("barf").unwrap()])); - assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("editor").unwrap()])); + assert!(keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("barf").unwrap()], + ) + .0 + .is_empty()); + assert!(!keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ) + .0 + .is_empty()); // binding is disabled in a more specific context - assert!(!keymap.binding_enabled( - &bindings[0], - &[KeyContext::parse("editor mode=full").unwrap()] - )); + assert!(keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("editor mode=full").unwrap()], + ) + .0 + .is_empty()); // binding is globally disabled - assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf").unwrap()])); + assert!(keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-b").unwrap()], + &[KeyContext::parse("barf").unwrap()], + ) + .0 + .is_empty()); } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 813ce91d39..c5bada903e 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3253,19 +3253,6 @@ impl<'a> WindowContext<'a> { currently_pending = PendingInput::default(); } - // base case: - // d: returns [`delete`, ''] - // j: returns [None, 'j'] - // k: returns [`escape`, ''] - // l: returns ['j`, `escape`, ''] - // ctrl-s: returns ['j`, `save`, ''] - - // c: returns ['', None, 'c'] - // a: returns ['', None, 'ca'] - // c: returns ['ca', None, 'c'] - // a: returns ['', None' ,'ca'] - // t: returns ['', `dispatch`, ''] - let match_result = self.window.rendered_frame.dispatch_tree.dispatch_key( currently_pending.keystrokes, keystroke, @@ -3300,7 +3287,7 @@ impl<'a> WindowContext<'a> { .window .rendered_frame .dispatch_tree - .flush(currently_pending.keystrokes, &dispatch_path); + .flush_dispatch(currently_pending.keystrokes, &dispatch_path); cx.replay_pending_input(to_replay) }) @@ -3313,7 +3300,6 @@ impl<'a> WindowContext<'a> { } self.pending_input_changed(); - self.propagate_event = true; for binding in match_result.bindings { self.dispatch_action_on_node(node_id, binding.action.as_ref()); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 869db4b7a0..5d027aec73 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -6,7 +6,7 @@ use std::time::Duration; use collections::HashMap; use command_palette::CommandPalette; -use editor::{display_map::DisplayRow, DisplayPoint}; +use editor::{actions::DeleteLine, display_map::DisplayRow, DisplayPoint}; use futures::StreamExt; use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext}; pub use neovim_backed_test_context::*; @@ -1393,3 +1393,23 @@ async fn test_remap_nested_pineapple(cx: &mut gpui::TestAppContext) { cx.simulate_shared_keystrokes("i p i n e a p p l e").await; cx.shared_state().await.assert_eq("🍍ˇ"); } + +#[gpui::test] +async fn test_escape_while_waiting(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state("ˇhi").await; + cx.simulate_shared_keystrokes("\" + escape x").await; + cx.shared_state().await.assert_eq("ˇi"); +} + +#[gpui::test] +async fn test_ctrl_w_override(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.update(|cx| { + cx.bind_keys([KeyBinding::new("ctrl-w", DeleteLine, None)]); + }); + cx.neovim.exec("map D").await; + cx.set_shared_state("ˇhi").await; + cx.simulate_shared_keystrokes("ctrl-w").await; + cx.shared_state().await.assert_eq("ˇ"); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 74c17f7bf1..74c3d9a99a 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -400,6 +400,7 @@ impl Vim { state.last_mode = last_mode; state.mode = mode; state.operator_stack.clear(); + state.selected_register.take(); if mode == Mode::Normal || mode != last_mode { state.current_tx.take(); state.current_anchor.take(); diff --git a/crates/vim/test_data/test_ctrl_w_override.json b/crates/vim/test_data/test_ctrl_w_override.json new file mode 100644 index 0000000000..fe8ae94a77 --- /dev/null +++ b/crates/vim/test_data/test_ctrl_w_override.json @@ -0,0 +1,4 @@ +{"Exec":{"command":"map D"}} +{"Put":{"state":"ˇhi"}} +{"Key":"ctrl-w"} +{"Get":{"state":"ˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_escape_while_waiting.json b/crates/vim/test_data/test_escape_while_waiting.json new file mode 100644 index 0000000000..d81822cf79 --- /dev/null +++ b/crates/vim/test_data/test_escape_while_waiting.json @@ -0,0 +1,6 @@ +{"Put":{"state":"ˇhi"}} +{"Key":"\""} +{"Key":"+"} +{"Key":"escape"} +{"Key":"x"} +{"Get":{"state":"ˇi","mode":"Normal"}}