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::r#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::r#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 struct MacroActionBuilder(pub 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::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
187}
188
189inventory::collect!(MacroActionBuilder);
190
191impl ActionRegistry {
192 /// Load all registered actions into the registry.
193 pub(crate) fn load_actions(&mut self) {
194 for builder in inventory::iter::<MacroActionBuilder> {
195 let action = builder.0();
196 self.insert_action(action);
197 }
198 }
199
200 #[cfg(test)]
201 pub(crate) fn load_action<A: Action>(&mut self) {
202 self.insert_action(MacroActionData {
203 name: A::debug_name(),
204 aliases: A::deprecated_aliases(),
205 type_id: TypeId::of::<A>(),
206 build: A::build,
207 json_schema: A::action_json_schema,
208 });
209 }
210
211 fn insert_action(&mut self, action: MacroActionData) {
212 let name: SharedString = action.name.into();
213 self.by_name.insert(
214 name.clone(),
215 ActionData {
216 build: action.build,
217 json_schema: action.json_schema,
218 },
219 );
220 for &alias in action.aliases {
221 let alias: SharedString = alias.into();
222 self.by_name.insert(
223 alias.clone(),
224 ActionData {
225 build: action.build,
226 json_schema: action.json_schema,
227 },
228 );
229 self.deprecations.insert(alias.clone(), name.clone());
230 self.all_names.push(alias);
231 }
232 self.names_by_type_id.insert(action.type_id, name.clone());
233 self.all_names.push(name);
234 }
235
236 /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
237 pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
238 let name = self
239 .names_by_type_id
240 .get(type_id)
241 .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
242 .clone();
243
244 Ok(self.build_action(&name, None)?)
245 }
246
247 /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
248 pub fn build_action(
249 &self,
250 name: &str,
251 params: Option<serde_json::Value>,
252 ) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
253 let build_action = self
254 .by_name
255 .get(name)
256 .ok_or_else(|| ActionBuildError::NotFound {
257 name: name.to_owned(),
258 })?
259 .build;
260 (build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| {
261 ActionBuildError::BuildError {
262 name: name.to_owned(),
263 error: e,
264 }
265 })
266 }
267
268 pub fn all_action_names(&self) -> &[SharedString] {
269 self.all_names.as_slice()
270 }
271
272 pub fn action_schemas(
273 &self,
274 generator: &mut schemars::r#gen::SchemaGenerator,
275 ) -> Vec<(SharedString, Option<schemars::schema::Schema>)> {
276 // Use the order from all_names so that the resulting schema has sensible order.
277 self.all_names
278 .iter()
279 .map(|name| {
280 let action_data = self
281 .by_name
282 .get(name)
283 .expect("All actions in all_names should be registered");
284 (name.clone(), (action_data.json_schema)(generator))
285 })
286 .collect::<Vec<_>>()
287 }
288
289 pub fn action_deprecations(&self) -> &HashMap<SharedString, SharedString> {
290 &self.deprecations
291 }
292}
293
294/// Defines and registers unit structs that can be used as actions.
295///
296/// To use more complex data types as actions, use `impl_actions!`
297#[macro_export]
298macro_rules! actions {
299 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
300 $(
301 // Unfortunately rust-analyzer doesn't display the name due to
302 // https://github.com/rust-lang/rust-analyzer/issues/8092
303 #[doc = stringify!($name)]
304 #[doc = "action generated by `gpui::actions!`"]
305 #[derive(::std::clone::Clone,::std::cmp::PartialEq, ::std::default::Default)]
306 pub struct $name;
307
308 gpui::__impl_action!($namespace, $name, $name,
309 fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
310 Ok(Box::new(Self))
311 },
312 fn action_json_schema(
313 _: &mut gpui::private::schemars::r#gen::SchemaGenerator,
314 ) -> Option<gpui::private::schemars::schema::Schema> {
315 None
316 }
317 );
318
319 gpui::register_action!($name);
320 )*
321 };
322}
323
324/// Defines and registers a unit struct that can be used as an actions, with a name that differs
325/// from it's type name.
326///
327/// To use more complex data types as actions, and rename them use `impl_action_as!`
328#[macro_export]
329macro_rules! action_as {
330 ($namespace:path, $name:ident as $visual_name:ident) => {
331 // Unfortunately rust-analyzer doesn't display the name due to
332 // https://github.com/rust-lang/rust-analyzer/issues/8092
333 #[doc = stringify!($name)]
334 #[doc = "action generated by `gpui::action_as!`"]
335 #[derive(
336 ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq,
337 )]
338 pub struct $name;
339
340 gpui::__impl_action!(
341 $namespace,
342 $name,
343 $visual_name,
344 fn build(
345 _: gpui::private::serde_json::Value,
346 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
347 Ok(Box::new(Self))
348 },
349 fn action_json_schema(
350 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
351 ) -> Option<gpui::private::schemars::schema::Schema> {
352 None
353 }
354 );
355
356 gpui::register_action!($name);
357 };
358}
359
360/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
361#[macro_export]
362macro_rules! action_with_deprecated_aliases {
363 ($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
364 // Unfortunately rust-analyzer doesn't display the name due to
365 // https://github.com/rust-lang/rust-analyzer/issues/8092
366 #[doc = stringify!($name)]
367 #[doc = "action, generated by `gpui::action_with_deprecated_aliases!`"]
368 #[derive(
369 ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq,
370 )]
371 pub struct $name;
372
373 gpui::__impl_action!(
374 $namespace,
375 $name,
376 $name,
377 fn build(
378 value: gpui::private::serde_json::Value,
379 ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
380 Ok(Box::new(Self))
381 },
382
383 fn action_json_schema(
384 generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
385 ) -> Option<gpui::private::schemars::schema::Schema> {
386 None
387 },
388
389 fn deprecated_aliases() -> &'static [&'static str] {
390 &[
391 $($alias),*
392 ]
393 }
394 );
395
396 gpui::register_action!($name);
397 };
398}
399
400/// Defines and registers a unit struct that can be used as an action, with 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 fn as_any(&self) -> &dyn ::std::any::Any {
561 self
562 }
563
564 $($items)*
565 }
566 };
567}
568
569mod no_action {
570 use crate as gpui;
571 use std::any::Any as _;
572
573 actions!(zed, [NoAction]);
574
575 /// Returns whether or not this action represents a removed key binding.
576 pub fn is_no_action(action: &dyn gpui::Action) -> bool {
577 action.as_any().type_id() == (NoAction {}).type_id()
578 }
579}