Compare commits

...

5 Commits

Author SHA1 Message Date
Conrad Irwin
203c4b2406 Finish the thought 2024-07-22 10:55:52 -06:00
Conrad Irwin
33dcfbbc4a Tidy 2024-07-21 23:09:13 -06:00
Conrad Irwin
c758744687 Tidy 2024-07-21 21:19:21 -06:00
Conrad Irwin
8d3c7b6eb8 Add vim tests for multichar bindings
Co-Authored-By: @haruleekim
2024-07-21 21:15:12 -06:00
Conrad Irwin
2c53899c5d COMPILIGN 2024-07-21 20:31:52 -06:00
13 changed files with 464 additions and 422 deletions

View File

@@ -1130,14 +1130,7 @@ impl AppContext {
for window in self.windows() { for window in self.windows() {
window window
.update(self, |_, cx| { .update(self, |_, cx| {
cx.window cx.clear_pending_keystrokes();
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
cx.window
.next_frame
.dispatch_tree
.clear_pending_keystrokes();
}) })
.ok(); .ok();
} }

View File

@@ -51,7 +51,7 @@
/// ///
use crate::{ use crate::{
Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap,
KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent, WindowContext, Keystroke, ModifiersChangedEvent, WindowContext,
}; };
use collections::FxHashMap; use collections::FxHashMap;
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -73,7 +73,6 @@ pub(crate) struct DispatchTree {
nodes: Vec<DispatchNode>, nodes: Vec<DispatchNode>,
focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>, focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
view_node_ids: FxHashMap<EntityId, DispatchNodeId>, view_node_ids: FxHashMap<EntityId, DispatchNodeId>,
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Rc<RefCell<Keymap>>, keymap: Rc<RefCell<Keymap>>,
action_registry: Rc<ActionRegistry>, 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 KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut WindowContext)>; type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut WindowContext)>;
@@ -129,7 +141,6 @@ impl DispatchTree {
nodes: Vec::new(), nodes: Vec::new(),
focusable_node_ids: FxHashMap::default(), focusable_node_ids: FxHashMap::default(),
view_node_ids: FxHashMap::default(), view_node_ids: FxHashMap::default(),
keystroke_matchers: FxHashMap::default(),
keymap, keymap,
action_registry, action_registry,
} }
@@ -142,7 +153,6 @@ impl DispatchTree {
self.nodes.clear(); self.nodes.clear();
self.focusable_node_ids.clear(); self.focusable_node_ids.clear();
self.view_node_ids.clear(); self.view_node_ids.clear();
self.keystroke_matchers.clear();
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
@@ -310,33 +320,6 @@ impl DispatchTree {
self.nodes.truncate(index); 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) { pub fn on_key_event(&mut self, listener: KeyListener) {
self.active_node().key_listeners.push(listener); self.active_node().key_listeners.push(listener);
} }
@@ -419,74 +402,110 @@ impl DispatchTree {
keymap keymap
.bindings_for_action(action) .bindings_for_action(action)
.filter(|binding| { .filter(|binding| {
for i in 0..context_stack.len() { let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, &context_stack);
let context = &context_stack[0..=i]; bindings
if keymap.binding_enabled(binding, context) { .iter()
return true; .next()
} .is_some_and(|b| b.action.partial_eq(action))
}
false
}) })
.cloned() .cloned()
.collect() .collect()
} }
// dispatch_key pushes the next keystroke into any key binding matchers. fn bindings_for_input(
// any matching bindings are returned in the order that they should be dispatched: &self,
// * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first) input: &[Keystroke],
// * 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,
dispatch_path: &SmallVec<[DispatchNodeId; 32]>, dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) -> KeymatchResult { ) -> (SmallVec<[KeyBinding; 1]>, bool) {
let mut bindings = SmallVec::<[KeyBinding; 1]>::new(); let context_stack: SmallVec<[KeyContext; 4]> = dispatch_path
let mut pending = false; .iter()
.filter_map(|node_id| self.node(*node_id).context.clone())
.collect();
let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new(); self.keymap
for node_id in dispatch_path { .borrow()
let node = self.node(*node_id); .bindings_for_input(&input, &context_stack)
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 }
} }
pub fn has_pending_keystrokes(&self) -> bool { /// dispatch_key processes the keystroke
self.keystroke_matchers /// input should be set to the value of `pending` from the previous call to dispatch_key.
.iter() /// This returns three instructions to the input handler:
.any(|(_, matcher)| matcher.has_pending_keystrokes()) /// - 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]> { pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {

View File

@@ -1,13 +1,11 @@
mod binding; mod binding;
mod context; mod context;
mod matcher;
pub use binding::*; pub use binding::*;
pub use context::*; pub use context::*;
pub(crate) use matcher::*;
use crate::{Action, Keystroke, NoAction}; use crate::{Action, Keystroke, NoAction};
use collections::{HashMap, HashSet}; use collections::HashMap;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
@@ -21,8 +19,6 @@ pub struct KeymapVersion(usize);
pub struct Keymap { pub struct Keymap {
bindings: Vec<KeyBinding>, bindings: Vec<KeyBinding>,
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>, binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
disabled_keystrokes:
HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
version: KeymapVersion, version: KeymapVersion,
} }
@@ -41,22 +37,13 @@ impl Keymap {
/// Add more bindings to the keymap. /// Add more bindings to the keymap.
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) { pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
let no_action_id = (NoAction {}).type_id();
for binding in bindings { for binding in bindings {
let action_id = binding.action().as_any().type_id(); let action_id = binding.action().as_any().type_id();
if action_id == no_action_id { self.binding_indices_by_action_id
self.disabled_keystrokes .entry(action_id)
.entry(binding.keystrokes) .or_default()
.or_default() .push(self.bindings.len());
.insert(binding.context_predicate); self.bindings.push(binding);
} else {
self.binding_indices_by_action_id
.entry(action_id)
.or_default()
.push(self.bindings.len());
self.bindings.push(binding);
}
} }
self.version.0 += 1; self.version.0 += 1;
@@ -66,7 +53,6 @@ impl Keymap {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.bindings.clear(); self.bindings.clear();
self.binding_indices_by_action_id.clear(); self.binding_indices_by_action_id.clear();
self.disabled_keystrokes.clear();
self.version.0 += 1; self.version.0 += 1;
} }
@@ -89,8 +75,68 @@ impl Keymap {
.filter(move |binding| binding.action().partial_eq(action)) .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 where a binding conflicts with a longer binding, precedence is resolved
/// only using the order in the keymap file. So binding `cmd-k` in the workspace will disable
/// built-in bindings for `cmd-k X` throughout the app.
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. /// 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 binding has a context predicate, it must match the current context,
if let Some(predicate) = &binding.context_predicate { if let Some(predicate) = &binding.context_predicate {
if !predicate.eval(context) { if !predicate.eval(context) {
@@ -98,22 +144,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 true
} }
} }
@@ -168,16 +198,37 @@ mod tests {
keymap.add_bindings(bindings.clone()); keymap.add_bindings(bindings.clone());
// binding is only enabled in a specific context // binding is only enabled in a specific context
assert!(!keymap.binding_enabled(&bindings[0], &[KeyContext::parse("barf").unwrap()])); assert!(keymap
assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("editor").unwrap()])); .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 // binding is disabled in a more specific context
assert!(!keymap.binding_enabled( assert!(keymap
&bindings[0], .bindings_for_input(
&[KeyContext::parse("editor mode=full").unwrap()] &[Keystroke::parse("ctrl-a").unwrap()],
)); &[KeyContext::parse("editor mode=full").unwrap()],
)
.0
.is_empty());
// binding is globally disabled // 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 anyhow::Result;
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -46,17 +46,18 @@ impl KeyBinding {
} }
/// Check if the given keystrokes match this binding. /// Check if the given keystrokes match this binding.
pub fn match_keystrokes(&self, pending_keystrokes: &[Keystroke]) -> KeyMatch { pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
if self.keystrokes.as_ref().starts_with(pending_keystrokes) { if self.keystrokes.len() < typed.len() {
// If the binding is completed, push it onto the matches list return None;
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
KeyMatch::Matched
} else {
KeyMatch::Pending
}
} else {
KeyMatch::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 /// 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 anyhow::anyhow;
use serde::Deserialize; use serde::Deserialize;
use smallvec::SmallVec;
use std::fmt::Write; use std::fmt::Write;
/// A keystroke and associated metadata generated by the platform /// 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 /// 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). /// 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 /// This method assumes that `self` was typed and `target' is in the keymap, and checks
/// against when resolving a keybinding. /// both possibilities for self against the target.
pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
let mut possibilities = SmallVec::new(); if let Some(ime_key) = self
match self.ime_key.as_ref() { .ime_key
Some(ime_key) => { .as_ref()
if ime_key != &self.key { .filter(|ime_key| ime_key != &&self.key)
possibilities.push(Keystroke { {
modifiers: Modifiers { let ime_modifiers = Modifiers {
control: self.modifiers.control, control: self.modifiers.control,
alt: false, ..Default::default()
shift: false, };
platform: false,
function: false, if &target.key == ime_key && target.modifiers == ime_modifiers {
}, return true;
key: ime_key.to_string(),
ime_key: None,
});
}
possibilities.push(Keystroke {
ime_key: None,
..self.clone()
});
} }
None => possibilities.push(self.clone()),
} }
possibilities
target.modifiers == self.modifiers && target.key == self.key
} }
/// key syntax is: /// key syntax is:

