Merge pull request #773 from zed-industries/namespace-actions

Max Brunsfeld created

Change how actions are declared so that they are always associated with a namespace

Change summary

crates/chat_panel/src/chat_panel.rs           |   5 
crates/client/src/client.rs                   |   4 
crates/diagnostics/src/diagnostics.rs         |   4 
crates/editor/src/editor.rs                   | 288 ++++++++++++++------
crates/file_finder/src/file_finder.rs         |  10 
crates/go_to_line/src/go_to_line.rs           |   5 
crates/gpui/src/app.rs                        | 112 +------
crates/gpui/src/app/action.rs                 |  56 ++++
crates/gpui/src/keymap.rs                     |  16 
crates/gpui/src/platform.rs                   |   4 
crates/gpui/src/platform/mac/platform.rs      |   8 
crates/gpui/src/platform/test.rs              |   4 
crates/gpui/src/presenter.rs                  |   4 
crates/gpui/src/views/select.rs               |  11 
crates/journal/src/journal.rs                 |   4 
crates/outline/src/outline.rs                 |   4 
crates/project_panel/src/project_panel.rs     |  15 
crates/project_symbols/src/project_symbols.rs |   4 
crates/search/src/buffer_search.rs            |  14 
crates/search/src/project_search.rs           |   7 
crates/search/src/search.rs                   |  11 
crates/server/src/rpc.rs                      |  26 +
crates/theme_selector/src/theme_selector.rs   |  13 
crates/vim/src/insert.rs                      |   7 
crates/vim/src/normal.rs                      |  47 ++-
crates/vim/src/normal/g_prefix.rs             |   7 
crates/vim/src/vim.rs                         |   7 
crates/workspace/src/lsp_status.rs            |   8 
crates/workspace/src/menu.rs                  |  11 
crates/workspace/src/pane.rs                  |  38 ++
crates/workspace/src/sidebar.rs               |  11 
crates/workspace/src/workspace.rs             |  64 ++-
crates/zed/src/main.rs                        |   4 
crates/zed/src/zed.rs                         |  13 
34 files changed, 512 insertions(+), 334 deletions(-)

Detailed changes

crates/chat_panel/src/chat_panel.rs 🔗

