Re-introduce a macro for defining actions for ease of use

Julia , Piotr Osiewicz , and Conrad Irwin created

Co-Authored-By: Piotr Osiewicz <piotr@zed.dev>
Co-Authored-By: Conrad Irwin <conrad@zed.dev>

Change summary

crates/client2/src/client2.rs          | 13 +-----
crates/gpui2/src/action.rs             | 52 ++++++++++++++++++++++++++-
crates/gpui2/src/gpui2.rs              |  1 
crates/gpui2/src/interactive.rs        |  5 --
crates/storybook2/src/stories/focus.rs | 14 +-----
5 files changed, 57 insertions(+), 28 deletions(-)

Detailed changes

crates/client2/src/client2.rs 🔗

@@ -15,8 +15,8 @@ use futures::{
     TryStreamExt,
 };
 use gpui::{
-    serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task,
-    WeakModel,
+    actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model,
+    SemanticVersion, Task, WeakModel,
 };
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
@@ -70,14 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
 pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
 pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
 
-#[derive(Clone, Default, PartialEq, Deserialize)]
-pub struct SignIn;
-
-#[derive(Clone, Default, PartialEq, Deserialize)]
-pub struct SignOut;
-
-#[derive(Clone, Default, PartialEq, Deserialize)]
-pub struct Reconnect;
+actions!(SignIn, SignOut, Reconnect);
 
 pub fn init_settings(cx: &mut AppContext) {
     TelemetrySettings::register(cx);

crates/gpui2/src/action.rs 🔗

@@ -4,7 +4,7 @@ use collections::{HashMap, HashSet};
 use serde::Deserialize;
 use std::any::{type_name, Any};
 
-pub trait Action: 'static {
+pub trait Action: std::fmt::Debug + 'static {
     fn qualified_name() -> SharedString
     where
         Self: Sized;
@@ -17,12 +17,39 @@ pub trait Action: 'static {
     fn as_any(&self) -> &dyn Any;
 }
 
+// actions defines structs that can be used as actions.
+#[macro_export]
+macro_rules! actions {
+    () => {};
+
+    ( $name:ident ) => {
+        #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)]
+        struct $name;
+    };
+
+    ( $name:ident { $($token:tt)* } ) => {
+        #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)]
+        struct $name { $($token)* }
+    };
+
+    ( $name:ident, $($rest:tt)* ) => {
+        actions!($name);
+        actions!($($rest)*);
+    };
+
+    ( $name:ident { $($token:tt)* }, $($rest:tt)* ) => {
+        actions!($name { $($token)* });
+        actions!($($rest)*);
+    };
+}
+
 impl<A> Action for A
 where
-    A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + 'static,
+    A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
 {
     fn qualified_name() -> SharedString {
-        type_name::<A>().into()
+        // todo!() remove the 2 replacement when migration is done
+        type_name::<A>().replace("2::", "::").into()
     }
 
     fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
@@ -292,6 +319,25 @@ mod tests {
     use super::*;
     use DispatchContextPredicate::*;
 
+    #[test]
+    fn test_actions_definition() {
+        {
+            actions!(A, B { field: i32 }, C, D, E, F {}, G);
+        }
+
+        {
+            actions!(
+                A,
+                B { field: i32 },
+                C,
+                D,
+                E,
+                F {},
+                G, // Don't wrap, test the trailing comma
+            );
+        }
+    }
+
     #[test]
     fn test_parse_context() {
         let mut expected = DispatchContext::default();

crates/gpui2/src/interactive.rs 🔗

@@ -1233,8 +1233,6 @@ pub type KeyListener<V> = Box<
 
 #[cfg(test)]
 mod test {
-    use serde_derive::Deserialize;
-
     use crate::{
         self as gpui, div, Div, FocusHandle, KeyBinding, Keystroke, ParentElement, Render,
         StatefulInteraction, StatelessInteractive, TestAppContext, VisualContext,
@@ -1246,8 +1244,7 @@ mod test {
         focus_handle: FocusHandle,
     }
 
-    #[derive(PartialEq, Clone, Default, Deserialize)]
-    struct TestAction;
+    actions!(TestAction);
 
     impl Render for TestView {
         type Element = Div<Self, StatefulInteraction<Self>>;

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

@@ -1,18 +1,10 @@
 use gpui::{
-    div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, StatefulInteraction,
-    StatelessInteractive, Styled, View, VisualContext, WindowContext,
+    actions, div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render,
+    StatefulInteraction, StatelessInteractive, Styled, View, VisualContext, WindowContext,
 };
-use serde::Deserialize;
 use theme2::ActiveTheme;
 
-#[derive(Clone, Default, PartialEq, Deserialize)]
-struct ActionA;
-
-#[derive(Clone, Default, PartialEq, Deserialize)]
-struct ActionB;
-
-#[derive(Clone, Default, PartialEq, Deserialize)]
-struct ActionC;
+actions!(ActionA, ActionB, ActionC);
 
 pub struct FocusStory {}