Merge branch 'gpui2' of github.com:zed-industries/zed into gpui2

Marshall Bowers created

Change summary

crates/gpui3/src/action.rs                  | 328 +++++++++++++++
crates/gpui3/src/app.rs                     |  28 +
crates/gpui3/src/element.rs                 |   2 
crates/gpui3/src/elements/div.rs            |  73 ++-
crates/gpui3/src/events.rs                  |  11 
crates/gpui3/src/focus.rs                   |  52 --
crates/gpui3/src/gpui3.rs                   |   6 
crates/gpui3/src/interactive.rs             |  73 +++
crates/gpui3/src/keymap/binding.rs          |  80 +++
crates/gpui3/src/keymap/keymap.rs           | 397 +++++++++++++++++++
crates/gpui3/src/keymap/matcher.rs          | 473 +++++++++++++++++++++++
crates/gpui3/src/keymap/mod.rs              |   7 
crates/gpui3/src/platform/keystroke.rs      | 110 +++-
crates/gpui3/src/platform/mac/events.rs     |   1 
crates/gpui3/src/platform/mac/window.rs     |   2 
crates/gpui3/src/text_system.rs             |   4 
crates/gpui3/src/text_system/line_layout.rs |   2 
crates/gpui3/src/view.rs                    |   6 
crates/gpui3/src/window.rs                  | 244 +++++++----
crates/storybook2/src/stories/focus.rs      |  59 ++
20 files changed, 1,726 insertions(+), 232 deletions(-)

Detailed changes

crates/gpui3/src/action.rs ๐Ÿ”—

@@ -0,0 +1,328 @@
+use crate::SharedString;
+use anyhow::{anyhow, Result};
+use collections::{HashMap, HashSet};
+use std::any::Any;
+
+pub trait Action: Any + Send + Sync {
+    fn eq(&self, action: &dyn Action) -> bool;
+    fn boxed_clone(&self) -> Box<dyn Action>;
+    fn as_any(&self) -> &dyn Any;
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct ActionContext {
+    set: HashSet<SharedString>,
+    map: HashMap<SharedString, SharedString>,
+}
+
+impl ActionContext {
+    pub fn new() -> Self {
+        ActionContext {
+            set: HashSet::default(),
+            map: HashMap::default(),
+        }
+    }
+
+    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 add_identifier<I: Into<SharedString>>(&mut self, identifier: I) {
+        self.set.insert(identifier.into());
+    }
+
+    pub fn add_key<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 ActionContextPredicate {
+    Identifier(SharedString),
+    Equal(SharedString, SharedString),
+    NotEqual(SharedString, SharedString),
+    Child(Box<ActionContextPredicate>, Box<ActionContextPredicate>),
+    Not(Box<ActionContextPredicate>),
+    And(Box<ActionContextPredicate>, Box<ActionContextPredicate>),
+    Or(Box<ActionContextPredicate>, Box<ActionContextPredicate>),
+}
+
+impl ActionContextPredicate {
+    pub fn parse(source: &str) -> Result<Self> {
+        let source = Self::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: &[ActionContext]) -> bool {
+        let Some(context) = contexts.first() 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[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(ActionContextPredicate, ActionContextPredicate) -> Result<ActionContextPredicate>;
+
+        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 = Self::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 = Self::skip_whitespace(&source[1..]);
+                let (predicate, rest) = Self::parse_expr(source, 0)?;
+                if rest.starts_with(')') {
+                    source = Self::skip_whitespace(&rest[1..]);
+                    Ok((predicate, source))
+                } else {
+                    Err(anyhow!("expected a ')'"))
+                }
+            }
+            '!' => {
+                let source = Self::skip_whitespace(&source[1..]);
+                let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
+                Ok((ActionContextPredicate::Not(Box::new(predicate)), source))
+            }
+            _ if next.is_alphanumeric() || next == '_' => {
+                let len = source
+                    .find(|c: char| !(c.is_alphanumeric() || c == '_'))
+                    .unwrap_or(source.len());
+                let (identifier, rest) = source.split_at(len);
+                source = Self::skip_whitespace(rest);
+                Ok((
+                    ActionContextPredicate::Identifier(identifier.to_string().into()),
+                    source,
+                ))
+            }
+            _ => Err(anyhow!("unexpected character {next:?}")),
+        }
+    }
+
+    fn skip_whitespace(source: &str) -> &str {
+        let len = source
+            .find(|c: char| !c.is_whitespace())
+            .unwrap_or(source.len());
+        &source[len..]
+    }
+
+    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;
+
+#[cfg(test)]
+mod tests {
+    use super::ActionContextPredicate::{self, *};
+
+    #[test]
+    fn test_parse_identifiers() {
+        // Identifiers
+        assert_eq!(
+            ActionContextPredicate::parse("abc12").unwrap(),
+            Identifier("abc12".into())
+        );
+        assert_eq!(
+            ActionContextPredicate::parse("_1a").unwrap(),
+            Identifier("_1a".into())
+        );
+    }
+
+    #[test]
+    fn test_parse_negations() {
+        assert_eq!(
+            ActionContextPredicate::parse("!abc").unwrap(),
+            Not(Box::new(Identifier("abc".into())))
+        );
+        assert_eq!(
+            ActionContextPredicate::parse(" ! ! abc").unwrap(),
+            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
+        );
+    }
+
+    #[test]
+    fn test_parse_equality_operators() {
+        assert_eq!(
+            ActionContextPredicate::parse("a == b").unwrap(),
+            Equal("a".into(), "b".into())
+        );
+        assert_eq!(
+            ActionContextPredicate::parse("c!=d").unwrap(),
+            NotEqual("c".into(), "d".into())
+        );
+        assert_eq!(
+            ActionContextPredicate::parse("c == !d")
+                .unwrap_err()
+                .to_string(),
+            "operands must be identifiers"
+        );
+    }
+
+    #[test]
+    fn test_parse_boolean_operators() {
+        assert_eq!(
+            ActionContextPredicate::parse("a || b").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into()))
+            )
+        );
+        assert_eq!(
+            ActionContextPredicate::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!(
+            ActionContextPredicate::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!(
+            ActionContextPredicate::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!(
+            ActionContextPredicate::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!(
+            ActionContextPredicate::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!(
+            ActionContextPredicate::parse(" ( a || b ) ").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into())),
+            )
+        );
+    }
+}

crates/gpui3/src/app.rs ๐Ÿ”—

@@ -10,14 +10,14 @@ use smallvec::SmallVec;
 
 use crate::{
     current_platform, image_cache::ImageCache, AssetSource, Context, DisplayId, Executor,
-    FocusEvent, FocusHandle, FocusId, LayoutId, MainThread, MainThreadOnly, Platform,
-    SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window,
-    WindowContext, WindowHandle, WindowId,
+    FocusEvent, FocusHandle, FocusId, KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly,
+    Platform, SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View,
+    Window, WindowContext, WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
 use futures::Future;
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use slotmap::SlotMap;
 use std::{
     any::{type_name, Any, TypeId},
@@ -67,6 +67,7 @@ impl App {
                 unit_entity,
                 entities,
                 windows: SlotMap::with_key(),
+                keymap: Arc::new(RwLock::new(Keymap::default())),
                 pending_notifications: Default::default(),
                 pending_effects: Default::default(),
                 observers: SubscriberSet::new(),
@@ -111,6 +112,7 @@ pub struct AppContext {
     pub(crate) unit_entity: Handle<()>,
     pub(crate) entities: EntityMap,
     pub(crate) windows: SlotMap<WindowId, Option<Window>>,
+    pub(crate) keymap: Arc<RwLock<Keymap>>,
     pub(crate) pending_notifications: HashSet<EntityId>,
     pending_effects: VecDeque<Effect>,
     pub(crate) observers: SubscriberSet<EntityId, Handler>,
@@ -165,6 +167,7 @@ impl AppContext {
             }
             Effect::Emit { .. } => self.pending_effects.push_back(effect),
             Effect::FocusChanged { .. } => self.pending_effects.push_back(effect),
+            Effect::Refresh => self.pending_effects.push_back(effect),
         }
     }
 
@@ -179,6 +182,9 @@ impl AppContext {
                     Effect::FocusChanged { window_id, focused } => {
                         self.apply_focus_changed(window_id, focused)
                     }
+                    Effect::Refresh => {
+                        self.apply_refresh();
+                    }
                 }
             } else {
                 break;
@@ -284,6 +290,14 @@ impl AppContext {
         .ok();
     }
 
+    pub fn apply_refresh(&mut self) {
+        for window in self.windows.values_mut() {
+            if let Some(window) = window.as_mut() {
+                window.dirty = true;
+            }
+        }
+    }
+
     pub fn to_async(&self) -> AsyncAppContext {
         AsyncAppContext(unsafe { mem::transmute(self.this.clone()) })
     }
@@ -403,6 +417,11 @@ impl AppContext {
     pub(crate) fn pop_text_style(&mut self) {
         self.text_style_stack.pop();
     }
+
+    pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
+        self.keymap.write().add_bindings(bindings);
+        self.push_effect(Effect::Refresh);
+    }
 }
 
 impl Context for AppContext {
@@ -492,6 +511,7 @@ pub(crate) enum Effect {
         window_id: WindowId,
         focused: Option<FocusId>,
     },
+    Refresh,
 }
 
 #[cfg(test)]

crates/gpui3/src/element.rs ๐Ÿ”—

@@ -33,7 +33,7 @@ pub trait Element: 'static + Send + Sync + IntoAnyElement<Self::ViewState> {
 }
 
 #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
-pub(crate) struct GlobalElementId(SmallVec<[ElementId; 8]>);
+pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
 
 pub trait ElementIdentity: 'static + Send + Sync {
     fn id(&self) -> Option<ElementId>;

crates/gpui3/src/elements/div.rs ๐Ÿ”—

@@ -1,15 +1,16 @@
 use crate::{
     Active, Anonymous, AnyElement, AppContext, BorrowWindow, Bounds, Click, DispatchPhase, Element,
     ElementFocusability, ElementId, ElementIdentity, EventListeners, Focus, FocusHandle, Focusable,
-    Hover, Identified, Interactive, IntoAnyElement, LayoutId, MouseClickEvent, MouseDownEvent,
-    MouseMoveEvent, MouseUpEvent, NonFocusable, Overflow, ParentElement, Pixels, Point,
-    ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, ViewContext,
+    GlobalElementId, Hover, Identified, Interactive, IntoAnyElement, KeyDownEvent, KeyMatch,
+    LayoutId, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, NonFocusable,
+    Overflow, ParentElement, Pixels, Point, ScrollWheelEvent, SharedString, Style, StyleRefinement,
+    Styled, ViewContext,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
 use refineable::Refineable;
 use smallvec::SmallVec;
-use std::{mem, sync::Arc};
+use std::{any::TypeId, mem, sync::Arc};
 
 #[derive(Default)]
 pub struct DivState {
@@ -187,12 +188,12 @@ where
     fn with_element_id<R>(
         &mut self,
         cx: &mut ViewContext<V>,
-        f: impl FnOnce(&mut Self, &mut ViewContext<V>) -> R,
+        f: impl FnOnce(&mut Self, Option<GlobalElementId>, &mut ViewContext<V>) -> R,
     ) -> R {
         if let Some(id) = self.id() {
-            cx.with_element_id(id, |cx| f(self, cx))
+            cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx))
         } else {
-            f(self, cx)
+            f(self, None, cx)
         }
     }
 
@@ -423,27 +424,49 @@ where
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<Self::ViewState>,
     ) -> Self::ElementState {
-        for listener in self.listeners.focus.iter().cloned() {
-            cx.on_focus_changed(move |view, event, cx| listener(view, event, cx));
-        }
+        self.with_element_id(cx, |this, global_id, cx| {
+            let element_state = element_state.unwrap_or_default();
+            for listener in this.listeners.focus.iter().cloned() {
+                cx.on_focus_changed(move |view, event, cx| listener(view, event, cx));
+            }
+
+            let mut key_listeners = mem::take(&mut this.listeners.key);
+
+            if let Some(global_id) = global_id {
+                key_listeners.push((
+                    TypeId::of::<KeyDownEvent>(),
+                    Arc::new(move |_, key_down, 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)
+                            {
+                                return Some(action);
+                            }
+                        }
+
+                        None
+                    }),
+                ));
+            }
 
