1use crate::SharedString;
2use anyhow::{Result, anyhow};
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 .ok_or_else(|| anyhow!("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/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
398#[macro_export]
399macro_rules! impl_action_with_deprecated_aliases {
400 ($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
401 gpui::__impl_action!(
402 $namespace,
403 $name,
404 $name,
405 fn build(
406 value: gpui::private::serde_json::Value,
407 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
408 Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
409 },
410
411 fn action_json_schema(
412 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
413 ) -> Option<gpui::private::schemars::schema::Schema> {
414 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
415 generator,
416 ))
417 },
418
419 fn deprecated_aliases() -> &'static [&'static str] {
420 &[
421 $($alias),*
422 ]
423 }
424 );
425
426 gpui::register_action!($name);
427 };
428}
429
430/// Registers the action and implements the Action trait for any struct that implements Clone,
431/// Default, PartialEq, serde_deserialize::Deserialize, and schemars::JsonSchema.
432///
433/// Similar to `actions!`, but accepts structs with fields.
434///
435/// Fields and variants that don't make sense for user configuration should be annotated with
436/// #[serde(skip)].
437#[macro_export]
438macro_rules! impl_actions {
439 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
440 $(
441 gpui::__impl_action!($namespace, $name, $name,
442 fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
443 Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
444 },
445 fn action_json_schema(
446 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
447 ) -> Option<gpui::private::schemars::schema::Schema> {
448 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
449 generator,
450 ))
451 }
452 );
453
454 gpui::register_action!($name);
455 )*
456 };
457}
458
459/// Implements the Action trait for internal action structs that implement Clone, Default,
460/// PartialEq. The purpose of this is to conveniently define values that can be passed in `dyn
461/// Action`.
462///
463/// These actions are internal and so are not registered and do not support deserialization.
464#[macro_export]
465macro_rules! impl_internal_actions {
466 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
467 $(
468 gpui::__impl_action!($namespace, $name, $name,
469 fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
470 gpui::Result::Err(gpui::private::anyhow::anyhow!(
471 concat!(
472 stringify!($namespace),
473 "::",
474 stringify!($visual_name),
475 " is an internal action, so cannot be built from JSON."
476 )))
477 },
478 fn action_json_schema(
479 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
480 ) -> Option<gpui::private::schemars::schema::Schema> {
481 None
482 }
483 );
484 )*
485 };
486}
487
488/// Implements the Action trait for a struct that implements Clone, Default, PartialEq, and
489/// serde_deserialize::Deserialize. Allows you to rename the action visually, without changing the
490/// struct's name.
491///
492/// Fields and variants that don't make sense for user configuration should be annotated with
493/// #[serde(skip)].
494#[macro_export]
495macro_rules! impl_action_as {
496 ($namespace:path, $name:ident as $visual_name:tt ) => {
497 gpui::__impl_action!(
498 $namespace,
499 $name,
500 $visual_name,
501 fn build(
502 value: gpui::private::serde_json::Value,
503 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
504 Ok(std::boxed::Box::new(
505 gpui::private::serde_json::from_value::<Self>(value)?,
506 ))
507 },
508 fn action_json_schema(
509 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
510 ) -> Option<gpui::private::schemars::schema::Schema> {
511 Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
512 generator,
513 ))
514 }
515 );
516
517 gpui::register_action!($name);
518 };
519}
520
521#[doc(hidden)]
522#[macro_export]
523macro_rules! __impl_action {
524 ($namespace:path, $name:ident, $visual_name:tt, $($items:item),*) => {
525 impl gpui::Action for $name {
526 fn name(&self) -> &'static str
527 {
528 concat!(
529 stringify!($namespace),
530 "::",
531 stringify!($visual_name),
532 )
533 }
534
535 fn debug_name() -> &'static str
536 where
537 Self: ::std::marker::Sized
538 {
539 concat!(
540 stringify!($namespace),
541 "::",
542 stringify!($visual_name),
543 )
544 }
545
546 fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
547 action
548 .as_any()
549 .downcast_ref::<Self>()
550 .map_or(false, |a| self == a)
551 }
552
553 fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
554 ::std::boxed::Box::new(self.clone())
555 }
556
557
558 $($items)*
559 }
560 };
561}
562
563mod no_action {
564 use crate as gpui;
565 use std::any::Any as _;
566
567 actions!(zed, [NoAction]);
568
569 /// Returns whether or not this action represents a removed key binding.
570 pub fn is_no_action(action: &dyn gpui::Action) -> bool {
571 action.as_any().type_id() == (NoAction {}).type_id()
572 }
573}