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