aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src')
-rw-r--r--mingling_macros/src/completion.rs175
-rw-r--r--mingling_macros/src/lib.rs16
-rw-r--r--mingling_macros/src/suggest.rs72
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()
+}