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() {
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,68 @@ 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 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.
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 +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
}
}
@@ -168,16 +198,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

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

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