diff options
| -rw-r--r-- | CHANGELOG.md | 46 | ||||
| -rw-r--r-- | GETTING-STARTED.md | 9 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | docs/_zh_CN/pages/advanced/1-completion.md | 2 | ||||
| -rw-r--r-- | docs/pages/advanced/1-completion.md | 2 | ||||
| -rw-r--r-- | examples/example-completion/src/main.rs | 12 | ||||
| -rw-r--r-- | examples/example-enum-tag/src/main.rs | 2 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 14 | ||||
| -rw-r--r-- | mingling_cli/src/config/cmd_cfg.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/lib.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/linter/cmd_explain.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/linter/cmd_lint.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_install.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_uninstall.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/proj_mgr/cmd_class_add.rs | 2 | ||||
| -rw-r--r-- | mingling_core/src/comp/shell_ctx.rs | 8 | ||||
| -rw-r--r-- | mingling_core/src/comp/suggest.rs | 13 | ||||
| -rw-r--r-- | mingling_macros/src/attr/completion.rs | 228 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 49 |
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) } ``` @@ -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] |
