1use crate::SharedString;
2use anyhow::{Context as _, Result};
3use collections::HashMap;
4pub use no_action::{NoAction, is_no_action};
5use serde_json::json;
6use std::{
7 any::{Any, TypeId},
8 fmt::Display,
9};
10
11/// Actions are used to implement keyboard-driven UI.
12/// When you declare an action, you can bind keys to the action in the keymap and
13/// listeners for that action in the element tree.
14///
15/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
16/// action for each listed action name in the given namespace.
17/// ```rust
18/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
19/// ```
20/// More complex data types can also be actions, providing they implement Clone, PartialEq,
21/// and serde_derive::Deserialize.
22/// Use `impl_actions!` to automatically implement the action in the given namespace.
23/// ```
24/// #[derive(Clone, PartialEq, serde_derive::Deserialize)]
25/// pub struct SelectNext {
26/// pub replace_newest: bool,
27/// }
28/// impl_actions!(editor, [SelectNext]);
29/// ```
30///
31/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
32/// macro, which only generates the code needed to register your action before `main`.
33///
34/// ```
35/// #[derive(gpui::private::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
36/// pub struct Paste {
37/// pub content: SharedString,
38/// }
39///
40/// impl gpui::Action for Paste {
41/// ///...
42/// }
43/// register_action!(Paste);
44/// ```
45pub trait Action: Any + Send {
46 /// Clone the action into a new box
47 fn boxed_clone(&self) -> Box<dyn Action>;
48
49 /// Do a partial equality check on this action and the other
50 fn partial_eq(&self, action: &dyn Action) -> bool;
51
52 /// Get the name of this action, for displaying in UI
53 fn name(&self) -> &str;
54
55 /// Get the name of this action for debugging
56 fn debug_name() -> &'static str
57 where
58 Self: Sized;
59
60 /// Build this action from a JSON value. This is used to construct actions from the keymap.
61 /// A value of `{}` will be passed for actions that don't have any parameters.
62 fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
63 where
64 Self: Sized;
65
66 /// Optional JSON schema for the action's input data.
67 fn action_json_schema(
68 _: &mut schemars::r#gen::SchemaGenerator,
69 ) -> Option<schemars::schema::Schema>
70 where
71 Self: Sized,
72 {
73 None
74 }
75
76 /// A list of alternate, deprecated names for this action.
77 fn deprecated_aliases() -> &'static [&'static str]
78 where
79 Self: Sized,
80 {
81 &[]
82 }
83}
84
85impl std::fmt::Debug for dyn Action {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("dyn Action")
88 .field("name", &self.name())
89 .finish()
90 }
91}
92
93impl dyn Action {
94 /// Type-erase Action type.
95 pub fn as_any(&self) -> &dyn Any {
96 self as &dyn Any
97 }
98}
99
100/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
101/// markdown to display it.
102#[derive(Debug)]
103pub enum ActionBuildError {
104 /// Indicates that an action with this name has not been registered.
105 NotFound {
106 /// Name of the action that was not found.
107 name: String,
108 },
109 /// Indicates that an error occurred while building the action, typically a JSON deserialization
110 /// error.
111 BuildError {
112 /// Name of the action that was attempting to be built.
113 name: String,
114 /// Error that occurred while building the action.
115 error: anyhow::Error,
116 },
117}
118
119impl std::error::Error for ActionBuildError {
120 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
121 match self {
122 ActionBuildError::NotFound { .. } => None,
123 ActionBuildError::BuildError { error, .. } => error.source(),
124 }
125 }
126}
127
128impl Display for ActionBuildError {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 match self {
131 ActionBuildError::NotFound { name } => {
132 write!(f, "Didn't find an action named \"{name}\"")
133 }
134 ActionBuildError::BuildError { name, error } => {
135 write!(f, "Error while building action \"{name}\": {error}")
136 }
137 }
138 }
139}
140
141type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
142
143pub(crate) struct ActionRegistry {
144 by_name: HashMap<SharedString, ActionData>,
145 names_by_type_id: HashMap<TypeId, SharedString>,
146 all_names: Vec<SharedString>, // So we can return a static slice.
147 deprecations: HashMap<SharedString, SharedString>,
148}
149
150impl Default for ActionRegistry {
151 fn default() -> Self {
152 let mut this = ActionRegistry {
153 by_name: Default::default(),
154 names_by_type_id: Default::default(),
155 all_names: Default::default(),
156 deprecations: Default::default(),
157 };
158
159 this.load_actions();
160
161 this
162 }
163}
164
165struct ActionData {
166 pub build: ActionBuilder,
167 pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
168}
169
170/// This type must be public so that our macros can build it in other crates.
171/// But this is an implementation detail and should not be used directly.
172#[doc(hidden)]
173pub struct MacroActionBuilder(pub fn() -> MacroActionData);
174
175/// This type must be public so that our macros can build it in other crates.
176/// But this is an implementation detail and should not be used directly.
177#[doc(hidden)]
178pub struct MacroActionData {
179 pub name: &'static str,
180 pub aliases: &'static [&'static str],
181 pub type_id: TypeId,
182 pub build: ActionBuilder,
183 pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
184}
185
186inventory::collect!(MacroActionBuilder);
187
188impl ActionRegistry {
189 /// Load all registered actions into the registry.
190 pub(crate) fn load_actions(&mut self) {
191 for builder in inventory::iter::<MacroActionBuilder> {
192 let action = builder.0();
193 self.insert_action(action);
194 }
195 }
196
197 #[cfg(test)]
198 pub(crate) fn load_action<A: Action>(&mut self) {
199 self.insert_action(MacroActionData {
200 name: A::debug_name(),
201 aliases: A::deprecated_aliases(),
202 type_id: TypeId::of::<A>(),
203 build: A::build,
204 json_schema: A::action_json_schema,
205 });
206 }
207
208 fn insert_action(&mut self, action: MacroActionData) {
209 let name: SharedString = action.name.into();
210 self.by_name.insert(
211 name.clone(),
212 ActionData {
213 build: action.build,
214 json_schema: action.json_schema,
215 },
216 );
217 for &alias in action.aliases {
218 let alias: SharedString = alias.into();
219 self.by_name.insert(
220 alias.clone(),
221 ActionData {
222 build: action.build,
223 json_schema: action.json_schema,
224 },
225 );
226 self.deprecations.insert(alias.clone(), name.clone());
227 self.all_names.push(alias);
228 }
229 self.names_by_type_id.insert(action.type_id, name.clone());
230 self.all_names.push(name);
231 }
232
233 /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
234 pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
235 let name = self
236 .names_by_type_id
237 .get(type_id)
238 .with_context(|| format!("no action type registered for {type_id:?}"))?
239 .clone();
240
241 Ok(self.build_action(&name, None)?)
242 }
243
244 /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
245 pub fn build_action(
246 &self,
247 name: &str,
248 params: Option<serde_json::Value>,
249 ) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
250 let build_action = self
251 .by_name
252 .get(name)
253 .ok_or_else(|| ActionBuildError::NotFound {
254 name: name.to_owned(),
255 })?
256 .build;
257 (build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| {
258 ActionBuildError::BuildError {
259 name: name.to_owned(),
260 error: e,
261 }
262 })
263 }
264
265 pub fn all_action_names(&self) -> &[SharedString] {
266 self.all_names.as_slice()
267 }
268
269 pub fn action_schemas(
270 &self,
271 generator: &mut schemars::r#gen::SchemaGenerator,
272 ) -> Vec<(SharedString, Option<schemars::schema::Schema>)> {
273 // Use the order from all_names so that the resulting schema has sensible order.
274 self.all_names
275 .iter()
276 .map(|name| {
277 let action_data = self
278 .by_name
279 .get(name)
280 .expect("All actions in all_names should be registered");
281 (name.clone(), (action_data.json_schema)(generator))
282 })
283 .collect::<Vec<_>>()
284 }
285
286 pub fn action_deprecations(&self) -> &HashMap<SharedString, SharedString> {
287 &self.deprecations
288 }
289}
290
291/// Generate a list of all the registered actions.
292/// Useful for transforming the list of available actions into a
293/// format suited for static analysis such as in validating keymaps, or
294/// generating documentation.
295pub fn generate_list_of_all_registered_actions() -> Vec<MacroActionData> {
296 let mut actions = Vec::new();
297 for builder in inventory::iter::<MacroActionBuilder> {
298 actions.push(builder.0());
299 }
300 actions
301}
302
303/// Defines and registers unit structs that can be used as actions.
304///
305/// To use more complex data types as actions, use `impl_actions!`
306#[macro_export]
307macro_rules! actions {
308 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
309 $(
310 // Unfortunately rust-analyzer doesn't display the name due to
311 // https://github.com/rust-lang/rust-analyzer/issues/8092
312 #[doc = stringify!($name)]
313 #[doc = "action generated by `gpui::actions!`"]
314 #[derive(::std::clone::Clone,::std::cmp::PartialEq, ::std::default::Default)]
315 pub struct $name;
316
317 gpui::__impl_action!($namespace, $name, $name,
318 fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
319 Ok(Box::new(Self))
320 },
321 fn action_json_schema(
322 _: &mut gpui::private::schemars::r#gen::SchemaGenerator,
323 ) -> Option<gpui::private::schemars::schema::Schema> {
324 None
325 }
326 );
327
328 gpui::register_action!($name);
329 )*
330 };
331}
332
333/// Defines and registers a unit struct that can be used as an actions, with a name that differs
334/// from it's type name.
335///
336/// To use more complex data types as actions, and rename them use `impl_action_as!`
337#[macro_export]
338macro_rules! action_as {
339 ($namespace:path, $name:ident as $visual_name:ident) => {
340 // Unfortunately rust-analyzer doesn't display the name due to
341 // https://github.com/rust-lang/rust-analyzer/issues/8092
342 #[doc = stringify!($name)]
343 #[doc = "action generated by `gpui::action_as!`"]
344 #[derive(
345 ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq,
346 )]
347 pub struct $name;
348 gpui::__impl_action!(
349 $namespace,
350 $name,
351 $visual_name,
352 fn build(
353 _: gpui::private::serde_json::Value,
354 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
355 Ok(Box::new(Self))
356 },
357 fn action_json_schema(
358 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
359 ) -> Option<gpui::private::schemars::schema::Schema> {
360 None
361 }
362 );
363
364 gpui::register_action!($name);
365 };
366}
367
368/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
369#[macro_export]
370macro_rules! action_with_deprecated_aliases {
371 ($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
372 // Unfortunately rust-analyzer doesn't display the name due to
373 // https://github.com/rust-lang/rust-analyzer/issues/8092
374 #[doc = stringify!($name)]
375 #[doc = "action, generated by `gpui::action_with_deprecated_aliases!`"]
376 #[derive(
377 ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq,
378 )]
379 pub struct $name;
380
381 gpui::__impl_action!(
382 $namespace,
383 $name,
384 $name,
385 fn build(
386 value: gpui::private::serde_json::Value,
387 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
388 Ok(Box::new(Self))
389 },
390
391 fn action_json_schema(
392 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
393 ) -> Option<gpui::private::schemars::schema::Schema> {
394 None
395 },
396
397 fn deprecated_aliases() -> &'static [&'static str] {
398 &[
399 $($alias),*
400 ]
401 }
402 );
403
404 gpui::register_action!($name);
405 };
406}
407
408/// Registers the action and implements the Action trait for any struct that implements Clone,
409/// Default, PartialEq, serde_deserialize::Deserialize, and schemars::JsonSchema.
410///
411/// Similar to `impl_actions!`, but only handles one struct, and registers some deprecated aliases.
412#[macro_export]
413macro_rules! impl_action_with_deprecated_aliases {
414 ($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
415 gpui::__impl_action!(
416 $namespace,
417 $name,
418 $name,
419 fn build(
420 value: gpui::private::serde_json::Value,
421 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
422 Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
423 },
424
425 fn action_json_schema(
426 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
427 ) -> Option<gpui::private::schemars::schema::Schema> {
428 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
429 generator,
430 ))
431 },
432
433 fn deprecated_aliases() -> &'static [&'static str] {
434 &[
435 $($alias),*
436 ]
437 }
438 );
439
440 gpui::register_action!($name);
441 };
442}
443
444/// Registers the action and implements the Action trait for any struct that implements Clone,
445/// Default, PartialEq, serde_deserialize::Deserialize, and schemars::JsonSchema.
446///
447/// Similar to `actions!`, but accepts structs with fields.
448///
449/// Fields and variants that don't make sense for user configuration should be annotated with
450/// #[serde(skip)].
451#[macro_export]
452macro_rules! impl_actions {
453 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
454 $(
455 gpui::__impl_action!($namespace, $name, $name,
456 fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
457 Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
458 },
459 fn action_json_schema(
460 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
461 ) -> Option<gpui::private::schemars::schema::Schema> {
462 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
463 generator,
464 ))
465 }
466 );
467
468 gpui::register_action!($name);
469 )*
470 };
471}
472
473/// Implements the Action trait for internal action structs that implement Clone, Default,
474/// PartialEq. The purpose of this is to conveniently define values that can be passed in `dyn
475/// Action`.
476///
477/// These actions are internal and so are not registered and do not support deserialization.
478#[macro_export]
479macro_rules! impl_internal_actions {
480 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
481 $(
482 gpui::__impl_action!($namespace, $name, $name,
483 fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
484 gpui::Result::Err(gpui::private::anyhow::anyhow!(
485 concat!(
486 stringify!($namespace),
487 "::",
488 stringify!($visual_name),
489 " is an internal action, so cannot be built from JSON."
490 )))
491 },
492 fn action_json_schema(
493 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
494 ) -> Option<gpui::private::schemars::schema::Schema> {
495 None
496 }
497 );
498 )*
499 };
500}
501
502/// Implements the Action trait for a struct that implements Clone, Default, PartialEq, and
503/// serde_deserialize::Deserialize. Allows you to rename the action visually, without changing the
504/// struct's name.
505///
506/// Fields and variants that don't make sense for user configuration should be annotated with
507/// #[serde(skip)].
508#[macro_export]
509macro_rules! impl_action_as {
510 ($namespace:path, $name:ident as $visual_name:tt ) => {
511 gpui::__impl_action!(
512 $namespace,
513 $name,
514 $visual_name,
515 fn build(
516 value: gpui::private::serde_json::Value,
517 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
518 Ok(std::boxed::Box::new(
519 gpui::private::serde_json::from_value::<Self>(value)?,
520 ))
521 },
522 fn action_json_schema(
523 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
524 ) -> Option<gpui::private::schemars::schema::Schema> {
525 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
526 generator,
527 ))
528 }
529 );
530
531 gpui::register_action!($name);
532 };
533}
534
535#[doc(hidden)]
536#[macro_export]
537macro_rules! __impl_action {
538 ($namespace:path, $name:ident, $visual_name:tt, $($items:item),*) => {
539 impl gpui::Action for $name {
540 fn name(&self) -> &'static str
541 {
542 concat!(
543 stringify!($namespace),
544 "::",
545 stringify!($visual_name),
546 )
547 }
548
549 fn debug_name() -> &'static str
550 where
551 Self: ::std::marker::Sized
552 {
553 concat!(
554 stringify!($namespace),
555 "::",
556 stringify!($visual_name),
557 )
558 }
559
560 fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
561 action
562 .as_any()
563 .downcast_ref::<Self>()
564 .map_or(false, |a| self == a)
565 }
566
567 fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
568 ::std::boxed::Box::new(self.clone())
569 }
570
571
572 $($items)*
573 }
574 };
575}
576
577mod no_action {
578 use crate as gpui;
579 use std::any::Any as _;
580
581 actions!(zed, [NoAction]);
582
583 /// Returns whether or not this action represents a removed key binding.
584 pub fn is_no_action(action: &dyn gpui::Action) -> bool {
585 action.as_any().type_id() == (NoAction {}).type_id()
586 }
587}