Add '>' child operator in keymap context predicates

Max Brunsfeld created

Change summary

crates/gpui/src/app.rs                           | 54 ++++++++---
crates/gpui/src/keymap_matcher.rs                | 84 ++++++++++++++---
crates/gpui/src/keymap_matcher/binding.rs        | 12 +-
crates/gpui/src/keymap_matcher/keymap_context.rs | 40 ++++---
crates/gpui/src/presenter.rs                     |  2 
5 files changed, 136 insertions(+), 56 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -1349,21 +1349,24 @@ impl MutableAppContext {
 
     /// Return keystrokes that would dispatch the given action closest to the focused view, if there are any.
     pub(crate) fn keystrokes_for_action(
-        &self,
+        &mut self,
         window_id: usize,
-        dispatch_path: &[usize],
+        view_stack: &[usize],
         action: &dyn Action,
     ) -> 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
                 .cx
                 .views
                 .get(&(window_id, *view_id))
                 .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
                 .keystroke_matcher
-                .keystrokes_for_action(action, &keymap_context);
+                .keystrokes_for_action(action, &self.keystroke_matcher.contexts);
             if keystrokes.is_some() {
                 return keystrokes;
             }
@@ -6681,7 +6684,7 @@ mod tests {
             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".
         cx.add_bindings(vec![Binding::new(
             "a",
@@ -6691,16 +6694,31 @@ mod tests {
 
         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()));
         cx.add_action({
             let actions = actions.clone();
             move |view: &mut View, action: &Action, cx| {
-                if action.0 == "a" {
-                    actions.borrow_mut().push(format!("{} a", view.id));
-                } else {
-                    actions
-                        .borrow_mut()
-                        .push(format!("{} {}", view.id, action.0));
+                actions
+                    .borrow_mut()
+                    .push(format!("{} {}", view.id, action.0));
+
+                if action.0 == "b" {
                     cx.propagate_action();
                 }
             }
@@ -6714,14 +6732,20 @@ mod tests {
         });
 
         cx.dispatch_keystroke(window_id, &Keystroke::parse("a").unwrap());
-
         assert_eq!(&*actions.borrow(), &["2 a"]);
-
         actions.borrow_mut().clear();
 
         cx.dispatch_keystroke(window_id, &Keystroke::parse("b").unwrap());
-
         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)]

crates/gpui/src/keymap_matcher.rs 🔗

@@ -25,6 +25,7 @@ pub struct KeyPressed {
 impl_actions!(gpui, [KeyPressed]);
 
 pub struct KeymapMatcher {
+    pub contexts: Vec<KeymapContext>,
     pending_views: HashMap<usize, KeymapContext>,
     pending_keystrokes: Vec<Keystroke>,
     keymap: Keymap,
@@ -33,6 +34,7 @@ pub struct KeymapMatcher {
 impl KeymapMatcher {
     pub fn new(keymap: Keymap) -> Self {
         Self {
+            contexts: Vec::new(),
             pending_views: Default::default(),
             pending_keystrokes: Vec::new(),
             keymap,
@@ -70,7 +72,7 @@ impl KeymapMatcher {
     pub fn push_keystroke(
         &mut self,
         keystroke: Keystroke,
-        dispatch_path: Vec<(usize, KeymapContext)>,
+        mut dispatch_path: Vec<(usize, KeymapContext)>,
     ) -> MatchResult {
         let mut any_pending = false;
         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();
         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
             if !first_keystroke && !self.pending_views.contains_key(&view_id) {
                 continue;
@@ -87,14 +93,15 @@ impl KeymapMatcher {
             // If there is a previous view context, invalidate that view if it
             // has changed
             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;
                 }
             }
 
             // Find the bindings which map the pending keystrokes and current context
             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) => {
                         // Swap in keystroke for special KeyPressed action
                         if action.name() == "KeyPressed" && action.namespace() == "gpui" {
@@ -105,7 +112,7 @@ impl KeymapMatcher {
                         matched_bindings.push((view_id, action))
                     }
                     BindingMatchResult::Partial => {
-                        self.pending_views.insert(view_id, context.clone());
+                        self.pending_views.insert(view_id, self.contexts[i].clone());
                         any_pending = true;
                     }
                     _ => {}
@@ -129,13 +136,13 @@ impl KeymapMatcher {
     pub fn keystrokes_for_action(
         &self,
         action: &dyn Action,
-        context: &KeymapContext,
+        contexts: &[KeymapContext],
     ) -> Option<SmallVec<[Keystroke; 2]>> {
         self.keymap
             .bindings()
             .iter()
             .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]
-    fn test_context_predicate_eval() -> Result<()> {
-        let predicate = KeymapContextPredicate::parse("a && b || c == d")?;
+    fn test_context_predicate_eval() {
+        let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
 
         let mut context = KeymapContext::default();
         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());
-        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());
-        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());
-        assert!(predicate.eval(&context));
+        assert!(predicate.eval(&[context]));
 
-        let predicate = KeymapContextPredicate::parse("!a")?;
-        assert!(predicate.eval(&KeymapContext::default()));
+        let predicate = KeymapContextPredicate::parse("!a").unwrap();
+        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]

crates/gpui/src/keymap_matcher/binding.rs 🔗

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

crates/gpui/src/keymap_matcher/keymap_context.rs 🔗

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

crates/gpui/src/presenter.rs 🔗

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