1use crate::register_action::generate_register_action;
2use proc_macro::TokenStream;
3use proc_macro2::Ident;
4use quote::quote;
5use syn::{Data, DeriveInput, LitStr, Token, parse::ParseStream};
6
7pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
8 let input = syn::parse_macro_input!(input as DeriveInput);
9
10 let struct_name = &input.ident;
11 let mut name_argument = None;
12 let mut deprecated_aliases = Vec::new();
13 let mut no_json = false;
14 let mut no_register = false;
15 let mut namespace = None;
16 let mut deprecated = None;
17 let mut doc_str: Option<String> = None;
18
19 for attr in &input.attrs {
20 if attr.path().is_ident("action") {
21 attr.parse_nested_meta(|meta| {
22 if meta.path.is_ident("name") {
23 if name_argument.is_some() {
24 return Err(meta.error("'name' argument specified multiple times"));
25 }
26 meta.input.parse::<Token![=]>()?;
27 let lit: LitStr = meta.input.parse()?;
28 name_argument = Some(lit.value());
29 } else if meta.path.is_ident("namespace") {
30 if namespace.is_some() {
31 return Err(meta.error("'namespace' argument specified multiple times"));
32 }
33 meta.input.parse::<Token![=]>()?;
34 let ident: Ident = meta.input.parse()?;
35 namespace = Some(ident.to_string());
36 } else if meta.path.is_ident("no_json") {
37 if no_json {
38 return Err(meta.error("'no_json' argument specified multiple times"));
39 }
40 no_json = true;
41 } else if meta.path.is_ident("no_register") {
42 if no_register {
43 return Err(meta.error("'no_register' argument specified multiple times"));
44 }
45 no_register = true;
46 } else if meta.path.is_ident("deprecated_aliases") {
47 if !deprecated_aliases.is_empty() {
48 return Err(
49 meta.error("'deprecated_aliases' argument specified multiple times")
50 );
51 }
52 meta.input.parse::<Token![=]>()?;
53 // Parse array of string literals
54 let content;
55 syn::bracketed!(content in meta.input);
56 let aliases = content.parse_terminated(
57 |input: ParseStream| input.parse::<LitStr>(),
58 Token![,],
59 )?;
60 deprecated_aliases.extend(aliases.into_iter().map(|lit| lit.value()));
61 } else if meta.path.is_ident("deprecated") {
62 if deprecated.is_some() {
63 return Err(meta.error("'deprecated' argument specified multiple times"));
64 }
65 meta.input.parse::<Token![=]>()?;
66 let lit: LitStr = meta.input.parse()?;
67 deprecated = Some(lit.value());
68 } else {
69 return Err(meta.error(format!(
70 "'{:?}' argument not recognized, expected \
71 'namespace', 'no_json', 'no_register, 'deprecated_aliases', or 'deprecated'",
72 meta.path
73 )));
74 }
75 Ok(())
76 })
77 .unwrap_or_else(|e| panic!("in #[action] attribute: {}", e));
78 } else if attr.path().is_ident("doc") {
79 use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue};
80 if let Meta::NameValue(MetaNameValue {
81 value:
82 Lit(ExprLit {
83 lit: Str(ref lit_str),
84 ..
85 }),
86 ..
87 }) = attr.meta
88 {
89 let doc = lit_str.value();
90 let doc_str = doc_str.get_or_insert_default();
91 doc_str.push_str(doc.trim());
92 doc_str.push('\n');
93 }
94 }
95 }
96
97 let name = name_argument.unwrap_or_else(|| struct_name.to_string());
98
99 if name.contains("::") {
100 panic!(
101 "in #[action] attribute: `name = \"{name}\"` must not contain `::`, \
102 also specify `namespace` instead"
103 );
104 }
105
106 let full_name = if let Some(namespace) = namespace {
107 format!("{namespace}::{name}")
108 } else {
109 name
110 };
111
112 let is_unit_struct = matches!(&input.data, Data::Struct(data) if data.fields.is_empty());
113
114 let build_fn_body = if no_json {
115 let error_msg = format!("{} cannot be built from JSON", full_name);
116 quote! { Err(gpui::private::anyhow::anyhow!(#error_msg)) }
117 } else if is_unit_struct {
118 quote! { Ok(Box::new(Self)) }
119 } else {
120 quote! { Ok(Box::new(gpui::private::serde_json::from_value::<Self>(_value)?)) }
121 };
122
123 let json_schema_fn_body = if no_json || is_unit_struct {
124 quote! { None }
125 } else {
126 quote! { Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(_generator)) }
127 };
128
129 let deprecated_aliases_fn_body = if deprecated_aliases.is_empty() {
130 quote! { &[] }
131 } else {
132 let aliases = deprecated_aliases.iter();
133 quote! { &[#(#aliases),*] }
134 };
135
136 let deprecation_fn_body = if let Some(message) = deprecated {
137 quote! { Some(#message) }
138 } else {
139 quote! { None }
140 };
141
142 let documentation_fn_body = if let Some(doc) = doc_str {
143 let doc = doc.trim();
144 quote! { Some(#doc) }
145 } else {
146 quote! { None }
147 };
148
149 let registration = if no_register {
150 quote! {}
151 } else {
152 generate_register_action(struct_name)
153 };
154
155 TokenStream::from(quote! {
156 #registration
157
158 impl gpui::Action for #struct_name {
159 fn name(&self) -> &'static str {
160 #full_name
161 }
162
163 fn name_for_type() -> &'static str
164 where
165 Self: Sized
166 {
167 #full_name
168 }
169
170 fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
171 action
172 .as_any()
173 .downcast_ref::<Self>()
174 .map_or(false, |a| self == a)
175 }
176
177 fn boxed_clone(&self) -> Box<dyn gpui::Action> {
178 Box::new(self.clone())
179 }
180
181 fn build(_value: gpui::private::serde_json::Value) -> gpui::Result<Box<dyn gpui::Action>> {
182 #build_fn_body
183 }
184
185 fn action_json_schema(
186 _generator: &mut gpui::private::schemars::SchemaGenerator,
187 ) -> Option<gpui::private::schemars::Schema> {
188 #json_schema_fn_body
189 }
190
191 fn deprecated_aliases() -> &'static [&'static str] {
192 #deprecated_aliases_fn_body
193 }
194
195 fn deprecation_message() -> Option<&'static str> {
196 #deprecation_fn_body
197 }
198
199 fn documentation() -> Option<&'static str> {
200 #documentation_fn_body
201 }
202 }
203 })
204}