aboutsummaryrefslogtreecommitdiff
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
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.
-rw-r--r--CHANGELOG.md18
-rw-r--r--mingling_core/src/comp/shell_ctx.rs8
-rw-r--r--mingling_core/src/comp/suggest.rs13
-rw-r--r--mingling_macros/src/attr/completion.rs82
-rw-r--r--mingling_macros/src/lib.rs41
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<Suggest>`.** Previously the function had to return `Suggest` exactly. Now any type implementing `Into<Suggest>` is valid — `Suggest` itself, `Vec<String>`, `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<T> for Suggest where T: IntoIterator` was widened from `T::Item: Into<String>` to `T::Item: Into<SuggestItem>`, 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<Vec<String>> 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<T> From<T> for Suggest
where
T: IntoIterator,
- T::Item: Into<String>,
+ T::Item: Into<SuggestItem>,
{
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<String> 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<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]