From 2e23527e09edc9cb2dd4b8fc386c32d8a72b6b9a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 22 Jul 2024 10:46:16 -0600 Subject: [PATCH] Refactor key dispatch (#14942) Simplify key dispatch code. Previously we would maintain a cache of key matchers for each context that would store the pending input. For the last while we've also stored the typed prefix on the window. This is redundant, we only need one copy, so now it's just stored on the window, which lets us avoid the boilerplate of keeping all the matchers in sync. This stops us from losing multikey bindings when the context on a node changes (#11009) (though we still interrupt multikey bindings if the focus changes). While in the code, I fixed up a few other things with multi-key bindings that were causing problems: Previously we assumed that all multi-key bindings took precedence over any single-key binding, now this is done such that if a user binds a single-key binding, it will take precedence over all system-defined multi-key bindings (irrespective of the depth in the context tree). This was a common cause of confusion for new users trying to bind to `cmd-k` or `ctrl-w` in vim mode (#13543). Previously after a pending multi-key keystroke failed to match, we would drop the prefix if it was an input event. Now we correctly replay it (#14725). Release Notes: - Fixed multi-key shortcuts not working across completion menu changes ([#11009](https://github.com/zed-industries/zed/issues/11009)) - Fixed multi-key shortcuts discarding earlier input ([#14445](https://github.com/zed-industries/zed/pull/14445)) - vim: Fixed `jk` binding preventing you from repeating `j` ([#14725](https://github.com/zed-industries/zed/issues/14725)) - vim: Fixed `escape` in normal mode to also clear the selected register. - Fixed key maps so user-defined mappings take precedence over builtin multi-key mappings ([#13543](https://github.com/zed-industries/zed/issues/13543)) - Fixed a bug where overridden shortcuts would still show in the Command Palette --- crates/gpui/src/app.rs | 9 +- crates/gpui/src/key_dispatch.rs | 199 +++++++++-------- crates/gpui/src/keymap.rs | 137 ++++++++---- crates/gpui/src/keymap/binding.rs | 23 +- crates/gpui/src/keymap/matcher.rs | 102 --------- crates/gpui/src/platform/keystroke.rs | 43 ++-- crates/gpui/src/window.rs | 210 ++++++------------ crates/vim/src/test.rs | 98 +++++++- crates/vim/src/vim.rs | 1 + .../vim/test_data/test_ctrl_w_override.json | 4 + .../test_data/test_escape_while_waiting.json | 6 + .../test_remap_adjacent_dog_cat.json | 24 ++ .../test_remap_nested_pineapple.json | 28 +++ 13 files changed, 462 insertions(+), 422 deletions(-) delete mode 100644 crates/gpui/src/keymap/matcher.rs create mode 100644 crates/vim/test_data/test_ctrl_w_override.json create mode 100644 crates/vim/test_data/test_escape_while_waiting.json create mode 100644 crates/vim/test_data/test_remap_adjacent_dog_cat.json create mode 100644 crates/vim/test_data/test_remap_nested_pineapple.json diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 7da2fefcff..4dde1f8f10 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1130,14 +1130,7 @@ impl AppContext { for window in self.windows() { window .update(self, |_, cx| { - cx.window - .rendered_frame - .dispatch_tree - .clear_pending_keystrokes(); - cx.window - .next_frame - .dispatch_tree - .clear_pending_keystrokes(); + cx.clear_pending_keystrokes(); }) .ok(); } diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 7483235ae6..a48d55c19f 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -51,7 +51,7 @@ /// use crate::{ Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, - KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent, WindowContext, + Keystroke, ModifiersChangedEvent, WindowContext, }; use collections::FxHashMap; use smallvec::SmallVec; @@ -73,7 +73,6 @@ pub(crate) struct DispatchTree { nodes: Vec, focusable_node_ids: FxHashMap, view_node_ids: FxHashMap, - keystroke_matchers: FxHashMap, KeystrokeMatcher>, keymap: Rc>, action_registry: Rc, } @@ -111,6 +110,19 @@ impl ReusedSubtree { } } +#[derive(Default, Debug)] +pub(crate) struct Replay { + pub(crate) keystroke: Keystroke, + pub(crate) bindings: SmallVec<[KeyBinding; 1]>, +} + +#[derive(Default, Debug)] +pub(crate) struct DispatchResult { + pub(crate) pending: SmallVec<[Keystroke; 1]>, + pub(crate) bindings: SmallVec<[KeyBinding; 1]>, + pub(crate) to_replay: SmallVec<[Replay; 1]>, +} + type KeyListener = Rc; type ModifiersChangedListener = Rc; @@ -129,7 +141,6 @@ impl DispatchTree { nodes: Vec::new(), focusable_node_ids: FxHashMap::default(), view_node_ids: FxHashMap::default(), - keystroke_matchers: FxHashMap::default(), keymap, action_registry, } @@ -142,7 +153,6 @@ impl DispatchTree { self.nodes.clear(); self.focusable_node_ids.clear(); self.view_node_ids.clear(); - self.keystroke_matchers.clear(); } pub fn len(&self) -> usize { @@ -310,33 +320,6 @@ impl DispatchTree { self.nodes.truncate(index); } - pub fn clear_pending_keystrokes(&mut self) { - self.keystroke_matchers.clear(); - } - - /// Preserve keystroke matchers from previous frames to support multi-stroke - /// bindings across multiple frames. - pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option) { - if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) { - let dispatch_path = self.dispatch_path(node_id); - - self.context_stack.clear(); - for node_id in dispatch_path { - let node = self.node(node_id); - if let Some(context) = node.context.clone() { - self.context_stack.push(context); - } - - if let Some((context_stack, matcher)) = old_tree - .keystroke_matchers - .remove_entry(self.context_stack.as_slice()) - { - self.keystroke_matchers.insert(context_stack, matcher); - } - } - } - } - pub fn on_key_event(&mut self, listener: KeyListener) { self.active_node().key_listeners.push(listener); } @@ -419,74 +402,110 @@ 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() } - // dispatch_key pushes 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, + fn bindings_for_input( + &self, + input: &[Keystroke], dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> KeymatchResult { - let mut bindings = SmallVec::<[KeyBinding; 1]>::new(); - let mut pending = false; + ) -> (SmallVec<[KeyBinding; 1]>, bool) { + let context_stack: SmallVec<[KeyContext; 4]> = dispatch_path + .iter() + .filter_map(|node_id| self.node(*node_id).context.clone()) + .collect(); - 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); - } - } - - while !context_stack.is_empty() { - let keystroke_matcher = self - .keystroke_matchers - .entry(context_stack.clone()) - .or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone())); - - let result = keystroke_matcher.match_keystroke(keystroke, &context_stack); - if result.pending && !pending && !bindings.is_empty() { - context_stack.pop(); - continue; - } - - pending = result.pending || pending; - 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(); - } - - KeymatchResult { bindings, pending } + self.keymap + .borrow() + .bindings_for_input(&input, &context_stack) } - pub fn has_pending_keystrokes(&self) -> bool { - self.keystroke_matchers - .iter() - .any(|(_, matcher)| matcher.has_pending_keystrokes()) + /// dispatch_key processes the keystroke + /// input should be set to the value of `pending` from the previous call to dispatch_key. + /// 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]>, + ) -> DispatchResult { + input.push(keystroke.clone()); + let (bindings, pending) = self.bindings_for_input(&input, dispatch_path); + + if pending { + return DispatchResult { + pending: input, + ..Default::default() + }; + } else if !bindings.is_empty() { + return DispatchResult { + bindings, + ..Default::default() + }; + } else if input.len() == 1 { + return DispatchResult::default(); + } + input.pop(); + + let (suffix, mut to_replay) = self.replay_prefix(input, 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; + } + + /// 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<[Keystroke; 1]>, SmallVec<[Replay; 1]>) { + let mut to_replay: SmallVec<[Replay; 1]> = Default::default(); + for last in (0..input.len()).rev() { + 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(), + bindings, + }); + break; + } + } + if to_replay.is_empty() { + to_replay.push(Replay { + keystroke: input.remove(0), + ..Default::default() + }); + } + (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 d6b84f10e6..f17300d9e7 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -1,13 +1,11 @@ mod binding; mod context; -mod matcher; pub use binding::*; pub use context::*; -pub(crate) use matcher::*; use crate::{Action, Keystroke, NoAction}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use smallvec::SmallVec; use std::any::{Any, TypeId}; @@ -21,8 +19,6 @@ pub struct KeymapVersion(usize); pub struct Keymap { bindings: Vec, binding_indices_by_action_id: HashMap>, - disabled_keystrokes: - HashMap, HashSet>>, version: KeymapVersion, } @@ -41,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; @@ -66,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; } @@ -89,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) { @@ -98,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 } } @@ -168,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/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 5e97e26cdd..b062642659 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,4 +1,4 @@ -use crate::{Action, KeyBindingContextPredicate, KeyMatch, Keystroke}; +use crate::{Action, KeyBindingContextPredicate, Keystroke}; use anyhow::Result; use smallvec::SmallVec; @@ -46,17 +46,18 @@ impl KeyBinding { } /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, pending_keystrokes: &[Keystroke]) -> KeyMatch { - if self.keystrokes.as_ref().starts_with(pending_keystrokes) { - // If the binding is completed, push it onto the matches list - if self.keystrokes.as_ref().len() == pending_keystrokes.len() { - KeyMatch::Matched - } else { - KeyMatch::Pending - } - } else { - KeyMatch::None + pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option { + if self.keystrokes.len() < typed.len() { + return None; } + + for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { + if !typed.should_match(target) { + return None; + } + } + + return Some(self.keystrokes.len() > typed.len()); } /// Get the keystrokes associated with this binding diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs deleted file mode 100644 index c2dec94a51..0000000000 --- a/crates/gpui/src/keymap/matcher.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::{KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke}; -use smallvec::SmallVec; -use std::{cell::RefCell, rc::Rc}; - -pub(crate) struct KeystrokeMatcher { - pending_keystrokes: Vec, - keymap: Rc>, - keymap_version: KeymapVersion, -} - -pub struct KeymatchResult { - pub bindings: SmallVec<[KeyBinding; 1]>, - pub pending: bool, -} - -impl KeystrokeMatcher { - pub fn new(keymap: Rc>) -> Self { - let keymap_version = keymap.borrow().version(); - Self { - pending_keystrokes: Vec::new(), - keymap_version, - keymap, - } - } - - pub fn has_pending_keystrokes(&self) -> bool { - !self.pending_keystrokes.is_empty() - } - - /// Pushes a keystroke onto the matcher. - /// The result of the new keystroke is returned: - /// - KeyMatch::None => - /// No match is valid for this key given any pending keystrokes. - /// - KeyMatch::Pending => - /// There exist bindings which are still waiting for more keys. - /// - KeyMatch::Complete(matches) => - /// One or more bindings have received the necessary key presses. - /// Bindings added later will take precedence over earlier bindings. - pub(crate) fn match_keystroke( - &mut self, - keystroke: &Keystroke, - context_stack: &[KeyContext], - ) -> KeymatchResult { - let keymap = self.keymap.borrow(); - - // Clear pending keystrokes if the keymap has changed since the last matched keystroke. - if keymap.version() != self.keymap_version { - self.keymap_version = keymap.version(); - self.pending_keystrokes.clear(); - } - - let mut pending_key = None; - let mut bindings = SmallVec::new(); - - for binding in keymap.bindings().rev() { - if !keymap.binding_enabled(binding, context_stack) { - continue; - } - - for candidate in keystroke.match_candidates() { - self.pending_keystrokes.push(candidate.clone()); - match binding.match_keystrokes(&self.pending_keystrokes) { - KeyMatch::Matched => { - bindings.push(binding.clone()); - } - KeyMatch::Pending => { - pending_key.get_or_insert(candidate); - } - KeyMatch::None => {} - } - self.pending_keystrokes.pop(); - } - } - - if bindings.is_empty() && pending_key.is_none() && !self.pending_keystrokes.is_empty() { - 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 - } else { - self.pending_keystrokes.clear(); - false - }; - - KeymatchResult { bindings, pending } - } -} - -/// The result of matching a keystroke against a given keybinding. -/// - KeyMatch::None => No match is valid for this key given any pending keystrokes. -/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys. -/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses. -#[derive(Debug, PartialEq)] -pub enum KeyMatch { - None, - Pending, - Matched, -} diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 64682d69d3..f8f0150b77 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -1,6 +1,5 @@ use anyhow::anyhow; use serde::Deserialize; -use smallvec::SmallVec; use std::fmt::Write; /// A keystroke and associated metadata generated by the platform @@ -25,33 +24,25 @@ impl Keystroke { /// and on some keyboards the IME handler converts a sequence of keys into a /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). /// - /// This method generates a list of potential keystroke candidates that could be matched - /// against when resolving a keybinding. - pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { - let mut possibilities = SmallVec::new(); - match self.ime_key.as_ref() { - Some(ime_key) => { - if ime_key != &self.key { - possibilities.push(Keystroke { - modifiers: Modifiers { - control: self.modifiers.control, - alt: false, - shift: false, - platform: false, - function: false, - }, - key: ime_key.to_string(), - ime_key: None, - }); - } - possibilities.push(Keystroke { - ime_key: None, - ..self.clone() - }); + /// This method assumes that `self` was typed and `target' is in the keymap, and checks + /// both possibilities for self against the target. + pub(crate) fn should_match(&self, target: &Keystroke) -> bool { + if let Some(ime_key) = self + .ime_key + .as_ref() + .filter(|ime_key| ime_key != &&self.key) + { + let ime_modifiers = Modifiers { + control: self.modifiers.control, + ..Default::default() + }; + + if &target.key == ime_key && target.modifiers == ime_modifiers { + return true; } - None => possibilities.push(self.clone()), } - possibilities + + target.modifiers == self.modifiers && target.key == self.key } /// key syntax is: diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f2795c727e..c5bada903e 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4,14 +4,14 @@ use crate::{ Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, - InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, - Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, + InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, + KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, - RenderImageParams, RenderSvgParams, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, - Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, - TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View, + RenderImageParams, RenderSvgParams, Replay, ResizeEdge, ScaledPixels, Scene, Shadow, + SharedString, Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, + Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, SUBPIXEL_VARIANTS, @@ -574,34 +574,10 @@ pub(crate) enum DrawPhase { #[derive(Default, Debug)] struct PendingInput { keystrokes: SmallVec<[Keystroke; 1]>, - bindings: SmallVec<[KeyBinding; 1]>, focus: Option, timer: Option>, } -impl PendingInput { - fn input(&self) -> String { - self.keystrokes - .iter() - .flat_map(|k| k.ime_key.clone()) - .collect::>() - .join("") - } - - fn used_by_binding(&self, binding: &KeyBinding) -> bool { - if self.keystrokes.is_empty() { - return true; - } - let keystroke = &self.keystrokes[0]; - for candidate in keystroke.match_candidates() { - if binding.match_keystrokes(&[candidate]) == KeyMatch::Pending { - return true; - } - } - false - } -} - pub(crate) struct ElementStateBox { pub(crate) inner: Box, #[cfg(debug_assertions)] @@ -969,10 +945,7 @@ impl<'a> WindowContext<'a> { } self.window.focus = Some(handle.id); - self.window - .rendered_frame - .dispatch_tree - .clear_pending_keystrokes(); + self.clear_pending_keystrokes(); self.refresh(); } @@ -1074,17 +1047,6 @@ impl<'a> WindowContext<'a> { }); } - pub(crate) fn clear_pending_keystrokes(&mut self) { - self.window - .rendered_frame - .dispatch_tree - .clear_pending_keystrokes(); - self.window - .next_frame - .dispatch_tree - .clear_pending_keystrokes(); - } - /// Schedules the given function to be run at the end of the current effect cycle, allowing entities /// that are currently on the stack to be returned to the app. pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { @@ -1453,14 +1415,6 @@ impl<'a> WindowContext<'a> { self.draw_roots(); self.window.dirty_views.clear(); - - self.window - .next_frame - .dispatch_tree - .preserve_pending_keystrokes( - &mut self.window.rendered_frame.dispatch_tree, - self.window.focus, - ); self.window.next_frame.window_active = self.window.active.get(); // Register requested input handler with the platform window. @@ -3253,8 +3207,6 @@ impl<'a> WindowContext<'a> { .dispatch_tree .dispatch_path(node_id); - let mut bindings: SmallVec<[KeyBinding; 1]> = SmallVec::new(); - let mut pending = false; let mut keystroke: Option = None; if let Some(event) = event.downcast_ref::() { @@ -3272,23 +3224,11 @@ impl<'a> WindowContext<'a> { _ => None, }; if let Some(key) = key { - let key = Keystroke { + keystroke = Some(Keystroke { key: key.to_string(), ime_key: None, modifiers: Modifiers::default(), - }; - let KeymatchResult { - bindings: modifier_bindings, - pending: pending_bindings, - } = self - .window - .rendered_frame - .dispatch_tree - .dispatch_key(&key, &dispatch_path); - - keystroke = Some(key); - bindings = modifier_bindings; - pending = pending_bindings; + }); } } } @@ -3300,73 +3240,68 @@ impl<'a> WindowContext<'a> { self.window.pending_modifier.modifiers = event.modifiers } else if let Some(key_down_event) = event.downcast_ref::() { self.window.pending_modifier.saw_keystroke = true; - let KeymatchResult { - bindings: key_down_bindings, - pending: key_down_pending, - } = self - .window - .rendered_frame - .dispatch_tree - .dispatch_key(&key_down_event.keystroke, &dispatch_path); - keystroke = Some(key_down_event.keystroke.clone()); - - bindings = key_down_bindings; - pending = key_down_pending; } - if keystroke.is_none() { + let Some(keystroke) = keystroke else { self.finish_dispatch_key_event(event, dispatch_path); return; + }; + + let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); + if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus { + currently_pending = PendingInput::default(); } - if pending { - let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); - if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus { - currently_pending = PendingInput::default(); - } - currently_pending.focus = self.window.focus; - if let Some(keystroke) = keystroke { - currently_pending.keystrokes.push(keystroke.clone()); - } - for binding in bindings { - currently_pending.bindings.push(binding); - } + let match_result = self.window.rendered_frame.dispatch_tree.dispatch_key( + currently_pending.keystrokes, + keystroke, + &dispatch_path, + ); + if !match_result.to_replay.is_empty() { + self.replay_pending_input(match_result.to_replay) + } + if !match_result.pending.is_empty() { + currently_pending.keystrokes = match_result.pending; + currently_pending.focus = self.window.focus; 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 { + let Some(currently_pending) = cx + .window + .pending_input + .take() + .filter(|pending| pending.focus == cx.window.focus) + else { return; }; - cx.replay_pending_input(currently_pending); - cx.pending_input_changed(); + + let dispatch_path = cx + .window + .rendered_frame + .dispatch_tree + .dispatch_path(node_id); + + let to_replay = cx + .window + .rendered_frame + .dispatch_tree + .flush_dispatch(currently_pending.keystrokes, &dispatch_path); + + cx.replay_pending_input(to_replay) }) .log_err(); })); - self.window.pending_input = Some(currently_pending); self.pending_input_changed(); - self.propagate_event = false; return; - } else if let Some(currently_pending) = self.window.pending_input.take() { - self.pending_input_changed(); - if bindings - .iter() - .all(|binding| !currently_pending.used_by_binding(binding)) - { - self.replay_pending_input(currently_pending) - } - } - - if !bindings.is_empty() { - self.clear_pending_keystrokes(); } + self.pending_input_changed(); self.propagate_event = true; - for binding in bindings { + for binding in match_result.bindings { self.dispatch_action_on_node(node_id, binding.action.as_ref()); if !self.propagate_event { self.dispatch_keystroke_observers(event, Some(binding.action)); @@ -3453,10 +3388,11 @@ impl<'a> WindowContext<'a> { /// Determine whether a potential multi-stroke key binding is in progress on this window. pub fn has_pending_keystrokes(&self) -> bool { - self.window - .rendered_frame - .dispatch_tree - .has_pending_keystrokes() + self.window.pending_input.is_some() + } + + fn clear_pending_keystrokes(&mut self) { + self.window.pending_input.take(); } /// Returns the currently pending input keystrokes that might result in a multi-stroke key binding. @@ -3467,7 +3403,7 @@ impl<'a> WindowContext<'a> { .map(|pending_input| pending_input.keystrokes.as_slice()) } - fn replay_pending_input(&mut self, currently_pending: PendingInput) { + fn replay_pending_input(&mut self, replays: SmallVec<[Replay; 1]>) { let node_id = self .window .focus @@ -3479,42 +3415,36 @@ impl<'a> WindowContext<'a> { }) .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); - if self.window.focus != currently_pending.focus { - return; - } - - let input = currently_pending.input(); - - self.propagate_event = true; - for binding in currently_pending.bindings { - self.dispatch_action_on_node(node_id, binding.action.as_ref()); - if !self.propagate_event { - return; - } - } - let dispatch_path = self .window .rendered_frame .dispatch_tree .dispatch_path(node_id); - for keystroke in currently_pending.keystrokes { + 'replay: for replay in replays { let event = KeyDownEvent { - keystroke, + keystroke: replay.keystroke.clone(), is_held: false, }; + self.propagate_event = true; + for binding in replay.bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref()); + if !self.propagate_event { + self.dispatch_keystroke_observers(&event, Some(binding.action)); + continue 'replay; + } + } + self.dispatch_key_down_up_event(&event, &dispatch_path); if !self.propagate_event { - return; + continue 'replay; } - } - - if !input.is_empty() { - if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { - input_handler.dispatch_input(&input, self); - self.window.platform_window.set_input_handler(input_handler) + if let Some(input) = replay.keystroke.ime_key.as_ref().cloned() { + if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { + input_handler.dispatch_input(&input, self); + self.window.platform_window.set_input_handler(input_handler) + } } } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 2ff2cc4f6b..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::*; @@ -1317,3 +1317,99 @@ async fn test_command_alias(cx: &mut gpui::TestAppContext) { cx.simulate_keystrokes(": Q"); cx.set_state("Λ‡Hello world", Mode::Normal); } + +#[gpui::test] +async fn test_remap_adjacent_dog_cat(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.update(|cx| { + cx.bind_keys([ + KeyBinding::new( + "d o g", + workspace::SendKeystrokes("🐢".to_string()), + Some("vim_mode == insert"), + ), + KeyBinding::new( + "c a t", + workspace::SendKeystrokes("🐱".to_string()), + Some("vim_mode == insert"), + ), + ]) + }); + cx.neovim.exec("imap dog 🐢").await; + cx.neovim.exec("imap cat 🐱").await; + + cx.set_shared_state("Λ‡").await; + cx.simulate_shared_keystrokes("i d o g").await; + cx.shared_state().await.assert_eq("πŸΆΛ‡"); + + cx.set_shared_state("Λ‡").await; + cx.simulate_shared_keystrokes("i d o d o g").await; + cx.shared_state().await.assert_eq("doπŸΆΛ‡"); + + cx.set_shared_state("Λ‡").await; + cx.simulate_shared_keystrokes("i d o c a t").await; + cx.shared_state().await.assert_eq("doπŸ±Λ‡"); +} + +#[gpui::test] +async fn test_remap_nested_pineapple(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.update(|cx| { + cx.bind_keys([ + KeyBinding::new( + "p i n", + workspace::SendKeystrokes("πŸ“Œ".to_string()), + Some("vim_mode == insert"), + ), + KeyBinding::new( + "p i n e", + workspace::SendKeystrokes("🌲".to_string()), + Some("vim_mode == insert"), + ), + KeyBinding::new( + "p i n e a p p l e", + workspace::SendKeystrokes("🍍".to_string()), + Some("vim_mode == insert"), + ), + ]) + }); + cx.neovim.exec("imap pin πŸ“Œ").await; + cx.neovim.exec("imap pine 🌲").await; + cx.neovim.exec("imap pineapple 🍍").await; + + cx.set_shared_state("Λ‡").await; + cx.simulate_shared_keystrokes("i p i n").await; + cx.executor().advance_clock(Duration::from_millis(1000)); + cx.run_until_parked(); + cx.shared_state().await.assert_eq("πŸ“ŒΛ‡"); + + cx.set_shared_state("Λ‡").await; + cx.simulate_shared_keystrokes("i p i n e").await; + cx.executor().advance_clock(Duration::from_millis(1000)); + cx.run_until_parked(); + cx.shared_state().await.assert_eq("πŸŒ²Λ‡"); + + cx.set_shared_state("Λ‡").await; + 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 17e4449be4..3595106625 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -409,6 +409,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"}} diff --git a/crates/vim/test_data/test_remap_adjacent_dog_cat.json b/crates/vim/test_data/test_remap_adjacent_dog_cat.json new file mode 100644 index 0000000000..91af9ccac6 --- /dev/null +++ b/crates/vim/test_data/test_remap_adjacent_dog_cat.json @@ -0,0 +1,24 @@ +{"Exec":{"command":"imap dog 🐢"}} +{"Exec":{"command":"imap cat 🐱"}} +{"Put":{"state":"Λ‡"}} +{"Key":"i"} +{"Key":"d"} +{"Key":"o"} +{"Key":"g"} +{"Get":{"state":"πŸΆΛ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡"}} +{"Key":"i"} +{"Key":"d"} +{"Key":"o"} +{"Key":"d"} +{"Key":"o"} +{"Key":"g"} +{"Get":{"state":"doπŸΆΛ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡"}} +{"Key":"i"} +{"Key":"d"} +{"Key":"o"} +{"Key":"c"} +{"Key":"a"} +{"Key":"t"} +{"Get":{"state":"doπŸ±Λ‡","mode":"Insert"}} diff --git a/crates/vim/test_data/test_remap_nested_pineapple.json b/crates/vim/test_data/test_remap_nested_pineapple.json new file mode 100644 index 0000000000..b4a4acdd2b --- /dev/null +++ b/crates/vim/test_data/test_remap_nested_pineapple.json @@ -0,0 +1,28 @@ +{"Exec":{"command":"imap pin πŸ“Œ"}} +{"Exec":{"command":"imap pine 🌲"}} +{"Exec":{"command":"imap pineapple 🍍"}} +{"Put":{"state":"Λ‡"}} +{"Key":"i"} +{"Key":"p"} +{"Key":"i"} +{"Key":"n"} +{"Get":{"state":"πŸ“ŒΛ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡"}} +{"Key":"i"} +{"Key":"p"} +{"Key":"i"} +{"Key":"n"} +{"Key":"e"} +{"Get":{"state":"πŸŒ²Λ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡"}} +{"Key":"i"} +{"Key":"p"} +{"Key":"i"} +{"Key":"n"} +{"Key":"e"} +{"Key":"a"} +{"Key":"p"} +{"Key":"p"} +{"Key":"l"} +{"Key":"e"} +{"Get":{"state":"πŸΛ‡","mode":"Insert"}}