Actionsβ€½ (#3349)

Mikayla Maki created

This PR re-implements our actions with macros instead of a blanket impl.

Release Notes:

- N/A

Change summary

Cargo.lock                                     |  21 +
crates/collab_ui2/src/collab_titlebar_item.rs  |   6 
crates/command_palette2/src/command_palette.rs |   2 
crates/editor2/src/editor.rs                   |  28 +-
crates/gpui2/Cargo.toml                        |   1 
crates/gpui2/src/action.rs                     | 247 +++++++++----------
crates/gpui2/src/app.rs                        |  27 +
crates/gpui2/src/elements/div.rs               |  10 
crates/gpui2/src/gpui2.rs                      |   2 
crates/gpui2/src/key_dispatch.rs               |  13 
crates/gpui2/src/keymap/keymap.rs              |  30 +-
crates/gpui2/src/window.rs                     |   4 
crates/gpui2/tests/action_macros.rs            |  45 +++
crates/gpui2_macros/Cargo.toml                 |   2 
crates/gpui2_macros/src/action.rs              | 103 +++++--
crates/gpui2_macros/src/gpui2_macros.rs        |   8 
crates/gpui2_macros/src/register_action.rs     |  78 +++++
crates/live_kit_client2/examples/test_app.rs   |   4 
crates/settings2/src/keymap_file.rs            |   4 
crates/terminal_view2/src/terminal_view.rs     |   8 
crates/ui2/src/components/context_menu.rs      |   5 
crates/ui2/src/components/keybinding.rs        |   5 
crates/workspace2/src/pane.rs                  |  10 
crates/workspace2/src/workspace2.rs            |  13 
crates/zed2/src/languages/json.rs              |   2 
crates/zed_actions2/src/lib.rs                 |   7 
26 files changed, 421 insertions(+), 264 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -3797,6 +3797,7 @@ dependencies = [
  "image",
  "itertools 0.10.5",
  "lazy_static",
+ "linkme",
  "log",
  "media",
  "metal",
@@ -4815,6 +4816,26 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linkme"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.0.42"

crates/collab_ui2/src/collab_titlebar_item.rs πŸ”—

@@ -136,14 +136,12 @@ impl Render for CollabTitlebarItem {
                                     .color(Some(TextColor::Muted)),
                             )
                             .tooltip(move |_, cx| {
-                                // todo!() Replace with real action.
-                                #[gpui::action]
-                                struct NoAction {}
                                 cx.build_view(|_| {
                                     Tooltip::new("Recent Branches")
                                         .key_binding(KeyBinding::new(gpui::KeyBinding::new(
                                             "cmd-b",
-                                            NoAction {},
+                                            // todo!() Replace with real action.
+                                            gpui::NoAction,
                                             None,
                                         )))
                                         .meta("Only local branches shown")

crates/command_palette2/src/command_palette.rs πŸ”—

@@ -47,7 +47,7 @@ impl CommandPalette {
             .available_actions()
             .into_iter()
             .filter_map(|action| {
-                let name = action.name();
+                let name = gpui::remove_the_2(action.name());
                 let namespace = name.split("::").next().unwrap_or("malformed action name");
                 if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
                     return None;

crates/editor2/src/editor.rs πŸ”—

@@ -39,7 +39,7 @@ use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
-    action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement,
+    actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
     AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
     EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
     Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
@@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 //     //     .with_soft_wrap(true)
 // }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectNext {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectPrevious {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectAllMatches {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageUp {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageDown {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToEndOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleCodeActions {
     #[serde(default)]
     pub deployed_from_indicator: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCompletion {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCodeAction {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleComments {
     #[serde(default)]
     pub advance_downwards: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct FoldAt {
     pub buffer_row: u32,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct UnfoldAt {
     pub buffer_row: u32,
 }

crates/gpui2/Cargo.toml πŸ”—

@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
 async-task = "4.0.3"
 backtrace = { version = "0.3", optional = true }
 ctor.workspace = true
+linkme = "0.3"
 derive_more.workspace = true
 dhat = { version = "0.3", optional = true }
 env_logger = { version = "0.9", optional = true }

crates/gpui2/src/action.rs πŸ”—

@@ -1,10 +1,12 @@
 use crate::SharedString;
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
-use lazy_static::lazy_static;
-use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
-use serde::Deserialize;
-use std::any::{type_name, Any, TypeId};
+pub use no_action::NoAction;
+use serde_json::json;
+use std::{
+    any::{Any, TypeId},
+    ops::Deref,
+};
 
 /// Actions are used to implement keyboard-driven UI.
 /// When you declare an action, you can bind keys to the action in the keymap and
@@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId};
 /// ```rust
 /// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
 /// ```
-/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
-/// it will automatically
+/// More complex data types can also be actions. If you annotate your type with the action derive macro
+/// it will be implemented and registered automatically.
 /// ```
-/// #[action]
+/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
 /// pub struct SelectNext {
 ///     pub replace_newest: bool,
 /// }
 ///
-/// Any type A that satisfies the following bounds is automatically an action:
-///
-/// ```
-/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
-/// ```
-///
-/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
-/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
-/// generates the code needed to register your action before `main`. Then you'll need to implement all
-/// the traits manually.
+/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
+/// macro, which only generates the code needed to register your action before `main`.
 ///
 /// ```
 /// #[gpui::register_action]
@@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId};
 ///     pub content: SharedString,
 /// }
 ///
-/// impl std::default::Default for Paste {
-///     fn default() -> Self {
-///         Self {
-///             content: SharedString::from("🍝"),
-///         }
-///     }
+/// impl gpui::Action for Paste {
+///      ///...
 /// }
 /// ```
-pub trait Action: std::fmt::Debug + 'static {
-    fn qualified_name() -> SharedString
-    where
-        Self: Sized;
-    fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+pub trait Action: 'static {
+    fn boxed_clone(&self) -> Box<dyn Action>;
+    fn as_any(&self) -> &dyn Any;
+    fn partial_eq(&self, action: &dyn Action) -> bool;
+    fn name(&self) -> &str;
+
+    fn debug_name() -> &'static str
     where
         Self: Sized;
-    fn is_registered() -> bool
+    fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
     where
         Self: Sized;
-
-    fn partial_eq(&self, action: &dyn Action) -> bool;
-    fn boxed_clone(&self) -> Box<dyn Action>;
-    fn as_any(&self) -> &dyn Any;
 }
 
-// Types become actions by satisfying a list of trait bounds.
-impl<A> Action for A
-where
-    A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static,
-{
-    fn qualified_name() -> SharedString {
-        let name = type_name::<A>();
-        let mut separator_matches = name.rmatch_indices("::");
-        separator_matches.next().unwrap();
-        let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
-        // todo!() remove the 2 replacement when migration is done
-        name[name_start_ix..].replace("2::", "::").into()
-    }
-
-    fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
-    where
-        Self: Sized,
-    {
-        let action = if let Some(params) = params {
-            serde_json::from_value(params).context("failed to deserialize action")?
-        } else {
-            Self::default()
-        };
-        Ok(Box::new(action))
-    }
-
-    fn is_registered() -> bool {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&TypeId::of::<A>())
-            .is_some()
-    }
-
-    fn partial_eq(&self, action: &dyn Action) -> bool {
-        action
-            .as_any()
-            .downcast_ref::<Self>()
-            .map_or(false, |a| self == a)
-    }
-
-    fn boxed_clone(&self) -> Box<dyn Action> {
-        Box::new(self.clone())
-    }
-
-    fn as_any(&self) -> &dyn Any {
-        self
+impl std::fmt::Debug for dyn Action {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn Action")
+            .field("type_name", &self.name())
+            .finish()
     }
 }
 
@@ -119,69 +65,93 @@ impl dyn Action {
     pub fn type_id(&self) -> TypeId {
         self.as_any().type_id()
     }
-
-    pub fn name(&self) -> SharedString {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&self.type_id())
-            .expect("type is not a registered action")
-            .clone()
-    }
 }
 
-type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
-
-lazy_static! {
-    static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
-}
+type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
 
-#[derive(Default)]
-struct ActionRegistry {
+pub(crate) struct ActionRegistry {
     builders_by_name: HashMap<SharedString, ActionBuilder>,
     names_by_type_id: HashMap<TypeId, SharedString>,
     all_names: Vec<SharedString>, // So we can return a static slice.
 }
 
-/// Register an action type to allow it to be referenced in keymaps.
-pub fn register_action<A: Action>() {
-    let name = A::qualified_name();
-    let mut lock = ACTION_REGISTRY.write();
-    lock.builders_by_name.insert(name.clone(), A::build);
-    lock.names_by_type_id
-        .insert(TypeId::of::<A>(), name.clone());
-    lock.all_names.push(name);
+impl Default for ActionRegistry {
+    fn default() -> Self {
+        let mut this = ActionRegistry {
+            builders_by_name: Default::default(),
+            names_by_type_id: Default::default(),
+            all_names: Default::default(),
+        };
+
+        this.load_actions();
+
+        this
+    }
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
-    let name = lock
-        .names_by_type_id
-        .get(type_id)
-        .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
-        .clone();
-    drop(lock);
-
-    build_action(&name, None)
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub type MacroActionBuilder = fn() -> ActionData;
+
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub struct ActionData {
+    pub name: &'static str,
+    pub type_id: TypeId,
+    pub build: ActionBuilder,
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
+/// This constant must be public to be accessible from other crates.
+/// But it's existence is an implementation detail and should not be used directly.
+#[doc(hidden)]
+#[linkme::distributed_slice]
+pub static __GPUI_ACTIONS: [MacroActionBuilder];
+
+impl ActionRegistry {
+    /// Load all registered actions into the registry.
+    pub(crate) fn load_actions(&mut self) {
+        for builder in __GPUI_ACTIONS {
+            let action = builder();
+            //todo(remove)
+            let name: SharedString = remove_the_2(action.name).into();
+            self.builders_by_name.insert(name.clone(), action.build);
+            self.names_by_type_id.insert(action.type_id, name.clone());
+            self.all_names.push(name);
+        }
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
+        let name = self
+            .names_by_type_id
+            .get(type_id)
+            .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
+            .clone();
 
-    let build_action = lock
-        .builders_by_name
-        .get(name)
-        .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
-    (build_action)(params)
-}
+        self.build_action(&name, None)
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action(
+        &self,
+        name: &str,
+        params: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        //todo(remove)
+        let name = remove_the_2(name);
+        let build_action = self
+            .builders_by_name
+            .get(name.deref())
+            .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
+        (build_action)(params.unwrap_or_else(|| json!({})))
+            .with_context(|| format!("Attempting to build action {}", name))
+    }
 
-pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
-    let lock = ACTION_REGISTRY.read();
-    RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
-        registry.all_names.as_slice()
-    })
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.all_names.as_slice()
+    }
 }
 
 /// Defines unit structs that can be used as actions.
@@ -191,7 +161,7 @@ macro_rules! actions {
     () => {};
 
     ( $name:ident ) => {
-        #[gpui::action]
+        #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
         pub struct $name;
     };
 
@@ -200,3 +170,20 @@ macro_rules! actions {
         actions!($($rest)*);
     };
 }
+
+//todo!(remove)
+pub fn remove_the_2(action_name: &str) -> String {
+    let mut separator_matches = action_name.rmatch_indices("::");
+    separator_matches.next().unwrap();
+    let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
+    // todo!() remove the 2 replacement when migration is done
+    action_name[name_start_ix..]
+        .replace("2::", "::")
+        .to_string()
+}
+
+mod no_action {
+    use crate as gpui;
+
+    actions!(NoAction);
+}

crates/gpui2/src/app.rs πŸ”—

@@ -14,12 +14,13 @@ use smallvec::SmallVec;
 pub use test_context::*;
 
 use crate::{
-    current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle,
-    AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
-    Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap,
-    LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet,
-    Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
-    Window, WindowContext, WindowHandle, WindowId,
+    current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
+    AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
+    DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
+    ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
+    PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
+    TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
+    WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -182,6 +183,7 @@ pub struct AppContext {
     text_system: Arc<TextSystem>,
     flushing_effects: bool,
     pending_updates: usize,
+    pub(crate) actions: Rc<ActionRegistry>,
     pub(crate) active_drag: Option<AnyDrag>,
     pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
@@ -240,6 +242,7 @@ impl AppContext {
                 platform: platform.clone(),
                 app_metadata,
                 text_system,
+                actions: Rc::new(ActionRegistry::default()),
                 flushing_effects: false,
                 pending_updates: 0,
                 active_drag: None,
@@ -964,6 +967,18 @@ impl AppContext {
     pub fn propagate(&mut self) {
         self.propagate_event = true;
     }
+
+    pub fn build_action(
+        &self,
+        name: &str,
+        data: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        self.actions.build_action(name, data)
+    }
+
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.actions.all_action_names()
+    }
 }
 
 impl Context for AppContext {

crates/gpui2/src/elements/div.rs πŸ”—

@@ -237,11 +237,11 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
         //
         // if we are relying on this side-effect still, removing the debug_assert!
         // likely breaks the command_palette tests.
-        debug_assert!(
-            A::is_registered(),
-            "{:?} is not registered as an action",
-            A::qualified_name()
-        );
+        // debug_assert!(
+        //     A::is_registered(),
+        //     "{:?} is not registered as an action",
+        //     A::qualified_name()
+        // );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |view, action, phase, cx| {

crates/gpui2/src/gpui2.rs πŸ”—

@@ -49,11 +49,13 @@ pub use input::*;
 pub use interactive::*;
 pub use key_dispatch::*;
 pub use keymap::*;
+pub use linkme;
 pub use platform::*;
 use private::Sealed;
 pub use refineable::*;
 pub use scene::*;
 pub use serde;
+pub use serde_derive;
 pub use serde_json;
 pub use smallvec;
 pub use smol::Timer;

crates/gpui2/src/key_dispatch.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
-    build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
-    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+    Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
+    Keystroke, KeystrokeMatcher, WindowContext,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -10,7 +10,6 @@ use std::{
     rc::Rc,
     sync::Arc,
 };
-use util::ResultExt;
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub struct DispatchNodeId(usize);
@@ -22,6 +21,7 @@ pub(crate) struct DispatchTree {
     focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
     keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
     keymap: Arc<Mutex<Keymap>>,
+    action_registry: Rc<ActionRegistry>,
 }
 
 #[derive(Default)]
@@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener {
 }
 
 impl DispatchTree {
-    pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+    pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
         Self {
             node_stack: Vec::new(),
             context_stack: Vec::new(),
@@ -49,6 +49,7 @@ impl DispatchTree {
             focusable_node_ids: HashMap::default(),
             keystroke_matchers: HashMap::default(),
             keymap,
+            action_registry,
         }
     }
 
@@ -153,7 +154,9 @@ impl DispatchTree {
             for node_id in self.dispatch_path(*node) {
                 let node = &self.nodes[node_id.0];
                 for DispatchActionListener { action_type, .. } in &node.action_listeners {
-                    actions.extend(build_action_from_type(action_type).log_err());
+                    // Intentionally silence these errors without logging.
+                    // If an action cannot be built by default, it's not available.
+                    actions.extend(self.action_registry.build_action_type(action_type).ok());
                 }
             }
         }

crates/gpui2/src/keymap/keymap.rs πŸ”—

@@ -1,7 +1,10 @@
-use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
+use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
 use collections::HashSet;
 use smallvec::SmallVec;
-use std::{any::TypeId, collections::HashMap};
+use std::{
+    any::{Any, TypeId},
+    collections::HashMap,
+};
 
 #[derive(Copy, Clone, Eq, PartialEq, Default)]
 pub struct KeymapVersion(usize);
@@ -37,20 +40,19 @@ impl Keymap {
     }
 
     pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
-        // todo!("no action")
-        // let no_action_id = (NoAction {}).id();
+        let no_action_id = &(NoAction {}).type_id();
         let mut new_bindings = Vec::new();
-        let has_new_disabled_keystrokes = false;
+        let mut has_new_disabled_keystrokes = false;
         for binding in bindings {
-            // if binding.action().id() == no_action_id {
-            //     has_new_disabled_keystrokes |= self
-            //         .disabled_keystrokes
-            //         .entry(binding.keystrokes)
-            //         .or_default()
-            //         .insert(binding.context_predicate);
-            // } else {
-            new_bindings.push(binding);
-            // }
+            if binding.action.type_id() == *no_action_id {
+                has_new_disabled_keystrokes |= self
+                    .disabled_keystrokes
+                    .entry(binding.keystrokes)
+                    .or_default()
+                    .insert(binding.context_predicate);
+            } else {
+                new_bindings.push(binding);
+            }
         }
 
         if has_new_disabled_keystrokes {

crates/gpui2/src/window.rs πŸ”—

@@ -311,8 +311,8 @@ impl Window {
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
-            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
-            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
+            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
+            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
             focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             default_prevented: true,

crates/gpui2/tests/action_macros.rs πŸ”—

@@ -0,0 +1,45 @@
+use serde_derive::Deserialize;
+
+#[test]
+fn test_derive() {
+    use gpui2 as gpui;
+
+    #[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
+    struct AnotherTestAction;
+
+    #[gpui2_macros::register_action]
+    #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
+    struct RegisterableAction {}
+
+    impl gpui::Action for RegisterableAction {
+        fn boxed_clone(&self) -> Box<dyn gpui::Action> {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
+            todo!()
+        }
+
+        fn name(&self) -> &str {
+            todo!()
+        }
+
+        fn debug_name() -> &'static str
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+
+        fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+    }
+}

crates/gpui2_macros/Cargo.toml πŸ”—

@@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs"
 proc-macro = true
 
 [dependencies]
-syn = "1.0.72"
+syn = { version = "1.0.72", features = ["full"] }
 quote = "1.0.9"
 proc-macro2 = "1.0.66"

crates/gpui2_macros/src/action.rs πŸ”—

@@ -15,48 +15,81 @@
 
 use proc_macro::TokenStream;
 use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
+
+use crate::register_action::register_action;
+
+pub fn action(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
 
-pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(item as DeriveInput);
     let name = &input.ident;
-    let attrs = input
-        .attrs
-        .into_iter()
-        .filter(|attr| !attr.path.is_ident("action"))
-        .collect::<Vec<_>>();
-
-    let attributes = quote! {
-        #[gpui::register_action]
-        #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
-        #(#attrs)*
+
+    if input.generics.lt_token.is_some() {
+        return Error::new(name.span(), "Actions must be a concrete type")
+            .into_compile_error()
+            .into();
+    }
+
+    let is_unit_struct = match input.data {
+        syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
+        syn::Data::Enum(_) => false,
+        syn::Data::Union(_) => false,
+    };
+
+    let build_impl = if is_unit_struct {
+        quote! {
+            Ok(std::boxed::Box::new(Self {}))
+        }
+    } else {
+        quote! {
+            Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
+        }
     };
-    let visibility = input.vis;
-
-    let output = match input.data {
-        syn::Data::Struct(ref struct_data) => match &struct_data.fields {
-            syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
-                let fields = &struct_data.fields;
-                quote! {
-                    #attributes
-                    #visibility struct #name #fields
-                }
+
+    let register_action = register_action(&name);
+
+    let output = quote! {
+        const _: fn() = || {
+            fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> +  ::std::cmp::PartialEq + ::std::clone::Clone>() {}
+            assert_impl::<#name>();
+        };
+
+        impl gpui::Action for #name {
+            fn name(&self) -> &'static str
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn debug_name() -> &'static str
+            where
+                Self: ::std::marker::Sized
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
+            where
+                Self: ::std::marker::Sized {
+                    #build_impl
             }
-            syn::Fields::Unit => {
-                quote! {
-                    #attributes
-                    #visibility struct #name;
-                }
+
+            fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
+                action
+                    .as_any()
+                    .downcast_ref::<Self>()
+                    .map_or(false, |a| self == a)
             }
-        },
-        syn::Data::Enum(ref enum_data) => {
-            let variants = &enum_data.variants;
-            quote! {
-                #attributes
-                #visibility enum #name { #variants }
+
+            fn boxed_clone(&self) ->  std::boxed::Box<dyn gpui::Action> {
+                ::std::boxed::Box::new(self.clone())
+            }
+
+            fn as_any(&self) -> &dyn ::std::any::Any {
+                self
             }
         }
-        _ => panic!("Expected a struct or an enum."),
+
+        #register_action
     };
 
     TokenStream::from(output)

crates/gpui2_macros/src/gpui2_macros.rs πŸ”—

@@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream {
     style_helpers::style_helpers(args)
 }
 
-#[proc_macro_attribute]
-pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    action::action(attr, item)
+#[proc_macro_derive(Action)]
+pub fn action(input: TokenStream) -> TokenStream {
+    action::action(input)
 }
 
 #[proc_macro_attribute]
 pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    register_action::register_action(attr, item)
+    register_action::register_action_macro(attr, item)
 }
 
 #[proc_macro_derive(Component, attributes(component))]

crates/gpui2_macros/src/register_action.rs πŸ”—

@@ -12,22 +12,76 @@
 //     gpui2::register_action_builder::<Foo>()
 // }
 use proc_macro::TokenStream;
+use proc_macro2::Ident;
 use quote::{format_ident, quote};
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
 
-pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
     let input = parse_macro_input!(item as DeriveInput);
-    let type_name = &input.ident;
-    let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase());
+    let registration = register_action(&input.ident);
 
-    let expanded = quote! {
+    let has_action_derive = input
+        .attrs
+        .iter()
+        .find(|attr| {
+            (|| {
+                let meta = attr.parse_meta().ok()?;
+                meta.path().is_ident("derive").then(|| match meta {
+                    syn::Meta::Path(_) => None,
+                    syn::Meta::NameValue(_) => None,
+                    syn::Meta::List(list) => list
+                        .nested
+                        .iter()
+                        .find(|list| match list {
+                            syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
+                            syn::NestedMeta::Lit(_) => false,
+                        })
+                        .map(|_| true),
+                })?
+            })()
+            .unwrap_or(false)
+        })
+        .is_some();
+
+    if has_action_derive {
+        return Error::new(
+            input.ident.span(),
+            "The Action derive macro has already registered this action",
+        )
+        .into_compile_error()
+        .into();
+    }
+
+    TokenStream::from(quote! {
         #input
-        #[allow(non_snake_case)]
-        #[gpui::ctor]
-        fn #ctor_fn_name() {
-            gpui::register_action::<#type_name>()
-        }
-    };
 
-    TokenStream::from(expanded)
+        #registration
+    })
+}
+
+pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
+    let static_slice_name =
+        format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase());
+
+    let action_builder_fn_name = format_ident!(
+        "__gpui_actions_builder_{}",
+        type_name.to_string().to_lowercase()
+    );
+
+    quote! {
+        #[doc(hidden)]
+        #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
+        #[linkme(crate = gpui::linkme)]
+        static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
+
+        /// This is an auto generated function, do not use.
+        #[doc(hidden)]
+        fn #action_builder_fn_name() -> gpui::ActionData {
+            gpui::ActionData {
+                name: ::std::any::type_name::<#type_name>(),
+                type_id: ::std::any::TypeId::of::<#type_name>(),
+                build: <#type_name as gpui::Action>::build,
+            }
+        }
+    }
 }

crates/live_kit_client2/examples/test_app.rs πŸ”—

@@ -1,7 +1,7 @@
 use std::{sync::Arc, time::Duration};
 
 use futures::StreamExt;
-use gpui::KeyBinding;
+use gpui::{Action, KeyBinding};
 use live_kit_client2::{
     LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
 };
@@ -10,7 +10,7 @@ use log::LevelFilter;
 use serde_derive::Deserialize;
 use simplelog::SimpleLogger;
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
 struct Quit;
 
 fn main() {

crates/settings2/src/keymap_file.rs πŸ”—

@@ -73,9 +73,9 @@ impl KeymapFile {
                                     "Expected first item in array to be a string."
                                 )));
                             };
-                            gpui::build_action(&name, Some(data))
+                            cx.build_action(&name, Some(data))
                         }
-                        Value::String(name) => gpui::build_action(&name, None),
+                        Value::String(name) => cx.build_action(&name, None),
                         Value::Null => Ok(no_action()),
                         _ => {
                             return Some(Err(anyhow!("Expected two-element array, got {action:?}")))

crates/terminal_view2/src/terminal_view.rs πŸ”—

@@ -9,7 +9,7 @@ pub mod terminal_panel;
 // use crate::terminal_element::TerminalElement;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div,
+    actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
     EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
     InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
     ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
@@ -55,12 +55,10 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 #[derive(Clone, Debug, PartialEq)]
 pub struct ScrollTerminal(pub i32);
 
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
 pub struct SendText(String);
 
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
 pub struct SendKeystroke(String);
 
 actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);

crates/ui2/src/components/context_menu.rs πŸ”—

@@ -84,7 +84,8 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    use gpui::{action, Div, Render};
+    use gpui::{Div, Render};
+    use serde::Deserialize;
 
     pub struct ContextMenuStory;
 
@@ -92,7 +93,7 @@ mod stories {
         type Element = Div<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            #[action]
+            #[derive(PartialEq, Clone, Deserialize, gpui::Action)]
             struct PrintCurrentDate {}
 
             Story::container(cx)

crates/ui2/src/components/keybinding.rs πŸ”—

@@ -81,13 +81,12 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::Story;
-    use gpui::{action, Div, Render};
+    use gpui::{actions, Div, Render};
     use itertools::Itertools;
 
     pub struct KeybindingStory;
 
-    #[action]
-    struct NoAction {}
+    actions!(NoAction);
 
     pub fn binding(key: &str) -> gpui::KeyBinding {
         gpui::KeyBinding::new(key, NoAction {}, None)

crates/workspace2/src/pane.rs πŸ”—

@@ -7,7 +7,7 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
+    actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId,
     EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View,
     ViewContext, VisualContext, WeakView, WindowContext,
 };
@@ -70,15 +70,13 @@ pub struct ActivateItem(pub usize);
 //     pub pane: WeakView<Pane>,
 // }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseAllItems {
     pub save_intent: Option<SaveIntent>,
@@ -1917,7 +1915,7 @@ impl Render for Pane {
             .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
             .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
             .size_full()
-            .on_action(|pane: &mut Self, action, cx| {
+            .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| {
                 pane.close_active_item(action, cx)
                     .map(|task| task.detach_and_log_err(cx));
             })

crates/workspace2/src/workspace2.rs πŸ”—

@@ -29,11 +29,11 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, register_action, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
-    AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model,
-    ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
+    AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
+    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model, ModelContext,
+    ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext,
+    VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -194,8 +194,7 @@ impl Clone for Toast {
     }
 }
 
-#[register_action]
-#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Action)]
 pub struct OpenTerminal {
     pub working_directory: PathBuf,
 }

crates/zed2/src/languages/json.rs πŸ”—

@@ -107,7 +107,7 @@ impl LspAdapter for JsonLspAdapter {
         &self,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
-        let action_names = gpui::all_action_names();
+        let action_names = cx.all_action_names();
         let staff_mode = cx.is_staff();
         let language_names = &self.languages.language_names();
         let settings_schema = cx.global::<SettingsStore>().json_schema(

crates/zed_actions2/src/lib.rs πŸ”—

@@ -1,4 +1,5 @@
-use gpui::action;
+use gpui::Action;
+use serde::Deserialize;
 
 // If the zed binary doesn't use anything in this crate, it will be optimized away
 // and the actions won't initialize. So we just provide an empty initialization function
@@ -9,12 +10,12 @@ use gpui::action;
 // https://github.com/mmastrac/rust-ctor/issues/280
 pub fn init() {}
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenBrowser {
     pub url: String,
 }
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenZedURL {
     pub url: String,
 }