-        let key_listeners = mem::take(&mut self.listeners.key);
-        cx.with_key_listeners(&key_listeners, |cx| {
-            if let Some(focus_handle) = self.focusability.focus_handle().cloned() {
-                cx.with_focus(focus_handle, |cx| {
-                    for child in &mut self.children {
+            cx.with_key_listeners(&key_listeners, |cx| {
+                if let Some(focus_handle) = this.focusability.focus_handle().cloned() {
+                    cx.with_focus(focus_handle, |cx| {
+                        for child in &mut this.children {
+                            child.initialize(view_state, cx);
+                        }
+                    })
+                } else {
+                    for child in &mut this.children {
                         child.initialize(view_state, cx);
                     }
-                })
-            } else {
-                for child in &mut self.children {
-                    child.initialize(view_state, cx);
                 }
-            }
-        });
-        self.listeners.key = key_listeners;
+            });
+            this.listeners.key = key_listeners;
 
-        element_state.unwrap_or_default()
+            element_state
+        })
     }
 
     fn layout(
@@ -454,7 +477,7 @@ where
     ) -> LayoutId {
         let style = self.compute_style(Bounds::default(), element_state, cx);
         style.apply_text_style(cx, |cx| {
-            self.with_element_id(cx, |this, cx| {
+            self.with_element_id(cx, |this, _global_id, cx| {
                 let layout_ids = this
                     .children
                     .iter_mut()
@@ -472,7 +495,7 @@ where
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<Self::ViewState>,
     ) {
-        self.with_element_id(cx, |this, cx| {
+        self.with_element_id(cx, |this, _global_id, cx| {
             if let Some(group) = this.group.clone() {
                 cx.default_global::<GroupBounds>()
                     .0

crates/gpui3/src/events.rs ๐Ÿ”—

@@ -1,5 +1,6 @@
 use crate::{
-    point, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point, ViewContext,
+    point, Action, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point,
+    ViewContext,
 };
 use smallvec::SmallVec;
 use std::{
@@ -254,8 +255,12 @@ pub type ScrollWheelListener<V> = Arc<
         + 'static,
 >;
 
-pub type KeyListener<V> =
-    Arc<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static>;
+pub type KeyListener<V> = Arc<
+    dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) -> Option<Box<dyn Action>>
+        + Send
+        + Sync
+        + 'static,
+>;
 
 pub type FocusListener<V> =
     Arc<dyn Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static>;

crates/gpui3/src/focus.rs ๐Ÿ”—

@@ -1,9 +1,5 @@
-use std::{any::TypeId, sync::Arc};
-
-use crate::{
-    DispatchPhase, FocusEvent, FocusHandle, Interactive, KeyDownEvent, KeyUpEvent, StyleRefinement,
-    ViewContext,
-};
+use crate::{FocusEvent, FocusHandle, Interactive, StyleRefinement, ViewContext};
+use std::sync::Arc;
 
 pub trait Focus: Interactive {
     fn set_focus_style(&mut self, style: StyleRefinement);
@@ -135,48 +131,4 @@ pub trait Focus: Interactive {
             }));
         self
     }
-
-    fn on_key_down(
-        mut self,
-        listener: impl Fn(
-                &mut Self::ViewState,
-                &KeyDownEvent,
-                DispatchPhase,
-                &mut ViewContext<Self::ViewState>,
-            ) + Send
-            + Sync
-            + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.listeners().key.push((
-            TypeId::of::<KeyDownEvent>(),
-            Arc::new(move |view, event, phase, cx| {
-                let event = event.downcast_ref().unwrap();
-                listener(view, event, phase, cx)
-            }),
-        ));
-        self
-    }
-
-    fn on_key_up(
-        mut self,
-        listener: impl Fn(&mut Self::ViewState, &KeyUpEvent, DispatchPhase, &mut ViewContext<Self::ViewState>)
-            + Send
-            + Sync
-            + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.listeners().key.push((
-            TypeId::of::<KeyUpEvent>(),
-            Arc::new(move |view, event, phase, cx| {
-                let event = event.downcast_ref().unwrap();
-                listener(view, event, phase, cx)
-            }),
-        ));
-        self
-    }
 }

crates/gpui3/src/gpui3.rs ๐Ÿ”—

@@ -1,3 +1,4 @@
+mod action;
 mod active;
 mod app;
 mod assets;
@@ -11,6 +12,7 @@ mod geometry;
 mod hover;
 mod image_cache;
 mod interactive;
+mod keymap;
 mod platform;
 mod scene;
 mod style;
@@ -23,6 +25,7 @@ mod util;
 mod view;
 mod window;
 
+pub use action::*;
 pub use active::*;
 pub use anyhow::Result;
 pub use app::*;
@@ -38,6 +41,7 @@ pub use gpui3_macros::*;
 pub use hover::*;
 pub use image_cache::*;
 pub use interactive::*;
+pub use keymap::*;
 pub use platform::*;
 pub use refineable::*;
 pub use scene::*;
@@ -64,7 +68,7 @@ use std::{
 };
 use taffy::TaffyLayoutEngine;
 
-type AnyBox = Box<dyn Any + Send + Sync + 'static>;
+type AnyBox = Box<dyn Any + Send + Sync>;
 
 pub trait Context {
     type EntityContext<'a, 'w, T: 'static + Send + Sync>;

crates/gpui3/src/interactive.rs ๐Ÿ”—

@@ -1,8 +1,8 @@
-use std::sync::Arc;
+use std::{any::TypeId, sync::Arc};
 
 use crate::{
-    DispatchPhase, Element, EventListeners, MouseButton, MouseClickEvent, MouseDownEvent,
-    MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, ViewContext,
+    DispatchPhase, Element, EventListeners, KeyDownEvent, KeyUpEvent, MouseButton, MouseClickEvent,
+    MouseDownEvent, MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, ViewContext,
 };
 
 pub trait Interactive: Element {
@@ -143,6 +143,73 @@ pub trait Interactive: Element {
             }));
         self
     }
+
+    fn on_key_down(
+        mut self,
+        listener: impl Fn(
+                &mut Self::ViewState,
+                &KeyDownEvent,
+                DispatchPhase,
+                &mut ViewContext<Self::ViewState>,
+            ) + Send
+            + Sync
+            + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.listeners().key.push((
+            TypeId::of::<KeyDownEvent>(),
+            Arc::new(move |view, event, phase, cx| {
+                let event = event.downcast_ref().unwrap();
+                listener(view, event, phase, cx);
+                None
+            }),
+        ));
+        self
+    }
+
+    fn on_key_up(
+        mut self,
+        listener: impl Fn(&mut Self::ViewState, &KeyUpEvent, DispatchPhase, &mut ViewContext<Self::ViewState>)
+            + Send
+            + Sync
+            + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.listeners().key.push((
+            TypeId::of::<KeyUpEvent>(),
+            Arc::new(move |view, event, phase, cx| {
+                let event = event.downcast_ref().unwrap();
+                listener(view, event, phase, cx);
+                None
+            }),
+        ));
+        self
+    }
+
+    fn on_action<A: 'static>(
+        mut self,
+        listener: impl Fn(&mut Self::ViewState, &A, DispatchPhase, &mut ViewContext<Self::ViewState>)
+            + Send
+            + Sync
+            + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.listeners().key.push((
+            TypeId::of::<A>(),
+            Arc::new(move |view, event, phase, cx| {
+                let event = event.downcast_ref().unwrap();
+                listener(view, event, phase, cx);
+                None
+            }),
+        ));
+        self
+    }
 }
 
 pub trait Click: Interactive {

crates/gpui3/src/keymap/binding.rs ๐Ÿ”—

@@ -0,0 +1,80 @@
+use crate::{Action, ActionContext, ActionContextPredicate, 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<ActionContextPredicate>,
+}
+
+impl KeyBinding {
+    pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
+        Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
+    }
+
+    pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
+        let context = if let Some(context) = context {
+            Some(ActionContextPredicate::parse(context)?)
+        } else {
+            None
+        };
+
+        let keystrokes = keystrokes
+            .split_whitespace()
+            .map(Keystroke::parse)
+            .collect::<Result<_>>()?;
+
+        Ok(Self {
+            keystrokes,
+            action,
+            context_predicate: context,
+        })
+    }
+
+    pub fn matches_context(&self, contexts: &[ActionContext]) -> bool {
+        self.context_predicate
+            .as_ref()
+            .map(|predicate| predicate.eval(contexts))
+            .unwrap_or(true)
+    }
+
+    pub fn match_keystrokes(
+        &self,
+        pending_keystrokes: &[Keystroke],
+        contexts: &[ActionContext],
+    ) -> KeyMatch {
+        if self.keystrokes.as_ref().starts_with(&pending_keystrokes)
+            && self.matches_context(contexts)
+        {
+            // If the binding is completed, push it onto the matches list
+            if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
+                KeyMatch::Some(self.action.boxed_clone())
+            } else {
+                KeyMatch::Pending
+            }
+        } else {
+            KeyMatch::None
+        }
+    }
+
+    pub fn keystrokes_for_action(
+        &self,
+        action: &dyn Action,
+        contexts: &[ActionContext],
+    ) -> Option<SmallVec<[Keystroke; 2]>> {
+        if self.action.eq(action) && self.matches_context(contexts) {
+            Some(self.keystrokes.clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn keystrokes(&self) -> &[Keystroke] {
+        self.keystrokes.as_slice()
+    }
+
+    pub fn action(&self) -> &dyn Action {
+        self.action.as_ref()
+    }
+}

crates/gpui3/src/keymap/keymap.rs ๐Ÿ”—

@@ -0,0 +1,397 @@
+use crate::{ActionContextPredicate, KeyBinding, Keystroke};
+use collections::HashSet;
+use smallvec::SmallVec;
+use std::{any::TypeId, collections::HashMap};
+
+#[derive(Copy, Clone, Eq, PartialEq, Default)]
+pub struct KeymapVersion(usize);
+
+#[derive(Default)]
+pub struct Keymap {
+    bindings: Vec<KeyBinding>,
+    binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
+    disabled_keystrokes: HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<ActionContextPredicate>>>,
+    version: KeymapVersion,
+}
+
+impl Keymap {
+    pub fn new(bindings: Vec<KeyBinding>) -> Self {
+        let mut this = Self::default();
+        this.add_bindings(bindings);
+        this
+    }
+
+    pub fn version(&self) -> KeymapVersion {
+        self.version
+    }
+
+    pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &'_ KeyBinding> {
+        self.binding_indices_by_action_id
+            .get(&action_id)
+            .map(SmallVec::as_slice)
+            .unwrap_or(&[])
+            .iter()
+            .map(|ix| &self.bindings[*ix])
+            .filter(|binding| !self.binding_disabled(binding))
+    }
+
+    pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
+        // todo!("no action")
+        // let no_action_id = (NoAction {}).id();
+        let mut new_bindings = Vec::new();
+        let has_new_disabled_keystrokes = false;
+        for binding in bindings {
+            // if binding.action().id() == no_action_id {
+            //     has_new_disabled_keystrokes |= self
+            //         .disabled_keystrokes
+            //         .entry(binding.keystrokes)
+            //         .or_default()
+            //         .insert(binding.context_predicate);
+            // } else {
+            new_bindings.push(binding);
+            // }
+        }
+
+        if has_new_disabled_keystrokes {
+            self.binding_indices_by_action_id.retain(|_, indices| {
+                indices.retain(|ix| {
+                    let binding = &self.bindings[*ix];
+                    match self.disabled_keystrokes.get(&binding.keystrokes) {
+                        Some(disabled_predicates) => {
+                            !disabled_predicates.contains(&binding.context_predicate)
+                        }
+                        None => true,
+                    }
+                });
+                !indices.is_empty()
+            });
+        }
+
+        for new_binding in new_bindings {
+            if !self.binding_disabled(&new_binding) {
+                self.binding_indices_by_action_id
+                    .entry(new_binding.action().as_any().type_id())
+                    .or_default()
+                    .push(self.bindings.len());
+                self.bindings.push(new_binding);
+            }
+        }
+
+        self.version.0 += 1;
+    }
+
+    pub fn clear(&mut self) {
+        self.bindings.clear();
+        self.binding_indices_by_action_id.clear();
+        self.disabled_keystrokes.clear();
+        self.version.0 += 1;
+    }
+
+    pub fn bindings(&self) -> Vec<&KeyBinding> {
+        self.bindings
+            .iter()
+            .filter(|binding| !self.binding_disabled(binding))
+            .collect()
+    }
+
+    fn binding_disabled(&self, binding: &KeyBinding) -> bool {
+        match self.disabled_keystrokes.get(&binding.keystrokes) {
+            Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate),
+            None => false,
+        }
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use crate::actions;
+
+//     use super::*;
+
+//     actions!(
+//         keymap_test,
+//         [Present1, Present2, Present3, Duplicate, Missing]
+//     );
+
+//     #[test]
+//     fn regular_keymap() {
+//         let present_1 = Binding::new("ctrl-q", Present1 {}, None);
+//         let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
+//         let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
+//         let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let missing = Binding::new("ctrl-r", Missing {}, None);
+//         let all_bindings = [
+//             &present_1,
+//             &present_2,
+//             &present_3,
+//             &keystroke_duplicate_to_1,
+//             &full_duplicate_to_2,
+//             &missing,
+//         ];
+
+//         let mut keymap = Keymap::default();
+//         assert_absent(&keymap, &all_bindings);
+//         assert!(keymap.bindings().is_empty());
+
+//         keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]);
+//         assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]);
+//         assert_present(
+//             &keymap,
+//             &[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")],
+//         );
+
+//         keymap.add_bindings([
+//             keystroke_duplicate_to_1.clone(),
+//             full_duplicate_to_2.clone(),
+//         ]);
+//         assert_absent(&keymap, &[&missing]);
+//         assert!(
+//             !keymap.binding_disabled(&keystroke_duplicate_to_1),
+//             "Duplicate binding 1 was added and should not be disabled"
+//         );
+//         assert!(
+//             !keymap.binding_disabled(&full_duplicate_to_2),
+//             "Duplicate binding 2 was added and should not be disabled"
+//         );
+
+//         assert_eq!(
+//             keymap
+//                 .bindings_for_action(keystroke_duplicate_to_1.action().id())
+//                 .map(|binding| &binding.keystrokes)
+//                 .flatten()
+//                 .collect::<Vec<_>>(),
+//             vec![&Keystroke {
+//                 ctrl: true,
+//                 alt: false,
+//                 shift: false,
+//                 cmd: false,
+//                 function: false,
+//                 key: "q".to_string(),
+//                 ime_key: None,
+//             }],
+//             "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
+//         );
+//         assert_eq!(
+//             keymap
+//                 .bindings_for_action(full_duplicate_to_2.action().id())
+//                 .map(|binding| &binding.keystrokes)
+//                 .flatten()
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 &Keystroke {
+//                     ctrl: true,
+//                     alt: false,
+//                     shift: false,
+//                     cmd: false,
+//                     function: false,
+//                     key: "w".to_string(),
+//                     ime_key: None,
+//                 },
+//                 &Keystroke {
+//                     ctrl: true,
+//                     alt: false,
+//                     shift: false,
+//                     cmd: false,
+//                     function: false,
+//                     key: "w".to_string(),
+//                     ime_key: None,
+//                 }
+//             ],
+//             "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
+//         );
+
+//         let updated_bindings = keymap.bindings();
+//         let expected_updated_bindings = vec![
+//             &present_1,
+//             &present_2,
+//             &present_3,
+//             &keystroke_duplicate_to_1,
+//             &full_duplicate_to_2,
+//         ];
+//         assert_eq!(
+//             updated_bindings.len(),
+//             expected_updated_bindings.len(),
+//             "Unexpected updated keymap bindings {updated_bindings:?}"
+//         );
+//         for (i, expected) in expected_updated_bindings.iter().enumerate() {
+//             let keymap_binding = &updated_bindings[i];
+//             assert_eq!(
+//                 keymap_binding.context_predicate, expected.context_predicate,
+//                 "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
+//             );
+//             assert_eq!(
+//                 keymap_binding.keystrokes, expected.keystrokes,
+//                 "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
+//             );
+//         }
+
+//         keymap.clear();
+//         assert_absent(&keymap, &all_bindings);
+//         assert!(keymap.bindings().is_empty());
+//     }
+
+//     #[test]
+//     fn keymap_with_ignored() {
+//         let present_1 = Binding::new("ctrl-q", Present1 {}, None);
+//         let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
+//         let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
+//         let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let ignored_1 = Binding::new("ctrl-q", NoAction {}, None);
+//         let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane"));
+//         let ignored_3_with_other_context =
+//             Binding::new("ctrl-e", NoAction {}, Some("other_context"));
+
+//         let mut keymap = Keymap::default();
+
+//         keymap.add_bindings([
+//             ignored_1.clone(),
+//             ignored_2.clone(),
+//             ignored_3_with_other_context.clone(),
+//         ]);
+//         assert_absent(&keymap, &[&present_3]);
+//         assert_disabled(
+//             &keymap,
+//             &[
+//                 &present_1,
+//                 &present_2,
+//                 &ignored_1,
+//                 &ignored_2,
+//                 &ignored_3_with_other_context,
+//             ],
+//         );
+//         assert!(keymap.bindings().is_empty());
+//         keymap.clear();
+
+//         keymap.add_bindings([
+//             present_1.clone(),
+//             present_2.clone(),
+//             present_3.clone(),
+//             ignored_1.clone(),
+//             ignored_2.clone(),
+//             ignored_3_with_other_context.clone(),
+//         ]);
+//         assert_present(&keymap, &[(&present_3, "e")]);
+//         assert_disabled(
+//             &keymap,
+//             &[
+//                 &present_1,
+//                 &present_2,
+//                 &ignored_1,
+//                 &ignored_2,
+//                 &ignored_3_with_other_context,
+//             ],
+//         );
+//         keymap.clear();
+
+//         keymap.add_bindings([
+//             present_1.clone(),
+//             present_2.clone(),
+//             present_3.clone(),
+//             ignored_1.clone(),
+//         ]);
+//         assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]);
+//         assert_disabled(&keymap, &[&present_1, &ignored_1]);
+//         assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]);
+//         keymap.clear();
+
+//         keymap.add_bindings([
+//             present_1.clone(),
+//             present_2.clone(),
+//             present_3.clone(),
+//             keystroke_duplicate_to_1.clone(),
+//             full_duplicate_to_2.clone(),
+//             ignored_1.clone(),
+//             ignored_2.clone(),
+//             ignored_3_with_other_context.clone(),
+//         ]);
+//         assert_present(&keymap, &[(&present_3, "e")]);
+//         assert_disabled(
+//             &keymap,
+//             &[
+//                 &present_1,
+//                 &present_2,
+//                 &keystroke_duplicate_to_1,
+//                 &full_duplicate_to_2,
+//                 &ignored_1,
+//                 &ignored_2,
+//                 &ignored_3_with_other_context,
+//             ],
+//         );
+//         keymap.clear();
+//     }
+
+//     #[track_caller]
+//     fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) {
+//         let keymap_bindings = keymap.bindings();
+//         assert_eq!(
+//             expected_bindings.len(),
+//             keymap_bindings.len(),
+//             "Unexpected keymap bindings {keymap_bindings:?}"
+//         );
+//         for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() {
+//             assert!(
+//                 !keymap.binding_disabled(expected),
+//                 "{expected:?} should not be disabled as it was added into keymap for element {i}"
+//             );
+//             assert_eq!(
+//                 keymap
+//                     .bindings_for_action(expected.action().id())
+//                     .map(|binding| &binding.keystrokes)
+//                     .flatten()
+//                     .collect::<Vec<_>>(),
+//                 vec![&Keystroke {
+//                     ctrl: true,
+//                     alt: false,
+//                     shift: false,
+//                     cmd: false,
+//                     function: false,
+//                     key: expected_key.to_string(),
+//                     ime_key: None,
+//                 }],
+//                 "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
+//             );
+
+//             let keymap_binding = &keymap_bindings[i];
+//             assert_eq!(
+//                 keymap_binding.context_predicate, expected.context_predicate,
+//                 "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
+//             );
+//             assert_eq!(
+//                 keymap_binding.keystrokes, expected.keystrokes,
+//                 "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
+//             );
+//         }
+//     }
+
+//     #[track_caller]
+//     fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) {
+//         for binding in bindings.iter() {
+//             assert!(
+//                 !keymap.binding_disabled(binding),
+//                 "{binding:?} should not be disabled in the keymap where was not added"
+//             );
+//             assert_eq!(
+//                 keymap.bindings_for_action(binding.action().id()).count(),
+//                 0,
+//                 "{binding:?} should have no actions in the keymap where was not added"
+//             );
+//         }
+//     }
+
+//     #[track_caller]
+//     fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) {
+//         for binding in bindings.iter() {
+//             assert!(
+//                 keymap.binding_disabled(binding),
+//                 "{binding:?} should be disabled in the keymap"
+//             );
+//             assert_eq!(
+//                 keymap.bindings_for_action(binding.action().id()).count(),
+//                 0,
+//                 "{binding:?} should have no actions in the keymap where it was disabled"
+//             );
+//         }
+//     }
+// }

crates/gpui3/src/keymap/matcher.rs ๐Ÿ”—

@@ -0,0 +1,473 @@
+use crate::{Action, ActionContext, Keymap, KeymapVersion, Keystroke};
+use parking_lot::RwLock;
+use smallvec::SmallVec;
+use std::sync::Arc;
+
+pub struct KeyMatcher {
+    pending_keystrokes: Vec<Keystroke>,
+    keymap: Arc<RwLock<Keymap>>,
+    keymap_version: KeymapVersion,
+}
+
+impl KeyMatcher {
+    pub fn new(keymap: Arc<RwLock<Keymap>>) -> Self {
+        let keymap_version = keymap.read().version();
+        Self {
+            pending_keystrokes: Vec::new(),
+            keymap_version,
+            keymap,
+        }
+    }
+
+    // todo!("replace with a function that calls an FnMut for every binding matching the action")
+    // pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &Binding> {
+    //     self.keymap.read().bindings_for_action(action_id)
+    // }
+
+    pub fn clear_pending(&mut self) {
+        self.pending_keystrokes.clear();
+    }
+
+    pub fn has_pending_keystrokes(&self) -> bool {
+        !self.pending_keystrokes.is_empty()
+    }
+
+    /// Pushes a keystroke onto the matcher.
+    /// The result of the new keystroke is returned:
+    ///     KeyMatch::None =>
+    ///         No match is valid for this key given any pending keystrokes.
+    ///     KeyMatch::Pending =>
+    ///         There exist bindings which are still waiting for more keys.
+    ///     KeyMatch::Complete(matches) =>
+    ///         One or more bindings have received the necessary key presses.
+    ///         Bindings added later will take precedence over earlier bindings.
+    pub fn match_keystroke(
+        &mut self,
+        keystroke: &Keystroke,
+        context_stack: &[ActionContext],
+    ) -> KeyMatch {
+        let keymap = self.keymap.read();
+        // Clear pending keystrokes if the keymap has changed since the last matched keystroke.
+        if keymap.version() != self.keymap_version {
+            self.keymap_version = keymap.version();
+            self.pending_keystrokes.clear();
+        }
+
+        let mut pending_key = None;
+
+        for binding in keymap.bindings().iter().rev() {
+            for candidate in keystroke.match_candidates() {
+                self.pending_keystrokes.push(candidate.clone());
+                match binding.match_keystrokes(&self.pending_keystrokes, context_stack) {
+                    KeyMatch::Some(action) => {
+                        self.pending_keystrokes.clear();
+                        return KeyMatch::Some(action);
+                    }
+                    KeyMatch::Pending => {
+                        pending_key.get_or_insert(candidate);
+                    }
+                    KeyMatch::None => {}
+                }
+                self.pending_keystrokes.pop();
+            }
+        }
+
+        if let Some(pending_key) = pending_key {
+            self.pending_keystrokes.push(pending_key);
+        }
+
+        if self.pending_keystrokes.is_empty() {
+            KeyMatch::None
+        } else {
+            KeyMatch::Pending
+        }
+    }
+
+    pub fn keystrokes_for_action(
+        &self,
+        action: &dyn Action,
+        contexts: &[ActionContext],
+    ) -> Option<SmallVec<[Keystroke; 2]>> {
+        self.keymap
+            .read()
+            .bindings()
+            .iter()
+            .rev()
+            .find_map(|binding| binding.keystrokes_for_action(action, contexts))
+    }
+}
+
+pub enum KeyMatch {
+    None,
+    Pending,
+    Some(Box<dyn Action>),
+}
+
+impl KeyMatch {
+    pub fn is_some(&self) -> bool {
+        matches!(self, KeyMatch::Some(_))
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use anyhow::Result;
+//     use serde::Deserialize;
+
+//     use crate::{actions, impl_actions, keymap_matcher::ActionContext};
+
+//     use super::*;
+
+//     #[test]
+//     fn test_keymap_and_view_ordering() -> Result<()> {
+//         actions!(test, [EditorAction, ProjectPanelAction]);
+
+//         let mut editor = ActionContext::default();
+//         editor.add_identifier("Editor");
+
+//         let mut project_panel = ActionContext::default();
+//         project_panel.add_identifier("ProjectPanel");
+
+//         // Editor 'deeper' in than project panel
+//         let dispatch_path = vec![(2, editor), (1, project_panel)];
+
+//         // But editor actions 'higher' up in keymap
+//         let keymap = Keymap::new(vec![
+//             Binding::new("left", EditorAction, Some("Editor")),
+//             Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
+//         ]);
+
+//         let mut matcher = KeymapMatcher::new(keymap);
+
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![
+//                 (2, Box::new(EditorAction)),
+//                 (1, Box::new(ProjectPanelAction)),
+//             ]),
+//         );
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_push_keystroke() -> Result<()> {
+//         actions!(test, [B, AB, C, D, DA, E, EF]);
+
+//         let mut context1 = ActionContext::default();
+//         context1.add_identifier("1");
+
+//         let mut context2 = ActionContext::default();
+//         context2.add_identifier("2");
+
+//         let dispatch_path = vec![(2, context2), (1, context1)];
+
+//         let keymap = Keymap::new(vec![
+//             Binding::new("a b", AB, Some("1")),
+//             Binding::new("b", B, Some("2")),
+//             Binding::new("c", C, Some("2")),
+//             Binding::new("d", D, Some("1")),
+//             Binding::new("d", D, Some("2")),
+//             Binding::new("d a", DA, Some("2")),
+//         ]);
+
+//         let mut matcher = KeymapMatcher::new(keymap);
+
+//         // Binding with pending prefix always takes precedence
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+//             KeyMatch::Pending,
+//         );
+//         // B alone doesn't match because a was pending, so AB is returned instead
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(1, Box::new(AB))]),
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         // Without an a prefix, B is dispatched like expected
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(2, Box::new(B))]),
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         // If a is prefixed, C will not be dispatched because there
+//         // was a pending binding for it
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+//             KeyMatch::Pending,
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
+//             KeyMatch::None,
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         // If a single keystroke matches multiple bindings in the tree
+//         // all of them are returned so that we can fallback if the action
+//         // handler decides to propagate the action
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
+//         );
+
+//         // If none of the d action handlers consume the binding, a pending
+//         // binding may then be used
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(2, Box::new(DA))]),
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_keystroke_parsing() -> Result<()> {
+//         assert_eq!(
+//             Keystroke::parse("ctrl-p")?,
+//             Keystroke {
+//                 key: "p".into(),
+//                 ctrl: true,
+//                 alt: false,
+//                 shift: false,
+//                 cmd: false,
+//                 function: false,
+//                 ime_key: None,
+//             }
+//         );
+
+//         assert_eq!(
+//             Keystroke::parse("alt-shift-down")?,
+//             Keystroke {
+//                 key: "down".into(),
+//                 ctrl: false,
+//                 alt: true,
+//                 shift: true,
+//                 cmd: false,
+//                 function: false,
+//                 ime_key: None,
+//             }
+//         );
+
+//         assert_eq!(
+//             Keystroke::parse("shift-cmd--")?,
+//             Keystroke {
+//                 key: "-".into(),
+//                 ctrl: false,
+//                 alt: false,
+//                 shift: true,
+//                 cmd: true,
+//                 function: false,
+//                 ime_key: None,
+//             }
+//         );
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_context_predicate_parsing() -> Result<()> {
+//         use KeymapContextPredicate::*;
+
+//         assert_eq!(
+//             KeymapContextPredicate::parse("a && (b == c || d != e)")?,
+//             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!(
+//             KeymapContextPredicate::parse("!a")?,
+//             Not(Box::new(Identifier("a".into())),)
+//         );
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_context_predicate_eval() {
+//         let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         assert!(!predicate.eval(&[context]));
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         context.add_identifier("b");
+//         assert!(predicate.eval(&[context]));
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         context.add_key("c", "x");
+//         assert!(!predicate.eval(&[context]));
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         context.add_key("c", "d");
+//         assert!(predicate.eval(&[context]));
+
+//         let predicate = KeymapContextPredicate::parse("!a").unwrap();
+//         assert!(predicate.eval(&[ActionContext::default()]));
+//     }
+
+//     #[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]) -> ActionContext {
+//             let mut keymap = ActionContext::new();
+//             names
+//                 .iter()
+//                 .for_each(|name| keymap.add_identifier(name.to_string()));
+//             keymap
+//         }
+//     }
+
+//     #[test]
+//     fn test_matcher() -> Result<()> {
+//         #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+//         pub struct A(pub String);
+//         impl_actions!(test, [A]);
+//         actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
+
+//         #[derive(Clone, Debug, Eq, PartialEq)]
+//         struct ActionArg {
+//             a: &'static str,
+//         }
+
+//         let keymap = Keymap::new(vec![
+//             Binding::new("a", A("x".to_string()), Some("a")),
+//             Binding::new("b", B, Some("a")),
+//             Binding::new("a b", Ab, Some("a || b")),
+//             Binding::new("$", Dollar, Some("a")),
+//             Binding::new("\"", Quote, Some("a")),
+//             Binding::new("alt-s", Ess, Some("a")),
+//             Binding::new("ctrl-`", Backtick, Some("a")),
+//         ]);
+
+//         let mut context_a = ActionContext::default();
+//         context_a.add_identifier("a");
+
+//         let mut context_b = ActionContext::default();
+//         context_b.add_identifier("b");
+
+//         let mut matcher = KeymapMatcher::new(keymap);
+
+//         // Basic match
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
+//         );
+//         matcher.clear_pending();
+
+//         // Multi-keystroke match
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Pending
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Ab))])
+//         );
+//         matcher.clear_pending();
+
+//         // Failed matches don't interfere with matching subsequent keys
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
+//             KeyMatch::None
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
+//         );
+//         matcher.clear_pending();
+
+//         // Pending keystrokes are cleared when the context changes
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Pending
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
+//             KeyMatch::None
+//         );
+//         matcher.clear_pending();
+
+//         let mut context_c = ActionContext::default();
+//         context_c.add_identifier("c");
+
+//         // Pending keystrokes are maintained per-view
+//         assert_eq!(
+//             matcher.match_keystroke(
+//                 Keystroke::parse("a")?,
+//                 vec![(1, context_b.clone()), (2, context_c.clone())]
+//             ),
+//             KeyMatch::Pending
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Ab))])
+//         );
+
+//         // handle Czech $ (option + 4 key)
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("alt-รง->$")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Dollar))])
+//         );
+
+//         // handle Brazillian quote (quote key then space key)
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Quote))])
+//         );
+
+//         // handle ctrl+` on a brazillian keyboard
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Backtick))])
+//         );
+
+//         // handle alt-s on a US keyboard
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("alt-s->รŸ")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Ess))])
+//         );
+
+//         Ok(())
+//     }
+// }

