derive_action.rs

  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}