Add '>' child operator in keymap context predicates

This commit is contained in:
Max Brunsfeld 2023-01-16 16:00:46 -08:00
parent f62d13de21
commit 373902d933
5 changed files with 136 additions and 56 deletions

View file

@ -1349,21 +1349,24 @@ impl MutableAppContext {
/// Return keystrokes that would dispatch the given action closest to the focused view, if there are any. /// Return keystrokes that would dispatch the given action closest to the focused view, if there are any.
pub(crate) fn keystrokes_for_action( pub(crate) fn keystrokes_for_action(
&self, &mut self,
window_id: usize, window_id: usize,
dispatch_path: &[usize], view_stack: &[usize],
action: &dyn Action, action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> { ) -> Option<SmallVec<[Keystroke; 2]>> {
for view_id in dispatch_path.iter().rev() { self.keystroke_matcher.contexts.clear();
for view_id in view_stack.iter().rev() {
let view = self let view = self
.cx .cx
.views .views
.get(&(window_id, *view_id)) .get(&(window_id, *view_id))
.expect("view in responder chain does not exist"); .expect("view in responder chain does not exist");
let keymap_context = view.keymap_context(self.as_ref()); self.keystroke_matcher
.contexts
.push(view.keymap_context(self.as_ref()));
let keystrokes = self let keystrokes = self
.keystroke_matcher .keystroke_matcher
.keystrokes_for_action(action, &keymap_context); .keystrokes_for_action(action, &self.keystroke_matcher.contexts);
if keystrokes.is_some() { if keystrokes.is_some() {
return keystrokes; return keystrokes;
} }
@ -6681,7 +6684,7 @@ mod tests {
view_3 view_3
}); });
// This keymap's only binding dispatches an action on view 2 because that view will have // This binding only dispatches an action on view 2 because that view will have
// "a" and "b" in its context, but not "c". // "a" and "b" in its context, but not "c".
cx.add_bindings(vec![Binding::new( cx.add_bindings(vec![Binding::new(
"a", "a",
@ -6691,16 +6694,31 @@ mod tests {
cx.add_bindings(vec![Binding::new("b", Action("b".to_string()), None)]); cx.add_bindings(vec![Binding::new("b", Action("b".to_string()), None)]);
// This binding only dispatches an action on views 2 and 3, because they have
// a parent view with a in its context
cx.add_bindings(vec![Binding::new(
"c",
Action("c".to_string()),
Some("b > c"),
)]);
// This binding only dispatches an action on view 2, because they have
// a parent view with a in its context
cx.add_bindings(vec![Binding::new(
"d",
Action("d".to_string()),
Some("a && !b > b"),
)]);
let actions = Rc::new(RefCell::new(Vec::new())); let actions = Rc::new(RefCell::new(Vec::new()));
cx.add_action({ cx.add_action({
let actions = actions.clone(); let actions = actions.clone();
move |view: &mut View, action: &Action, cx| { move |view: &mut View, action: &Action, cx| {
if action.0 == "a" { actions
actions.borrow_mut().push(format!("{} a", view.id)); .borrow_mut()
} else { .push(format!("{} {}", view.id, action.0));
actions
.borrow_mut() if action.0 == "b" {
.push(format!("{} {}", view.id, action.0));
cx.propagate_action(); cx.propagate_action();
} }
} }
@ -6714,14 +6732,20 @@ mod tests {
}); });
cx.dispatch_keystroke(window_id, &Keystroke::parse("a").unwrap()); cx.dispatch_keystroke(window_id, &Keystroke::parse("a").unwrap());
assert_eq!(&*actions.borrow(), &["2 a"]); assert_eq!(&*actions.borrow(), &["2 a"]);
actions.borrow_mut().clear(); actions.borrow_mut().clear();
cx.dispatch_keystroke(window_id, &Keystroke::parse("b").unwrap()); cx.dispatch_keystroke(window_id, &Keystroke::parse("b").unwrap());
assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]); assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]);
actions.borrow_mut().clear();
cx.dispatch_keystroke(window_id, &Keystroke::parse("c").unwrap());
assert_eq!(&*actions.borrow(), &["3 c"]);
actions.borrow_mut().clear();
cx.dispatch_keystroke(window_id, &Keystroke::parse("d").unwrap());
assert_eq!(&*actions.borrow(), &["2 d"]);
actions.borrow_mut().clear();
} }
#[crate::test(self)] #[crate::test(self)]

View file

@ -25,6 +25,7 @@ pub struct KeyPressed {
impl_actions!(gpui, [KeyPressed]); impl_actions!(gpui, [KeyPressed]);
pub struct KeymapMatcher { pub struct KeymapMatcher {
pub contexts: Vec<KeymapContext>,
pending_views: HashMap<usize, KeymapContext>, pending_views: HashMap<usize, KeymapContext>,
pending_keystrokes: Vec<Keystroke>, pending_keystrokes: Vec<Keystroke>,
keymap: Keymap, keymap: Keymap,
@ -33,6 +34,7 @@ pub struct KeymapMatcher {
impl KeymapMatcher { impl KeymapMatcher {
pub fn new(keymap: Keymap) -> Self { pub fn new(keymap: Keymap) -> Self {
Self { Self {
contexts: Vec::new(),
pending_views: Default::default(), pending_views: Default::default(),
pending_keystrokes: Vec::new(), pending_keystrokes: Vec::new(),
keymap, keymap,
@ -70,7 +72,7 @@ impl KeymapMatcher {
pub fn push_keystroke( pub fn push_keystroke(
&mut self, &mut self,
keystroke: Keystroke, keystroke: Keystroke,
dispatch_path: Vec<(usize, KeymapContext)>, mut dispatch_path: Vec<(usize, KeymapContext)>,
) -> MatchResult { ) -> MatchResult {
let mut any_pending = false; let mut any_pending = false;
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new(); let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
@ -78,7 +80,11 @@ impl KeymapMatcher {
let first_keystroke = self.pending_keystrokes.is_empty(); let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke.clone()); self.pending_keystrokes.push(keystroke.clone());
for (view_id, context) in dispatch_path { self.contexts.clear();
self.contexts
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1)));
for (i, (view_id, _)) in dispatch_path.into_iter().enumerate() {
// Don't require pending view entry if there are no pending keystrokes // Don't require pending view entry if there are no pending keystrokes
if !first_keystroke && !self.pending_views.contains_key(&view_id) { if !first_keystroke && !self.pending_views.contains_key(&view_id) {
continue; continue;
@ -87,14 +93,15 @@ impl KeymapMatcher {
// If there is a previous view context, invalidate that view if it // If there is a previous view context, invalidate that view if it
// has changed // has changed
if let Some(previous_view_context) = self.pending_views.remove(&view_id) { if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
if previous_view_context != context { if previous_view_context != self.contexts[i] {
continue; continue;
} }
} }
// Find the bindings which map the pending keystrokes and current context // Find the bindings which map the pending keystrokes and current context
for binding in self.keymap.bindings().iter().rev() { for binding in self.keymap.bindings().iter().rev() {
match binding.match_keys_and_context(&self.pending_keystrokes, &context) { match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
{
BindingMatchResult::Complete(mut action) => { BindingMatchResult::Complete(mut action) => {
// Swap in keystroke for special KeyPressed action // Swap in keystroke for special KeyPressed action
if action.name() == "KeyPressed" && action.namespace() == "gpui" { if action.name() == "KeyPressed" && action.namespace() == "gpui" {
@ -105,7 +112,7 @@ impl KeymapMatcher {
matched_bindings.push((view_id, action)) matched_bindings.push((view_id, action))
} }
BindingMatchResult::Partial => { BindingMatchResult::Partial => {
self.pending_views.insert(view_id, context.clone()); self.pending_views.insert(view_id, self.contexts[i].clone());
any_pending = true; any_pending = true;
} }
_ => {} _ => {}
@ -129,13 +136,13 @@ impl KeymapMatcher {
pub fn keystrokes_for_action( pub fn keystrokes_for_action(
&self, &self,
action: &dyn Action, action: &dyn Action,
context: &KeymapContext, contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> { ) -> Option<SmallVec<[Keystroke; 2]>> {
self.keymap self.keymap
.bindings() .bindings()
.iter() .iter()
.rev() .rev()
.find_map(|binding| binding.keystrokes_for_action(action, context)) .find_map(|binding| binding.keystrokes_for_action(action, contexts))
} }
} }
@ -349,27 +356,70 @@ mod tests {
} }
#[test] #[test]
fn test_context_predicate_eval() -> Result<()> { fn test_context_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b || c == d")?; let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
let mut context = KeymapContext::default(); let mut context = KeymapContext::default();
context.set.insert("a".into()); context.set.insert("a".into());
assert!(!predicate.eval(&context)); assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.set.insert("b".into()); context.set.insert("b".into());
assert!(predicate.eval(&context)); assert!(predicate.eval(&[context]));
context.set.remove("b"); let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "x".into()); context.map.insert("c".into(), "x".into());
assert!(!predicate.eval(&context)); assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "d".into()); context.map.insert("c".into(), "d".into());
assert!(predicate.eval(&context)); assert!(predicate.eval(&[context]));
let predicate = KeymapContextPredicate::parse("!a")?; let predicate = KeymapContextPredicate::parse("!a").unwrap();
assert!(predicate.eval(&KeymapContext::default())); assert!(predicate.eval(&[KeymapContext::default()]));
}
Ok(()) #[test]
fn test_context_child_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
let contexts = [
context_set(&["e", "f"]),
context_set(&["c", "d"]), // match this context
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
let contexts = [
context_set(&["f"]),
context_set(&["e"]), // only match this context
context_set(&["c"]),
context_set(&["a", "b"]),
context_set(&["e"]),
context_set(&["c", "d"]),
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
assert!(!predicate.eval(&contexts[3..]));
assert!(!predicate.eval(&contexts[4..]));
assert!(!predicate.eval(&contexts[5..]));
assert!(!predicate.eval(&contexts[6..]));
fn context_set(names: &[&str]) -> KeymapContext {
KeymapContext {
set: names.iter().copied().map(str::to_string).collect(),
..Default::default()
}
}
} }
#[test] #[test]

View file

@ -41,24 +41,24 @@ impl Binding {
}) })
} }
fn match_context(&self, context: &KeymapContext) -> bool { fn match_context(&self, contexts: &[KeymapContext]) -> bool {
self.context_predicate self.context_predicate
.as_ref() .as_ref()
.map(|predicate| predicate.eval(context)) .map(|predicate| predicate.eval(contexts))
.unwrap_or(true) .unwrap_or(true)
} }
pub fn match_keys_and_context( pub fn match_keys_and_context(
&self, &self,
pending_keystrokes: &Vec<Keystroke>, pending_keystrokes: &Vec<Keystroke>,
context: &KeymapContext, contexts: &[KeymapContext],
) -> BindingMatchResult { ) -> BindingMatchResult {
if self if self
.keystrokes .keystrokes
.as_ref() .as_ref()
.map(|keystrokes| keystrokes.starts_with(&pending_keystrokes)) .map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
.unwrap_or(true) .unwrap_or(true)
&& self.match_context(context) && self.match_context(contexts)
{ {
// If the binding is completed, push it onto the matches list // If the binding is completed, push it onto the matches list
if self if self
@ -79,9 +79,9 @@ impl Binding {
pub fn keystrokes_for_action( pub fn keystrokes_for_action(
&self, &self,
action: &dyn Action, action: &dyn Action,
context: &KeymapContext, contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> { ) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.eq(action) && self.match_context(context) { if self.action.eq(action) && self.match_context(contexts) {
self.keystrokes.clone() self.keystrokes.clone()
} else { } else {
None None

View file

@ -23,6 +23,7 @@ pub enum KeymapContextPredicate {
Identifier(String), Identifier(String),
Equal(String, String), Equal(String, String),
NotEqual(String, String), NotEqual(String, String),
Child(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Not(Box<KeymapContextPredicate>), Not(Box<KeymapContextPredicate>),
And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>), And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>), Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
@ -39,7 +40,8 @@ impl KeymapContextPredicate {
} }
} }
pub fn eval(&self, context: &KeymapContext) -> bool { pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
let Some(context) = contexts.first() else { return false };
match self { match self {
Self::Identifier(name) => context.set.contains(name.as_str()), Self::Identifier(name) => context.set.contains(name.as_str()),
Self::Equal(left, right) => context Self::Equal(left, right) => context
@ -52,16 +54,14 @@ impl KeymapContextPredicate {
.get(left) .get(left)
.map(|value| value != right) .map(|value| value != right)
.unwrap_or(true), .unwrap_or(true),
Self::Not(pred) => !pred.eval(context), Self::Not(pred) => !pred.eval(contexts),
Self::And(left, right) => left.eval(context) && right.eval(context), Self::Child(parent, child) => parent.eval(&contexts[1..]) && child.eval(contexts),
Self::Or(left, right) => left.eval(context) || right.eval(context), Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
} }
} }
fn parse_expr( fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
mut source: &str,
min_precedence: u32,
) -> anyhow::Result<(KeymapContextPredicate, &str)> {
type Op = type Op =
fn(KeymapContextPredicate, KeymapContextPredicate) -> Result<KeymapContextPredicate>; fn(KeymapContextPredicate, KeymapContextPredicate) -> Result<KeymapContextPredicate>;
@ -70,10 +70,11 @@ impl KeymapContextPredicate {
'parse: loop { 'parse: loop {
for (operator, precedence, constructor) in [ for (operator, precedence, constructor) in [
("&&", PRECEDENCE_AND, KeymapContextPredicate::new_and as Op), (">", PRECEDENCE_CHILD, Self::new_child as Op),
("||", PRECEDENCE_OR, KeymapContextPredicate::new_or as Op), ("&&", PRECEDENCE_AND, Self::new_and as Op),
("==", PRECEDENCE_EQ, KeymapContextPredicate::new_eq as Op), ("||", PRECEDENCE_OR, Self::new_or as Op),
("!=", PRECEDENCE_EQ, KeymapContextPredicate::new_neq as Op), ("==", PRECEDENCE_EQ, Self::new_eq as Op),
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
] { ] {
if source.starts_with(operator) && precedence >= min_precedence { if source.starts_with(operator) && precedence >= min_precedence {
source = Self::skip_whitespace(&source[operator.len()..]); source = Self::skip_whitespace(&source[operator.len()..]);
@ -89,7 +90,7 @@ impl KeymapContextPredicate {
Ok((predicate, source)) Ok((predicate, source))
} }
fn parse_primary(mut source: &str) -> anyhow::Result<(KeymapContextPredicate, &str)> { fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
let next = source let next = source
.chars() .chars()
.next() .next()
@ -140,6 +141,10 @@ impl KeymapContextPredicate {
Ok(Self::And(Box::new(self), Box::new(other))) Ok(Self::And(Box::new(self), Box::new(other)))
} }
fn new_child(self, other: Self) -> Result<Self> {
Ok(Self::Child(Box::new(self), Box::new(other)))
}
fn new_eq(self, other: Self) -> Result<Self> { fn new_eq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::Equal(left, right)) Ok(Self::Equal(left, right))
@ -157,10 +162,11 @@ impl KeymapContextPredicate {
} }
} }
const PRECEDENCE_OR: u32 = 1; const PRECEDENCE_CHILD: u32 = 1;
const PRECEDENCE_AND: u32 = 2; const PRECEDENCE_OR: u32 = 2;
const PRECEDENCE_EQ: u32 = 3; const PRECEDENCE_AND: u32 = 3;
const PRECEDENCE_NOT: u32 = 4; const PRECEDENCE_EQ: u32 = 4;
const PRECEDENCE_NOT: u32 = 5;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View file

@ -573,7 +573,7 @@ pub struct LayoutContext<'a> {
impl<'a> LayoutContext<'a> { impl<'a> LayoutContext<'a> {
pub(crate) fn keystrokes_for_action( pub(crate) fn keystrokes_for_action(
&self, &mut self,
action: &dyn Action, action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> { ) -> Option<SmallVec<[Keystroke; 2]>> {
self.app self.app