@@ -4,7 +4,7 @@ use client::{
 };
 use editor::Editor;
 use gpui::{
-    action,
+    actions,
     elements::*,
     keymap::Binding,
     platform::CursorStyle,
@@ -33,8 +33,7 @@ pub struct ChatPanel {
 
 pub enum Event {}
 
-action!(Send);
-action!(LoadMoreMessages);
+actions!(chat_panel, [Send, LoadMoreMessages]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ChatPanel::send);

crates/client/src/client.rs 🔗

@@ -13,7 +13,7 @@ use async_tungstenite::tungstenite::{
 };
 use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
 use gpui::{
-    action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
+    actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
     Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
 };
 use http::HttpClient;
@@ -50,7 +50,7 @@ lazy_static! {
         .and_then(|s| if s.is_empty() { None } else { Some(s) });
 }
 
-action!(Authenticate);
+actions!(client, [Authenticate]);
 
 pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
     cx.add_global_action(move |_: &Authenticate, cx| {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -8,7 +8,7 @@ use editor::{
     highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
 };
 use gpui::{
-    action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
+    actions, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
@@ -28,7 +28,7 @@ use std::{
 use util::TryFutureExt;
 use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
 
-action!(Deploy);
+actions!(diagnostics, [Deploy]);
 
 const CONTEXT_LINE_COUNT: u32 = 1;
 

crates/editor/src/editor.rs 🔗

@@ -16,12 +16,13 @@ use display_map::*;
 pub use element::*;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    action,
+    actions,
     color::Color,
     elements::*,
     executor,
     fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::{vec2f, Vector2F},
+    impl_actions,
     keymap::Binding,
     platform::CursorStyle,
     text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
@@ -65,84 +66,132 @@ const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 
-action!(Cancel);
-action!(Backspace);
-action!(Delete);
-action!(Input, String);
-action!(Newline);
-action!(Tab, Direction);
-action!(Indent);
-action!(Outdent);
-action!(DeleteLine);
-action!(DeleteToPreviousWordStart);
-action!(DeleteToPreviousSubwordStart);
-action!(DeleteToNextWordEnd);
-action!(DeleteToNextSubwordEnd);
-action!(DeleteToBeginningOfLine);
-action!(DeleteToEndOfLine);
-action!(CutToEndOfLine);
-action!(DuplicateLine);
-action!(MoveLineUp);
-action!(MoveLineDown);
-action!(Cut);
-action!(Copy);
-action!(Paste);
-action!(Undo);
-action!(Redo);
-action!(MoveUp);
-action!(MoveDown);
-action!(MoveLeft);
-action!(MoveRight);
-action!(MoveToPreviousWordStart);
-action!(MoveToPreviousSubwordStart);
-action!(MoveToNextWordEnd);
-action!(MoveToNextSubwordEnd);
-action!(MoveToBeginningOfLine);
-action!(MoveToEndOfLine);
-action!(MoveToBeginning);
-action!(MoveToEnd);
-action!(SelectUp);
-action!(SelectDown);
-action!(SelectLeft);
-action!(SelectRight);
-action!(SelectToPreviousWordStart);
-action!(SelectToPreviousSubwordStart);
-action!(SelectToNextWordEnd);
-action!(SelectToNextSubwordEnd);
-action!(SelectToBeginningOfLine, bool);
-action!(SelectToEndOfLine, bool);
-action!(SelectToBeginning);
-action!(SelectToEnd);
-action!(SelectAll);
-action!(SelectLine);
-action!(SplitSelectionIntoLines);
-action!(AddSelectionAbove);
-action!(AddSelectionBelow);
-action!(SelectNext, bool);
-action!(ToggleComments);
-action!(SelectLargerSyntaxNode);
-action!(SelectSmallerSyntaxNode);
-action!(MoveToEnclosingBracket);
-action!(UndoSelection);
-action!(RedoSelection);
-action!(GoToDiagnostic, Direction);
-action!(GoToDefinition);
-action!(FindAllReferences);
-action!(Rename);
-action!(ConfirmRename);
-action!(PageUp);
-action!(PageDown);
-action!(Fold);
-action!(UnfoldLines);
-action!(FoldSelectedRanges);
-action!(Scroll, Vector2F);
-action!(Select, SelectPhase);
-action!(ShowCompletions);
-action!(ToggleCodeActions, bool);
-action!(ConfirmCompletion, Option<usize>);
-action!(ConfirmCodeAction, Option<usize>);
-action!(OpenExcerpts);
-action!(RestartLanguageServer);
+#[derive(Clone)]
+pub struct SelectNext(pub bool);
+
+#[derive(Clone)]
+pub struct GoToDiagnostic(pub Direction);
+
+#[derive(Clone)]
+pub struct Scroll(pub Vector2F);
+
+#[derive(Clone)]
+pub struct Select(pub SelectPhase);
+
+#[derive(Clone)]
+pub struct Input(pub String);
+
+#[derive(Clone)]
+pub struct Tab(pub Direction);
+
+#[derive(Clone)]
+pub struct SelectToBeginningOfLine {
+    stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone)]
+pub struct SelectToEndOfLine {
+    stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone)]
+pub struct ToggleCodeActions(pub bool);
+
+#[derive(Clone)]
+pub struct ConfirmCompletion(pub Option<usize>);
+
+#[derive(Clone)]
+pub struct ConfirmCodeAction(pub Option<usize>);
+
+impl_actions!(
+    editor,
+    [
+        SelectNext,
+        GoToDiagnostic,
+        Scroll,
+        Select,
+        Input,
+        Tab,
+        SelectToBeginningOfLine,
+        SelectToEndOfLine,
+        ToggleCodeActions,
+        ConfirmCompletion,
+        ConfirmCodeAction,
+    ]
+);
+
+actions!(
+    editor,
+    [
+        Cancel,
+        Backspace,
+        Delete,
+        Newline,
+        Indent,
+        Outdent,
+        DeleteLine,
+        DeleteToPreviousWordStart,
+        DeleteToPreviousSubwordStart,
+        DeleteToNextWordEnd,
+        DeleteToNextSubwordEnd,
+        DeleteToBeginningOfLine,
+        DeleteToEndOfLine,
+        CutToEndOfLine,
+        DuplicateLine,
+        MoveLineUp,
+        MoveLineDown,
+        Cut,
+        Copy,
+        Paste,
+        Undo,
+        Redo,
+        MoveUp,
+        MoveDown,
+        MoveLeft,
+        MoveRight,
+        MoveToPreviousWordStart,
+        MoveToPreviousSubwordStart,
+        MoveToNextWordEnd,
+        MoveToNextSubwordEnd,
+        MoveToBeginningOfLine,
+        MoveToEndOfLine,
+        MoveToBeginning,
+        MoveToEnd,
+        SelectUp,
+        SelectDown,
+        SelectLeft,
+        SelectRight,
+        SelectToPreviousWordStart,
+        SelectToPreviousSubwordStart,
+        SelectToNextWordEnd,
+        SelectToNextSubwordEnd,
+        SelectToBeginning,
+        SelectToEnd,
+        SelectAll,
+        SelectLine,
+        SplitSelectionIntoLines,
+        AddSelectionAbove,
+        AddSelectionBelow,
+        ToggleComments,
+        SelectLargerSyntaxNode,
+        SelectSmallerSyntaxNode,
+        MoveToEnclosingBracket,
+        UndoSelection,
+        RedoSelection,
+        GoToDefinition,
+        FindAllReferences,
+        Rename,
+        ConfirmRename,
+        PageUp,
+        PageDown,
+        Fold,
+        UnfoldLines,
+        FoldSelectedRanges,
+        ShowCompletions,
+        OpenExcerpts,
+        RestartLanguageServer,
+    ]
+);
 
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
@@ -256,7 +305,9 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("alt-shift-F", SelectToNextWordEnd, Some("Editor")),
         Binding::new(
             "cmd-shift-left",
-            SelectToBeginningOfLine(true),
+            SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
             Some("Editor"),
         ),
         Binding::new(
@@ -267,11 +318,25 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("ctrl-alt-shift-F", SelectToNextSubwordEnd, Some("Editor")),
         Binding::new(
             "ctrl-shift-A",
-            SelectToBeginningOfLine(true),
+            SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
+            Some("Editor"),
+        ),
+        Binding::new(
+            "cmd-shift-right",
+            SelectToEndOfLine {
+                stop_at_soft_wraps: true,
+            },
+            Some("Editor"),
+        ),
+        Binding::new(
+            "ctrl-shift-E",
+            SelectToEndOfLine {
+                stop_at_soft_wraps: true,
+            },
             Some("Editor"),
         ),
-        Binding::new("cmd-shift-right", SelectToEndOfLine(true), Some("Editor")),
-        Binding::new("ctrl-shift-E", SelectToEndOfLine(true), Some("Editor")),
         Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")),
         Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")),
         Binding::new("cmd-a", SelectAll, Some("Editor")),
@@ -3860,12 +3925,12 @@ impl Editor {
 
     pub fn select_to_beginning_of_line(
         &mut self,
-        SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine,
+        action: &SelectToBeginningOfLine,
         cx: &mut ViewContext<Self>,
     ) {
         self.move_selection_heads(cx, |map, head, _| {
             (
-                movement::line_beginning(map, head, *stop_at_soft_boundaries),
+                movement::line_beginning(map, head, action.stop_at_soft_wraps),
                 SelectionGoal::None,
             )
         });
@@ -3877,7 +3942,12 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            this.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx);
+            this.select_to_beginning_of_line(
+                &SelectToBeginningOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
             this.backspace(&Backspace, cx);
         });
     }
@@ -3890,12 +3960,12 @@ impl Editor {
 
     pub fn select_to_end_of_line(
         &mut self,
-        SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine,
+        action: &SelectToEndOfLine,
         cx: &mut ViewContext<Self>,
     ) {
         self.move_selection_heads(cx, |map, head, _| {
             (
-                movement::line_end(map, head, *stop_at_soft_boundaries),
+                movement::line_end(map, head, action.stop_at_soft_wraps),
                 SelectionGoal::None,
             )
         });
@@ -3903,14 +3973,24 @@ impl Editor {
 
     pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
-            this.select_to_end_of_line(&SelectToEndOfLine(false), cx);
+            this.select_to_end_of_line(
+                &SelectToEndOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
             this.delete(&Delete, cx);
         });
     }
 
     pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
-            this.select_to_end_of_line(&SelectToEndOfLine(false), cx);
+            this.select_to_end_of_line(
+                &SelectToEndOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
             this.cut(&Cut, cx);
         });
     }
@@ -7251,7 +7331,12 @@ mod tests {
 
         view.update(cx, |view, cx| {
             view.move_left(&MoveLeft, cx);
-            view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx);
+            view.select_to_beginning_of_line(
+                &SelectToBeginningOfLine {
+                    stop_at_soft_wraps: true,
+                },
+                cx,
+            );
             assert_eq!(
                 view.selected_display_ranges(cx),
                 &[
@@ -7262,7 +7347,12 @@ mod tests {
         });
 
         view.update(cx, |view, cx| {
-            view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx);
+            view.select_to_beginning_of_line(
+                &SelectToBeginningOfLine {
+                    stop_at_soft_wraps: true,
+                },
+                cx,
+            );
             assert_eq!(
                 view.selected_display_ranges(cx),
                 &[
@@ -7273,7 +7363,12 @@ mod tests {
         });
 
         view.update(cx, |view, cx| {
-            view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx);
+            view.select_to_beginning_of_line(
+                &SelectToBeginningOfLine {
+                    stop_at_soft_wraps: true,
+                },
+                cx,
+            );
             assert_eq!(
                 view.selected_display_ranges(cx),
                 &[
@@ -7284,7 +7379,12 @@ mod tests {
         });
 
         view.update(cx, |view, cx| {
-            view.select_to_end_of_line(&SelectToEndOfLine(true), cx);
+            view.select_to_end_of_line(
+                &SelectToEndOfLine {
+                    stop_at_soft_wraps: true,
+                },
+                cx,
+            );
             assert_eq!(
                 view.selected_display_ranges(cx),
                 &[

crates/file_finder/src/file_finder.rs 🔗

@@ -1,8 +1,9 @@
 use editor::Editor;
 use fuzzy::PathMatch;
 use gpui::{
-    action,
+    actions,
     elements::*,
+    impl_actions,
     keymap::{self, Binding},
     AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
@@ -37,8 +38,11 @@ pub struct FileFinder {
     list_state: UniformListState,
 }
 
-action!(Toggle);
-action!(Select, ProjectPath);
+#[derive(Clone)]
+pub struct Select(pub ProjectPath);
+
+impl_actions!(file_finder, [Select]);
+actions!(file_finder, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(FileFinder::toggle);

crates/go_to_line/src/go_to_line.rs 🔗

@@ -1,14 +1,13 @@
 use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
 use gpui::{
-    action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
+    actions, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
     MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
 };
 use settings::Settings;
 use text::{Bias, Point};
 use workspace::Workspace;
 
-action!(Toggle);
-action!(Confirm);
+actions!(go_to_line, [Toggle, Confirm]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings([

crates/gpui/src/app.rs 🔗

@@ -1,3 +1,5 @@
+pub mod action;
+
 use crate::{
     elements::ElementBox,
     executor::{self, Task},
@@ -7,6 +9,7 @@ use crate::{
     util::post_inc,
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
 };
+pub use action::*;
 use anyhow::{anyhow, Result};
 use collections::btree_map;
 use keymap::MatchResult;
@@ -142,89 +145,6 @@ pub trait ElementStateContext: DerefMut<Target = MutableAppContext> {
     }
 }
 
-pub trait Action: 'static + AnyAction {
-    type Argument: 'static + Clone;
-}
-
-pub trait AnyAction {
-    fn id(&self) -> TypeId;
-    fn name(&self) -> &'static str;
-    fn as_any(&self) -> &dyn Any;
-    fn boxed_clone(&self) -> Box<dyn AnyAction>;
-    fn boxed_clone_as_any(&self) -> Box<dyn Any>;
-}
-
-#[macro_export]
-macro_rules! action {
-    ($name:ident, $arg:ty) => {
-        #[derive(Clone)]
-        pub struct $name(pub $arg);
-
-        impl $crate::Action for $name {
-            type Argument = $arg;
-        }
-
-        impl $crate::AnyAction for $name {
-            fn id(&self) -> std::any::TypeId {
-                std::any::TypeId::of::<$name>()
-            }
-
-            fn name(&self) -> &'static str {
-                stringify!($name)
-            }
-
-            fn as_any(&self) -> &dyn std::any::Any {
-                self
-            }
-
-            fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
-                Box::new(self.clone())
-            }
-
-            fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
-                Box::new(self.clone())
-            }
-        }
-
-        impl From<$arg> for $name {
-            fn from(arg: $arg) -> Self {
-                Self(arg)
-            }
-        }
-    };
-
-    ($name:ident) => {
-        #[derive(Clone, Debug, Eq, PartialEq)]
-        pub struct $name;
-
-        impl $crate::Action for $name {
-            type Argument = ();
-        }
-
-        impl $crate::AnyAction for $name {
-            fn id(&self) -> std::any::TypeId {
-                std::any::TypeId::of::<$name>()
-            }
-
-            fn name(&self) -> &'static str {
-                stringify!($name)
-            }
-
-            fn as_any(&self) -> &dyn std::any::Any {
-                self
-            }
-
-            fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
-                Box::new(self.clone())
-            }
-
-            fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
-                Box::new(self.clone())
-            }
-        }
-    };
-}
-
 pub struct Menu<'a> {
     pub name: &'a str,
     pub items: Vec<MenuItem<'a>>,
@@ -234,7 +154,7 @@ pub enum MenuItem<'a> {
     Action {
         name: &'a str,
         keystroke: Option<&'a str>,
-        action: Box<dyn AnyAction>,
+        action: Box<dyn Action>,
     },
     Separator,
 }
@@ -787,8 +707,8 @@ impl ReadViewWith for TestAppContext {
 }
 
 type ActionCallback =
-    dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize);
-type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
+    dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
+type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
 
 type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
 type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
@@ -963,7 +883,7 @@ impl MutableAppContext {
     {
         let handler = Box::new(
             move |view: &mut dyn AnyView,
-                  action: &dyn AnyAction,
+                  action: &dyn Action,
                   cx: &mut MutableAppContext,
                   window_id: usize,
                   view_id: usize| {
@@ -1009,7 +929,7 @@ impl MutableAppContext {
         A: Action,
         F: 'static + FnMut(&A, &mut MutableAppContext),
     {
-        let handler = Box::new(move |action: &dyn AnyAction, cx: &mut MutableAppContext| {
+        let handler = Box::new(move |action: &dyn Action, cx: &mut MutableAppContext| {
             let action = action.as_any().downcast_ref().unwrap();
             handler(action, cx);
         });
@@ -1338,7 +1258,7 @@ impl MutableAppContext {
         &mut self,
         window_id: usize,
         path: &[usize],
-        action: &dyn AnyAction,
+        action: &dyn Action,
     ) -> bool {
         self.update(|this| {
             this.halt_action_dispatch = false;
@@ -1398,7 +1318,7 @@ impl MutableAppContext {
         self.dispatch_global_action_any(&action);
     }
 
-    fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) -> bool {
+    fn dispatch_global_action_any(&mut self, action: &dyn Action) -> bool {
         self.update(|this| {
             if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) {
                 handler(action, this);
@@ -4655,7 +4575,7 @@ impl RefCounts {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::elements::*;
+    use crate::{elements::*, impl_actions};
     use smol::future::poll_once;
     use std::{
         cell::Cell,
@@ -5801,7 +5721,10 @@ mod tests {
             }
         }
 
-        action!(Action, &'static str);
+        #[derive(Clone)]
+        pub struct Action(pub &'static str);
+
+        impl_actions!(test, [Action]);
 
         let actions = Rc::new(RefCell::new(Vec::new()));
 
@@ -5909,7 +5832,10 @@ mod tests {
 
     #[crate::test(self)]
     fn test_dispatch_keystroke(cx: &mut MutableAppContext) {
-        action!(Action, &'static str);
+        #[derive(Clone)]
+        pub struct Action(pub &'static str);
+
+        impl_actions!(test, [Action]);
 
         struct View {
             id: usize,

crates/gpui/src/app/action.rs 🔗

@@ -0,0 +1,56 @@
+use std::any::{Any, TypeId};
+
+pub trait Action: 'static {
+    fn id(&self) -> TypeId;
+    fn namespace(&self) -> &'static str;
+    fn name(&self) -> &'static str;
+    fn as_any(&self) -> &dyn Any;
+    fn boxed_clone(&self) -> Box<dyn Action>;
+    fn boxed_clone_as_any(&self) -> Box<dyn Any>;
+}
+
+#[macro_export]
+macro_rules! impl_actions {
+    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+        $(
+            impl $crate::action::Action for $name {
+                fn id(&self) -> std::any::TypeId {
+                    std::any::TypeId::of::<$name>()
+                }
+
+                fn namespace(&self) -> &'static str {
+                    stringify!($namespace)
+                }
+
+                fn name(&self) -> &'static str {
+                    stringify!($name)
+                }
+
+                fn as_any(&self) -> &dyn std::any::Any {
+                    self
+                }
+
+                fn boxed_clone(&self) -> Box<dyn $crate::action::Action> {
+                    Box::new(self.clone())
+                }
+
+                fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
+                    Box::new(self.clone())
+                }
+            }
+        )*
+    };
+}
+
+#[macro_export]
+macro_rules! actions {
+    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+
+        $(
+            #[derive(Clone, Debug, Default, PartialEq, Eq)]
+            pub struct $name;
+        )*
+
+        $crate::impl_actions!($namespace, [ $($name),* ]);
+    };
+}

crates/gpui/src/keymap.rs 🔗

@@ -1,3 +1,4 @@
+use crate::Action;
 use anyhow::anyhow;
 use std::{
     any::Any,
@@ -6,8 +7,6 @@ use std::{
 };
 use tree_sitter::{Language, Node, Parser};
 
-use crate::{Action, AnyAction};
-
 extern "C" {
     fn tree_sitter_context_predicate() -> Language;
 }
@@ -28,7 +27,7 @@ pub struct Keymap(Vec<Binding>);
 
 pub struct Binding {
     keystrokes: Vec<Keystroke>,
-    action: Box<dyn AnyAction>,
+    action: Box<dyn Action>,
     context: Option<ContextPredicate>,
 }
 
@@ -73,7 +72,7 @@ where
 pub enum MatchResult {
     None,
     Pending,
-    Action(Box<dyn AnyAction>),
+    Action(Box<dyn Action>),
 }
 
 impl Debug for MatchResult {
@@ -329,7 +328,7 @@ impl ContextPredicate {
 
 #[cfg(test)]
 mod tests {
-    use crate::action;
+    use crate::{actions, impl_actions};
 
     use super::*;
 
@@ -420,9 +419,10 @@ mod tests {
 
     #[test]
     fn test_matcher() -> anyhow::Result<()> {
-        action!(A, &'static str);
-        action!(B);
-        action!(Ab);
+        #[derive(Clone)]
+        pub struct A(pub &'static str);
+        impl_actions!(test, [A]);
+        actions!(test, [B, Ab]);
 
         impl PartialEq for A {
             fn eq(&self, other: &Self) -> bool {

crates/gpui/src/platform.rs 🔗

@@ -15,7 +15,7 @@ use crate::{
         vector::Vector2F,
     },
     text_layout::{LineLayout, RunStyle},
-    AnyAction, ClipboardItem, Menu, Scene,
+    Action, ClipboardItem, Menu, Scene,
 };
 use anyhow::Result;
 use async_task::Runnable;
@@ -66,7 +66,7 @@ pub(crate) trait ForegroundPlatform {
     fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
     fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
 
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>);
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
     fn set_menus(&self, menus: Vec<Menu>);
     fn prompt_for_paths(
         &self,

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
     executor,
     keymap::Keystroke,
     platform::{self, CursorStyle},
-    AnyAction, ClipboardItem, Event, Menu, MenuItem,
+    Action, ClipboardItem, Event, Menu, MenuItem,
 };
 use anyhow::{anyhow, Result};
 use block::ConcreteBlock;
@@ -107,10 +107,10 @@ pub struct MacForegroundPlatformState {
     resign_active: Option<Box<dyn FnMut()>>,
     quit: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
-    menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
+    menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
     open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
     finish_launching: Option<Box<dyn FnOnce() -> ()>>,
-    menu_actions: Vec<Box<dyn AnyAction>>,
+    menu_actions: Vec<Box<dyn Action>>,
 }
 
 impl MacForegroundPlatform {
@@ -235,7 +235,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         }
     }
 
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>) {
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
         self.0.borrow_mut().menu_command = Some(callback);
     }
 

crates/gpui/src/platform/test.rs 🔗

@@ -1,7 +1,7 @@
 use super::{CursorStyle, WindowBounds};
 use crate::{
     geometry::vector::{vec2f, Vector2F},
-    AnyAction, ClipboardItem,
+    Action, ClipboardItem,
 };
 use anyhow::{anyhow, Result};
 use parking_lot::Mutex;
@@ -72,7 +72,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
         unimplemented!()
     }
 
-    fn on_menu_command(&self, _: Box<dyn FnMut(&dyn AnyAction)>) {}
+    fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
 
     fn set_menus(&self, _: Vec<crate::Menu>) {}
 

crates/gpui/src/presenter.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
     json::{self, ToJson},
     platform::Event,
     text_layout::TextLayoutCache,
-    Action, AnyAction, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
+    Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
     ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene,
     UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
@@ -223,7 +223,7 @@ impl Presenter {
 
 pub struct DispatchDirective {
     pub path: Vec<usize>,
-    pub action: Box<dyn AnyAction>,
+    pub action: Box<dyn Action>,
 }
 
 pub struct LayoutContext<'a> {

crates/gpui/src/views/select.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext,
-    WeakViewHandle,
+    actions, elements::*, impl_actions, AppContext, Entity, MutableAppContext, RenderContext, View,
+    ViewContext, WeakViewHandle,
 };
 
 pub struct Select {
@@ -25,8 +25,11 @@ pub enum ItemType {
     Unselected,
 }
 
-action!(ToggleSelect);
-action!(SelectItem, usize);
+#[derive(Clone)]
+pub struct SelectItem(pub usize);
+
+actions!(select, [ToggleSelect]);
+impl_actions!(select, [SelectItem]);
 
 pub enum Event {}
 

crates/journal/src/journal.rs 🔗

@@ -1,11 +1,11 @@
 use chrono::{Datelike, Local, Timelike};
 use editor::{Autoscroll, Editor};
-use gpui::{action, keymap::Binding, MutableAppContext};
+use gpui::{actions, keymap::Binding, MutableAppContext};
 use std::{fs::OpenOptions, sync::Arc};
 use util::TryFutureExt as _;
 use workspace::AppState;
 
-action!(NewJournalEntry);
+actions!(journal, [NewJournalEntry]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.add_bindings(vec![Binding::new("ctrl-alt-cmd-j", NewJournalEntry, None)]);

crates/outline/src/outline.rs 🔗

@@ -4,7 +4,7 @@ use editor::{
 };
 use fuzzy::StringMatch;
 use gpui::{
-    action,
+    actions,
     elements::*,
     geometry::vector::Vector2F,
     keymap::{self, Binding},
@@ -20,7 +20,7 @@ use workspace::{
     Workspace,
 };
 
-action!(Toggle);
+actions!(outline, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings([

crates/project_panel/src/project_panel.rs 🔗

@@ -1,9 +1,10 @@
 use gpui::{
-    action,
+    actions,
     elements::{
         Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
         Svg, UniformList, UniformListState,
     },
+    impl_actions,
     keymap::{self, Binding},
     platform::CursorStyle,
     AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
@@ -46,10 +47,14 @@ struct EntryDetails {
     is_selected: bool,
 }
 
-action!(ExpandSelectedEntry);
-action!(CollapseSelectedEntry);
-action!(ToggleExpanded, ProjectEntryId);
-action!(Open, ProjectEntryId);
+#[derive(Clone)]
+pub struct ToggleExpanded(pub ProjectEntryId);
+
+#[derive(Clone)]
+pub struct Open(pub ProjectEntryId);
+
+actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]);
+impl_actions!(project_panel, [Open, ToggleExpanded]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectPanel::expand_selected_entry);

crates/project_symbols/src/project_symbols.rs 🔗

@@ -3,7 +3,7 @@ use editor::{
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    action,
+    actions,
     elements::*,
     keymap::{self, Binding},
     AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
@@ -22,7 +22,7 @@ use workspace::{
     Workspace,
 };
 
-action!(Toggle);
+actions!(project_symbols, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings([

crates/search/src/buffer_search.rs 🔗

@@ -2,7 +2,7 @@ use crate::{active_match_index, match_index_for_direction, Direction, SearchOpti
 use collections::HashMap;
 use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
 use gpui::{
-    action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, Entity,
+    actions, elements::*, impl_actions, keymap::Binding, platform::CursorStyle, AppContext, Entity,
     MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
@@ -12,10 +12,14 @@ use settings::Settings;
 use std::ops::Range;
 use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
 
-action!(Deploy, bool);
-action!(Dismiss);
-action!(FocusEditor);
-action!(ToggleSearchOption, SearchOption);
+#[derive(Clone)]
+pub struct Deploy(pub bool);
+
+#[derive(Clone)]
+pub struct ToggleSearchOption(pub SearchOption);
+
+actions!(buffer_search, [Dismiss, FocusEditor]);
+impl_actions!(buffer_search, [Deploy, ToggleSearchOption]);
 
 pub enum Event {
     UpdateLocation,

crates/search/src/project_search.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
 use collections::HashMap;
 use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
 use gpui::{
-    action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
+    actions, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
     ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
     ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
@@ -19,10 +19,7 @@ use std::{
 use util::ResultExt as _;
 use workspace::{Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace};
 
-action!(Deploy);
-action!(Search);
-action!(SearchInNew);
-action!(ToggleFocus);
+actions!(project_search, [Deploy, Search, SearchInNew, ToggleFocus]);
 
 const MAX_TAB_TITLE_LEN: usize = 24;
 

crates/search/src/search.rs 🔗

@@ -1,6 +1,6 @@
 pub use buffer_search::BufferSearchBar;
 use editor::{Anchor, MultiBufferSnapshot};
-use gpui::{action, MutableAppContext};
+use gpui::{impl_actions, MutableAppContext};
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
 use std::{
     cmp::{self, Ordering},
@@ -15,8 +15,13 @@ pub fn init(cx: &mut MutableAppContext) {
     project_search::init(cx);
 }
 
-action!(ToggleSearchOption, SearchOption);
-action!(SelectMatch, Direction);
+#[derive(Clone)]
+pub struct ToggleSearchOption(pub SearchOption);
+
+#[derive(Clone)]
+pub struct SelectMatch(pub Direction);
+
+impl_actions!(search, [ToggleSearchOption, SelectMatch]);
 
 #[derive(Clone, Copy)]
 pub enum SearchOption {

crates/server/src/rpc.rs 🔗

@@ -1118,7 +1118,7 @@ mod tests {
         time::Duration,
     };
     use util::TryFutureExt;
-    use workspace::{Item, SplitDirection, Workspace, WorkspaceParams};
+    use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
 
     #[cfg(test)]
     #[ctor::ctor]
@@ -4527,7 +4527,9 @@ mod tests {
         editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
         workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.toggle_follow(&client_a_id.into(), cx).unwrap()
+                workspace
+                    .toggle_follow(&ToggleFollow(client_a_id), cx)
+                    .unwrap()
             })
             .await
             .unwrap();
@@ -4627,7 +4629,9 @@ mod tests {
         // Client A starts following client B.
         workspace_a
             .update(cx_a, |workspace, cx| {
-                workspace.toggle_follow(&client_b_id.into(), cx).unwrap()
+                workspace
+                    .toggle_follow(&ToggleFollow(client_b_id), cx)
+                    .unwrap()
             })
             .await
             .unwrap();
@@ -4856,7 +4860,9 @@ mod tests {
         });
         workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+                workspace
+                    .toggle_follow(&ToggleFollow(leader_id), cx)
+                    .unwrap()
             })
             .await
             .unwrap();
@@ -4881,7 +4887,9 @@ mod tests {
 
         workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+                workspace
+                    .toggle_follow(&ToggleFollow(leader_id), cx)
+                    .unwrap()
             })
             .await
             .unwrap();
@@ -4899,7 +4907,9 @@ mod tests {
 
         workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+                workspace
+                    .toggle_follow(&ToggleFollow(leader_id), cx)
+                    .unwrap()
             })
             .await
             .unwrap();
@@ -4919,7 +4929,9 @@ mod tests {
 
         workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+                workspace
+                    .toggle_follow(&ToggleFollow(leader_id), cx)
+                    .unwrap()
             })
             .await
             .unwrap();

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,15 +1,15 @@
 use editor::Editor;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    action,
     elements::*,
+    impl_actions,
     keymap::{self, Binding},
     AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
     ViewContext, ViewHandle,
 };
+use settings::Settings;
 use std::{cmp, sync::Arc};
 use theme::{Theme, ThemeRegistry};
-use settings::Settings;
 use workspace::{
     menu::{Confirm, SelectNext, SelectPrev},
     Workspace,
@@ -25,8 +25,13 @@ pub struct ThemeSelector {
     selection_completed: bool,
 }
 
-action!(Toggle, Arc<ThemeRegistry>);
-action!(Reload, Arc<ThemeRegistry>);
+#[derive(Clone)]
+pub struct Toggle(pub Arc<ThemeRegistry>);
+
+#[derive(Clone)]
+pub struct Reload(pub Arc<ThemeRegistry>);
+
+impl_actions!(theme_selector, [Toggle, Reload]);
 
 pub fn init(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
     cx.add_action(ThemeSelector::confirm);

crates/vim/src/insert.rs 🔗

@@ -1,11 +1,10 @@
+use crate::{mode::Mode, SwitchMode, VimState};
 use editor::Bias;
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use gpui::{actions, keymap::Binding, MutableAppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
 
-use crate::{mode::Mode, SwitchMode, VimState};
-
-action!(NormalBefore);
+actions!(vim, [NormalBefore]);
 
 pub fn init(cx: &mut MutableAppContext) {
     let context = Some("Editor && vim_mode == insert");

crates/vim/src/normal.rs 🔗

@@ -1,23 +1,42 @@
 mod g_prefix;
 
+use crate::{mode::NormalState, Mode, SwitchMode, VimState};
 use editor::{char_kind, movement, Bias};
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use gpui::{actions, impl_actions, keymap::Binding, MutableAppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
 
-use crate::{mode::NormalState, Mode, SwitchMode, VimState};
-
-action!(GPrefix);
-action!(MoveLeft);
-action!(MoveDown);
-action!(MoveUp);
-action!(MoveRight);
-action!(MoveToStartOfLine);
-action!(MoveToEndOfLine);
-action!(MoveToEnd);
-action!(MoveToNextWordStart, bool);
-action!(MoveToNextWordEnd, bool);
-action!(MoveToPreviousWordStart, bool);
+#[derive(Clone)]
+struct MoveToNextWordStart(pub bool);
+
+#[derive(Clone)]
+struct MoveToNextWordEnd(pub bool);
+
+#[derive(Clone)]
+struct MoveToPreviousWordStart(pub bool);
+
+impl_actions!(
+    vim,
+    [
+        MoveToNextWordStart,
+        MoveToNextWordEnd,
+        MoveToPreviousWordStart,
+    ]
+);
+
+actions!(
+    vim,
+    [
+        GPrefix,
+        MoveLeft,
+        MoveDown,
+        MoveUp,
+        MoveRight,
+        MoveToStartOfLine,
+        MoveToEndOfLine,
+        MoveToEnd,
+    ]
+);
 
 pub fn init(cx: &mut MutableAppContext) {
     let context = Some("Editor && vim_mode == normal");

crates/vim/src/normal/g_prefix.rs 🔗

@@ -1,9 +1,8 @@
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
-use workspace::Workspace;
-
 use crate::{mode::Mode, SwitchMode, VimState};
+use gpui::{actions, keymap::Binding, MutableAppContext, ViewContext};
+use workspace::Workspace;
 
-action!(MoveToStart);
+actions!(vim, [MoveToStart]);
 
 pub fn init(cx: &mut MutableAppContext) {
     let context = Some("Editor && vim_mode == normal && vim_submode == g");

crates/vim/src/vim.rs 🔗

@@ -7,13 +7,16 @@ mod vim_test_context;
 
 use collections::HashMap;
 use editor::{CursorShape, Editor};
-use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
+use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
 
 use mode::Mode;
 use settings::Settings;
 use workspace::{self, Workspace};
 
-action!(SwitchMode, Mode);
+#[derive(Clone)]
+pub struct SwitchMode(pub Mode);
+
+impl_actions!(vim, [SwitchMode]);
 
 pub fn init(cx: &mut MutableAppContext) {
     editor_events::init(cx);

crates/workspace/src/lsp_status.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{ItemHandle, StatusItemView};
 use futures::StreamExt;
-use gpui::AppContext;
+use gpui::{actions, AppContext};
 use gpui::{
-    action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext,
-    RenderContext, View, ViewContext,
+    elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, RenderContext,
+    View, ViewContext,
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
 use project::{LanguageServerProgress, Project};
@@ -13,7 +13,7 @@ use std::cmp::Reverse;
 use std::fmt::Write;
 use std::sync::Arc;
 
-action!(DismissErrorMessage);
+actions!(lsp_status, [DismissErrorMessage]);
 
 pub struct LspStatus {
     checking_for_update: Vec<String>,

crates/workspace/src/menu.rs 🔗

@@ -1,10 +1,9 @@
-use gpui::{action, keymap::Binding, MutableAppContext};
+use gpui::{actions, keymap::Binding, MutableAppContext};
 
-action!(Confirm);
-action!(SelectPrev);
-action!(SelectNext);
-action!(SelectFirst);
-action!(SelectLast);
+actions!(
+    menu,
+    [Confirm, SelectPrev, SelectNext, SelectFirst, SelectLast,]
+);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings([

crates/workspace/src/pane.rs 🔗

@@ -4,9 +4,10 @@ use anyhow::Result;
 use collections::{HashMap, VecDeque};
 use futures::StreamExt;
 use gpui::{
-    action,
+    actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
+    impl_actions,
     keymap::Binding,
     platform::{CursorStyle, NavigationDirection},
     AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
@@ -17,15 +18,32 @@ use settings::Settings;
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use util::ResultExt;
 
-action!(Split, SplitDirection);
-action!(ActivateItem, usize);
-action!(ActivatePrevItem);
-action!(ActivateNextItem);
-action!(CloseActiveItem);
-action!(CloseInactiveItems);
-action!(CloseItem, CloseItemParams);
-action!(GoBack, Option<WeakViewHandle<Pane>>);
-action!(GoForward, Option<WeakViewHandle<Pane>>);
+actions!(
+    pane,
+    [
+        ActivatePrevItem,
+        ActivateNextItem,
+        CloseActiveItem,
+        CloseInactiveItems,
+    ]
+);
+
+#[derive(Clone)]
+pub struct Split(pub SplitDirection);
+
+#[derive(Clone)]
+pub struct CloseItem(pub CloseItemParams);
+
+#[derive(Clone)]
+pub struct ActivateItem(pub usize);
+
+#[derive(Clone)]
+pub struct GoBack(pub Option<WeakViewHandle<Pane>>);
+
+#[derive(Clone)]
+pub struct GoForward(pub Option<WeakViewHandle<Pane>>);
+
+impl_actions!(pane, [Split, CloseItem, ActivateItem, GoBack, GoForward,]);
 
 #[derive(Clone)]
 pub struct CloseItemParams {

crates/workspace/src/sidebar.rs 🔗

@@ -1,5 +1,5 @@
 use super::Workspace;
-use gpui::{action, elements::*, platform::CursorStyle, AnyViewHandle, RenderContext};
+use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext};
 use std::{cell::RefCell, rc::Rc};
 use theme::Theme;
 
@@ -21,8 +21,13 @@ struct Item {
     view: AnyViewHandle,
 }
 
-action!(ToggleSidebarItem, SidebarItemId);
-action!(ToggleSidebarItemFocus, SidebarItemId);
+#[derive(Clone)]
+pub struct ToggleSidebarItem(pub SidebarItemId);
+
+#[derive(Clone)]
+pub struct ToggleSidebarItemFocus(pub SidebarItemId);
+
+impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
 
 #[derive(Clone)]
 pub struct SidebarItemId {

crates/workspace/src/workspace.rs 🔗

@@ -13,10 +13,11 @@ use client::{
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
 use gpui::{
-    action,
+    actions,
     color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
+    impl_actions,
     json::{self, to_string_pretty, ToJson},
     keymap::Binding,
     platform::{CursorStyle, WindowOptions},
@@ -69,18 +70,41 @@ type FollowableItemBuilders = HashMap<
     ),
 >;
 
-action!(Open, Arc<AppState>);
-action!(OpenNew, Arc<AppState>);
-action!(OpenPaths, OpenParams);
-action!(ToggleShare);
-action!(ToggleFollow, PeerId);
-action!(FollowNextCollaborator);
-action!(Unfollow);
-action!(JoinProject, JoinProjectParams);
-action!(Save);
-action!(DebugElements);
-action!(ActivatePreviousPane);
-action!(ActivateNextPane);
+actions!(
+    workspace,
+    [
+        ToggleShare,
+        Unfollow,
+        Save,
+        DebugElements,
+        ActivatePreviousPane,
+        ActivateNextPane,
+        FollowNextCollaborator,
+    ]
+);
+
+#[derive(Clone)]
+pub struct Open(pub Arc<AppState>);
+
+#[derive(Clone)]
+pub struct OpenNew(pub Arc<AppState>);
+
+#[derive(Clone)]
+pub struct OpenPaths {
+    pub paths: Vec<PathBuf>,
+    pub app_state: Arc<AppState>,
+}
+
+#[derive(Clone)]
+pub struct ToggleFollow(pub PeerId);
+
+#[derive(Clone)]
+pub struct JoinProject(pub JoinProjectParams);
+
+impl_actions!(
+    workspace,
+    [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject]
+);
 
 pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
     pane::init(cx);
@@ -88,7 +112,7 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
 
     cx.add_global_action(open);
     cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
-        open_paths(&action.0.paths, &action.0.app_state, cx).detach();
+        open_paths(&action.paths, &action.app_state, cx).detach();
     });
     cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
         open_new(&action.0, cx)
@@ -190,12 +214,6 @@ pub struct AppState {
     ) -> Workspace,
 }
 
-#[derive(Clone)]
-pub struct OpenParams {
-    pub paths: Vec<PathBuf>,
-    pub app_state: Arc<AppState>,
-}
-
 #[derive(Clone)]
 pub struct JoinProjectParams {
     pub project_id: u64,
@@ -2092,9 +2110,9 @@ impl Element for AvatarRibbon {
     }
 }
 
-impl std::fmt::Debug for OpenParams {
+impl std::fmt::Debug for OpenPaths {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("OpenParams")
+        f.debug_struct("OpenPaths")
             .field("paths", &self.paths)
             .finish()
     }
@@ -2109,7 +2127,7 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
     });
     cx.spawn(|mut cx| async move {
         if let Some(paths) = paths.recv().await.flatten() {
-            cx.update(|cx| cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })));
+            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths, app_state }));
         }
     })
     .detach();

crates/zed/src/main.rs 🔗

@@ -14,7 +14,7 @@ use smol::process::Command;
 use std::{env, fs, path::PathBuf, sync::Arc};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::ResultExt;
-use workspace::{self, AppState, OpenNew, OpenParams, OpenPaths};
+use workspace::{self, AppState, OpenNew, OpenPaths};
 use zed::{
     self,
     assets::Assets,
@@ -158,7 +158,7 @@ fn main() {
         if paths.is_empty() {
             cx.dispatch_global_action(OpenNew(app_state.clone()));
         } else {
-            cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
+            cx.dispatch_global_action(OpenPaths { paths, app_state });
         }
     });
 }

crates/zed/src/zed.rs 🔗

@@ -12,8 +12,9 @@ pub use contacts_panel;
 use contacts_panel::ContactsPanel;
 pub use editor;
 use gpui::{
-    action,
+    actions,
     geometry::vector::vec2f,
+    impl_actions,
     keymap::Binding,
     platform::{WindowBounds, WindowOptions},
     ModelHandle, ViewContext,
@@ -29,10 +30,12 @@ use std::{path::PathBuf, sync::Arc};
 pub use workspace;
 use workspace::{AppState, Workspace, WorkspaceParams};
 
-action!(About);
-action!(Quit);
-action!(OpenSettings);
-action!(AdjustBufferFontSize, f32);
+actions!(zed, [About, Quit, OpenSettings]);
+
+#[derive(Clone)]
+pub struct AdjustBufferFontSize(pub f32);
+
+impl_actions!(zed, [AdjustBufferFontSize]);
 
 const MIN_FONT_SIZE: f32 = 6.0;