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