This commit is contained in:
Conrad Irwin
2024-07-21 22:23:49 -06:00
parent c758744687
commit 33dcfbbc4a
7 changed files with 180 additions and 187 deletions

View File

@@ -117,7 +117,7 @@ pub(crate) struct Replay {
}
#[derive(Default, Debug)]
pub(crate) struct MatchResult {
pub(crate) struct DispatchResult {
pub(crate) pending: SmallVec<[Keystroke; 1]>,
pub(crate) bindings: SmallVec<[KeyBinding; 1]>,
pub(crate) to_replay: SmallVec<[Replay; 1]>,
@@ -402,164 +402,95 @@ 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()
}
fn match_input(
fn bindings_for_input(
&self,
input: &[Keystroke],
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let keymap = self.keymap.borrow();
let context_stack: SmallVec<[KeyContext; 4]> = dispatch_path
.iter()
.filter_map(|node_id| self.node(*node_id).context.clone())
.collect();
// We want the following behaviour:
// - If there are multiple bindings matching completely,
// return them in order of depth in the context.
// - If there are some bindings that are pending,
// they take priority over bindings defined earlier in the keymap.
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);
}
}
let possibilities = keymap.bindings().rev().filter_map(|binding| {
binding
.match_keystrokes(input)
.map(|pending| (binding, pending))
});
let mut bindings: SmallVec<[(KeyBinding, usize); 1]> = SmallVec::new();
let mut first_pending = None;
for (binding, pending) in possibilities {
for depth in (0..=context_stack.len()).rev() {
if keymap.binding_enabled(binding, &context_stack[0..depth]) {
if first_pending.is_none() {
first_pending = Some(pending);
}
if !pending {
bindings.push((binding.clone(), depth));
}
}
}
}
bindings.sort_by(|a, b| a.1.cmp(&b.1).reverse());
let bindings = bindings.into_iter().map(|(binding, _)| binding).collect();
// Precedence:
// - if the user has defined a longer keystroke (e.g. ,=), we should say 'pending' for the builtin ','
// - if the user has defined a shorter keystroke (e.g. ctrl-w), we should say 'not pending' for the builtin `ctrl-w q`
// - hypoethetically, if they have defined *both*, then what?
// - ,: vim::Next
// - ,=: custom wev
// - ,: custom wev
// -ctrl-w q: vim::Close
// -ctrl-w: terminal::Close
// -ctrl-w q: vim::Close
return (bindings, first_pending.unwrap_or_default());
self.keymap
.borrow()
.bindings_for_input(&input, &context_stack)
}
/// dispatch_key processes the keystroke
/// input should be set to the value of `pending` from the previous call to dispatch_key.
/// In the common case, input will be empty, and the MatchResult will return just actions to do.
/// If input is not empty, then either:
/// - input + keystroke matches a set of bindings, which will be returned.
/// - input + keystroke is still a prefix, so only pending
/// - input + keystroke did not match anything. In that case, the caller will need to replay
/// what the original input should have done before handling any of the returned actions.
/// 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]>,
) -> MatchResult {
println!("---------- {:?} {:?} ---------------", input, keystroke);
) -> DispatchResult {
input.push(keystroke.clone());
let (bindings, pending) = self.match_input(&input, dispatch_path);
let (bindings, pending) = self.bindings_for_input(&input, dispatch_path);
if pending {
return MatchResult {
return DispatchResult {
pending: input,
..Default::default()
};
}
if !bindings.is_empty() {
return MatchResult {
} else if !bindings.is_empty() {
return DispatchResult {
bindings,
..Default::default()
};
} else if input.len() == 1 {
return DispatchResult::default();
}
input.pop();
if input.len() == 0 {
return MatchResult::default();
}
let mut to_replay: SmallVec<[Replay; 1]> = Default::default();
let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path);
// At this point we know that we have multiple keys in flight, and the
// full set matches nothing.
// Try matching with prefixes of the full set, and if that doesn't work,
// replay the first input key, and recurse.
for last in (0..input.len()).rev() {
let (bindings, _) = self.match_input(&input[0..=last], dispatch_path);
if !bindings.is_empty() {
to_replay.push(Replay {
keystroke: input.drain(0..=last).last().unwrap(),
bindings,
});
break;
}
if last == 0 {
to_replay.push(Replay {
keystroke: input.remove(0),
..Default::default()
});
break;
}
}
let mut result = self.dispatch_key(input, keystroke, 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;
}
/// flush converts any remaining pending input to replay events.
/// This is called when a user hits the timeout mid-way through typing a key.
pub fn flush(
/// 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<[Replay; 1]> {
) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) {
let mut to_replay: SmallVec<[Replay; 1]> = Default::default();
// At this point we know that we have multiple keys in flight, and the
// full set matches nothing.
// Try matching with prefixes of the full set, and if that doesn't work,
// replay the first input key, and recurse.
for last in (0..input.len()).rev() {
let (bindings, _) = self.match_input(&input[0..=last], dispatch_path);
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(),
@@ -567,20 +498,14 @@ impl DispatchTree {
});
break;
}
if last == 0 {
to_replay.push(Replay {
keystroke: input.remove(0),
..Default::default()
});
break;
}
}
if input.len() > 0 {
to_replay.extend(self.flush(input, dispatch_path))
if to_replay.is_empty() {
to_replay.push(Replay {
keystroke: input.remove(0),
..Default::default()
});
}
to_replay
(input, to_replay)
}
pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {

View File

@@ -5,7 +5,7 @@ pub use binding::*;
pub use context::*;
use crate::{Action, Keystroke, NoAction};
use collections::{HashMap, HashSet};
use collections::HashMap;
use smallvec::SmallVec;
use std::any::{Any, TypeId};
@@ -19,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,
}
@@ -39,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;
@@ -64,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;
}
@@ -87,8 +75,66 @@ impl Keymap {
.filter(move |binding| binding.action().partial_eq(action))
}
/// bindings_for_input returns a list of bindings that match the given input,
/// and a boolean indicating whether or not more bindings might match if
/// the input was longer.
///
/// Precedence is defined by the depth in the tree (matches on the Editor take
/// precedence over matches on the Pane, then the Workspace, etc.). Bindings with
/// no context are treated as the same as the deepest context.
///
/// In the case of multiple bindings at the same depth, the ones defined later in the
/// keymap take precedence (so user bindings take precedence over built-in bindings).
///
/// If a user has disabled a binding with `"x": null` it will not be returned. Disabled
/// bindings are evaluated with the same precedence rules so you can disable a rule in
/// a given context only.
///
/// In the case of multi-key bindings, the
pub fn bindings_for_input(
&self,
input: &[Keystroke],
context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let possibilities = self.bindings().rev().filter_map(|binding| {
binding
.match_keystrokes(input)
.map(|pending| (binding, pending))
});
let mut bindings: SmallVec<[(KeyBinding, usize); 1]> = SmallVec::new();
let mut is_pending = None;
'outer: for (binding, pending) in possibilities {
for depth in (0..=context_stack.len()).rev() {
if self.binding_enabled(binding, &context_stack[0..depth]) {
if is_pending.is_none() {
is_pending = Some(pending);
}
if !pending {
bindings.push((binding.clone(), depth));
continue 'outer;
}
}
}
}
bindings.sort_by(|a, b| a.1.cmp(&b.1).reverse());
let bindings = bindings
.into_iter()
.map_while(|(binding, _)| {
if binding.action.as_any().type_id() == (NoAction {}).type_id() {
None
} else {
Some(binding)
}
})
.collect();
return (bindings, is_pending.unwrap_or_default());
}
/// Check if the given binding is enabled, given a certain key context.
pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool {
fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool {
// If binding has a context predicate, it must match the current context,
if let Some(predicate) = &binding.context_predicate {
if !predicate.eval(context) {
@@ -96,22 +142,6 @@ impl Keymap {
}
}
if let Some(disabled_predicates) = self.disabled_keystrokes.get(&binding.keystrokes) {
for disabled_predicate in disabled_predicates {
match disabled_predicate {
// The binding must not be globally disabled.
None => return false,
// The binding must not be disabled in the current context.
Some(predicate) => {
if predicate.eval(context) {
return false;
}
}
}
}
}
true
}
}
@@ -166,16 +196,37 @@ mod tests {
keymap.add_bindings(bindings.clone());
// binding is only enabled in a specific context
assert!(!keymap.binding_enabled(&bindings[0], &[KeyContext::parse("barf").unwrap()]));
assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("editor").unwrap()]));
assert!(keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
.is_empty());
assert!(!keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("editor").unwrap()],
)
.0
.is_empty());
// binding is disabled in a more specific context
assert!(!keymap.binding_enabled(
&bindings[0],
&[KeyContext::parse("editor mode=full").unwrap()]
));
assert!(keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("editor mode=full").unwrap()],
)
.0
.is_empty());
// binding is globally disabled
assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf").unwrap()]));
assert!(keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-b").unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
.is_empty());
}
}

