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    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}