diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 5bc67a57b61feb5074852bb4a4b567c121a0f968..ad17f96055f560210790f783894b18774eec0fab 100644 --- a/crates/editor2/src/editor.rs +++ b/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, workspace: Option<(WeakView, i64)>, - keymap_context_layers: BTreeMap, + keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, leader_peer_id: Option, @@ -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 => {} } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 67fcbaa4ba11acf260ffad4c29f7f9c217d1f727..2cd319f66bc4d616437b42ba2f7bf72396a6ca83 100644 --- a/crates/editor2/src/element.rs +++ b/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( listener: impl Fn( &mut Editor, &T, - &[&DispatchContext], + &[&KeyBindingContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui/src/dispatch.rs b/crates/gpui/src/dispatch.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/crates/gpui/src/dispatch.rs @@ -0,0 +1 @@ + diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 170ddf942f2bfcdacda710ef89094cd8aef726ec..6526f96cb9cbc0f5be445906cc55eb2801dc429d 100644 --- a/crates/gpui2/src/action.rs +++ b/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, - map: HashMap, -} - -impl<'a> TryFrom<&'a str> for DispatchContext { - type Error = anyhow::Error; - - fn try_from(value: &'a str) -> Result { - Self::parse(value) - } -} - -impl DispatchContext { - pub fn parse(source: &str) -> Result { - 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::(); - 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::(); - 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>(&mut self, identifier: I) { - self.set.insert(identifier.into()); - } - - pub fn set, S2: Into>(&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, Box), - Not(Box), - And(Box, Box), - Or(Box, Box), -} - -impl DispatchContextPredicate { - pub fn parse(source: &str) -> Result { - 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; - - 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 { - Ok(Self::Or(Box::new(self), Box::new(other))) - } - - fn new_and(self, other: Self) -> Result { - Ok(Self::And(Box::new(self), Box::new(other))) - } - - fn new_child(self, other: Self) -> Result { - Ok(Self::Child(Box::new(self), Box::new(other))) - } - - fn new_eq(self, other: Self) -> Result { - if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { - Ok(Self::Equal(left, right)) - } else { - Err(anyhow!("operands must be identifiers")) - } - } - - fn new_neq(self, other: Self) -> Result { - 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())), - ) - ); - } -} diff --git a/crates/gpui2/src/dispatch.rs b/crates/gpui2/src/dispatch.rs new file mode 100644 index 0000000000000000000000000000000000000000..372c8c26104e7cabbffb9ba703af976025faadc3 --- /dev/null +++ b/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) +type AnyKeyListener = Box; +type AnyActionListener = Box; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct DispatchNodeId(usize); + +pub struct DispatchTree { + node_stack: Vec, + context_stack: Vec, + nodes: Vec, + focused: Option, + focusable_node_ids: HashMap, + keystroke_matchers: HashMap, KeystrokeMatcher>, + keymap: Arc>, +} + +#[derive(Default)] +pub struct DispatchNode { + key_listeners: SmallVec<[AnyKeyListener; 2]>, + action_listeners: SmallVec<[AnyActionListener; 16]>, + context: KeyBindingContext, + parent: Option, +} + +impl DispatchTree { + pub fn clear(&mut self) { + self.node_stack.clear(); + self.nodes.clear(); + } + + pub fn push_node(&mut self, context: Option, 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) { + 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::() { + 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, 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, + 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; + } + } + } + } +} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 79275005d21423071e780624090bc651e094415f..42aea446f1e4bac7c7eaf2399ff72573b9f61242 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/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; diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 243eb3cb07844a2fa558d6c1bf2555f75cf1af95..946a59a809b0a758cd74e1ae1bc1c558a53a3704 100644 --- a/crates/gpui2/src/interactive.rs +++ b/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: Element { fn context(mut self, context: C) -> Self where Self: Sized, - C: TryInto, + C: TryInto, C::Error: Debug, { self.stateless_interactivity().dispatch_context = @@ -403,24 +403,6 @@ pub trait ElementInteractivity: '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::(), - Box::new(move |_, key_down, context, phase, cx| { - if phase == DispatchPhase::Bubble { - let key_down = key_down.downcast_ref::().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 ElementInteractivity for StatefulInteractivity { type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub struct StatelessInteractivity { - pub dispatch_context: DispatchContext, + pub dispatch_context: KeyBindingContext, pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, @@ -910,7 +892,7 @@ impl InteractiveElementState { impl Default for StatelessInteractivity { 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 = Box< dyn Fn( &mut V, &dyn Any, - &[&DispatchContext], + &[&KeyBindingContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 829f7a3b2cfa7a816b76ebc7c1acd3229b57aa18..1cf62484b98c943d33227853b9f2d16f883aae94 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/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, pub(super) keystrokes: SmallVec<[Keystroke; 2]>, - pub(super) context_predicate: Option, + pub(super) context_predicate: Option, } impl KeyBinding { @@ -15,7 +15,7 @@ impl KeyBinding { pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { 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> { if self.action.partial_eq(action) && self.matches_context(contexts) { Some(self.keystrokes.clone()) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs new file mode 100644 index 0000000000000000000000000000000000000000..834bd4989a463d01b196a523aacd53869163da1c --- /dev/null +++ b/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, +} + +impl<'a> TryFrom<&'a str> for KeyBindingContext { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + Self::parse(value) + } +} + +impl KeyBindingContext { + pub fn parse(source: &str) -> Result { + 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::(); + 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::(); + 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>(&mut self, identifier: I) { + let key = identifier.into(); + + if !self.contains(&key) { + self.0.push(ContextEntry { key, value: None }) + } + } + + pub fn set, S2: Into>(&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, + Box, + ), + Not(Box), + And( + Box, + Box, + ), + Or( + Box, + Box, + ), +} + +impl KeyBindingContextPredicate { + pub fn parse(source: &str) -> Result { + 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; + + 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 { + Ok(Self::Or(Box::new(self), Box::new(other))) + } + + fn new_and(self, other: Self) -> Result { + Ok(Self::And(Box::new(self), Box::new(other))) + } + + fn new_child(self, other: Self) -> Result { + Ok(Self::Child(Box::new(self), Box::new(other))) + } + + fn new_eq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::Equal(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } + + fn new_neq(self, other: Self) -> Result { + 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())), + ) + ); + } +} diff --git a/crates/gpui2/src/keymap/keymap.rs b/crates/gpui2/src/keymap/keymap.rs index eda493a4607837783e830069931d617692feeb9f..989ee7a8d5515182769eb10cfb5bacc3eeaf8501 100644 --- a/crates/gpui2/src/keymap/keymap.rs +++ b/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, binding_indices_by_action_id: HashMap>, disabled_keystrokes: - HashMap, HashSet>>, + HashMap, HashSet>>, version: KeymapVersion, } diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c2033a95953a46479c02bfef69f432f0c877b3ae..c9b5d26ecbce22d98e001a3b815759fd8605cd9c 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/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, keymap: Arc>, keymap_version: KeymapVersion, } -impl KeyMatcher { +impl KeystrokeMatcher { pub fn new(keymap: Arc>) -> 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> { self.keymap .lock() diff --git a/crates/gpui2/src/keymap/mod.rs b/crates/gpui2/src/keymap/mod.rs index 449b5427bf288dbf7647ed3ebd2451e15b10dd15..09e222c09552a80e7d187ec3303d1993d28979e8 100644 --- a/crates/gpui2/src/keymap/mod.rs +++ b/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::*; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index fd3890d644e7890334c2cdbe629ee661ec8bc0b4..cde7b31754d295739ad91f04dfda38db6ebd9b95 100644 --- a/crates/gpui2/src/window.rs +++ b/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 Option> @@ -230,7 +230,7 @@ pub struct Window { #[derive(Default)] pub(crate) struct Frame { element_states: HashMap, - key_matchers: HashMap, + key_matchers: HashMap, mouse_listeners: HashMap>, pub(crate) focus_listeners: Vec, pub(crate) key_dispatch_stack: Vec, @@ -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 + BorrowMut { .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( &mut self, - context: DispatchContext, + context: KeyBindingContext, f: impl FnOnce(&mut Self) -> R, ) -> R { if context.is_empty() { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5c678df317ac9a97e858f8e0ba3be8e9fdb89b6c..1522b4ec4e6142ff440a32842c8f0229c5eb350f 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/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; fn render(&mut self, cx: &mut ViewContext) -> 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()