View File

@@ -3253,19 +3253,6 @@ impl<'a> WindowContext<'a> {
currently_pending = PendingInput::default();
}
// base case:
// d: returns [`delete`, '']
// j: returns [None, 'j']
// k: returns [`escape`, '']
// l: returns ['j`, `escape`, '']
// ctrl-s: returns ['j`, `save`, '']
// c: returns ['', None, 'c']
// a: returns ['', None, 'ca']
// c: returns ['ca', None, 'c']
// a: returns ['', None' ,'ca']
// t: returns ['', `dispatch`, '']
let match_result = self.window.rendered_frame.dispatch_tree.dispatch_key(
currently_pending.keystrokes,
keystroke,
@@ -3300,7 +3287,7 @@ impl<'a> WindowContext<'a> {
.window
.rendered_frame
.dispatch_tree
.flush(currently_pending.keystrokes, &dispatch_path);
.flush_dispatch(currently_pending.keystrokes, &dispatch_path);
cx.replay_pending_input(to_replay)
})
@@ -3313,7 +3300,6 @@ impl<'a> WindowContext<'a> {
}
self.pending_input_changed();
self.propagate_event = true;
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref());

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::*;
@@ -1393,3 +1393,23 @@ async fn test_remap_nested_pineapple(cx: &mut gpui::TestAppContext) {
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"}}