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"}}