aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-17 06:57:09 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-17 06:57:09 +0800
commitc6ab865d5b19e3a57b76562adb9540cbf03d77c4 (patch)
treeb501bbc4cd2df84029e2eb875dd3f054c32b55df /mingling_macros/src
parent14374ac30f61085d80d59ad76c8ab1392acc76e7 (diff)
feat(macros): relax completion attribute signature
Allow completion functions to omit the context parameter, accept owned ShellContext or any From<&ShellContext> type, and return any type implementing Into<Suggest>. Add From<&str> for SuggestItem and derive Clone for ShellContext.
Diffstat (limited to 'mingling_macros/src')
-rw-r--r--mingling_macros/src/attr/completion.rs82
-rw-r--r--mingling_macros/src/lib.rs41
2 files changed, 90 insertions, 33 deletions
diff --git a/mingling_macros/src/attr/completion.rs b/mingling_macros/src/attr/completion.rs
index 6d94685..cbc0fef 100644
--- a/mingling_macros/src/attr/completion.rs
+++ b/mingling_macros/src/attr/completion.rs
@@ -34,33 +34,52 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
let inputs = &sig.inputs;
let output = &sig.output;
- if inputs.is_empty() {
- return syn::Error::new(
- inputs.span(),
- "Completion function must have at least one parameter: `ctx: &ShellContext`",
- )
- .to_compile_error()
- .into();
- }
-
- let first_arg = &inputs[0];
- let _ctx_type = match first_arg {
- FnArg::Typed(PatType { ty, .. }) => (**ty).clone(),
- FnArg::Receiver(_) => {
+ // The first parameter (if any) is the completion context. It may be
+ // `&ShellContext`, an owned `ShellContext`, or any other type that
+ // implements `From<&ShellContext>`. With no parameters, the completion
+ // function simply ignores the shell context.
+ let ctx_ty: Option<Type> = match inputs.first() {
+ None => None,
+ Some(FnArg::Typed(PatType { ty, .. })) => Some((**ty).clone()),
+ Some(FnArg::Receiver(_)) => {
return syn::Error::new(
- first_arg.span(),
+ inputs.span(),
"Completion function cannot have self parameter",
)
.to_compile_error()
.into();
}
};
- let fixed_ctx: Pat = syn::parse_quote!(ctx);
- let resources = match extract_resources_from_args(sig, 1) {
+ // Resource injection starts after the context parameter.
+ let resource_skip = usize::from(ctx_ty.is_some());
+ let resources = match extract_resources_from_args(sig, resource_skip) {
Ok(r) => r,
Err(e) => return e.to_compile_error().into(),
};
+ if ctx_ty.is_none() && !resources.is_empty() {
+ return syn::Error::new(
+ inputs.span(),
+ "A completion function without a context parameter cannot inject resources",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Bind the shell context to the declared parameter type (identity `From`
+ // covers `&ShellContext` itself).
+ let (ctx_bind_stmt, ctx_call_arg) = ctx_ty.as_ref().map_or_else(
+ || (quote! { let _ = ctx; }, quote! {}),
+ |ty| {
+ (
+ quote! {
+ let __ctx: #ty =
+ <#ty as ::std::convert::From<&::mingling::ShellContext>>::from(ctx);
+ },
+ quote! { __ctx },
+ )
+ },
+ );
let fn_body = &input_fn.block;
@@ -91,9 +110,11 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
.collect();
let fn_call = if has_resources {
- quote! { #fn_name(#fixed_ctx, #(#resource_args),*) }
+ quote! { #fn_name(#ctx_call_arg, #(#resource_args),*) }
+ } else if ctx_ty.is_some() {
+ quote! { #fn_name(#ctx_call_arg) }
} else {
- quote! { #fn_name(#fixed_ctx) }
+ quote! { #fn_name() }
};
let inner_call = if mut_resources.is_empty() {
@@ -121,6 +142,26 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
quote! { #inner_call }
};
+ // A `()` return (or no return type) means "no suggestions": map it to an
+ // empty `Suggest` instead of requiring `Into<Suggest>`.
+ let returns_unit = match &sig.output {
+ syn::ReturnType::Default => true,
+ syn::ReturnType::Type(_, ty) => {
+ matches!(ty.as_ref(), syn::Type::Tuple(t) if t.elems.is_empty())
+ }
+ };
+ let return_stmt = if returns_unit {
+ quote! {
+ { #comp_body };
+ ::mingling::Suggest::new()
+ }
+ } else {
+ quote! {
+ let __completion_result = { #comp_body };
+ ::std::convert::Into::into(__completion_result)
+ }
+ };
+
let expanded: proc_macro2::TokenStream = quote! {
#(#fn_attrs)*
#[doc(hidden)]
@@ -130,8 +171,9 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
impl ::mingling::Completion for #struct_name {
type Previous = #previous_type_path;
- fn comp(#fixed_ctx: &::mingling::ShellContext) #output {
- #comp_body
+ fn comp(ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
+ #ctx_bind_stmt
+ #return_stmt
}
}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index d0556dc..2271e21 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -691,32 +691,47 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// # Syntax
///
+/// The completion function accepts a relaxed signature:
+///
+/// - **Context parameter (optional):** the first parameter may be `&ShellContext`,
+/// an owned `ShellContext`, or any type implementing `From<&ShellContext>`.
+/// With no parameters at all, the shell context is ignored.
+/// - **Return type:** anything implementing `Into<Suggest>`, e.g. `Suggest`,
+/// `Vec<String>`, `Vec<(String, String)>` (suggestion + description), or a
+/// set of [`SuggestItem`](https://docs.rs/mingling/latest/mingling/struct.SuggestItem.html)s.
+/// - **Resource injection:** remaining parameters are injected resources
+/// (only when a context parameter is present).
+///
/// ```rust,ignore
+/// // No context, return simple suggestions
/// #[completion(EntryType)]
-/// fn complete_my_entry(ctx: &ShellContext) -> Suggest {
-/// // Return suggestions based on current input state...
-/// }
+/// fn complete_static() -> Vec<String> { vec!["a", "b"].into_iter().map(str::to_string).collect() }
+///
+/// // Owned context (via `From<&ShellContext>`), suggestions with descriptions
+/// #[completion(EntryType)]
+/// fn complete_owned(ctx: ShellContext) -> Vec<(String, String)> { /* ... */ }
+///
+/// // Borrowed context (classic form)
+/// #[completion(EntryType)]
+/// fn complete_borrowed(ctx: &ShellContext) -> Suggest { /* ... */ }
/// ```
///
/// # Example
///
/// ```rust,ignore
-/// use mingling::macros::{completion, suggest, suggest_enum};
+/// use mingling::macros::{completion, suggest};
/// use mingling::{ShellContext, Suggest};
///
/// #[completion(MyEntry)]
/// fn complete_my_command(ctx: &ShellContext) -> Suggest {
-/// if ctx.filling_argument_first("--name") {
+/// if ctx.previous_word == "--type" {
/// return suggest!();
/// }
-/// if ctx.filling_argument_first("--type") {
-/// return suggest_enum!(MyEnum);
-/// }
-/// if ctx.typing_argument() {
+/// if ctx.current_word.starts_with('-') {
/// return suggest! {
/// "--name": "Provide a name",
-/// "--type": "Select a type"
-/// }.strip_typed_argument(ctx);
+/// "--type": "Select a type",
+/// };
/// }
/// suggest!()
/// }
@@ -725,8 +740,8 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Requirements
///
/// - The `comp` feature must be enabled.
-/// - The function must have exactly one parameter of type `&ShellContext`.
-/// - The function must return `Suggest`.
+/// - The first parameter (if any) must implement `From<&ShellContext>`.
+/// - The return type must implement `Into<Suggest>`.
/// - The function cannot be async.
#[cfg(feature = "comp")]
#[proc_macro_attribute]