From c6ab865d5b19e3a57b76562adb9540cbf03d77c4 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 17 Aug 2026 06:57:09 +0800 Subject: 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. Add From<&str> for SuggestItem and derive Clone for ShellContext. --- CHANGELOG.md | 18 ++++++++ mingling_core/src/comp/shell_ctx.rs | 8 +++- mingling_core/src/comp/suggest.rs | 13 +++--- mingling_macros/src/attr/completion.rs | 82 +++++++++++++++++++++++++--------- mingling_macros/src/lib.rs | 41 +++++++++++------ 5 files changed, 123 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b1f7d..7bbce12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,24 @@ None The macro is re-exported from `mingling::Wrap` and `mingling::prelude::Wrap` (feature-gated behind `macros`). +2. **[`macros:completion`]** Reworked the `#[completion]` attribute macro to accept a relaxed signature and fixed several code-generation details: + + **Relaxed function signature:** + - **Context parameter is now optional.** Previously, the completion function was required to have exactly one parameter of type `&ShellContext`. Now the first parameter (if present) may be `&ShellContext`, an owned `ShellContext`, or **any type implementing `From<&ShellContext>`**. The macro binds the shell context to the declared parameter type via `<#ty as From<&ShellContext>>::from(ctx)`, so identity `From` covers `&ShellContext` itself and `From<&Self>` covers owned `ShellContext` (a new `impl From<&Self> for ShellContext` added in `mingling_core/src/comp/shell_ctx.rs`). With **no parameters at all**, the completion function simply ignores the shell context. + - **Resource injection after the context.** `extract_resources_from_args` now starts after the context parameter (index 0 when present, index 0 when absent). A completion function with no context parameter cannot inject resources — the macro emits a compile error in that case. + - **Return type is now `Into`.** Previously the function had to return `Suggest` exactly. Now any type implementing `Into` is valid — `Suggest` itself, `Vec`, `Vec<(String, String)>` (suggestion + description), `&[&str]`, or a set of `SuggestItem`s. A `()` return (or no return type) is also accepted and mapped to an empty `Suggest`. + - **`SuggestItem` gains `From<&str>`** (in `mingling_core/src/comp/suggest.rs`), and the blanket `From for Suggest where T: IntoIterator` was widened from `T::Item: Into` to `T::Item: Into`, so iterators of `&str`, `String`, or `SuggestItem` all convert to `Suggest` uniformly. + + **Generated `Completion::comp` signature:** The generated `fn comp` now always returns `::mingling::Suggest` and always binds the ambient `ctx: &ShellContext` parameter (which the caller passes via `Completion::comp(&ctx)`), ignoring it when the user function takes no context. The generated body: + + - Declares `let _ = ctx;` when the function takes no context parameter (keeps the parameter used). + - Declares `let __ctx: #ty = <#ty as From<&ShellContext>>::from(ctx);` when a context parameter is present, then passes `__ctx` as the first argument. + - Wraps the user body; for `()` returns, evaluates the body then returns `Suggest::new()`; otherwise evaluates the body and converts the result via `Into::into`. + + **`ShellContext` is now `Clone`** (derive added in `mingling_core/src/comp/shell_ctx.rs`) and implements `From<&Self> for ShellContext`, enabling owned-context completion signatures. + + _No behavioral change for existing code that already used the classic `fn(ctx: &ShellContext) -> Suggest` form — the identity `From` and `Into` impls preserve that path exactly. + #### **BREAKING CHANGES** (API CHANGES): 1. **[`core:comp`]** **[`macros:dispatch_tree`]** **[BREAKING RENAME]** Renamed the prefix-tree dispatch method `dispatch_args_trie` to `dispatch_args` across the codebase. diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs index 552e514..3a1b8f2 100644 --- a/mingling_core/src/comp/shell_ctx.rs +++ b/mingling_core/src/comp/shell_ctx.rs @@ -96,7 +96,7 @@ use std::collections::HashSet; /// If the `-F` flag is present without a value, `shell_flag` becomes /// `ShellFlag::Other(String::new())`. If `-F` is absent, it defaults to /// `ShellFlag::Other("unknown".to_string())`. -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] #[cfg_attr(feature = "structural_renderer", derive(serde::Serialize))] pub struct ShellContext { /// The full command line @@ -182,6 +182,12 @@ impl TryFrom> for ShellContext { } } +impl From<&Self> for ShellContext { + fn from(ctx: &Self) -> Self { + ctx.clone() + } +} + impl ShellContext { /// Checks if a flag appears exactly once in the command line arguments. /// diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs index 4a26c38..2cbd391 100644 --- a/mingling_core/src/comp/suggest.rs +++ b/mingling_core/src/comp/suggest.rs @@ -321,13 +321,10 @@ impl Suggest { impl From for Suggest where T: IntoIterator, - T::Item: Into, + T::Item: Into, { fn from(items: T) -> Self { - let suggests = items - .into_iter() - .map(|item| SuggestItem::new(item.into())) - .collect(); + let suggests = items.into_iter().map(Into::into).collect(); Self::Suggest(suggests) } } @@ -701,6 +698,12 @@ impl From for SuggestItem { } } +impl From<&str> for SuggestItem { + fn from(suggest: &str) -> Self { + Self::new(suggest.to_string()) + } +} + impl From<(String, String)> for SuggestItem { fn from((suggest, description): (String, String)) -> Self { Self::new_with_desc(suggest, description) 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 = 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`. + 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`, e.g. `Suggest`, +/// `Vec`, `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 { 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`. /// - The function cannot be async. #[cfg(feature = "comp")] #[proc_macro_attribute] -- cgit