View File

@@ -4,14 +4,14 @@ use crate::{
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData,
InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke,
Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers,
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderImageParams, RenderSvgParams, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, ScaledPixels, Scene, Shadow,
Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, SharedString, Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View, Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View,
VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds, VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
SUBPIXEL_VARIANTS, SUBPIXEL_VARIANTS,
@@ -574,34 +574,10 @@ pub(crate) enum DrawPhase {
#[derive(Default, Debug)] #[derive(Default, Debug)]
struct PendingInput { struct PendingInput {
keystrokes: SmallVec<[Keystroke; 1]>, keystrokes: SmallVec<[Keystroke; 1]>,
bindings: SmallVec<[KeyBinding; 1]>,
focus: Option<FocusId>, focus: Option<FocusId>,
timer: Option<Task<()>>, 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) struct ElementStateBox {
pub(crate) inner: Box<dyn Any>, pub(crate) inner: Box<dyn Any>,
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -969,10 +945,7 @@ impl<'a> WindowContext<'a> {
} }
self.window.focus = Some(handle.id); self.window.focus = Some(handle.id);
self.window self.clear_pending_keystrokes();
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
self.refresh(); 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 /// 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. /// that are currently on the stack to be returned to the app.
pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
@@ -1453,14 +1415,6 @@ impl<'a> WindowContext<'a> {
self.draw_roots(); self.draw_roots();
self.window.dirty_views.clear(); 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(); self.window.next_frame.window_active = self.window.active.get();
// Register requested input handler with the platform window. // Register requested input handler with the platform window.
@@ -3253,8 +3207,6 @@ impl<'a> WindowContext<'a> {
.dispatch_tree .dispatch_tree
.dispatch_path(node_id); .dispatch_path(node_id);
let mut bindings: SmallVec<[KeyBinding; 1]> = SmallVec::new();
let mut pending = false;
let mut keystroke: Option<Keystroke> = None; let mut keystroke: Option<Keystroke> = None;
if let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() { if let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() {
@@ -3272,23 +3224,11 @@ impl<'a> WindowContext<'a> {
_ => None, _ => None,
}; };
if let Some(key) = key { if let Some(key) = key {
let key = Keystroke { keystroke = Some(Keystroke {
key: key.to_string(), key: key.to_string(),
ime_key: None, ime_key: None,
modifiers: Modifiers::default(), 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 self.window.pending_modifier.modifiers = event.modifiers
} else if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() { } else if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
self.window.pending_modifier.saw_keystroke = true; 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()); 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); self.finish_dispatch_key_event(event, dispatch_path);
return; 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 match_result = self.window.rendered_frame.dispatch_tree.dispatch_key(
let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); currently_pending.keystrokes,
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus { keystroke,
currently_pending = PendingInput::default(); &dispatch_path,
} );
currently_pending.focus = self.window.focus; if !match_result.to_replay.is_empty() {
if let Some(keystroke) = keystroke { self.replay_pending_input(match_result.to_replay)
currently_pending.keystrokes.push(keystroke.clone()); }
}
for binding in bindings {
currently_pending.bindings.push(binding);
}
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 { currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await; cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| { cx.update(move |cx| {
cx.clear_pending_keystrokes(); let Some(currently_pending) = cx
let Some(currently_pending) = cx.window.pending_input.take() else { .window
.pending_input
.take()
.filter(|pending| pending.focus == cx.window.focus)
else {
return; 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(); .log_err();
})); }));
self.window.pending_input = Some(currently_pending); self.window.pending_input = Some(currently_pending);
self.pending_input_changed(); self.pending_input_changed();
self.propagate_event = false; self.propagate_event = false;
return; 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; 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()); self.dispatch_action_on_node(node_id, binding.action.as_ref());
if !self.propagate_event { if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action)); 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. /// Determine whether a potential multi-stroke key binding is in progress on this window.
pub fn has_pending_keystrokes(&self) -> bool { pub fn has_pending_keystrokes(&self) -> bool {
self.window self.window.pending_input.is_some()
.rendered_frame }
.dispatch_tree
.has_pending_keystrokes() 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. /// 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()) .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 let node_id = self
.window .window
.focus .focus
@@ -3479,42 +3415,36 @@ impl<'a> WindowContext<'a> {
}) })
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); .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 let dispatch_path = self
.window .window
.rendered_frame .rendered_frame
.dispatch_tree .dispatch_tree
.dispatch_path(node_id); .dispatch_path(node_id);
for keystroke in currently_pending.keystrokes { 'replay: for replay in replays {
let event = KeyDownEvent { let event = KeyDownEvent {
keystroke, keystroke: replay.keystroke.clone(),
is_held: false, 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); self.dispatch_key_down_up_event(&event, &dispatch_path);
if !self.propagate_event { if !self.propagate_event {
return; continue 'replay;
} }
} if let Some(input) = replay.keystroke.ime_key.as_ref().cloned() {
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
if !input.is_empty() { input_handler.dispatch_input(&input, self);
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { self.window.platform_window.set_input_handler(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 collections::HashMap;
use command_palette::CommandPalette; use command_palette::CommandPalette;
use editor::{display_map::DisplayRow, DisplayPoint}; use editor::{actions::DeleteLine, display_map::DisplayRow, DisplayPoint};
use futures::StreamExt; use futures::StreamExt;
use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext}; use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext};
pub use neovim_backed_test_context::*; pub use neovim_backed_test_context::*;
@@ -1317,3 +1317,99 @@ async fn test_command_alias(cx: &mut gpui::TestAppContext) {
cx.simulate_keystrokes(": Q"); cx.simulate_keystrokes(": Q");
cx.set_state("ˇHello world", Mode::Normal); 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

@@ -400,6 +400,7 @@ impl Vim {
state.last_mode = last_mode; state.last_mode = last_mode;
state.mode = mode; state.mode = mode;
state.operator_stack.clear(); state.operator_stack.clear();
state.selected_register.take();
if mode == Mode::Normal || mode != last_mode { if mode == Mode::Normal || mode != last_mode {
state.current_tx.take(); state.current_tx.take();
state.current_anchor.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"}}