Skip to main content

gpui/
action.rs

1use anyhow::{Context as _, Result};
2use collections::{HashMap, TypeIdHashMap};
3pub use gpui_macros::Action;
4pub use no_action::{NoAction, Unbind, is_no_action, is_unbind};
5use serde_json::json;
6use std::{
7    any::{Any, TypeId},
8    fmt::Display,
9};
10
11/// Declares unit structs that are actions, and registers them with the app.
12///
13/// Each name becomes a struct that implements [`Action`]. Use this for actions that take no data.
14/// For actions with fields, use `#[derive(Action)]` instead.
15///
16/// The structs derive `Clone`, `PartialEq`, `Default` and `Debug`. Attributes written before a
17/// name, such as a doc comment, are passed on to the struct. A doc comment becomes the action's
18/// [`documentation`](Action::documentation).
19///
20/// With a namespace, the action's name is `namespace::Name`; without one, it is `Name`.
21/// Two actions with the same name make app creation panic.
22///
23/// # Examples
24///
25/// ```standalone_crate
26/// use gpui::{Action, actions};
27///
28/// actions!(editor_macro_docs, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
29///
30/// assert_eq!(MoveUp.name(), "editor_macro_docs::MoveUp");
31/// ```
32///
33#[macro_export]
34macro_rules! actions {
35    ($namespace:path, [ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => {
36        $(
37            #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, gpui::Action)]
38            #[action(namespace = $namespace)]
39            $(#[$attr])*
40            pub struct $name;
41        )*
42    };
43    ([ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => {
44        $(
45            #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, gpui::Action)]
46            $(#[$attr])*
47            pub struct $name;
48        )*
49    };
50}
51
52/// An action is a named command that the app can run, such as saving or moving the cursor up.
53///
54/// Declaring an action separates what the app can do from the keys that trigger it. A
55/// [`KeyBinding`](crate::KeyBinding) maps keystrokes to an action, a menu or button can run the
56/// same action, and [`InteractiveElement::on_action`](crate::InteractiveElement::on_action)
57/// handlers receive it. See [keyboard input](crate::_documentation::concepts::input_dispatch) for
58/// the dispatch order.
59///
60/// Declare unit actions with [`actions!`](crate::actions), and actions with data with
61/// `#[derive(Action)]`. The derive macro implements every method of this trait and registers the
62/// action by name, so you rarely write an impl yourself.
63///
64/// # Examples
65///
66/// This action has a field, so the keymap entry carries a value. The handler runs when `ctrl-r`
67/// is typed while the `Counter` element has focus:
68///
69/// ```standalone_crate
70/// use gpui::{Action, Context, FocusHandle, KeyBinding, TestApp, Window, div, prelude::*};
71///
72/// #[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, Action)]
73/// #[action(namespace = action_trait_docs)]
74/// pub struct Repeat {
75///     pub times: usize,
76/// }
77///
78/// struct Counter {
79///     focus_handle: FocusHandle,
80///     total: usize,
81/// }
82///
83/// impl Render for Counter {
84///     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
85///         div()
86///             .key_context("Counter")
87///             .track_focus(&self.focus_handle)
88///             .on_action(cx.listener(|counter, action: &Repeat, _window, _cx| {
89///                 counter.total += action.times;
90///             }))
91///     }
92/// }
93///
94/// let mut app = TestApp::new();
95/// app.update(|cx| cx.bind_keys([KeyBinding::new("ctrl-r", Repeat { times: 2 }, Some("Counter"))]));
96///
97/// let mut window = app.open_window(|window, cx| {
98///     let focus_handle = cx.focus_handle();
99///     window.focus(&focus_handle, cx);
100///     Counter { focus_handle, total: 0 }
101/// });
102///
103/// window.simulate_keystrokes("ctrl-r");
104/// assert_eq!(window.read(|counter, _| counter.total), 2);
105/// ```
106///
107/// # Derive attributes
108///
109/// `#[derive(Action)]` requires `Clone` and `PartialEq`. A struct with fields also requires
110/// `serde::Deserialize` and `schemars::JsonSchema`, which build the action from JSON and describe
111/// its shape. A unit struct, or a type with `no_json`, needs neither.
112///
113/// Options go in `#[action(...)]`, separated by commas:
114///
115/// - `namespace = ident` prefixes the name, so `namespace = editor` on `Save` gives
116///   `editor::Save`. The value is an identifier, not a string.
117/// - `name = "Text"` sets the name without a namespace. The name must not contain `::`.
118/// - `no_json` makes [`Action::build`] always return an error and makes
119///   [`Action::action_json_schema`] return `None`. The type then doesn't need `Deserialize` or
120///   `JsonSchema`.
121/// - `no_register` skips registration. The action can't be invoked by name or built from JSON.
122/// - `deprecated_aliases = ["editor::OldName"]` keeps old names working. Each old name must not be
123///   the name of a registered action.
124/// - `deprecated = "Message"` attaches a deprecation message, which [`Action::deprecation_message`]
125///   returns.
126///
127/// # Manual implementation
128///
129/// To control every method yourself, implement the trait and call
130/// [`register_action!`](crate::register_action!). It registers the action without the rest of the
131/// derive.
132///
133/// ```standalone_crate
134/// use gpui::{SharedString, register_action};
135/// #[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)]
136/// pub struct Paste {
137///     pub content: SharedString,
138/// }
139///
140/// impl gpui::Action for Paste {
141///     # fn boxed_clone(&self) -> Box<dyn gpui::Action> { unimplemented!()}
142///     # fn partial_eq(&self, other: &dyn gpui::Action) -> bool { unimplemented!() }
143///     # fn name(&self) -> &'static str { "Paste" }
144///     # fn name_for_type() -> &'static str { "Paste" }
145///     # fn build(value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>> {
146///     #     unimplemented!()
147///     # }
148/// }
149///
150/// register_action!(Paste);
151/// ```
152pub trait Action: Any + Send {
153    /// Returns a boxed copy of this action.
154    ///
155    /// Keymaps clone their bindings with this, so each binding owns its own action value.
156    fn boxed_clone(&self) -> Box<dyn Action>;
157
158    /// Returns whether `action` is the same action type as `self` and has equal fields.
159    ///
160    /// Returns `false` for an action of a different type.
161    fn partial_eq(&self, action: &dyn Action) -> bool;
162
163    /// Returns the action's full name, such as `editor::MoveUp`.
164    ///
165    /// Keymap files refer to actions by this name, and the name is what [`App::build_action`](crate::App::build_action)
166    /// accepts.
167    fn name(&self) -> &'static str;
168
169    /// Returns the same name as [`name`](Action::name), for the type rather than a value.
170    fn name_for_type() -> &'static str
171    where
172        Self: Sized;
173
174    /// Builds this action from JSON, as a keymap entry supplies it.
175    ///
176    /// `value` is `{}` for an action that takes no input. The derive macro reads `value` with
177    /// `serde`, so the JSON must match the type's fields. Returns an error if it doesn't match, and
178    /// always returns an error for an action declared with `no_json`. To build an action by name,
179    /// use [`App::build_action`](crate::App::build_action).
180    fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
181    where
182        Self: Sized;
183
184    /// Returns the JSON schema of the input this action accepts, or `None` if it takes no input.
185    ///
186    /// The derive macro fills this in from `schemars::JsonSchema`. Schema generation for keymap
187    /// files uses it.
188    fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option<schemars::Schema>
189    where
190        Self: Sized,
191    {
192        None
193    }
194
195    /// Returns the old names that still invoke this action. They are set with the
196    /// `deprecated_aliases` option of `#[action(...)]`.
197    fn deprecated_aliases() -> &'static [&'static str]
198    where
199        Self: Sized,
200    {
201        &[]
202    }
203
204    /// Returns the deprecation message, or `None` if the action isn't deprecated. It is set with
205    /// the `deprecated` option of `#[action(...)]`.
206    fn deprecation_message() -> Option<&'static str>
207    where
208        Self: Sized,
209    {
210        None
211    }
212
213    /// Returns the action's documentation, or `None` if it has none.
214    ///
215    /// The derive macro and [`actions!`](crate::actions) set this from the doc comments on the
216    /// type.
217    fn documentation() -> Option<&'static str>
218    where
219        Self: Sized,
220    {
221        None
222    }
223}
224
225impl std::fmt::Debug for dyn Action {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        f.debug_struct("dyn Action")
228            .field("name", &self.name())
229            .finish()
230    }
231}
232
233impl dyn Action {
234    /// Returns this action as `&dyn Any`, so that you can downcast it to its concrete type with
235    /// `downcast_ref`.
236    pub fn as_any(&self) -> &dyn Any {
237        self as &dyn Any
238    }
239}
240
241/// Error returned by [`App::build_action`](crate::App::build_action) when an action can't be
242/// built by name.
243///
244/// It is a plain error type rather than `anyhow::Error`.
245#[derive(Debug)]
246pub enum ActionBuildError {
247    /// No action with this name is registered.
248    NotFound {
249        /// The name that was not found.
250        name: String,
251    },
252    /// An action with this name exists, but building it failed. This is usually a JSON
253    /// deserialization error, such as a missing field.
254    BuildError {
255        /// The name of the action that failed to build.
256        name: String,
257        /// The error from building the action.
258        error: anyhow::Error,
259    },
260}
261
262impl std::error::Error for ActionBuildError {
263    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
264        match self {
265            ActionBuildError::NotFound { .. } => None,
266            ActionBuildError::BuildError { error, .. } => error.source(),
267        }
268    }
269}
270
271impl Display for ActionBuildError {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self {
274            ActionBuildError::NotFound { name } => {
275                write!(f, "Didn't find an action named \"{name}\"")
276            }
277            ActionBuildError::BuildError { name, error } => {
278                write!(f, "Error while building action \"{name}\": {error}")
279            }
280        }
281    }
282}
283
284type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
285
286/// Every action registered with the app, indexed by name and by type.
287pub(crate) struct ActionRegistry {
288    by_name: HashMap<&'static str, ActionData>,
289    names_by_type_id: TypeIdHashMap<&'static str>,
290    all_names: Vec<&'static str>, // So we can return a static slice.
291    deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name
292    deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message
293    documentation: HashMap<&'static str, &'static str>, // action name -> documentation
294}
295
296impl Default for ActionRegistry {
297    fn default() -> Self {
298        let mut this = ActionRegistry {
299            by_name: Default::default(),
300            names_by_type_id: Default::default(),
301            documentation: Default::default(),
302            all_names: Default::default(),
303            deprecated_aliases: Default::default(),
304            deprecation_messages: Default::default(),
305        };
306
307        this.load_actions();
308
309        this
310    }
311}
312
313struct ActionData {
314    pub build: ActionBuilder,
315    pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
316}
317
318/// This type must be public so that our macros can build it in other crates.
319/// But this is an implementation detail and should not be used directly.
320#[doc(hidden)]
321pub struct MacroActionBuilder(pub fn() -> MacroActionData);
322
323/// This type must be public so that our macros can build it in other crates.
324/// But this is an implementation detail and should not be used directly.
325#[doc(hidden)]
326pub struct MacroActionData {
327    pub name: &'static str,
328    pub type_id: TypeId,
329    pub build: ActionBuilder,
330    pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
331    pub deprecated_aliases: &'static [&'static str],
332    pub deprecation_message: Option<&'static str>,
333    pub documentation: Option<&'static str>,
334}
335
336inventory::collect!(MacroActionBuilder);
337
338impl ActionRegistry {
339    /// Load all registered actions into the registry.
340    pub(crate) fn load_actions(&mut self) {
341        for builder in inventory::iter::<MacroActionBuilder> {
342            let action = builder.0();
343            self.insert_action(action);
344        }
345    }
346
347    fn insert_action(&mut self, action: MacroActionData) {
348        let name = action.name;
349        if self.by_name.contains_key(name) {
350            panic!(
351                "Action with name `{name}` already registered \
352                (might be registered in `#[action(deprecated_aliases = [...])]`."
353            );
354        }
355        self.by_name.insert(
356            name,
357            ActionData {
358                build: action.build,
359                json_schema: action.json_schema,
360            },
361        );
362        for &alias in action.deprecated_aliases {
363            if self.by_name.contains_key(alias) {
364                panic!(
365                    "Action with name `{alias}` already registered. \
366                    `{alias}` is specified in `#[action(deprecated_aliases = [...])]` for action `{name}`."
367                );
368            }
369            self.by_name.insert(
370                alias,
371                ActionData {
372                    build: action.build,
373                    json_schema: action.json_schema,
374                },
375            );
376            self.deprecated_aliases.insert(alias, name);
377            self.all_names.push(alias);
378        }
379        self.names_by_type_id.insert(action.type_id, name);
380        self.all_names.push(name);
381        if let Some(deprecation_msg) = action.deprecation_message {
382            self.deprecation_messages.insert(name, deprecation_msg);
383        }
384        if let Some(documentation) = action.documentation {
385            self.documentation.insert(name, documentation);
386        }
387    }
388
389    /// Builds the action registered for `type_id`, with no input.
390    pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
391        let name = self
392            .names_by_type_id
393            .get(type_id)
394            .with_context(|| format!("no action type registered for {type_id:?}"))?;
395
396        Ok(self.build_action(name, None)?)
397    }
398
399    #[cfg(feature = "profiler")]
400    pub(crate) fn try_resolve_action(&self, type_id: &TypeId) -> Option<&'static str> {
401        self.names_by_type_id.get(type_id).copied()
402    }
403
404    /// Builds the action called `name` from optional JSON `params`, using `{}` when there are none.
405    pub fn build_action(
406        &self,
407        name: &str,
408        params: Option<serde_json::Value>,
409    ) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
410        let build_action = self
411            .by_name
412            .get(name)
413            .ok_or_else(|| ActionBuildError::NotFound {
414                name: name.to_owned(),
415            })?
416            .build;
417        (build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| {
418            ActionBuildError::BuildError {
419                name: name.to_owned(),
420                error: e,
421            }
422        })
423    }
424
425    /// Returns every registered action name, including deprecated aliases.
426    pub fn all_action_names(&self) -> &[&'static str] {
427        self.all_names.as_slice()
428    }
429
430    /// Returns each action name with its JSON schema, in registration order.
431    pub fn action_schemas(
432        &self,
433        generator: &mut schemars::SchemaGenerator,
434    ) -> Vec<(&'static str, Option<schemars::Schema>)> {
435        // Use the order from all_names so that the resulting schema has sensible order.
436        self.all_names
437            .iter()
438            .map(|name| {
439                let action_data = self
440                    .by_name
441                    .get(name)
442                    .expect("All actions in all_names should be registered");
443                (*name, (action_data.json_schema)(generator))
444            })
445            .collect::<Vec<_>>()
446    }
447
448    /// Returns the JSON schema of the action called `name`, or `None` if no action has that name.
449    pub fn action_schema_by_name(
450        &self,
451        name: &str,
452        generator: &mut schemars::SchemaGenerator,
453    ) -> Option<Option<schemars::Schema>> {
454        self.by_name
455            .get(name)
456            .map(|action_data| (action_data.json_schema)(generator))
457    }
458
459    /// Maps each deprecated alias to the action name it replaces.
460    pub fn deprecated_aliases(&self) -> &HashMap<&'static str, &'static str> {
461        &self.deprecated_aliases
462    }
463
464    /// Maps each deprecated action name to its deprecation message.
465    pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> {
466        &self.deprecation_messages
467    }
468
469    /// Maps each action name to its documentation.
470    pub fn documentation(&self) -> &HashMap<&'static str, &'static str> {
471        &self.documentation
472    }
473}
474
475/// Generate a list of all the registered actions.
476/// Useful for transforming the list of available actions into a
477/// format suited for static analysis such as in validating keymaps, or
478/// generating documentation.
479pub fn generate_list_of_all_registered_actions() -> impl Iterator<Item = MacroActionData> {
480    inventory::iter::<MacroActionBuilder>
481        .into_iter()
482        .map(|builder| builder.0())
483}
484
485mod no_action {
486    use crate as gpui;
487    use schemars::JsonSchema;
488    use serde::Deserialize;
489
490    actions!(
491        zed,
492        [
493            /// Disables the lower-precedence bindings for the same keystrokes.
494            ///
495            /// When a `NoAction` binding is the highest-precedence match for some keystrokes, the
496            /// bindings that rank below it for those keystrokes are ignored. Bindings from a
497            /// stronger source, such as user settings over defaults, still apply. Use it to turn
498            /// off a shortcut in one context without touching the binding in other contexts.
499            ///
500            /// See [`Keymap::bindings_for_input`](crate::Keymap::bindings_for_input) for the rule.
501            NoAction
502        ]
503    );
504
505    /// Removes a lower-precedence binding for the same keystrokes that runs the named action.
506    ///
507    /// Give the action's full name, such as `"editor::NewLine"`, as the field. When an `Unbind`
508    /// binding is the highest-precedence match, a later binding for the same keystrokes that runs
509    /// the named action is ignored, whatever context that binding is in. Unlike [`NoAction`], it
510    /// leaves other actions on the same keystrokes alone.
511    ///
512    /// Its input is the target name, such as `"editor::NewLine"`.
513    #[derive(Clone, Debug, PartialEq, Deserialize, JsonSchema, gpui::Action)]
514    #[action(namespace = zed)]
515    pub struct Unbind(pub gpui::SharedString);
516
517    /// Returns whether `action` is a [`NoAction`].
518    pub fn is_no_action(action: &dyn gpui::Action) -> bool {
519        action.as_any().is::<NoAction>()
520    }
521
522    /// Returns whether `action` is an [`Unbind`].
523    pub fn is_unbind(action: &dyn gpui::Action) -> bool {
524        action.as_any().is::<Unbind>()
525    }
526}