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}