aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md46
-rw-r--r--GETTING-STARTED.md9
-rw-r--r--README.md2
-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_core/src/comp/shell_ctx.rs8
-rw-r--r--mingling_core/src/comp/suggest.rs13
-rw-r--r--mingling_macros/src/attr/completion.rs228
-rw-r--r--mingling_macros/src/lib.rs49
21 files changed, 243 insertions, 162 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62b1f7d..969a3e2 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.
@@ -349,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/README.md b/README.md
index 379c4dd..a7781d0 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ pub struct StateNext;
#[chain]
fn handle_current(_: EntryCurrent) -> StateNext {
// 1. The first phase outputs the StateNext value
- StateNext // ^^^^^^^^^
+ StateNext // ^^^^^^^^^
} // |
// |
// 2. The second phase takes StateNext as input
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_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..71acaae 100644
--- a/mingling_macros/src/attr/completion.rs
+++ b/mingling_macros/src/attr/completion.rs
@@ -34,33 +34,92 @@ 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();
- }
+ // 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();
- let first_arg = &inputs[0];
- let _ctx_type = match first_arg {
- FnArg::Typed(PatType { ty, .. }) => (**ty).clone(),
- FnArg::Receiver(_) => {
- return syn::Error::new(
- first_arg.span(),
- "Completion function cannot have self parameter",
- )
- .to_compile_error()
- .into();
- }
- };
- let fixed_ctx: Pat = syn::parse_quote!(ctx);
+ 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();
+ }
- let resources = match extract_resources_from_args(sig, 1) {
- Ok(r) => r,
- Err(e) => return e.to_compile_error().into(),
- };
+ // 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;
@@ -77,24 +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(#fixed_ctx, #(#resource_args),*) }
- } else {
- quote! { #fn_name(#fixed_ctx) }
- };
+ let fn_call = quote! { #fn_name(#(#call_args),*) };
let inner_call = if mut_resources.is_empty() {
fn_call
@@ -112,13 +158,30 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
wrapped
};
- let comp_body = if has_resources {
+ let comp_body = quote! {
+ #(#derived_stmts)*
+ #(#immut_resource_stmts)*
+ #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! {
- #(#immut_resource_stmts)*
- #inner_call
+ { #comp_body };
+ ::mingling::Suggest::new()
}
} else {
- quote! { #inner_call }
+ quote! {
+ let __completion_result = { #comp_body };
+ ::std::convert::Into::into(__completion_result)
+ }
};
let expanded: proc_macro2::TokenStream = quote! {
@@ -130,8 +193,8 @@ 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 {
+ #return_stmt
}
}
@@ -166,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 d0556dc..d00773c 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -689,34 +689,50 @@ 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
+/// # Signature rules
+///
+/// - **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<&str> { vec!["a", "b"] }
///
-/// ```rust,ignore
+/// // Multiple shell-derived states + resource injection
/// #[completion(EntryType)]
-/// fn complete_my_entry(ctx: &ShellContext) -> Suggest {
-/// // Return suggestions based on current input state...
-/// }
+/// fn complete_mixed(pos: PositionState, flags: FlagState, db: &ResDb) -> Vec<(String, String)> { /* ... */ }
+///
+/// // Empty function: this command needs no completion
+/// #[completion(EntryType)]
+/// fn complete_nothing() {}
/// ```
///
/// # 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") {
+/// fn complete_my_command(ctx: ShellContext) -> Suggest {
+/// 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 +741,9 @@ 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`.
+/// - 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]