diff options
Diffstat (limited to 'mingling_macros/src')
| -rw-r--r-- | mingling_macros/src/completion.rs | 175 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 16 | ||||
| -rw-r--r-- | mingling_macros/src/suggest.rs | 72 |
3 files changed, 263 insertions, 0 deletions
diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/completion.rs new file mode 100644 index 0000000..cf66f13 --- /dev/null +++ b/mingling_macros/src/completion.rs @@ -0,0 +1,175 @@ +//! Completion Attribute Macro Implementation +//! +//! This module provides the `#[completion]` attribute macro for automatically +//! generating structs that implement the `Completion` trait from functions. + +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{ + FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, TypePath, parse_macro_input, +}; + +/// Extracts the previous type from function arguments +fn extract_previous_type(sig: &Signature) -> syn::Result<TypePath> { + // The function should have exactly one parameter: ShellContext + if sig.inputs.len() != 1 { + return Err(syn::Error::new( + sig.inputs.span(), + "Completion function must have exactly one parameter (ShellContext)", + )); + } + + let arg = &sig.inputs[0]; + match arg { + FnArg::Typed(PatType { ty, .. }) => { + match &**ty { + Type::Path(type_path) => { + // Check if it's ShellContext + let last_segment = type_path.path.segments.last().unwrap(); + if last_segment.ident != "ShellContext" { + return Err(syn::Error::new( + ty.span(), + "Parameter type must be ShellContext", + )); + } + Ok(type_path.clone()) + } + _ => Err(syn::Error::new( + ty.span(), + "Parameter type must be a type path", + )), + } + } + FnArg::Receiver(_) => Err(syn::Error::new( + arg.span(), + "Completion function cannot have self parameter", + )), + } +} + +/// Extracts the return type from the function signature +fn extract_return_type(sig: &Signature) -> syn::Result<TypePath> { + match &sig.output { + ReturnType::Type(_, ty) => match &**ty { + Type::Path(type_path) => { + // Check if it's Suggest + let last_segment = type_path.path.segments.last().unwrap(); + if last_segment.ident != "Suggest" { + return Err(syn::Error::new(ty.span(), "Return type must be Suggest")); + } + Ok(type_path.clone()) + } + _ => Err(syn::Error::new( + ty.span(), + "Return type must be a type path", + )), + }, + ReturnType::Default => Err(syn::Error::new( + sig.span(), + "Completion function must have a return type", + )), + } +} + +/// Extracts the parameter name from function arguments +fn extract_param_name(sig: &Signature) -> syn::Result<Pat> { + if sig.inputs.len() != 1 { + return Err(syn::Error::new( + sig.inputs.span(), + "Completion function must have exactly one parameter", + )); + } + + let arg = &sig.inputs[0]; + match arg { + FnArg::Typed(PatType { pat, .. }) => Ok((**pat).clone()), + FnArg::Receiver(_) => Err(syn::Error::new( + arg.span(), + "Completion function cannot have self parameter", + )), + } +} + +#[cfg(feature = "comp")] +pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the attribute arguments (e.g., HelloEntry from #[completion(HelloEntry)]) + let previous_type_ident = if attr.is_empty() { + return syn::Error::new( + proc_macro2::Span::call_site(), + "completion attribute requires a previous type argument, e.g. #[completion(HelloEntry)]", + ) + .to_compile_error() + .into(); + } else { + parse_macro_input!(attr as Ident) + }; + + // Parse the function item + let input_fn = parse_macro_input!(item as ItemFn); + + // Validate the function is not async + if input_fn.sig.asyncness.is_some() { + return syn::Error::new(input_fn.sig.span(), "Completion function cannot be async") + .to_compile_error() + .into(); + } + + // Extract the parameter name + let param_name = match extract_param_name(&input_fn.sig) { + Ok(name) => name, + Err(e) => return e.to_compile_error().into(), + }; + + // Extract and validate the parameter type (must be ShellContext) + if let Err(e) = extract_previous_type(&input_fn.sig) { + return e.to_compile_error().into(); + } + + // Extract and validate the return type (must be Suggest) + if let Err(e) = extract_return_type(&input_fn.sig) { + return e.to_compile_error().into(); + } + + // Get the function body + let fn_body = &input_fn.block; + + // Get function attributes (excluding the completion attribute) + let mut fn_attrs = input_fn.attrs.clone(); + fn_attrs.retain(|attr| !attr.path().is_ident("completion")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + let fn_name = &input_fn.sig.ident; + + // Generate struct name from function name using pascal_case + let pascal_case_name = just_fmt::pascal_case!(fn_name.to_string()); + let struct_name = Ident::new(&pascal_case_name, fn_name.span()); + + // Generate the struct and implementation + let expanded = quote! { + #(#fn_attrs)* + #vis struct #struct_name; + + impl ::mingling::Completion for #struct_name { + type Previous = #previous_type_ident; + + fn comp(#param_name: ::mingling::ShellContext) -> ::mingling::Suggest { + // This is just to prevent warnings about imported ShellContext and Suggest + let _ = ShellContext::default(); + let _ = Suggest::file_comp(); + #fn_body + } + } + + // Keep the original function for internal use + #(#fn_attrs)* + #vis fn #fn_name(#param_name: ::mingling::ShellContext) -> ::mingling::Suggest { + #fn_body + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 4341669..ff43482 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -9,6 +9,8 @@ use quote::quote; use syn::parse_macro_input; mod chain; +#[cfg(feature = "comp")] +mod completion; mod dispatcher_chain; mod groupped; mod node; @@ -16,6 +18,8 @@ mod pack; mod program_setup; mod render; mod renderer; +#[cfg(feature = "comp")] +mod suggest; use once_cell::sync::Lazy; use std::sync::Mutex; @@ -70,6 +74,12 @@ pub fn renderer(_attr: TokenStream, item: TokenStream) -> TokenStream { renderer::renderer_attr(item) } +#[cfg(feature = "comp")] +#[proc_macro_attribute] +pub fn completion(attr: TokenStream, item: TokenStream) -> TokenStream { + completion::completion_attr(attr, item) +} + #[proc_macro_attribute] pub fn program_setup(attr: TokenStream, item: TokenStream) -> TokenStream { program_setup::setup_attr(attr, item) @@ -243,3 +253,9 @@ pub fn __register_renderer(input: TokenStream) -> TokenStream { TokenStream::new() } + +#[cfg(feature = "comp")] +#[proc_macro] +pub fn suggest(input: TokenStream) -> TokenStream { + suggest::suggest(input) +} diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/suggest.rs new file mode 100644 index 0000000..886eee0 --- /dev/null +++ b/mingling_macros/src/suggest.rs @@ -0,0 +1,72 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Expr, LitStr, Token, parse_macro_input}; + +struct SuggestInput { + items: Punctuated<SuggestItem, Token![,]>, +} + +enum SuggestItem { + WithDesc(Box<(LitStr, Expr)>), // "-i" = "Insert something" + Simple(LitStr), // "-I" +} + +impl Parse for SuggestInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let items = Punctuated::parse_terminated(input)?; + Ok(SuggestInput { items }) + } +} + +impl Parse for SuggestItem { + fn parse(input: ParseStream) -> syn::Result<Self> { + let key: LitStr = input.parse()?; + + if input.peek(Token![:]) { + let _colon: Token![:] = input.parse()?; + let value: Expr = input.parse()?; + Ok(SuggestItem::WithDesc(Box::new((key, value)))) + } else { + Ok(SuggestItem::Simple(key)) + } + } +} + +#[cfg(feature = "comp")] +pub fn suggest(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as SuggestInput); + + let mut items = Vec::new(); + + for item in input.items { + match item { + SuggestItem::WithDesc(boxed) => { + let (key, value) = *boxed; + items.push(quote! { + ::mingling::SuggestItem::new_with_desc(#key.to_string(), #value.to_string()) + }); + } + SuggestItem::Simple(key) => { + items.push(quote! { + ::mingling::SuggestItem::new(#key.to_string()) + }); + } + } + } + + let expanded = if items.is_empty() { + quote! { + ::mingling::Suggest::default() + } + } else { + quote! {{ + let mut suggest = ::mingling::Suggest::default(); + #(suggest.insert(#items);)* + suggest + }} + }; + + expanded.into() +} |