crates/gpui3/src/platform/keystroke.rs ๐Ÿ”—

@@ -1,29 +1,54 @@
 use anyhow::anyhow;
 use serde::Deserialize;
+use smallvec::SmallVec;
 use std::fmt::Write;
 
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
-pub struct Modifiers {
-    pub control: bool,
-    pub alt: bool,
-    pub shift: bool,
-    pub command: bool,
-    pub function: bool,
-}
-
-impl Modifiers {
-    pub fn modified(&self) -> bool {
-        self.control || self.alt || self.shift || self.command || self.function
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Hash)]
+#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
 pub struct Keystroke {
-    pub key: String,
     pub modifiers: Modifiers,
+    /// key is the character printed on the key that was pressed
+    /// e.g. for option-s, key is "s"
+    pub key: String,
+    /// ime_key is the character inserted by the IME engine when that key was pressed.
+    /// e.g. for option-s, ime_key is "รŸ"
+    pub ime_key: Option<String>,
 }
 
 impl Keystroke {
+    // When matching a key we cannot know whether the user intended to type
+    // the ime_key or the key. On some non-US keyboards keys we use in our
+    // bindings are behind option (for example `$` is typed `alt-รง` on a Czech keyboard),
+    // and on some keyboards the IME handler converts a sequence of keys into a
+    // specific character (for example `"` is typed as `" space` on a brazillian keyboard).
+    pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
+        let mut possibilities = SmallVec::new();
+        match self.ime_key.as_ref() {
+            None => possibilities.push(self.clone()),
+            Some(ime_key) => {
+                possibilities.push(Keystroke {
+                    modifiers: Modifiers {
+                        control: self.modifiers.control,
+                        alt: false,
+                        shift: false,
+                        command: false,
+                        function: false,
+                    },
+                    key: ime_key.to_string(),
+                    ime_key: None,
+                });
+                possibilities.push(Keystroke {
+                    ime_key: None,
+                    ..self.clone()
+                });
+            }
+        }
+        possibilities
+    }
+
+    /// key syntax is:
+    /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
+    /// ime_key is only used for generating test events,
+    /// when matching a key with an ime_key set will be matched without it.
     pub fn parse(source: &str) -> anyhow::Result<Self> {
         let mut control = false;
         let mut alt = false;
@@ -31,6 +56,7 @@ impl Keystroke {
         let mut command = false;
         let mut function = false;
         let mut key = None;
+        let mut ime_key = None;
 
         let mut components = source.split('-').peekable();
         while let Some(component) = components.next() {
@@ -41,10 +67,14 @@ impl Keystroke {
                 "cmd" => command = true,
                 "fn" => function = true,
                 _ => {
-                    if let Some(component) = components.peek() {
-                        if component.is_empty() && source.ends_with('-') {
+                    if let Some(next) = components.peek() {
+                        if next.is_empty() && source.ends_with('-') {
                             key = Some(String::from("-"));
                             break;
+                        } else if next.len() > 1 && next.starts_with('>') {
+                            key = Some(String::from(component));
+                            ime_key = Some(String::from(&next[1..]));
+                            components.next();
                         } else {
                             return Err(anyhow!("Invalid keystroke `{}`", source));
                         }
@@ -66,40 +96,25 @@ impl Keystroke {
                 function,
             },
             key,
+            ime_key,
         })
     }
-
-    pub fn modified(&self) -> bool {
-        self.modifiers.modified()
-    }
 }
 
 impl std::fmt::Display for Keystroke {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let Modifiers {
-            control,
-            alt,
-            shift,
-            command,
-            function,
-        } = self.modifiers;
-
-        if control {
+        if self.modifiers.control {
             f.write_char('^')?;
         }
-        if alt {
-            f.write_char('โއ')?;
+        if self.modifiers.alt {
+            f.write_char('โŒฅ')?;
         }
-        if command {
+        if self.modifiers.command {
             f.write_char('โŒ˜')?;
         }
-        if shift {
+        if self.modifiers.shift {
             f.write_char('โ‡ง')?;
         }
-        if function {
-            f.write_char('๐™›')?;
-        }
-
         let key = match self.key.as_str() {
             "backspace" => 'โŒซ',
             "up" => 'โ†‘',
@@ -119,3 +134,18 @@ impl std::fmt::Display for Keystroke {
         f.write_char(key)
     }
 }
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
+pub struct Modifiers {
+    pub control: bool,
+    pub alt: bool,
+    pub shift: bool,
+    pub command: bool,
+    pub function: bool,
+}
+
+impl Modifiers {
+    pub fn modified(&self) -> bool {
+        self.control || self.alt || self.shift || self.command || self.function
+    }
+}

crates/gpui3/src/platform/mac/window.rs ๐Ÿ”—

@@ -1043,6 +1043,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                                 // we don't match cmd/fn because they don't seem to use IME
                                 modifiers: Default::default(),
                                 key: ime_text.clone().unwrap(),
+                                ime_key: None, // todo!("handle IME key")
                             },
                         };
                         handled = callback(InputEvent::KeyDown(event_with_ime_text));
@@ -1203,6 +1204,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
     let keystroke = Keystroke {
         modifiers: Default::default(),
         key: ".".into(),
+        ime_key: None,
     };
     let event = InputEvent::KeyDown(KeyDownEvent {
         keystroke: keystroke.clone(),

crates/gpui3/src/text_system.rs ๐Ÿ”—

@@ -219,8 +219,8 @@ impl TextSystem {
         Ok(lines)
     }
 
-    pub fn end_frame(&self) {
-        self.line_layout_cache.end_frame()
+    pub fn start_frame(&self) {
+        self.line_layout_cache.start_frame()
     }
 
     pub fn line_wrapper(

crates/gpui3/src/text_system/line_layout.rs ๐Ÿ”—

@@ -167,7 +167,7 @@ impl LineLayoutCache {
         }
     }
 
-    pub fn end_frame(&self) {
+    pub fn start_frame(&self) {
         let mut prev_frame = self.prev_frame.lock();
         let mut curr_frame = self.curr_frame.write();
         std::mem::swap(&mut *prev_frame, &mut *curr_frame);

crates/gpui3/src/view.rs ๐Ÿ”—

@@ -164,7 +164,7 @@ impl<V: Send + Sync + 'static> ViewObject for View<V> {
     }
 
     fn initialize(&mut self, cx: &mut WindowContext) -> AnyBox {
-        cx.with_element_id(self.entity_id(), |cx| {
+        cx.with_element_id(self.entity_id(), |_global_id, cx| {
             self.state.update(cx, |state, cx| {
                 let mut any_element = Box::new((self.render)(state, cx));
                 any_element.initialize(state, cx);
@@ -174,7 +174,7 @@ impl<V: Send + Sync + 'static> ViewObject for View<V> {
     }
 
     fn layout(&mut self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId {
-        cx.with_element_id(self.entity_id(), |cx| {
+        cx.with_element_id(self.entity_id(), |_global_id, cx| {
             self.state.update(cx, |state, cx| {
                 let element = element.downcast_mut::<AnyElement<V>>().unwrap();
                 element.layout(state, cx)
@@ -183,7 +183,7 @@ impl<V: Send + Sync + 'static> ViewObject for View<V> {
     }
 
     fn paint(&mut self, _: Bounds<Pixels>, element: &mut AnyBox, cx: &mut WindowContext) {
-        cx.with_element_id(self.entity_id(), |cx| {
+        cx.with_element_id(self.entity_id(), |_global_id, cx| {
             self.state.update(cx, |state, cx| {
                 let element = element.downcast_mut::<AnyElement<V>>().unwrap();
                 element.paint(state, None, cx);

crates/gpui3/src/window.rs ๐Ÿ”—

@@ -1,12 +1,13 @@
 use crate::{
-    px, size, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, BorrowAppContext,
-    Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect, Element, EntityId,
-    EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData,
-    InputEvent, IsZero, KeyListener, LayoutId, MainThread, MainThreadOnly, MonochromeSprite,
-    MouseMoveEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
-    Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
-    SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task,
-    Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
+    px, size, Action, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
+    BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect,
+    Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla,
+    ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId,
+    MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, Path, Pixels, Platform,
+    PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
+    Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle,
+    WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
 use collections::HashMap;
@@ -45,6 +46,12 @@ pub enum DispatchPhase {
 }
 
 type AnyListener = Arc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + Send + Sync + 'static>;
+type AnyKeyListener = Arc<
+    dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) -> Option<Box<dyn Action>>
+        + Send
+        + Sync
+        + 'static,
+>;
 type AnyFocusListener = Arc<dyn Fn(&FocusEvent, &mut WindowContext) + Send + Sync + 'static>;
 
 slotmap::new_key_type! { pub struct FocusId; }
@@ -141,18 +148,20 @@ pub struct Window {
     layout_engine: TaffyLayoutEngine,
     pub(crate) root_view: Option<AnyView>,
     pub(crate) element_id_stack: GlobalElementId,
-    prev_element_states: HashMap<GlobalElementId, AnyBox>,
+    prev_frame_element_states: HashMap<GlobalElementId, AnyBox>,
     element_states: HashMap<GlobalElementId, AnyBox>,
+    prev_frame_key_matchers: HashMap<GlobalElementId, KeyMatcher>,
+    key_matchers: HashMap<GlobalElementId, KeyMatcher>,
     z_index_stack: StackingOrder,
     content_mask_stack: Vec<ContentMask<Pixels>>,
     mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
-    key_listeners: HashMap<TypeId, Vec<AnyListener>>,
+    key_listeners: Vec<(TypeId, AnyKeyListener)>,
     key_events_enabled: bool,
     focus_stack: Vec<FocusId>,
     focus_parents_by_child: HashMap<FocusId, FocusId>,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
     pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
-    propagate_event: bool,
+    propagate: bool,
     default_prevented: bool,
     mouse_position: Point<Pixels>,
     scale_factor: f32,
@@ -214,17 +223,19 @@ impl Window {
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
-            prev_element_states: HashMap::default(),
+            prev_frame_element_states: HashMap::default(),
             element_states: HashMap::default(),
+            prev_frame_key_matchers: HashMap::default(),
+            key_matchers: HashMap::default(),
             z_index_stack: StackingOrder(SmallVec::new()),
             content_mask_stack: Vec::new(),
             mouse_listeners: HashMap::default(),
-            key_listeners: HashMap::default(),
+            key_listeners: Vec::new(),
             key_events_enabled: true,
             focus_stack: Vec::new(),
             focus_parents_by_child: HashMap::default(),
             focus_listeners: Vec::new(),
-            propagate_event: true,
+            propagate: true,
             default_prevented: true,
             mouse_position,
             scale_factor,
@@ -434,7 +445,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
     }
 
     pub fn stop_propagation(&mut self) {
-        self.window.propagate_event = false;
+        self.window.propagate = false;
     }
 
     pub fn prevent_default(&mut self) {
@@ -462,19 +473,6 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             ))
     }
 
-    pub fn on_keyboard_event<Event: 'static>(
-        &mut self,
-        handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + Send + Sync + 'static,
-    ) {
-        self.window
-            .key_listeners
-            .entry(TypeId::of::<Event>())
-            .or_default()
-            .push(Arc::new(move |event: &dyn Any, phase, cx| {
-                handler(event.downcast_ref().unwrap(), phase, cx)
-            }))
-    }
-
     pub fn mouse_position(&self) -> Point<Pixels> {
         self.window.mouse_position
     }
@@ -777,7 +775,6 @@ impl<'a, 'w> WindowContext<'a, 'w> {
 
             cx.window.root_view = Some(root_view);
             let scene = cx.window.scene_builder.build();
-            cx.end_frame();
 
             cx.run_on_main(view, |_, cx| {
                 cx.window
@@ -807,13 +804,28 @@ impl<'a, 'w> WindowContext<'a, 'w> {
     }
 
     fn start_frame(&mut self) {
-        // Make the current element states the previous, and then clear the current.
-        // The empty element states map will be populated for any element states we
-        // reference during the upcoming frame.
+        self.text_system().start_frame();
+
         let window = &mut *self.window;
-        mem::swap(&mut window.element_states, &mut window.prev_element_states);
+
+        // Move the current frame element states to the previous frame.
+        // The new empty element states map will be populated for any element states we
+        // reference during the upcoming frame.
+        mem::swap(
+            &mut window.element_states,
+            &mut window.prev_frame_element_states,
+        );
         window.element_states.clear();
 
+        // Make the current key matchers the previous, and then clear the current.
+        // An empty key matcher map will be created for every identified element in the
+        // upcoming frame.
+        mem::swap(
+            &mut window.key_matchers,
+            &mut window.prev_frame_key_matchers,
+        );
+        window.key_matchers.clear();
+
         // Clear mouse event listeners, because elements add new element listeners
         // when the upcoming frame is painted.
         window.mouse_listeners.values_mut().for_each(Vec::clear);
@@ -821,15 +833,11 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         // Clear focus state, because we determine what is focused when the new elements
         // in the upcoming frame are initialized.
         window.focus_listeners.clear();
-        window.key_listeners.values_mut().for_each(Vec::clear);
+        window.key_listeners.clear();
         window.focus_parents_by_child.clear();
         window.key_events_enabled = true;
     }
 
-    fn end_frame(&mut self) {
-        self.text_system().end_frame();
-    }
-
     fn dispatch_event(&mut self, event: InputEvent) -> bool {
         if let Some(any_mouse_event) = event.mouse_event() {
             if let Some(MouseMoveEvent { position, .. }) = any_mouse_event.downcast_ref() {
@@ -837,7 +845,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             }
 
             // Handlers may set this to false by calling `stop_propagation`
-            self.window.propagate_event = true;
+            self.window.propagate = true;
             self.window.default_prevented = false;
 
             if let Some(mut handlers) = self
@@ -852,16 +860,16 @@ impl<'a, 'w> WindowContext<'a, 'w> {
                 // special purposes, such as detecting events outside of a given Bounds.
                 for (_, handler) in &handlers {
                     handler(any_mouse_event, DispatchPhase::Capture, self);
-                    if !self.window.propagate_event {
+                    if !self.window.propagate {
                         break;
                     }
                 }
 
                 // Bubble phase, where most normal handlers do their work.
-                if self.window.propagate_event {
+                if self.window.propagate {
                     for (_, handler) in handlers.iter().rev() {
                         handler(any_mouse_event, DispatchPhase::Bubble, self);
-                        if !self.window.propagate_event {
+                        if !self.window.propagate {
                             break;
                         }
                     }
@@ -879,43 +887,85 @@ impl<'a, 'w> WindowContext<'a, 'w> {
                     .mouse_listeners
                     .insert(any_mouse_event.type_id(), handlers);
             }
-        } else if let Some(any_keyboard_event) = event.keyboard_event() {
-            if let Some(mut handlers) = self
-                .window
-                .key_listeners
-                .remove(&any_keyboard_event.type_id())
-            {
-                for handler in &handlers {
-                    handler(any_keyboard_event, DispatchPhase::Capture, self);
-                    if !self.window.propagate_event {
+        } else if let Some(any_key_event) = event.keyboard_event() {
+            let key_listeners = mem::take(&mut self.window.key_listeners);
+            let key_event_type = any_key_event.type_id();
+
+            for (ix, (listener_event_type, listener)) in key_listeners.iter().enumerate() {
+                if key_event_type == *listener_event_type {
+                    if let Some(action) = listener(any_key_event, DispatchPhase::Capture, self) {
+                        self.dispatch_action(action, &key_listeners[..ix]);
+                    }
+                    if !self.window.propagate {
                         break;
                     }
                 }
+            }
 
-                if self.window.propagate_event {
-                    for handler in handlers.iter().rev() {
-                        handler(any_keyboard_event, DispatchPhase::Bubble, self);
-                        if !self.window.propagate_event {
+            if self.window.propagate {
+                for (ix, (listener_event_type, listener)) in key_listeners.iter().enumerate().rev()
+                {
+                    if key_event_type == *listener_event_type {
+                        if let Some(action) = listener(any_key_event, DispatchPhase::Bubble, self) {
+                            self.dispatch_action(action, &key_listeners[..ix]);
+                        }
+
+                        if !self.window.propagate {
                             break;
                         }
                     }
                 }
-
-                handlers.extend(
-                    self.window
-                        .key_listeners
-                        .get_mut(&any_keyboard_event.type_id())
-                        .into_iter()
-                        .flat_map(|handlers| handlers.drain(..)),
-                );
-                self.window
-                    .key_listeners
-                    .insert(any_keyboard_event.type_id(), handlers);
             }
+
+            self.window.key_listeners = key_listeners;
         }
 
         true
     }
+
+    pub fn match_keystroke(
+        &mut self,
+        element_id: &GlobalElementId,
+        keystroke: &Keystroke,
+    ) -> KeyMatch {
+        let key_match = self
+            .window
+            .key_matchers
+            .get_mut(element_id)
+            .unwrap()
+            .match_keystroke(keystroke, &[]);
+
+        if key_match.is_some() {
+            for matcher in self.window.key_matchers.values_mut() {
+                matcher.clear_pending();
+            }
+        }
+
+        key_match
+    }
+
+    fn dispatch_action(&mut self, action: Box<dyn Action>, listeners: &[(TypeId, AnyKeyListener)]) {
+        let action_type = action.as_any().type_id();
+        for (event_type, listener) in listeners {
+            if action_type == *event_type {
+                listener(action.as_any(), DispatchPhase::Capture, self);
+                if !self.window.propagate {
+                    break;
+                }
+            }
+        }
+
+        if self.window.propagate {
+            for (event_type, listener) in listeners.iter().rev() {
+                if action_type == *event_type {
+                    listener(action.as_any(), DispatchPhase::Bubble, self);
+                    if !self.window.propagate {
+                        break;
+                    }
+                }
+            }
+        }
+    }
 }
 
 impl<'a, 'w> MainThread<WindowContext<'a, 'w>> {
@@ -983,10 +1033,24 @@ pub trait BorrowWindow: BorrowAppContext {
     fn with_element_id<R>(
         &mut self,
         id: impl Into<ElementId>,
-        f: impl FnOnce(&mut Self) -> R,
+        f: impl FnOnce(GlobalElementId, &mut Self) -> R,
     ) -> R {
-        self.window_mut().element_id_stack.push(id.into());
-        let result = f(self);
+        let keymap = self.app_mut().keymap.clone();
+        let window = self.window_mut();
+        window.element_id_stack.push(id.into());
+        let global_id = window.element_id_stack.clone();
+
+        if window.key_matchers.get(&global_id).is_none() {
+            window.key_matchers.insert(
+                global_id.clone(),
+                window
+                    .prev_frame_key_matchers
+                    .remove(&global_id)
+                    .unwrap_or_else(|| KeyMatcher::new(keymap)),
+            );
+        }
+
+        let result = f(global_id, self);
         self.window_mut().element_id_stack.pop();
         result
     }
@@ -1008,13 +1072,12 @@ pub trait BorrowWindow: BorrowAppContext {
         id: ElementId,
         f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
     ) -> R {
-        self.with_element_id(id, |cx| {
-            let global_id = cx.window_mut().element_id_stack.clone();
+        self.with_element_id(id, |global_id, cx| {
             if let Some(any) = cx
                 .window_mut()
                 .element_states
                 .remove(&global_id)
-                .or_else(|| cx.window_mut().prev_element_states.remove(&global_id))
+                .or_else(|| cx.window_mut().prev_frame_element_states.remove(&global_id))
             {
                 // Using the extra inner option to avoid needing to reallocate a new box.
                 let mut state_box = any
@@ -1225,28 +1288,25 @@ impl<'a, 'w, V: Send + Sync + 'static> ViewContext<'a, 'w, V> {
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
         if self.window.key_events_enabled {
-            let handle = self.handle();
-            for (type_id, listener) in key_listeners {
-                let handle = handle.clone();
-                let listener = listener.clone();
-                self.window
-                    .key_listeners
-                    .entry(*type_id)
-                    .or_default()
-                    .push(Arc::new(move |event, phase, cx| {
+            for (event_type, listener) in key_listeners.iter().cloned() {
+                let handle = self.handle();
+                let listener = Arc::new(
+                    move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_, '_>| {
                         handle
                             .update(cx, |view, cx| listener(view, event, phase, cx))
-                            .log_err();
-                    }));
+                            .log_err()
+                            .flatten()
+                    },
+                );
+                self.window.key_listeners.push((event_type, listener));
             }
         }
 
         let result = f(self);
 
         if self.window.key_events_enabled {
-            for (type_id, _) in key_listeners {
-                self.window.key_listeners.get_mut(type_id).unwrap().pop();
-            }
+            let prev_len = self.window.key_listeners.len() - key_listeners.len();
+            self.window.key_listeners.truncate(prev_len);
         }
 
         result
@@ -1317,18 +1377,6 @@ impl<'a, 'w, V: Send + Sync + 'static> ViewContext<'a, 'w, V> {
             })
         });
     }
-
-    pub fn on_keyboard_event<Event: 'static>(
-        &mut self,
-        handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static,
-    ) {
-        let handle = self.handle().upgrade(self).unwrap();
-        self.window_cx.on_keyboard_event(move |event, phase, cx| {
-            handle.update(cx, |view, cx| {
-                handler(view, event, phase, cx);
-            })
-        });
-    }
 }
 
 impl<'a, 'w, S: EventEmitter + Send + Sync + 'static> ViewContext<'a, 'w, S> {

crates/storybook2/src/stories/focus.rs ๐Ÿ”—

@@ -1,13 +1,56 @@
-use gpui3::{div, view, Context, Focus, ParentElement, Styled, View, WindowContext};
+use std::any::Any;
+
+use gpui3::{
+    div, view, Action, Context, Focus, Interactive, KeyBinding, ParentElement, Styled, View,
+    WindowContext,
+};
 
 use crate::themes::rose_pine;
 
+#[derive(Clone)]
+struct ActionA;
+
+impl Action for ActionA {
+    fn eq(&self, action: &dyn Action) -> bool {
+        action.as_any().downcast_ref::<Self>().is_some()
+    }
+
+    fn boxed_clone(&self) -> Box<dyn Action> {
+        Box::new(self.clone())
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+#[derive(Clone)]
+struct ActionB;
+
+impl Action for ActionB {
+    fn eq(&self, action: &dyn Action) -> bool {
+        action.as_any().downcast_ref::<Self>().is_some()
+    }
+
+    fn boxed_clone(&self) -> Box<dyn Action> {
+        Box::new(self.clone())
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
 pub struct FocusStory {
     text: View<()>,
 }
 
 impl FocusStory {
     pub fn view(cx: &mut WindowContext) -> View<()> {
+        cx.bind_keys([
+            KeyBinding::new("cmd-a", ActionA, None),
+            KeyBinding::new("cmd-b", ActionB, None),
+        ]);
         let theme = rose_pine();
 
         let color_1 = theme.lowest.negative.default.foreground;
@@ -22,6 +65,12 @@ impl FocusStory {
         let child_2 = cx.focus_handle();
         view(cx.entity(|cx| ()), move |_, cx| {
             div()
+                .on_action(|_, action: &ActionA, phase, cx| {
+                    println!("Action A dispatched on parent during {:?}", phase);
+                })
+                .on_action(|_, action: &ActionB, phase, cx| {
+                    println!("Action B dispatched on parent during {:?}", phase);
+                })
                 .focusable(&parent)
                 .on_focus(|_, _, _| println!("Parent focused"))
                 .on_blur(|_, _, _| println!("Parent blurred"))
@@ -39,6 +88,10 @@ impl FocusStory {
                 .focus_in(|style| style.bg(color_3))
                 .child(
                     div()
+                        .id("child 1")
+                        .on_action(|_, action: &ActionA, phase, cx| {
+                            println!("Action A dispatched on child 1 during {:?}", phase);
+                        })
                         .focusable(&child_1)
                         .w_full()
                         .h_6()
@@ -59,6 +112,10 @@ impl FocusStory {
                 )
                 .child(
                     div()
+                        .id("child 2")
+                        .on_action(|_, action: &ActionB, phase, cx| {
+                            println!("Action B dispatched on child 2 during {:?}", phase);
+                        })
                         .focusable(&child_2)
                         .w_full()
                         .h_6()