diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 6b784833c70a5dcf7c66990463ff070fc99afffe..6a4086c5eb15cc1b126ce343c78b00d745302eb3 100644 --- a/crates/gpui/src/app.rs +++ b/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> { - 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)] diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index e007605cff5fe7005d25d16b015f163f6fecc139..c7de0352328d287b1248b80699c06df7fd07ae0e 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -25,6 +25,7 @@ pub struct KeyPressed { impl_actions!(gpui, [KeyPressed]); pub struct KeymapMatcher { + pub contexts: Vec, pending_views: HashMap, pending_keystrokes: Vec, 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)> = 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> { 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] diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index b16b7f15523a3bd684aad1c3de5397be59f5df68..afd65d4f0424e0031090852d2441d1a6f8bd9420 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/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, - 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> { - if self.action.eq(action) && self.match_context(context) { + if self.action.eq(action) && self.match_context(contexts) { self.keystrokes.clone() } else { None diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs index b1dbd0e92d9cef855d0476c2bca1b7a07f162d88..28f5f80c8337696f06b47ccdb8ba595a3270d5df 100644 --- a/crates/gpui/src/keymap_matcher/keymap_context.rs +++ b/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, Box), Not(Box), And(Box, Box), Or(Box, Box), @@ -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; @@ -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 { + Ok(Self::Child(Box::new(self), Box::new(other))) + } + fn new_eq(self, other: Self) -> Result { 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 { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 5c13f7467aa1e5e0abd589babc49250a113d1329..bbaf1ed0bbce3ec2a096e37719354ce100d00db6 100644 --- a/crates/gpui/src/presenter.rs +++ b/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> { self.app