Compare commits
5 Commits
streaming-
...
keymap-shr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
203c4b2406 | ||
|
|
33dcfbbc4a | ||
|
|
c758744687 | ||
|
|
8d3c7b6eb8 | ||
|
|
2c53899c5d |
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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]> {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("ˇ");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
4
crates/vim/test_data/test_ctrl_w_override.json
Normal file
4
crates/vim/test_data/test_ctrl_w_override.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{"Exec":{"command":"map <c-w> D"}}
|
||||
{"Put":{"state":"ˇhi"}}
|
||||
{"Key":"ctrl-w"}
|
||||
{"Get":{"state":"ˇ","mode":"Normal"}}
|
||||
6
crates/vim/test_data/test_escape_while_waiting.json
Normal file
6
crates/vim/test_data/test_escape_while_waiting.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"ˇhi"}}
|
||||
{"Key":"\""}
|
||||
{"Key":"+"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"x"}
|
||||
{"Get":{"state":"ˇi","mode":"Normal"}}
|
||||
24
crates/vim/test_data/test_remap_adjacent_dog_cat.json
Normal file
24
crates/vim/test_data/test_remap_adjacent_dog_cat.json
Normal 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"}}
|
||||
28
crates/vim/test_data/test_remap_nested_pineapple.json
Normal file
28
crates/vim/test_data/test_remap_nested_pineapple.json
Normal 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"}}
|
||||
Reference in New Issue
Block a user