aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-17 07:35:42 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-17 07:35:42 +0800
commit40bb7ffd6954184fac718c8f99c9cdc3e054e4eb (patch)
tree3b152edba07fa561b6e2792fcaa4c3f7141f3b35
parentc6ab865d5b19e3a57b76562adb9540cbf03d77c4 (diff)
feat(macros)!: change completion context parameter to owned type
BREAKING CHANGE: `#[completion]` functions must now take `ShellContext` by value instead of `&ShellContext`. Reference parameters are reserved for resource injection.
-rw-r--r--CHANGELOG.md28
-rw-r--r--GETTING-STARTED.md9
-rw-r--r--docs/_zh_CN/pages/advanced/1-completion.md2
-rw-r--r--docs/pages/advanced/1-completion.md2
-rw-r--r--examples/example-completion/src/main.rs12
-rw-r--r--examples/example-enum-tag/src/main.rs2
-rw-r--r--mingling/src/example_docs.rs14
-rw-r--r--mingling_cli/src/config/cmd_cfg.rs2
-rw-r--r--mingling_cli/src/lib.rs2
-rw-r--r--mingling_cli/src/linter/cmd_explain.rs2
-rw-r--r--mingling_cli/src/linter/cmd_lint.rs2
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_install.rs2
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs2
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs2
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_uninstall.rs2
-rw-r--r--mingling_cli/src/proj_mgr/cmd_class_add.rs2
-rw-r--r--mingling_macros/src/attr/completion.rs232
-rw-r--r--mingling_macros/src/lib.rs40
18 files changed, 178 insertions, 181 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7bbce12..969a3e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -367,6 +367,34 @@ None
_Behavioral note:_ the runtime semantics of pipeline types are unchanged — `#[derive(Grouped, Wrap)]` produces types with the same `Grouped` identity, `Into<AnyOutput>`/`Into<ChainProcess>` routing, `Deref`/`DerefMut`, and `From`/`Into` conversions that `pack!` provided. The removal is purely an API move from magic macros to standard Rust derives, reducing macro surface area and making pipeline types inspectable and composable like any other struct.
+6. **[`macros:completion`]** **[BREAKING]** Changed the `#[completion]` attribute macro's context-parameter semantics: completion functions now take the **owned** `ShellContext` (or any `From<&ShellContext>` type) by value, and `&ShellContext` is no longer accepted.
+
+ ### What changed
+
+ Previously, the completion function's context parameter could be `&ShellContext` (the classic form) or an owned `ShellContext` / any `From<&ShellContext>` type. Now the reference form is rejected: reference parameters (`&T` / `&mut T`) are reserved exclusively for **resource injection**, matching `#[chain]` semantics, so the parser in `mingling_macros/src/attr/completion.rs` was reworked to classify each parameter as either:
+
+ - **Owned (non-reference) parameter** — a _shell source_: derived from `&ShellContext` via `<#ty as From<&ShellContext>>::from(ctx)`. This covers `ShellContext` itself (via its new `Clone`-based `From` impl), framework state types, and any user-defined type derived from the shell context. Multiple owned parameters are allowed; each gets its own derived binding (`__ctx_derived_{idx}`).
+ - **`&T` / `&mut T` reference parameter** — a _resource injection_, identical to the parameter position used by `#[chain]`. Requires a simple-identifier binding. `&ShellContext` specifically is rejected with a compile error: "`&ShellContext` is not supported; use the owned `ShellContext` (or any other type implementing `From<&ShellContext>`) as a value parameter".
+
+ A helper `is_shell_context_path(ty)` detects a path whose last segment is `ShellContext` (covering `ShellContext` and `mingling::ShellContext` alike).
+
+ Previously, resource injection only started _after_ the first (context) parameter, and a completion function with no context parameter could not inject resources (compile error). Now, ownership of the parameter — not its position — determines its role: owned parameters are shell sources, references are resources, and they may be freely interleaved. The "no context → no resources" restriction is gone entirely.
+
+ The generated `Completion::comp` body now emits:
+
+ 1. A derived-binding statement for each owned parameter.
+ 2. The immut-resource binding statements (for `&T` injections).
+ 3. The mut-resource wrapper / call (for `&mut T` injections).
+ 4. The return statement applying the `Into<Suggest>` conversion (`()` → empty `Suggest`).
+
+ **Migration guide:**
+
+ - Change every `ctx: &ShellContext` parameter to `ctx: ShellContext`. The owned type behaves identically for reads; only the declared parameter type changes.
+ - Code that previously relied on `&ShellContext` in the _middle_ of the signature no longer needs special treatment: owned parameters anywhere are treated as shell sources.
+ - `_ctx: &ShellContext` (unused parameter) becomes `_ctx: ShellContext`.
+
+ _All internal call sites, examples, docs, and tests updated_ (e.g., `mingling_cli` completion handlers, `example-completion`, `example-enum-tag`, `GETTING-STARTED.md`, `docs/pages/advanced/1-completion.md`, `docs/_zh_CN/pages/advanced/1-completion.md`, and `mingling/src/example_docs.rs`).
+
---
## Contents
diff --git a/GETTING-STARTED.md b/GETTING-STARTED.md
index 0730136..020e098 100644
--- a/GETTING-STARTED.md
+++ b/GETTING-STARTED.md
@@ -201,7 +201,7 @@ dispatcher!("greet", EntryGreet);
pub struct ResultName((u8, String));
#[completion(EntryGreet)]
-fn complete_greet(ctx: &ShellContext) -> Suggest {
+fn complete_greet(ctx: ShellContext) -> Suggest {
// Suggest positional arguments
if ctx.previous_word == "greet" {
return suggest! {
@@ -212,12 +212,11 @@ fn complete_greet(ctx: &ShellContext) -> Suggest {
}
// Suggest flag arguments
- if ctx.typing_argument() {
+ if ctx.current_word.starts_with('-') {
return suggest! {
"-r": "Number of repetitions",
"--repeat": "Number of repetitions",
- }
- .strip_typed_argument(ctx);
+ };
}
suggest!() // no suggestions
@@ -262,7 +261,7 @@ pub enum ProgrammingLanguages {
}
#[completion(EntryLang)]
-fn complete_lang(_: &ShellContext) -> Suggest {
+fn complete_lang(_: ShellContext) -> Suggest {
suggest_enum!(ProgrammingLanguages)
}
```
diff --git a/docs/_zh_CN/pages/advanced/1-completion.md b/docs/_zh_CN/pages/advanced/1-completion.md
index 28587b3..d70786e 100644
--- a/docs/_zh_CN/pages/advanced/1-completion.md
+++ b/docs/_zh_CN/pages/advanced/1-completion.md
@@ -43,7 +43,7 @@ features = [
@@@dispatcher!("greet", EntryGreet);
#[completion(EntryGreet)]
-fn complete_greet(ctx: &ShellContext) -> Suggest {
+fn complete_greet(ctx: ShellContext) -> Suggest {
if ctx.previous_word == "greet" {
let mut items = BTreeSet::new();
items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into()));
diff --git a/docs/pages/advanced/1-completion.md b/docs/pages/advanced/1-completion.md
index 55b8b1d..20bd59e 100644
--- a/docs/pages/advanced/1-completion.md
+++ b/docs/pages/advanced/1-completion.md
@@ -43,7 +43,7 @@ Use `#[completion(EntryType)]` to define completion logic for an Entry:
@@@dispatcher!("greet", EntryGreet);
#[completion(EntryGreet)]
-fn complete_greet(ctx: &ShellContext) -> Suggest {
+fn complete_greet(ctx: ShellContext) -> Suggest {
if ctx.previous_word == "greet" {
let mut items = BTreeSet::new();
items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into()));
diff --git a/examples/example-completion/src/main.rs b/examples/example-completion/src/main.rs
index e14326f..389cf74 100644
--- a/examples/example-completion/src/main.rs
+++ b/examples/example-completion/src/main.rs
@@ -56,12 +56,12 @@ fn main() {
}
// --------- IMPORTANT ---------
-// __________________________________________ Entry point bound to completion behavior
-// / _________________________ Shell context for obtaining user input state
-// | / ________ Suggest, used to return completion results
-// vvvvvvvvvv | /
-#[completion(EntryGreet)] // vvvvvvvvvvvv vvvvvvv
-fn complete_greet_entry(ctx: &ShellContext) -> Suggest {
+// _________________________________________ Entry point bound to completion behavior
+// / _________________________ Shell context for obtaining user input state
+// | / ________ Suggest, used to return completion results
+// vvvvvvvvvv | /
+#[completion(EntryGreet)] // vvvvvvvvvvvv vvvvvvv
+fn complete_greet_entry(ctx: ShellContext) -> Suggest {
// When the previous word is `greet` (the current command being typed)
if ctx.previous_word == "greet" {
// Return suggestions
diff --git a/examples/example-enum-tag/src/main.rs b/examples/example-enum-tag/src/main.rs
index c7fb502..63ac3ba 100644
--- a/examples/example-enum-tag/src/main.rs
+++ b/examples/example-enum-tag/src/main.rs
@@ -114,7 +114,7 @@ pub fn render_programming_language(lang: ProgrammingLanguages) -> RenderResult {
}
#[completion(EntryLanguageSelection)]
-fn complete_language_selection(_: &ShellContext) -> Suggest {
+fn complete_language_selection(_: ShellContext) -> Suggest {
// Use `suggest_enum!` directly to generate enum suggestions
suggest_enum!(ProgrammingLanguages)
}
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index d37e762..2b1d21e 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -893,12 +893,12 @@ pub mod example_command_macro {}
/// }
///
/// // --------- IMPORTANT ---------
-/// // __________________________________________ Entry point bound to completion behavior
-/// // / _________________________ Shell context for obtaining user input state
-/// // | / ________ Suggest, used to return completion results
-/// // vvvvvvvvvv | /
-/// #[completion(EntryGreet)] // vvvvvvvvvvvv vvvvvvv
-/// fn complete_greet_entry(ctx: &ShellContext) -> Suggest {
+/// // _________________________________________ Entry point bound to completion behavior
+/// // / _________________________ Shell context for obtaining user input state
+/// // | / ________ Suggest, used to return completion results
+/// // vvvvvvvvvv | /
+/// #[completion(EntryGreet)] // vvvvvvvvvvvv vvvvvvv
+/// fn complete_greet_entry(ctx: ShellContext) -> Suggest {
/// // When the previous word is `greet` (the current command being typed)
/// if ctx.previous_word == "greet" {
/// // Return suggestions
@@ -1178,7 +1178,7 @@ pub mod example_dispatch_tree {}
/// }
///
/// #[completion(EntryLanguageSelection)]
-/// fn complete_language_selection(_: &ShellContext) -> Suggest {
+/// fn complete_language_selection(_: ShellContext) -> Suggest {
/// // Use `suggest_enum!` directly to generate enum suggestions
/// suggest_enum!(ProgrammingLanguages)
/// }
diff --git a/mingling_cli/src/config/cmd_cfg.rs b/mingling_cli/src/config/cmd_cfg.rs
index 8baa4a1..cde7772 100644
--- a/mingling_cli/src/config/cmd_cfg.rs
+++ b/mingling_cli/src/config/cmd_cfg.rs
@@ -109,7 +109,7 @@ pub fn escape_config_value(input: &str) -> String {
}
#[completion(EntryCfg)]
-pub fn complete_config(_ctx: &ShellContext, config: &mut LazyRes<ResMlingConfig>) -> Suggest {
+pub fn complete_config(_ctx: ShellContext, config: &mut LazyRes<ResMlingConfig>) -> Suggest {
let config = config.get_ref();
let keys = config.get_hash_map().keys().cloned().collect::<Vec<_>>();
Suggest::from(keys).combine(suggest! {
diff --git a/mingling_cli/src/lib.rs b/mingling_cli/src/lib.rs
index e82300e..d5a5b22 100644
--- a/mingling_cli/src/lib.rs
+++ b/mingling_cli/src/lib.rs
@@ -30,7 +30,7 @@ pub fn help_global(_: EntryFallback) -> String {
}
#[completion(EntryFallback)]
-pub fn complete_global(_ctx: &ShellContext) -> Suggest {
+pub fn complete_global(_ctx: ShellContext) -> Suggest {
suggest! {
HELP_FLAG: "Show help messages",
ARG_FEATURES.clone(): "List of features to enable",
diff --git a/mingling_cli/src/linter/cmd_explain.rs b/mingling_cli/src/linter/cmd_explain.rs
index 923e890..1d99c62 100644
--- a/mingling_cli/src/linter/cmd_explain.rs
+++ b/mingling_cli/src/linter/cmd_explain.rs
@@ -97,7 +97,7 @@ pub fn render_error_no_such_lint(
}
#[completion(EntryExplain)]
-pub fn complete_explain(ctx: &ShellContext, registry: &mut LazyRes<ResLintRegistry>) -> Suggest {
+pub fn complete_explain(ctx: ShellContext, registry: &mut LazyRes<ResLintRegistry>) -> Suggest {
let registry = registry.get_ref();
if ctx.previous_word != "explain" {
return Suggest::FileCompletion;
diff --git a/mingling_cli/src/linter/cmd_lint.rs b/mingling_cli/src/linter/cmd_lint.rs
index 7cab63a..b49605f 100644
--- a/mingling_cli/src/linter/cmd_lint.rs
+++ b/mingling_cli/src/linter/cmd_lint.rs
@@ -141,7 +141,7 @@ pub async fn handle_state_begin_linter(
}
#[completion(EntryLint)]
-pub fn complete_lint(ctx: &ShellContext) -> Suggest {
+pub fn complete_lint(ctx: ShellContext) -> Suggest {
if mingling::picker::parselib::build_possible_flags(
ParserStyle::global_style(),
&ARG_WITH_CHECKER.into_info(),
diff --git a/mingling_cli/src/pkg_mgr/cmd_install.rs b/mingling_cli/src/pkg_mgr/cmd_install.rs
index 8b2f3b7..c7dcaef 100644
--- a/mingling_cli/src/pkg_mgr/cmd_install.rs
+++ b/mingling_cli/src/pkg_mgr/cmd_install.rs
@@ -256,7 +256,7 @@ pub fn render_error_pkg_enable_failed(err: ErrorPkgEnableFailed) -> RenderResult
}
#[completion(EntryInstall)]
-pub fn complete_install(ctx: &ShellContext) -> Suggest {
+pub fn complete_install(ctx: ShellContext) -> Suggest {
if ctx.previous_word != "install" {
return Suggest::FileCompletion;
}
diff --git a/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs b/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs
index b4989eb..ea6997f 100644
--- a/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs
+++ b/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs
@@ -93,7 +93,7 @@ pub fn render_error_package_not_enabled(err: ErrorPackageNotEnabled) -> RenderRe
}
#[completion(EntryPkgDisable)]
-pub fn complete_pkg_disable(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest {
+pub fn complete_pkg_disable(ctx: ShellContext, packages_dir: &ResPackagesDir) -> Suggest {
if ctx.previous_word != "pkg-disable" {
return Suggest::FileCompletion;
}
diff --git a/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs b/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs
index 1235cd3..377bd33 100644
--- a/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs
+++ b/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs
@@ -118,7 +118,7 @@ pub fn render_error_no_matching_version(err: ErrorNoMatchingVersion) -> RenderRe
}
#[completion(EntryPkgEnable)]
-pub fn complete_pkg_enable(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest {
+pub fn complete_pkg_enable(ctx: ShellContext, packages_dir: &ResPackagesDir) -> Suggest {
if ctx.previous_word != "pkg-enable" {
return Suggest::FileCompletion;
}
diff --git a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs
index 0c3886c..0c8781a 100644
--- a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs
+++ b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs
@@ -154,7 +154,7 @@ pub fn render_error_no_matching_packages(_: ErrorNoMatchingPackages) -> RenderRe
}
#[completion(EntryUninstall)]
-pub fn complete_uninstall(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest {
+pub fn complete_uninstall(ctx: ShellContext, packages_dir: &ResPackagesDir) -> Suggest {
if ctx.previous_word != "uninstall" {
return Suggest::FileCompletion;
}
diff --git a/mingling_cli/src/proj_mgr/cmd_class_add.rs b/mingling_cli/src/proj_mgr/cmd_class_add.rs
index f9272a5..f42bbff 100644
--- a/mingling_cli/src/proj_mgr/cmd_class_add.rs
+++ b/mingling_cli/src/proj_mgr/cmd_class_add.rs
@@ -250,7 +250,7 @@ pub fn render_error_class_write_failed(err: ErrorClassWriteFailed) -> RenderResu
}
#[completion(EntryClassAdd)]
-pub fn complete_class_add(ctx: &ShellContext, cwd: &ResCurrentDir) -> Suggest {
+pub fn complete_class_add(ctx: ShellContext, cwd: &ResCurrentDir) -> Suggest {
if ctx.previous_word != "class-add" {
return Suggest::file_comp();
}
diff --git a/mingling_macros/src/attr/completion.rs b/mingling_macros/src/attr/completion.rs
index cbc0fef..71acaae 100644
--- a/mingling_macros/src/attr/completion.rs
+++ b/mingling_macros/src/attr/completion.rs
@@ -34,52 +34,92 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
let inputs = &sig.inputs;
let output = &sig.output;
- // 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(
- inputs.span(),
- "Completion function cannot have self parameter",
- )
- .to_compile_error()
- .into();
- }
- };
+ // Parameter classification:
+ // - Owned (non-reference) parameters are **Shell sources**: each is derived
+ // from `&ShellContext` via `From<&ShellContext>` (identity `From` covers
+ // `ShellContext` itself).
+ // - `&T` / `&mut T` parameters are resource injections (unchanged).
+ // - `&ShellContext` is rejected with a helpful message: use the owned
+ // `ShellContext` (or any other `From<&ShellContext>` type) instead.
+ let mut derived_stmts: Vec<proc_macro2::TokenStream> = Vec::new();
+ let mut call_args: Vec<proc_macro2::TokenStream> = Vec::new();
+ let mut resources = Vec::new();
- // 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();
- }
+ for (idx, arg) in inputs.iter().enumerate() {
+ match arg {
+ FnArg::Typed(PatType { pat, ty, .. }) => {
+ if let Type::Reference(ref_type) = &**ty {
+ // `&ShellContext` is no longer allowed: it clashes with the
+ // resource-injection semantics of references.
+ if is_shell_context_path(&ref_type.elem) {
+ return syn::Error::new(
+ ty.span(),
+ "`&ShellContext` is not supported; use the owned `ShellContext` \
+ (or any other type implementing `From<&ShellContext>`) as a value \
+ parameter",
+ )
+ .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 },
- )
- },
- );
+ // Reference: resource injection (requires a named binding).
+ let var_name = match &**pat {
+ Pat::Ident(pat_ident) => pat_ident.ident.clone(),
+ _ => {
+ return syn::Error::new(
+ pat.span(),
+ "Resource injection parameter must be a simple identifier",
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ // Reference: resource injection.
+ let (inner_type, is_mut) = match &*ref_type.elem {
+ Type::Path(type_path) => {
+ let is_mut = ref_type.mutability.is_some();
+ (type_path.clone(), is_mut)
+ }
+ _ => {
+ return syn::Error::new(
+ ty.span(),
+ "Reference resource type must be a type path",
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+ resources.push(ResourceInjection {
+ var_name: var_name.clone(),
+ full_type: (**ty).clone(),
+ inner_type,
+ is_ref: true,
+ is_mut,
+ });
+ call_args.push(quote! { #var_name });
+ } else {
+ // Owned value: derive from `&ShellContext`. The parameter
+ // name is irrelevant (anonymous `_` is fine) since the
+ // derived binding is generated by this macro.
+ let derived_ident = Ident::new(&format!("__ctx_derived_{idx}"), pat.span());
+ derived_stmts.push(quote! {
+ let #derived_ident: #ty =
+ <#ty as ::std::convert::From<&::mingling::ShellContext>>::from(ctx);
+ });
+ call_args.push(quote! { #derived_ident });
+ }
+ }
+ FnArg::Receiver(_) => {
+ return syn::Error::new(
+ arg.span(),
+ "Completion function cannot have self parameter",
+ )
+ .to_compile_error()
+ .into();
+ }
+ }
+ }
let fn_body = &input_fn.block;
@@ -96,26 +136,11 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
let struct_name = Ident::new(&internal_name, fn_name.span());
let program_type = crate::default_program_path();
- let has_resources = !resources.is_empty();
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
- let resource_args: Vec<_> = resources
- .iter()
- .map(|res| {
- let var_name = &res.var_name;
- quote! { #var_name }
- })
- .collect();
-
- let fn_call = if has_resources {
- quote! { #fn_name(#ctx_call_arg, #(#resource_args),*) }
- } else if ctx_ty.is_some() {
- quote! { #fn_name(#ctx_call_arg) }
- } else {
- quote! { #fn_name() }
- };
+ let fn_call = quote! { #fn_name(#(#call_args),*) };
let inner_call = if mut_resources.is_empty() {
fn_call
@@ -133,13 +158,10 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
wrapped
};
- let comp_body = if has_resources {
- quote! {
- #(#immut_resource_stmts)*
- #inner_call
- }
- } else {
- quote! { #inner_call }
+ let comp_body = quote! {
+ #(#derived_stmts)*
+ #(#immut_resource_stmts)*
+ #inner_call
};
// A `()` return (or no return type) means "no suggestions": map it to an
@@ -172,7 +194,6 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
type Previous = #previous_type_path;
fn comp(ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
- #ctx_bind_stmt
#return_stmt
}
}
@@ -208,69 +229,16 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
expanded.into()
}
-/// Extract resource injection parameters from function arguments (skipping the first N params).
-fn extract_resources_from_args(
- sig: &syn::Signature,
- skip: usize,
-) -> syn::Result<Vec<ResourceInjection>> {
- let mut resources = Vec::new();
- for arg in sig.inputs.iter().skip(skip) {
- match arg {
- FnArg::Typed(PatType { pat, ty, .. }) => {
- let var_name = match &**pat {
- Pat::Ident(pat_ident) => pat_ident.ident.clone(),
- _ => {
- return Err(syn::Error::new(
- pat.span(),
- "Resource injection parameter must be a simple identifier",
- ));
- }
- };
-
- let full_type = *(*ty).clone();
-
- let (inner_type, is_ref, is_mut) = match &full_type {
- Type::Reference(ref_type) => match &*ref_type.elem {
- Type::Path(type_path) => {
- let is_mut = ref_type.mutability.is_some();
- (type_path.clone(), true, is_mut)
- }
- _ => {
- return Err(syn::Error::new(
- ty.span(),
- "Reference resource type must be a type path",
- ));
- }
- },
- Type::Path(_) => {
- return Err(syn::Error::new(
- ty.span(),
- "Resource injection parameter must be a reference (`&T` or `&mut T`)",
- ));
- }
- _ => {
- return Err(syn::Error::new(
- ty.span(),
- "Resource injection type must be a type path or reference",
- ));
- }
- };
-
- resources.push(ResourceInjection {
- var_name,
- full_type,
- inner_type,
- is_ref,
- is_mut,
- });
- }
- FnArg::Receiver(_) => {
- return Err(syn::Error::new(
- arg.span(),
- "Resource injection parameter cannot be self",
- ));
- }
- }
+/// Returns `true` when the type is a path whose last segment is `ShellContext`
+/// (e.g. `ShellContext` or `mingling::ShellContext`).
+fn is_shell_context_path(ty: &Type) -> bool {
+ if let Type::Path(type_path) = ty {
+ type_path
+ .path
+ .segments
+ .last()
+ .is_some_and(|seg| seg.ident == "ShellContext")
+ } else {
+ false
}
- Ok(resources)
}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 2271e21..d00773c 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -689,31 +689,32 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
/// 2. Registering the completion mapping for the specified entry type.
/// 3. Keeping the original function for direct calls.
///
-/// # Syntax
-///
-/// The completion function accepts a relaxed signature:
+/// # Signature rules
///
-/// - **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).
+/// - **Owned (non-reference) parameters** are *shell sources*: each one is derived
+/// from `&ShellContext` via `From<&ShellContext>`. This covers `ShellContext`
+/// itself (via its `Clone`-based `From` impl), framework state types, and any
+/// user-defined state derived from the shell context.
+/// - **`&T` / `&mut T` parameters** are resource injections (same as `#[chain]`).
+/// - **`&ShellContext` is rejected** — use the owned `ShellContext` instead, since
+/// reference parameters are reserved for resources.
+/// - The return type can be anything implementing `Into<Suggest>`: `Suggest`,
+/// `Vec<String>`, `Vec<&str>`, `Vec<(String, String)>` (suggestion + description),
+/// a set of [`SuggestItem`](https://docs.rs/mingling/latest/mingling/struct.SuggestItem.html)s,
+/// or `()` / no return type for "no suggestions".
///
/// ```rust,ignore
/// // No context, return simple suggestions
/// #[completion(EntryType)]
-/// fn complete_static() -> Vec<String> { vec!["a", "b"].into_iter().map(str::to_string).collect() }
+/// fn complete_static() -> Vec<&str> { vec!["a", "b"] }
///
-/// // Owned context (via `From<&ShellContext>`), suggestions with descriptions
+/// // Multiple shell-derived states + resource injection
/// #[completion(EntryType)]
-/// fn complete_owned(ctx: ShellContext) -> Vec<(String, String)> { /* ... */ }
+/// fn complete_mixed(pos: PositionState, flags: FlagState, db: &ResDb) -> Vec<(String, String)> { /* ... */ }
///
-/// // Borrowed context (classic form)
+/// // Empty function: this command needs no completion
/// #[completion(EntryType)]
-/// fn complete_borrowed(ctx: &ShellContext) -> Suggest { /* ... */ }
+/// fn complete_nothing() {}
/// ```
///
/// # Example
@@ -723,7 +724,7 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
/// use mingling::{ShellContext, Suggest};
///
/// #[completion(MyEntry)]
-/// fn complete_my_command(ctx: &ShellContext) -> Suggest {
+/// fn complete_my_command(ctx: ShellContext) -> Suggest {
/// if ctx.previous_word == "--type" {
/// return suggest!();
/// }
@@ -740,8 +741,9 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Requirements
///
/// - The `comp` feature must be enabled.
-/// - The first parameter (if any) must implement `From<&ShellContext>`.
-/// - The return type must implement `Into<Suggest>`.
+/// - Owned parameters must implement `From<&ShellContext>`.
+/// - Reference parameters are resource injections; `&ShellContext` is not allowed.
+/// - The return type must implement `Into<Suggest>` (or be `()`).
/// - The function cannot be async.
#[cfg(feature = "comp")]
#[proc_macro_attribute]