Checkpoint

Antonio Scandurra created

Change summary

crates/gpui3/src/action.rs             | 88 +++++++++++++++++++++------
crates/gpui3/src/interactive.rs        |  2 
crates/storybook2/src/stories/focus.rs | 29 +++++++-
crates/util/src/arc_cow.rs             | 12 +++
4 files changed, 104 insertions(+), 27 deletions(-)

Detailed changes

crates/gpui3/src/action.rs 🔗

@@ -15,12 +15,45 @@ pub struct DispatchContext {
     map: HashMap<SharedString, SharedString>,
 }
 
+impl<'a> TryFrom<&'a str> for DispatchContext {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'a str) -> Result<Self> {
+        Self::parse(value)
+    }
+}
+
 impl DispatchContext {
-    pub fn new() -> Self {
-        DispatchContext {
-            set: HashSet::default(),
-            map: HashMap::default(),
+    pub fn parse(source: &str) -> Result<Self> {
+        let mut context = Self::default();
+        let source = skip_whitespace(source);
+        Self::parse_expr(&source, &mut context)?;
+        Ok(context)
+    }
+
+    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
+        if source.is_empty() {
+            return Ok(());
+        }
+
+        let key = source
+            .chars()
+            .take_while(|ch| ch.is_alphanumeric())
+            .collect::<String>();
+        source = skip_whitespace(&source[key.len()..]);
+        if let Some(suffix) = source.strip_prefix('=') {
+            source = skip_whitespace(suffix);
+            let value = source
+                .chars()
+                .take_while(|ch| ch.is_alphanumeric())
+                .collect::<String>();
+            source = skip_whitespace(&source[value.len()..]);
+            context.set(key, value);
+        } else {
+            context.insert(key);
         }
+
+        Self::parse_expr(source, context)
     }
 
     pub fn is_empty(&self) -> bool {
@@ -41,11 +74,11 @@ impl DispatchContext {
         }
     }
 
-    pub fn add_identifier<I: Into<SharedString>>(&mut self, identifier: I) {
+    pub fn insert<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) {
+    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
         self.map.insert(key.into(), value.into());
     }
 }
@@ -63,7 +96,7 @@ pub enum DispatchContextPredicate {
 
 impl DispatchContextPredicate {
     pub fn parse(source: &str) -> Result<Self> {
-        let source = Self::skip_whitespace(source);
+        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:?}"))
@@ -113,7 +146,7 @@ impl DispatchContextPredicate {
                 ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
             ] {
                 if source.starts_with(operator) && precedence >= min_precedence {
-                    source = Self::skip_whitespace(&source[operator.len()..]);
+                    source = skip_whitespace(&source[operator.len()..]);
                     let (right, rest) = Self::parse_expr(source, precedence + 1)?;
                     predicate = constructor(predicate, right)?;
                     source = rest;
@@ -133,17 +166,17 @@ impl DispatchContextPredicate {
             .ok_or_else(|| anyhow!("unexpected eof"))?;
         match next {
             '(' => {
-                source = Self::skip_whitespace(&source[1..]);
+                source = skip_whitespace(&source[1..]);
                 let (predicate, rest) = Self::parse_expr(source, 0)?;
                 if rest.starts_with(')') {
-                    source = Self::skip_whitespace(&rest[1..]);
+                    source = skip_whitespace(&rest[1..]);
                     Ok((predicate, source))
                 } else {
                     Err(anyhow!("expected a ')'"))
                 }
             }
             '!' => {
-                let source = Self::skip_whitespace(&source[1..]);
+                let source = skip_whitespace(&source[1..]);
                 let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
                 Ok((DispatchContextPredicate::Not(Box::new(predicate)), source))
             }
@@ -152,7 +185,7 @@ impl DispatchContextPredicate {
                     .find(|c: char| !(c.is_alphanumeric() || c == '_'))
                     .unwrap_or(source.len());
                 let (identifier, rest) = source.split_at(len);
-                source = Self::skip_whitespace(rest);
+                source = skip_whitespace(rest);
                 Ok((
                     DispatchContextPredicate::Identifier(identifier.to_string().into()),
                     source,
@@ -162,13 +195,6 @@ impl DispatchContextPredicate {
         }
     }
 
-    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)))
     }
@@ -204,9 +230,31 @@ const PRECEDENCE_AND: u32 = 3;
 const PRECEDENCE_EQ: u32 = 4;
 const PRECEDENCE_NOT: u32 = 5;
 
+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::DispatchContextPredicate::{self, *};
+    use super::*;
+    use DispatchContextPredicate::*;
+
+    #[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() {

crates/gpui3/src/interactive.rs 🔗

@@ -595,7 +595,7 @@ pub struct InteractiveElementState {
 impl<V> Default for StatelessInteraction<V> {
     fn default() -> Self {
         Self {
-            dispatch_context: DispatchContext::new(),
+            dispatch_context: DispatchContext::default(),
             mouse_down_listeners: SmallVec::new(),
             mouse_up_listeners: SmallVec::new(),
             mouse_move_listeners: SmallVec::new(),

crates/storybook2/src/stories/focus.rs 🔗

@@ -39,6 +39,23 @@ impl Action for ActionB {
     }
 }
 
+#[derive(Clone)]
+struct ActionC;
+
+impl Action for ActionC {
+    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<()>,
 }
@@ -46,8 +63,9 @@ pub struct FocusStory {
 impl FocusStory {
     pub fn view(cx: &mut WindowContext) -> View<()> {
         cx.bind_keys([
-            KeyBinding::new("cmd-a", ActionA, None),
-            KeyBinding::new("cmd-b", ActionB, None),
+            KeyBinding::new("cmd-a", ActionA, Some("parent")),
+            KeyBinding::new("cmd-a", ActionB, Some("child-1")),
+            KeyBinding::new("cmd-c", ActionC, None),
         ]);
         let theme = rose_pine();
 
@@ -63,6 +81,7 @@ impl FocusStory {
         let child_2 = cx.focus_handle();
         view(cx.entity(|cx| ()), move |_, cx| {
             div()
+                .context("parent")
                 .on_action(|_, action: &ActionA, phase, cx| {
                     println!("Action A dispatched on parent during {:?}", phase);
                 })
@@ -86,7 +105,8 @@ impl FocusStory {
                 .focus_in(|style| style.bg(color_3))
                 .child(
                     div()
-                        .id("child 1")
+                        .id("child-1")
+                        .context("child-1")
                         .on_action(|_, action: &ActionA, phase, cx| {
                             println!("Action A dispatched on child 1 during {:?}", phase);
                         })
@@ -110,7 +130,8 @@ impl FocusStory {
                 )
                 .child(
                     div()
-                        .id("child 2")
+                        .id("child-2")
+                        .context("child-2")
                         .on_action(|_, action: &ActionB, phase, cx| {
                             println!("Action B dispatched on child 2 during {:?}", phase);
                         })

crates/util/src/arc_cow.rs 🔗

@@ -1,16 +1,24 @@
 use std::{
     borrow::Cow,
     fmt::{self, Debug},
+    hash::{Hash, Hasher},
     sync::Arc,
 };
 
-#[derive(PartialEq, Eq)]
 pub enum ArcCow<'a, T: ?Sized> {
     Borrowed(&'a T),
     Owned(Arc<T>),
 }
 
-use std::hash::{Hash, Hasher};
+impl<'a, T: ?Sized + PartialEq> PartialEq for ArcCow<'a, T> {
+    fn eq(&self, other: &Self) -> bool {
+        let a = self.as_ref();
+        let b = other.as_ref();
+        a == b
+    }
+}
+
+impl<'a, T: ?Sized + Eq> Eq for ArcCow<'a, T> {}
 
 impl<'a, T: ?Sized + Hash> Hash for ArcCow<'a, T> {
     fn hash<H: Hasher>(&self, state: &mut H) {