Add a DispatchTree which will replace the existing key dispatch strategy

Nathan Sobo and Antonio Scandurra created

Instead of freezing a stack, we will record the entire dispatch tree so we can
change focus.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

crates/editor2/src/editor.rs        |  22 
crates/editor2/src/element.rs       |  28 -
crates/gpui/src/dispatch.rs         |   1 
crates/gpui2/src/action.rs          | 400 ----------------------------
crates/gpui2/src/dispatch.rs        | 225 ++++++++++++++++
crates/gpui2/src/gpui2.rs           |   1 
crates/gpui2/src/interactive.rs     |  28 -
crates/gpui2/src/keymap/binding.rs  |  12 
crates/gpui2/src/keymap/context.rs  | 434 +++++++++++++++++++++++++++++++
crates/gpui2/src/keymap/keymap.rs   |   4 
crates/gpui2/src/keymap/matcher.rs  |  10 
crates/gpui2/src/keymap/mod.rs      |   2 
crates/gpui2/src/window.rs          |  34 +-
crates/workspace2/src/workspace2.rs |  14 
14 files changed, 724 insertions(+), 491 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -41,8 +41,8 @@ use git::diff_hunk_to_display;
 use gpui::{
     action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext,
     AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
-    DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
-    HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render,
+    EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla,
+    InputHandler, KeyBindingContext, Model, MouseButton, ParentElement, Pixels, Render,
     StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
     ViewContext, VisualContext, WeakView, WindowContext,
 };
