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/// Defines and registers unit structs that can be used as actions.
292///
293/// To use more complex data types as actions, use `impl_actions!`
294#[macro_export]
295macro_rules! actions {
296 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
297 $(
298 // Unfortunately rust-analyzer doesn't display the name due to
299 // https://github.com/rust-lang/rust-analyzer/issues/8092
300 #[doc = stringify!($name)]
301 #[doc = "action generated by `gpui::actions!`"]
302 #[derive(::std::clone::Clone,::std::cmp::PartialEq, ::std::default::Default)]
303 pub struct $name;
304
305 gpui::__impl_action!($namespace, $name, $name,
306 fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
307 Ok(Box::new(Self))
308 },
309 fn action_json_schema(
310 _: &mut gpui::private::schemars::r#gen::SchemaGenerator,
311 ) -> Option<gpui::private::schemars::schema::Schema> {
312 None
313 }
314 );
315
316 gpui::register_action!($name);
317 )*
318 };
319}
320
321/// Defines and registers a unit struct that can be used as an actions, with a name that differs
322/// from it's type name.
323///
324/// To use more complex data types as actions, and rename them use `impl_action_as!`
325#[macro_export]
326macro_rules! action_as {
327 ($namespace:path, $name:ident as $visual_name:ident) => {
328 // Unfortunately rust-analyzer doesn't display the name due to
329 // https://github.com/rust-lang/rust-analyzer/issues/8092
330 #[doc = stringify!($name)]
331 #[doc = "action generated by `gpui::action_as!`"]
332 #[derive(
333 ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq,
334 )]
335 pub struct $name;
336
337 gpui::__impl_action!(
338 $namespace,
339 $name,
340 $visual_name,
341 fn build(
342 _: gpui::private::serde_json::Value,
343 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
344 Ok(Box::new(Self))
345 },
346 fn action_json_schema(
347 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
348 ) -> Option<gpui::private::schemars::schema::Schema> {
349 None
350 }
351 );
352
353 gpui::register_action!($name);
354 };
355}
356
357/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
358#[macro_export]
359macro_rules! action_with_deprecated_aliases {
360 ($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
361 // Unfortunately rust-analyzer doesn't display the name due to
362 // https://github.com/rust-lang/rust-analyzer/issues/8092
363 #[doc = stringify!($name)]
364 #[doc = "action, generated by `gpui::action_with_deprecated_aliases!`"]
365 #[derive(
366 ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq,
367 )]
368 pub struct $name;
369
370 gpui::__impl_action!(
371 $namespace,
372 $name,
373 $name,
374 fn build(
375 value: gpui::private::serde_json::Value,
376 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
377 Ok(Box::new(Self))
378 },
379
380 fn action_json_schema(
381 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
382 ) -> Option<gpui::private::schemars::schema::Schema> {
383 None
384 },
385
386 fn deprecated_aliases() -> &'static [&'static str] {
387 &[
388 $($alias),*
389 ]
390 }
391 );
392
393 gpui::register_action!($name);
394 };
395}
396
397/// Registers the action and implements the Action trait for any struct that implements Clone,
398/// Default, PartialEq, serde_deserialize::Deserialize, and schemars::JsonSchema.
399///
400/// Similar to `impl_actions!`, but only handles one struct, and registers some deprecated aliases.
401#[macro_export]
402macro_rules! impl_action_with_deprecated_aliases {
403 ($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
404 gpui::__impl_action!(
405 $namespace,
406 $name,
407 $name,
408 fn build(
409 value: gpui::private::serde_json::Value,
410 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
411 Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
412 },
413
414 fn action_json_schema(
415 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
416 ) -> Option<gpui::private::schemars::schema::Schema> {
417 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
418 generator,
419 ))
420 },
421
422 fn deprecated_aliases() -> &'static [&'static str] {
423 &[
424 $($alias),*
425 ]
426 }
427 );
428
429 gpui::register_action!($name);
430 };
431}
432
433/// Registers the action and implements the Action trait for any struct that implements Clone,
434/// Default, PartialEq, serde_deserialize::Deserialize, and schemars::JsonSchema.
435///
436/// Similar to `actions!`, but accepts structs with fields.
437///
438/// Fields and variants that don't make sense for user configuration should be annotated with
439/// #[serde(skip)].
440#[macro_export]
441macro_rules! impl_actions {
442 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
443 $(
444 gpui::__impl_action!($namespace, $name, $name,
445 fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
446 Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
447 },
448 fn action_json_schema(
449 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
450 ) -> Option<gpui::private::schemars::schema::Schema> {
451 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
452 generator,
453 ))
454 }
455 );
456
457 gpui::register_action!($name);
458 )*
459 };
460}
461
462/// Implements the Action trait for internal action structs that implement Clone, Default,
463/// PartialEq. The purpose of this is to conveniently define values that can be passed in `dyn
464/// Action`.
465///
466/// These actions are internal and so are not registered and do not support deserialization.
467#[macro_export]
468macro_rules! impl_internal_actions {
469 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
470 $(
471 gpui::__impl_action!($namespace, $name, $name,
472 fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
473 gpui::Result::Err(gpui::private::anyhow::anyhow!(
474 concat!(
475 stringify!($namespace),
476 "::",
477 stringify!($visual_name),
478 " is an internal action, so cannot be built from JSON."
479 )))
480 },
481 fn action_json_schema(
482 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
483 ) -> Option<gpui::private::schemars::schema::Schema> {
484 None
485 }
486 );
487 )*
488 };
489}
490
491/// Implements the Action trait for a struct that implements Clone, Default, PartialEq, and
492/// serde_deserialize::Deserialize. Allows you to rename the action visually, without changing the
493/// struct's name.
494///
495/// Fields and variants that don't make sense for user configuration should be annotated with
496/// #[serde(skip)].
497#[macro_export]
498macro_rules! impl_action_as {
499 ($namespace:path, $name:ident as $visual_name:tt ) => {
500 gpui::__impl_action!(
501 $namespace,
502 $name,
503 $visual_name,
504 fn build(
505 value: gpui::private::serde_json::Value,
506 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
507 Ok(std::boxed::Box::new(
508 gpui::private::serde_json::from_value::<Self>(value)?,
509 ))
510 },
511 fn action_json_schema(
512 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
513 ) -> Option<gpui::private::schemars::schema::Schema> {
514 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
515 generator,
516 ))
517 }
518 );
519
520 gpui::register_action!($name);
521 };
522}
523
524#[doc(hidden)]
525#[macro_export]
526macro_rules! __impl_action {
527 ($namespace:path, $name:ident, $visual_name:tt, $($items:item),*) => {
528 impl gpui::Action for $name {
529 fn name(&self) -> &'static str
530 {
531 concat!(
532 stringify!($namespace),
533 "::",
534 stringify!($visual_name),
535 )
536 }
537
538 fn debug_name() -> &'static str
539 where
540 Self: ::std::marker::Sized
541 {
542 concat!(
543 stringify!($namespace),
544 "::",
545 stringify!($visual_name),
546 )
547 }
548
549 fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
550 action
551 .as_any()
552 .downcast_ref::<Self>()
553 .map_or(false, |a| self == a)
554 }
555
556 fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
557 ::std::boxed::Box::new(self.clone())
558 }
559
560
561 $($items)*
562 }
563 };
564}
565
566mod no_action {
567 use crate as gpui;
568 use std::any::Any as _;
569
570 actions!(zed, [NoAction]);
571
572 /// Returns whether or not this action represents a removed key binding.
573 pub fn is_no_action(action: &dyn gpui::Action) -> bool {
574 action.as_any().type_id() == (NoAction {}).type_id()
575 }
576}