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
This commit is contained in:
Conrad Irwin 2024-07-22 10:46:16 -06:00 committed by GitHub
parent 865904a0c9
commit 2e23527e09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 462 additions and 422 deletions

View File

@ -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();
}

View File

@ -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<DispatchNode>,
focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
view_node_ids: FxHashMap<EntityId, DispatchNodeId>,
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Rc<RefCell<Keymap>>,
action_registry: Rc<ActionRegistry>,
}
@ -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<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut WindowContext)>;
@ -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<FocusId>) {
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]> {

View File

@ -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<KeyBinding>,
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
disabled_keystrokes:
HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
version: KeymapVersion,
}
@ -41,22 +37,13 @@ impl Keymap {
/// Add more bindings to the keymap.
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&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());
}
}

View File

@ -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<bool> {
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

View File

@ -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<Keystroke>,
keymap: Rc<RefCell<Keymap>>,
keymap_version: KeymapVersion,
}
pub struct KeymatchResult {
pub bindings: SmallVec<[KeyBinding; 1]>,
pub pending: bool,
}
impl KeystrokeMatcher {
pub fn new(keymap: Rc<RefCell<Keymap>>) -> 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,
}

View File

@ -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:

View File

@ -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<FocusId>,
timer: Option<Task<()>>,
}
impl PendingInput {
fn input(&self) -> String {
self.keystrokes
.iter()
.flat_map(|k| k.ime_key.clone())
.collect::<Vec<String>>()
.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<dyn Any>,
#[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<Keystroke> = None;
if let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() {
@ -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::<KeyDownEvent>() {
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)
}
}
}
}

View File

@ -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 <c-w> D").await;
cx.set_shared_state("ˇhi").await;
cx.simulate_shared_keystrokes("ctrl-w").await;
cx.shared_state().await.assert_eq("ˇ");
}

View File

@ -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();

View File

@ -0,0 +1,4 @@
{"Exec":{"command":"map <c-w> D"}}
{"Put":{"state":"ˇhi"}}
{"Key":"ctrl-w"}
{"Get":{"state":"ˇ","mode":"Normal"}}

View File

@ -0,0 +1,6 @@
{"Put":{"state":"ˇhi"}}
{"Key":"\""}
{"Key":"+"}
{"Key":"escape"}
{"Key":"x"}
{"Get":{"state":"ˇi","mode":"Normal"}}

View File

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

View File

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