@@ -646,7 +646,7 @@ pub struct Editor {
     collapse_matches: bool,
     autoindent_mode: Option<AutoindentMode>,
     workspace: Option<(WeakView<Workspace>, i64)>,
-    keymap_context_layers: BTreeMap<TypeId, DispatchContext>,
+    keymap_context_layers: BTreeMap<TypeId, KeyBindingContext>,
     input_enabled: bool,
     read_only: bool,
     leader_peer_id: Option<PeerId>,
@@ -1980,9 +1980,9 @@ impl Editor {
         this
     }
 
-    fn dispatch_context(&self, cx: &AppContext) -> DispatchContext {
-        let mut dispatch_context = DispatchContext::default();
-        dispatch_context.insert("Editor");
+    fn dispatch_context(&self, cx: &AppContext) -> KeyBindingContext {
+        let mut dispatch_context = KeyBindingContext::default();
+        dispatch_context.add("Editor");
         let mode = match self.mode {
             EditorMode::SingleLine => "single_line",
             EditorMode::AutoHeight { .. } => "auto_height",
@@ -1990,17 +1990,17 @@ impl Editor {
         };
         dispatch_context.set("mode", mode);
         if self.pending_rename.is_some() {
-            dispatch_context.insert("renaming");
+            dispatch_context.add("renaming");
         }
         if self.context_menu_visible() {
             match self.context_menu.read().as_ref() {
                 Some(ContextMenu::Completions(_)) => {
-                    dispatch_context.insert("menu");
-                    dispatch_context.insert("showing_completions")
+                    dispatch_context.add("menu");
+                    dispatch_context.add("showing_completions")
                 }
                 Some(ContextMenu::CodeActions(_)) => {
-                    dispatch_context.insert("menu");
-                    dispatch_context.insert("showing_code_actions")
+                    dispatch_context.add("menu");
+                    dispatch_context.add("showing_code_actions")
                 }
                 None => {}
             }

crates/editor2/src/element.rs 🔗

@@ -16,11 +16,12 @@ use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use gpui::{
     black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
-    BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase,
-    Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla,
-    InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton,
-    MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size,
-    Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout,
+    BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element,
+    ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler,
+    KeyBindingContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers,
+    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent,
+    ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext,
+    WrappedLineLayout,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -4157,21 +4158,6 @@ fn build_key_listeners(
         build_action_listener(Editor::context_menu_prev),
         build_action_listener(Editor::context_menu_next),
         build_action_listener(Editor::context_menu_last),
-        build_key_listener(
-            move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| {
-                if phase == DispatchPhase::Bubble {
-                    if let KeyMatch::Some(action) = cx.match_keystroke(
-                        &global_element_id,
-                        &key_down.keystroke,
-                        dispatch_context,
-                    ) {
-                        return Some(action);
-                    }
-                }
-
-                None
-            },
-        ),
     ]
 }
 
@@ -4179,7 +4165,7 @@ fn build_key_listener<T: 'static>(
     listener: impl Fn(
             &mut Editor,
             &T,
-            &[&DispatchContext],
+            &[&KeyBindingContext],
             DispatchPhase,
             &mut ViewContext<Editor>,
         ) -> Option<Box<dyn Action>>

crates/gpui2/src/action.rs 🔗

@@ -1,6 +1,6 @@
 use crate::SharedString;
 use anyhow::{anyhow, Context, Result};
-use collections::{HashMap, HashSet};
+use collections::HashMap;
 use lazy_static::lazy_static;
 use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
 use serde::Deserialize;
@@ -186,401 +186,3 @@ macro_rules! actions {
         actions!($($rest)*);
     };
 }
-
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct DispatchContext {
-    set: HashSet<SharedString>,
-    map: HashMap<SharedString, SharedString>,
-}
-
-impl<'a> TryFrom<&'a str> for DispatchContext {
-    type Error = anyhow::Error;
-
-    fn try_from(value: &'a str) -> Result<Self> {
-        Self::parse(value)
-    }
-}
-
-impl DispatchContext {
-    pub fn parse(source: &str) -> Result<Self> {
-        let mut context = Self::default();
-        let source = skip_whitespace(source);
-        Self::parse_expr(&source, &mut context)?;
-        Ok(context)
-    }
-
-    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
-        if source.is_empty() {
-            return Ok(());
-        }
-
-        let key = source
-            .chars()
-            .take_while(|c| is_identifier_char(*c))
-            .collect::<String>();
-        source = skip_whitespace(&source[key.len()..]);
-        if let Some(suffix) = source.strip_prefix('=') {
-            source = skip_whitespace(suffix);
-            let value = source
-                .chars()
-                .take_while(|c| is_identifier_char(*c))
-                .collect::<String>();
-            source = skip_whitespace(&source[value.len()..]);
-            context.set(key, value);
-        } else {
-            context.insert(key);
-        }
-
-        Self::parse_expr(source, context)
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.set.is_empty() && self.map.is_empty()
-    }
-
-    pub fn clear(&mut self) {
-        self.set.clear();
-        self.map.clear();
-    }
-
-    pub fn extend(&mut self, other: &Self) {
-        for v in &other.set {
-            self.set.insert(v.clone());
-        }
-        for (k, v) in &other.map {
-            self.map.insert(k.clone(), v.clone());
-        }
-    }
-
-    pub fn insert<I: Into<SharedString>>(&mut self, identifier: I) {
-        self.set.insert(identifier.into());
-    }
-
-    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
-        self.map.insert(key.into(), value.into());
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Hash)]
-pub enum DispatchContextPredicate {
-    Identifier(SharedString),
-    Equal(SharedString, SharedString),
-    NotEqual(SharedString, SharedString),
-    Child(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
-    Not(Box<DispatchContextPredicate>),
-    And(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
-    Or(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
-}
-
-impl DispatchContextPredicate {
-    pub fn parse(source: &str) -> Result<Self> {
-        let source = skip_whitespace(source);
-        let (predicate, rest) = Self::parse_expr(source, 0)?;
-        if let Some(next) = rest.chars().next() {
-            Err(anyhow!("unexpected character {next:?}"))
-        } else {
-            Ok(predicate)
-        }
-    }
-
-    pub fn eval(&self, contexts: &[&DispatchContext]) -> bool {
-        let Some(context) = contexts.last() else {
-            return false;
-        };
-        match self {
-            Self::Identifier(name) => context.set.contains(name),
-            Self::Equal(left, right) => context
-                .map
-                .get(left)
-                .map(|value| value == right)
-                .unwrap_or(false),
-            Self::NotEqual(left, right) => context
-                .map
-                .get(left)
-                .map(|value| value != right)
-                .unwrap_or(true),
-            Self::Not(pred) => !pred.eval(contexts),
-            Self::Child(parent, child) => {
-                parent.eval(&contexts[..contexts.len() - 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<(Self, &str)> {
-        type Op = fn(
-            DispatchContextPredicate,
-            DispatchContextPredicate,
-        ) -> Result<DispatchContextPredicate>;
-
-        let (mut predicate, rest) = Self::parse_primary(source)?;
-        source = rest;
-
-        'parse: loop {
-            for (operator, precedence, constructor) in [
-                (">", 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 = skip_whitespace(&source[operator.len()..]);
-                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
-                    predicate = constructor(predicate, right)?;
-                    source = rest;
-                    continue 'parse;
-                }
-            }
-            break;
-        }
-
-        Ok((predicate, source))
-    }
-
-    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
-        let next = source
-            .chars()
-            .next()
-            .ok_or_else(|| anyhow!("unexpected eof"))?;
-        match next {
-            '(' => {
-                source = skip_whitespace(&source[1..]);
-                let (predicate, rest) = Self::parse_expr(source, 0)?;
-                if rest.starts_with(')') {
-                    source = skip_whitespace(&rest[1..]);
-                    Ok((predicate, source))
-                } else {
-                    Err(anyhow!("expected a ')'"))
-                }
-            }
-            '!' => {
-                let source = skip_whitespace(&source[1..]);
-                let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
-                Ok((DispatchContextPredicate::Not(Box::new(predicate)), source))
-            }
-            _ if is_identifier_char(next) => {
-                let len = source
-                    .find(|c: char| !is_identifier_char(c))
-                    .unwrap_or(source.len());
-                let (identifier, rest) = source.split_at(len);
-                source = skip_whitespace(rest);
-                Ok((
-                    DispatchContextPredicate::Identifier(identifier.to_string().into()),
-                    source,
-                ))
-            }
-            _ => Err(anyhow!("unexpected character {next:?}")),
-        }
-    }
-
-    fn new_or(self, other: Self) -> Result<Self> {
-        Ok(Self::Or(Box::new(self), Box::new(other)))
-    }
-
-    fn new_and(self, other: Self) -> Result<Self> {
-        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))
-        } else {
-            Err(anyhow!("operands must be identifiers"))
-        }
-    }
-
-    fn new_neq(self, other: Self) -> Result<Self> {
-        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
-            Ok(Self::NotEqual(left, right))
-        } else {
-            Err(anyhow!("operands must be identifiers"))
-        }
-    }
-}
-
-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;
-
-fn is_identifier_char(c: char) -> bool {
-    c.is_alphanumeric() || c == '_' || c == '-'
-}
-
-fn skip_whitespace(source: &str) -> &str {
-    let len = source
-        .find(|c: char| !c.is_whitespace())
-        .unwrap_or(source.len());
-    &source[len..]
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate as gpui;
-    use DispatchContextPredicate::*;
-
-    #[test]
-    fn test_actions_definition() {
-        {
-            actions!(A, B, C, D, E, F, G);
-        }
-
-        {
-            actions!(
-                A,
-                B,
-                C,
-                D,
-                E,
-                F,
-                G, // Don't wrap, test the trailing comma
-            );
-        }
-    }
-
-    #[test]
-    fn test_parse_context() {
-        let mut expected = DispatchContext::default();
-        expected.set("foo", "bar");
-        expected.insert("baz");
-        assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected);
-        assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected);
-        assert_eq!(
-            DispatchContext::parse("  baz foo   =   bar baz").unwrap(),
-            expected
-        );
-        assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected);
-    }
-
-    #[test]
-    fn test_parse_identifiers() {
-        // Identifiers
-        assert_eq!(
-            DispatchContextPredicate::parse("abc12").unwrap(),
-            Identifier("abc12".into())
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("_1a").unwrap(),
-            Identifier("_1a".into())
-        );
-    }
-
-    #[test]
-    fn test_parse_negations() {
-        assert_eq!(
-            DispatchContextPredicate::parse("!abc").unwrap(),
-            Not(Box::new(Identifier("abc".into())))
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse(" ! ! abc").unwrap(),
-            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
-        );
-    }
-
-    #[test]
-    fn test_parse_equality_operators() {
-        assert_eq!(
-            DispatchContextPredicate::parse("a == b").unwrap(),
-            Equal("a".into(), "b".into())
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("c!=d").unwrap(),
-            NotEqual("c".into(), "d".into())
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("c == !d")
-                .unwrap_err()
-                .to_string(),
-            "operands must be identifiers"
-        );
-    }
-
-    #[test]
-    fn test_parse_boolean_operators() {
-        assert_eq!(
-            DispatchContextPredicate::parse("a || b").unwrap(),
-            Or(
-                Box::new(Identifier("a".into())),
-                Box::new(Identifier("b".into()))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a || !b && c").unwrap(),
-            Or(
-                Box::new(Identifier("a".into())),
-                Box::new(And(
-                    Box::new(Not(Box::new(Identifier("b".into())))),
-                    Box::new(Identifier("c".into()))
-                ))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a && b || c&&d").unwrap(),
-            Or(
-                Box::new(And(
-                    Box::new(Identifier("a".into())),
-                    Box::new(Identifier("b".into()))
-                )),
-                Box::new(And(
-                    Box::new(Identifier("c".into())),
-                    Box::new(Identifier("d".into()))
-                ))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(),
-            Or(
-                Box::new(And(
-                    Box::new(Equal("a".into(), "b".into())),
-                    Box::new(Identifier("c".into()))
-                )),
-                Box::new(And(
-                    Box::new(Equal("d".into(), "e".into())),
-                    Box::new(Identifier("f".into()))
-                ))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a && b && c && d").unwrap(),
-            And(
-                Box::new(And(
-                    Box::new(And(
-                        Box::new(Identifier("a".into())),
-                        Box::new(Identifier("b".into()))
-                    )),
-                    Box::new(Identifier("c".into())),
-                )),
-                Box::new(Identifier("d".into()))
-            ),
-        );
-    }
-
-    #[test]
-    fn test_parse_parenthesized_expressions() {
-        assert_eq!(
-            DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(),
-            And(
-                Box::new(Identifier("a".into())),
-                Box::new(Or(
-                    Box::new(Equal("b".into(), "c".into())),
-                    Box::new(NotEqual("d".into(), "e".into())),
-                )),
-            ),
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse(" ( a || b ) ").unwrap(),
-            Or(
-                Box::new(Identifier("a".into())),
-                Box::new(Identifier("b".into())),
-            )
-        );
-    }
-}

crates/gpui2/src/dispatch.rs 🔗

@@ -0,0 +1,225 @@
+use crate::{
+    Action, DispatchPhase, FocusId, KeyBindingContext, KeyDownEvent, KeyMatch, Keymap,
+    KeystrokeMatcher, WindowContext,
+};
+use collections::HashMap;
+use parking_lot::Mutex;
+use smallvec::SmallVec;
+use std::{any::Any, sync::Arc};
+
+// trait KeyListener -> FnMut(&E, &mut V, &mut ViewContext<V>)
+type AnyKeyListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
+type AnyActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+pub struct DispatchNodeId(usize);
+
+pub struct DispatchTree {
+    node_stack: Vec<DispatchNodeId>,
+    context_stack: Vec<KeyBindingContext>,
+    nodes: Vec<DispatchNode>,
+    focused: Option<FocusId>,
+    focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
+    keystroke_matchers: HashMap<SmallVec<[KeyBindingContext; 4]>, KeystrokeMatcher>,
+    keymap: Arc<Mutex<Keymap>>,
+}
+
+#[derive(Default)]
+pub struct DispatchNode {
+    key_listeners: SmallVec<[AnyKeyListener; 2]>,
+    action_listeners: SmallVec<[AnyActionListener; 16]>,
+    context: KeyBindingContext,
+    parent: Option<DispatchNodeId>,
+}
+
+impl DispatchTree {
+    pub fn clear(&mut self) {
+        self.node_stack.clear();
+        self.nodes.clear();
+    }
+
+    pub fn push_node(&mut self, context: Option<KeyBindingContext>, old_tree: &mut Self) {
+        let parent = self.node_stack.last().copied();
+        let node_id = DispatchNodeId(self.nodes.len());
+        self.nodes.push(DispatchNode {
+            parent,
+            ..Default::default()
+        });
+        self.node_stack.push(node_id);
+        if let Some(context) = context {
+            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 pop_node(&mut self) -> DispatchNodeId {
+        self.node_stack.pop().unwrap()
+    }
+
+    pub fn on_key_event(&mut self, listener: AnyKeyListener) {
+        self.active_node().key_listeners.push(listener);
+    }
+
+    pub fn on_action(&mut self, listener: AnyActionListener) {
+        self.active_node().action_listeners.push(listener);
+    }
+
+    pub fn make_focusable(&mut self, focus_id: FocusId) {
+        self.focusable_node_ids
+            .insert(focus_id, self.active_node_id());
+    }
+
+    pub fn set_focus(&mut self, focus_id: Option<FocusId>) {
+        self.focused = focus_id;
+    }
+
+    pub fn active_node(&mut self) -> &mut DispatchNode {
+        let node_id = self.active_node_id();
+        &mut self.nodes[node_id.0]
+    }
+
+    fn active_node_id(&self) -> DispatchNodeId {
+        *self.node_stack.last().unwrap()
+    }
+
+    /// Returns the DispatchNodeIds from the root of the tree to the given target node id.
+    fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {
+        let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new();
+        let mut current_node_id = Some(target);
+        while let Some(node_id) = current_node_id {
+            dispatch_path.push(node_id);
+            current_node_id = self.nodes[node_id.0].parent;
+        }
+        dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
+        dispatch_path
+    }
+
+    pub fn dispatch_key(&mut self, event: &dyn Any, cx: &mut WindowContext) {
+        if let Some(focused_node_id) = self
+            .focused
+            .and_then(|focus_id| self.focusable_node_ids.get(&focus_id))
+            .copied()
+        {
+            self.dispatch_key_on_node(focused_node_id, event, cx);
+        }
+    }
+
+    fn dispatch_key_on_node(
+        &mut self,
+        node_id: DispatchNodeId,
+        event: &dyn Any,
+        cx: &mut WindowContext,
+    ) {
+        let dispatch_path = self.dispatch_path(node_id);
+
+        // Capture phase
+        self.context_stack.clear();
+        cx.propagate_event = true;
+        for node_id in &dispatch_path {
+            let node = &self.nodes[node_id.0];
+            if !node.context.is_empty() {
+                self.context_stack.push(node.context.clone());
+            }
+
+            for key_listener in &node.key_listeners {
+                key_listener(event, DispatchPhase::Capture, cx);
+                if !cx.propagate_event {
+                    return;
+                }
+            }
+        }
+
+        // Bubble phase
+        for node_id in dispatch_path.iter().rev() {
+            let node = &self.nodes[node_id.0];
+
+            // Handle low level key events
+            for key_listener in &node.key_listeners {
+                key_listener(event, DispatchPhase::Bubble, cx);
+                if !cx.propagate_event {
+                    return;
+                }
+            }
+
+            // Match keystrokes
+            if !node.context.is_empty() {
+                if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
+                    if !self
+                        .keystroke_matchers
+                        .contains_key(self.context_stack.as_slice())
+                    {
+                        let keystroke_contexts = self.context_stack.iter().cloned().collect();
+                        self.keystroke_matchers.insert(
+                            keystroke_contexts,
+                            KeystrokeMatcher::new(self.keymap.clone()),
+                        );
+                    }
+
+                    if let Some(keystroke_matcher) = self
+                        .keystroke_matchers
+                        .get_mut(self.context_stack.as_slice())
+                    {
+                        if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(
+                            &key_down_event.keystroke,
+                            self.context_stack.as_slice(),
+                        ) {
+                            self.dispatch_action_on_node(*node_id, action, cx);
+                            if !cx.propagate_event {
+                                return;
+                            }
+                        }
+                    }
+                }
+
+                self.context_stack.pop();
+            }
+        }
+    }
+
+    pub fn dispatch_action(&self, action: Box<dyn Action>, cx: &mut WindowContext) {
+        if let Some(focused_node_id) = self
+            .focused
+            .and_then(|focus_id| self.focusable_node_ids.get(&focus_id))
+            .copied()
+        {
+            self.dispatch_action_on_node(focused_node_id, action, cx);
+        }
+    }
+
+    fn dispatch_action_on_node(
+        &self,
+        node_id: DispatchNodeId,
+        action: Box<dyn Action>,
+        cx: &mut WindowContext,
+    ) {
+        let dispatch_path = self.dispatch_path(node_id);
+
+        // Capture phase
+        for node_id in &dispatch_path {
+            let node = &self.nodes[node_id.0];
+            for action_listener in &node.action_listeners {
+                action_listener(&action, DispatchPhase::Capture, cx);
+                if !cx.propagate_event {
+                    return;
+                }
+            }
+        }
+
+        // Bubble phase
+        for node_id in dispatch_path.iter().rev() {
+            let node = &self.nodes[node_id.0];
+            for action_listener in &node.action_listeners {
+                cx.propagate_event = false; // Actions stop propagation by default during the bubble phase
+                action_listener(&action, DispatchPhase::Capture, cx);
+                if !cx.propagate_event {
+                    return;
+                }
+            }
+        }
+    }
+}

crates/gpui2/src/gpui2.rs 🔗

@@ -3,6 +3,7 @@ mod action;
 mod app;
 mod assets;
 mod color;
+mod dispatch;
 mod element;
 mod elements;
 mod executor;

crates/gpui2/src/interactive.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds,
-    Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch,
+    Component, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyBindingContext, KeyMatch,
     Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style,
     StyleRefinement, Task, View, ViewContext,
 };
@@ -167,7 +167,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     fn context<C>(mut self, context: C) -> Self
     where
         Self: Sized,
-        C: TryInto<DispatchContext>,
+        C: TryInto<KeyBindingContext>,
         C::Error: Debug,
     {
         self.stateless_interactivity().dispatch_context =
@@ -403,24 +403,6 @@ pub trait ElementInteractivity<V: 'static>: 'static {
     ) -> R {
         if let Some(stateful) = self.as_stateful_mut() {
             cx.with_element_id(stateful.id.clone(), |global_id, cx| {
-                // In addition to any key down/up listeners registered directly on the element,
-                // we also add a key listener to match actions from the keymap.
-                stateful.key_listeners.push((
-                    TypeId::of::<KeyDownEvent>(),
-                    Box::new(move |_, key_down, context, phase, cx| {
-                        if phase == DispatchPhase::Bubble {
-                            let key_down = key_down.downcast_ref::<KeyDownEvent>().unwrap();
-                            if let KeyMatch::Some(action) =
-                                cx.match_keystroke(&global_id, &key_down.keystroke, context)
-                            {
-                                return Some(action);
-                            }
-                        }
-
-                        None
-                    }),
-                ));
-
                 cx.with_key_dispatch_context(stateful.dispatch_context.clone(), |cx| {
                     cx.with_key_listeners(mem::take(&mut stateful.key_listeners), f)
                 })
@@ -808,7 +790,7 @@ impl<V: 'static> ElementInteractivity<V> for StatefulInteractivity<V> {
 type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
 
 pub struct StatelessInteractivity<V> {
-    pub dispatch_context: DispatchContext,
+    pub dispatch_context: KeyBindingContext,
     pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
     pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
     pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
@@ -910,7 +892,7 @@ impl InteractiveElementState {
 impl<V> Default for StatelessInteractivity<V> {
     fn default() -> Self {
         Self {
-            dispatch_context: DispatchContext::default(),
+            dispatch_context: KeyBindingContext::default(),
             mouse_down_listeners: SmallVec::new(),
             mouse_up_listeners: SmallVec::new(),
             mouse_move_listeners: SmallVec::new(),
@@ -1254,7 +1236,7 @@ pub type KeyListener<V> = Box<
     dyn Fn(
             &mut V,
             &dyn Any,
-            &[&DispatchContext],
+            &[&KeyBindingContext],
             DispatchPhase,
             &mut ViewContext<V>,
         ) -> Option<Box<dyn Action>>

crates/gpui2/src/keymap/binding.rs 🔗

@@ -1,11 +1,11 @@
-use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke};
+use crate::{Action, KeyBindingContext, KeyBindingContextPredicate, KeyMatch, Keystroke};
 use anyhow::Result;
 use smallvec::SmallVec;
 
 pub struct KeyBinding {
     action: Box<dyn Action>,
     pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
-    pub(super) context_predicate: Option<DispatchContextPredicate>,
+    pub(super) context_predicate: Option<KeyBindingContextPredicate>,
 }
 
 impl KeyBinding {
@@ -15,7 +15,7 @@ impl KeyBinding {
 
     pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
         let context = if let Some(context) = context {
-            Some(DispatchContextPredicate::parse(context)?)
+            Some(KeyBindingContextPredicate::parse(context)?)
         } else {
             None
         };
@@ -32,7 +32,7 @@ impl KeyBinding {
         })
     }
 
-    pub fn matches_context(&self, contexts: &[&DispatchContext]) -> bool {
+    pub fn matches_context(&self, contexts: &[KeyBindingContext]) -> bool {
         self.context_predicate
             .as_ref()
             .map(|predicate| predicate.eval(contexts))
@@ -42,7 +42,7 @@ impl KeyBinding {
     pub fn match_keystrokes(
         &self,
         pending_keystrokes: &[Keystroke],
-        contexts: &[&DispatchContext],
+        contexts: &[KeyBindingContext],
     ) -> KeyMatch {
         if self.keystrokes.as_ref().starts_with(&pending_keystrokes)
             && self.matches_context(contexts)
@@ -61,7 +61,7 @@ impl KeyBinding {
     pub fn keystrokes_for_action(
         &self,
         action: &dyn Action,
-        contexts: &[&DispatchContext],
+        contexts: &[KeyBindingContext],
     ) -> Option<SmallVec<[Keystroke; 2]>> {
         if self.action.partial_eq(action) && self.matches_context(contexts) {
             Some(self.keystrokes.clone())

crates/gpui2/src/keymap/context.rs 🔗

@@ -0,0 +1,434 @@
+use crate::SharedString;
+use anyhow::{anyhow, Result};
+use smallvec::SmallVec;
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
+pub struct KeyBindingContext(SmallVec<[ContextEntry; 8]>);
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+struct ContextEntry {
+    key: SharedString,
+    value: Option<SharedString>,
+}
+
+impl<'a> TryFrom<&'a str> for KeyBindingContext {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'a str) -> Result<Self> {
+        Self::parse(value)
+    }
+}
+
+impl KeyBindingContext {
+    pub fn parse(source: &str) -> Result<Self> {
+        let mut context = Self::default();
+        let source = skip_whitespace(source);
+        Self::parse_expr(&source, &mut context)?;
+        Ok(context)
+    }
+
+    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
+        if source.is_empty() {
+            return Ok(());
+        }
+
+        let key = source
+            .chars()
+            .take_while(|c| is_identifier_char(*c))
+            .collect::<String>();
+        source = skip_whitespace(&source[key.len()..]);
+        if let Some(suffix) = source.strip_prefix('=') {
+            source = skip_whitespace(suffix);
+            let value = source
+                .chars()
+                .take_while(|c| is_identifier_char(*c))
+                .collect::<String>();
+            source = skip_whitespace(&source[value.len()..]);
+            context.set(key, value);
+        } else {
+            context.add(key);
+        }
+
+        Self::parse_expr(source, context)
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
+    pub fn clear(&mut self) {
+        self.0.clear();
+    }
+
+    pub fn extend(&mut self, other: &Self) {
+        for entry in &other.0 {
+            if !self.contains(&entry.key) {
+                self.0.push(entry.clone());
+            }
+        }
+    }
+
+    pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
+        let key = identifier.into();
+
+        if !self.contains(&key) {
+            self.0.push(ContextEntry { key, value: None })
+        }
+    }
+
+    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
+        let key = key.into();
+        if !self.contains(&key) {
+            self.0.push(ContextEntry {
+                key,
+                value: Some(value.into()),
+            })
+        }
+    }
+
+    pub fn contains(&self, key: &str) -> bool {
+        self.0.iter().any(|entry| entry.key.as_ref() == key)
+    }
+
+    pub fn get(&self, key: &str) -> Option<&SharedString> {
+        self.0
+            .iter()
+            .find(|entry| entry.key.as_ref() == key)?
+            .value
+            .as_ref()
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub enum KeyBindingContextPredicate {
+    Identifier(SharedString),
+    Equal(SharedString, SharedString),
+    NotEqual(SharedString, SharedString),
+    Child(
+        Box<KeyBindingContextPredicate>,
+        Box<KeyBindingContextPredicate>,
+    ),
+    Not(Box<KeyBindingContextPredicate>),
+    And(
+        Box<KeyBindingContextPredicate>,
+        Box<KeyBindingContextPredicate>,
+    ),
+    Or(
+        Box<KeyBindingContextPredicate>,
+        Box<KeyBindingContextPredicate>,
+    ),
+}
+
+impl KeyBindingContextPredicate {
+    pub fn parse(source: &str) -> Result<Self> {
+        let source = skip_whitespace(source);
+        let (predicate, rest) = Self::parse_expr(source, 0)?;
+        if let Some(next) = rest.chars().next() {
+            Err(anyhow!("unexpected character {next:?}"))
+        } else {
+            Ok(predicate)
+        }
+    }
+
+    pub fn eval(&self, contexts: &[KeyBindingContext]) -> bool {
+        let Some(context) = contexts.last() else {
+            return false;
+        };
+        match self {
+            Self::Identifier(name) => context.contains(name),
+            Self::Equal(left, right) => context
+                .get(left)
+                .map(|value| value == right)
+                .unwrap_or(false),
+            Self::NotEqual(left, right) => context
+                .get(left)
+                .map(|value| value != right)
+                .unwrap_or(true),
+            Self::Not(pred) => !pred.eval(contexts),
+            Self::Child(parent, child) => {
+                parent.eval(&contexts[..contexts.len() - 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<(Self, &str)> {
+        type Op = fn(
+            KeyBindingContextPredicate,
+            KeyBindingContextPredicate,
+        ) -> Result<KeyBindingContextPredicate>;
+
+        let (mut predicate, rest) = Self::parse_primary(source)?;
+        source = rest;
+
+        'parse: loop {
+            for (operator, precedence, constructor) in [
+                (">", 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 = skip_whitespace(&source[operator.len()..]);
+                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
+                    predicate = constructor(predicate, right)?;
+                    source = rest;
+                    continue 'parse;
+                }
+            }
+            break;
+        }
+
+        Ok((predicate, source))
+    }
+
+    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
+        let next = source
+            .chars()
+            .next()
+            .ok_or_else(|| anyhow!("unexpected eof"))?;
+        match next {
+            '(' => {
+                source = skip_whitespace(&source[1..]);
+                let (predicate, rest) = Self::parse_expr(source, 0)?;
+                if rest.starts_with(')') {
+                    source = skip_whitespace(&rest[1..]);
+                    Ok((predicate, source))
+                } else {
+                    Err(anyhow!("expected a ')'"))
+                }
+            }
+            '!' => {
+                let source = skip_whitespace(&source[1..]);
+                let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
+                Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
+            }
+            _ if is_identifier_char(next) => {
+                let len = source
+                    .find(|c: char| !is_identifier_char(c))
+                    .unwrap_or(source.len());
+                let (identifier, rest) = source.split_at(len);
+                source = skip_whitespace(rest);
+                Ok((
+                    KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
+                    source,
+                ))
+            }
+            _ => Err(anyhow!("unexpected character {next:?}")),
+        }
+    }
+
+    fn new_or(self, other: Self) -> Result<Self> {
+        Ok(Self::Or(Box::new(self), Box::new(other)))
+    }
+
+    fn new_and(self, other: Self) -> Result<Self> {
+        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))
+        } else {
+            Err(anyhow!("operands must be identifiers"))
+        }
+    }
+
+    fn new_neq(self, other: Self) -> Result<Self> {
+        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
+            Ok(Self::NotEqual(left, right))
+        } else {
+            Err(anyhow!("operands must be identifiers"))
+        }
+    }
+}
+
+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;
+
+fn is_identifier_char(c: char) -> bool {
+    c.is_alphanumeric() || c == '_' || c == '-'
+}
+
+fn skip_whitespace(source: &str) -> &str {
+    let len = source
+        .find(|c: char| !c.is_whitespace())
+        .unwrap_or(source.len());
+    &source[len..]
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate as gpui;
+    use KeyBindingContextPredicate::*;
+
+    #[test]
+    fn test_actions_definition() {
+        {
+            actions!(A, B, C, D, E, F, G);
+        }
+
+        {
+            actions!(
+                A,
+                B,
+                C,
+                D,
+                E,
+                F,
+                G, // Don't wrap, test the trailing comma
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_context() {
+        let mut expected = KeyBindingContext::default();
+        expected.set("foo", "bar");
+        expected.add("baz");
+        assert_eq!(KeyBindingContext::parse("baz foo=bar").unwrap(), expected);
+        assert_eq!(KeyBindingContext::parse("foo = bar baz").unwrap(), expected);
+        assert_eq!(
+            KeyBindingContext::parse("  baz foo   =   bar baz").unwrap(),
+            expected
+        );
+        assert_eq!(
+            KeyBindingContext::parse(" foo = bar baz").unwrap(),
+            expected
+        );
+    }
+
+    #[test]
+    fn test_parse_identifiers() {
+        // Identifiers
+        assert_eq!(
+            KeyBindingContextPredicate::parse("abc12").unwrap(),
+            Identifier("abc12".into())
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("_1a").unwrap(),
+            Identifier("_1a".into())
+        );
+    }
+
+    #[test]
+    fn test_parse_negations() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("!abc").unwrap(),
+            Not(Box::new(Identifier("abc".into())))
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
+            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
+        );
+    }
+
+    #[test]
+    fn test_parse_equality_operators() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a == b").unwrap(),
+            Equal("a".into(), "b".into())
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("c!=d").unwrap(),
+            NotEqual("c".into(), "d".into())
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("c == !d")
+                .unwrap_err()
+                .to_string(),
+            "operands must be identifiers"
+        );
+    }
+
+    #[test]
+    fn test_parse_boolean_operators() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a || b").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into()))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(And(
+                    Box::new(Not(Box::new(Identifier("b".into())))),
+                    Box::new(Identifier("c".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
+            Or(
+                Box::new(And(
+                    Box::new(Identifier("a".into())),
+                    Box::new(Identifier("b".into()))
+                )),
+                Box::new(And(
+                    Box::new(Identifier("c".into())),
+                    Box::new(Identifier("d".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
+            Or(
+                Box::new(And(
+                    Box::new(Equal("a".into(), "b".into())),
+                    Box::new(Identifier("c".into()))
+                )),
+                Box::new(And(
+                    Box::new(Equal("d".into(), "e".into())),
+                    Box::new(Identifier("f".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
+            And(
+                Box::new(And(
+                    Box::new(And(
+                        Box::new(Identifier("a".into())),
+                        Box::new(Identifier("b".into()))
+                    )),
+                    Box::new(Identifier("c".into())),
+                )),
+                Box::new(Identifier("d".into()))
+            ),
+        );
+    }
+
+    #[test]
+    fn test_parse_parenthesized_expressions() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
+            And(
+                Box::new(Identifier("a".into())),
+                Box::new(Or(
+                    Box::new(Equal("b".into(), "c".into())),
+                    Box::new(NotEqual("d".into(), "e".into())),
+                )),
+            ),
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into())),
+            )
+        );
+    }
+}

crates/gpui2/src/keymap/keymap.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{DispatchContextPredicate, KeyBinding, Keystroke};
+use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
 use collections::HashSet;
 use smallvec::SmallVec;
 use std::{any::TypeId, collections::HashMap};
@@ -11,7 +11,7 @@ pub struct Keymap {
     bindings: Vec<KeyBinding>,
     binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
     disabled_keystrokes:
-        HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<DispatchContextPredicate>>>,
+        HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
     version: KeymapVersion,
 }
 

crates/gpui2/src/keymap/matcher.rs 🔗

@@ -1,15 +1,15 @@
-use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke};
+use crate::{Action, KeyBindingContext, Keymap, KeymapVersion, Keystroke};
 use parking_lot::Mutex;
 use smallvec::SmallVec;
 use std::sync::Arc;
 
-pub struct KeyMatcher {
+pub struct KeystrokeMatcher {
     pending_keystrokes: Vec<Keystroke>,
     keymap: Arc<Mutex<Keymap>>,
     keymap_version: KeymapVersion,
 }
 
-impl KeyMatcher {
+impl KeystrokeMatcher {
     pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
         let keymap_version = keymap.lock().version();
         Self {
@@ -44,7 +44,7 @@ impl KeyMatcher {
     pub fn match_keystroke(
         &mut self,
         keystroke: &Keystroke,
-        context_stack: &[&DispatchContext],
+        context_stack: &[KeyBindingContext],
     ) -> KeyMatch {
         let keymap = self.keymap.lock();
         // Clear pending keystrokes if the keymap has changed since the last matched keystroke.
@@ -86,7 +86,7 @@ impl KeyMatcher {
     pub fn keystrokes_for_action(
         &self,
         action: &dyn Action,
-        contexts: &[&DispatchContext],
+        contexts: &[KeyBindingContext],
     ) -> Option<SmallVec<[Keystroke; 2]>> {
         self.keymap
             .lock()

crates/gpui2/src/keymap/mod.rs 🔗

@@ -1,7 +1,9 @@
 mod binding;
+mod context;
 mod keymap;
 mod matcher;
 
 pub use binding::*;
+pub use context::*;
 pub use keymap::*;
 pub use matcher::*;

crates/gpui2/src/window.rs 🔗

@@ -1,15 +1,15 @@
 use crate::{
     build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
     AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
-    DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
-    FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent,
-    IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers,
-    MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
-    PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite,
-    PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
-    SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription,
-    TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
-    WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    DevicePixels, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent,
+    FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero,
+    KeyBindingContext, KeyListener, KeyMatch, Keystroke, KeystrokeMatcher, LayoutId, Model,
+    ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
+    MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
+    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
+    Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
+    VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Result};
 use collections::HashMap;
@@ -64,7 +64,7 @@ type AnyListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) +
 type AnyKeyListener = Box<
     dyn Fn(
             &dyn Any,
-            &[&DispatchContext],
+            &[&KeyBindingContext],
             DispatchPhase,
             &mut WindowContext,
         ) -> Option<Box<dyn Action>>
@@ -230,7 +230,7 @@ pub struct Window {
 #[derive(Default)]
 pub(crate) struct Frame {
     element_states: HashMap<GlobalElementId, AnyBox>,
-    key_matchers: HashMap<GlobalElementId, KeyMatcher>,
+    key_matchers: HashMap<GlobalElementId, KeystrokeMatcher>,
     mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
     pub(crate) key_dispatch_stack: Vec<KeyDispatchStackFrame>,
@@ -337,7 +337,7 @@ pub(crate) enum KeyDispatchStackFrame {
         event_type: TypeId,
         listener: AnyKeyListener,
     },
-    Context(DispatchContext),
+    Context(KeyBindingContext),
 }
 
 /// Indicates which region of the window is visible. Content falling outside of this mask will not be
@@ -1228,7 +1228,7 @@ impl<'a> WindowContext<'a> {
         } else if let Some(any_key_event) = event.keyboard_event() {
             let key_dispatch_stack = mem::take(&mut self.window.current_frame.key_dispatch_stack);
             let key_event_type = any_key_event.type_id();
-            let mut context_stack = SmallVec::<[&DispatchContext; 16]>::new();
+            let mut context_stack = SmallVec::<[&KeyBindingContext; 16]>::new();
 
             for (ix, frame) in key_dispatch_stack.iter().enumerate() {
                 match frame {
@@ -1300,7 +1300,7 @@ impl<'a> WindowContext<'a> {
         &mut self,
         element_id: &GlobalElementId,
         keystroke: &Keystroke,
-        context_stack: &[&DispatchContext],
+        context_stack: &[KeyBindingContext],
     ) -> KeyMatch {
         let key_match = self
             .window
@@ -1621,7 +1621,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
                     .previous_frame
                     .key_matchers
                     .remove(&global_id)
-                    .unwrap_or_else(|| KeyMatcher::new(keymap)),
+                    .unwrap_or_else(|| KeystrokeMatcher::new(keymap)),
             );
         }
 
@@ -2120,7 +2120,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 let handle = self.view().downgrade();
                 let listener = Box::new(
                     move |event: &dyn Any,
-                          context_stack: &[&DispatchContext],
+                          context_stack: &[&KeyBindingContext],
                           phase: DispatchPhase,
                           cx: &mut WindowContext<'_>| {
                         handle
@@ -2154,7 +2154,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
 
     pub fn with_key_dispatch_context<R>(
         &mut self,
-        context: DispatchContext,
+        context: KeyBindingContext,
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
         if context.is_empty() {

crates/workspace2/src/workspace2.rs 🔗

@@ -37,11 +37,11 @@ use futures::{
 };
 use gpui::{
     actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
-    AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId,
-    EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render,
-    Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription,
-    Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
-    WindowOptions,
+    AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter,
+    FocusHandle, GlobalPixels, KeyBindingContext, Model, ModelContext, ParentElement, Point,
+    Render, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
+    WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -3743,8 +3743,8 @@ impl Render for Workspace {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let mut context = DispatchContext::default();
-        context.insert("Workspace");
+        let mut context = KeyBindingContext::default();
+        context.add("Workspace");
         cx.with_key_dispatch_context(context, |cx| {
             div()
                 .relative()