diff options
Diffstat (limited to 'mingling_macros/src')
| -rw-r--r-- | mingling_macros/src/attr/completion.rs | 232 | ||||
| -rw-r--r-- | mingling_macros/src/build.rs | 56 | ||||
| -rw-r--r-- | mingling_macros/src/build/comp.rs | 158 | ||||
| -rw-r--r-- | mingling_macros/src/build/pathf.rs | 19 | ||||
| -rw-r--r-- | mingling_macros/src/func/gen_program.rs | 65 | ||||
| -rw-r--r-- | mingling_macros/src/func/program_final_gen.rs | 22 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 87 |
7 files changed, 466 insertions, 173 deletions
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/build.rs b/mingling_macros/src/build.rs new file mode 100644 index 0000000..8f2949d --- /dev/null +++ b/mingling_macros/src/build.rs @@ -0,0 +1,56 @@ +//! Compile-time build logic for `build_comp!()` and `build_pathf!()`. +//! +//! The build steps run as a side effect of macro expansion (during `gen_program!`), +//! writing artifacts under `{target_directory}/mingling/`. + +#[doc(hidden)] +#[cfg(feature = "comp")] +pub(crate) mod comp; + +#[doc(hidden)] +#[cfg(feature = "pathf")] +pub(crate) mod pathf; + +/// Shared implementation behind `build_comp!()`. +/// +/// Accepts an optional string literal (the binary name); defaults to +/// `CARGO_PKG_NAME`. Returns an empty token stream on success, or a +/// `compile_error!` token stream on failure. +#[cfg(feature = "comp")] +pub(crate) fn comp_build_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let bin_name: String = if input.is_empty() { + std::env::var("CARGO_PKG_NAME").unwrap_or_default() + } else { + match syn::parse::<syn::LitStr>(input) { + Ok(lit) => lit.value(), + Err(e) => return e.to_compile_error().into(), + } + }; + + match comp::build_comp_scripts(&bin_name) { + Ok(()) => proc_macro::TokenStream::new(), + Err(e) => { + let msg = format!("build_comp: failed to generate completion scripts: {e}"); + syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into() + } + } +} + +/// Shared implementation behind `build_pathf!()`. +/// +/// Runs the pathf type-mapping analysis. Returns an empty token stream on +/// success, or a `compile_error!` token stream on failure. +#[cfg(feature = "pathf")] +pub(crate) fn pathf_build_impl(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { + match pathf::analyze_and_build_type_mapping() { + Ok(()) => proc_macro::TokenStream::new(), + Err(e) => { + let msg = format!("build_pathf: type mapping analysis failed: {e}"); + syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into() + } + } +} diff --git a/mingling_macros/src/build/comp.rs b/mingling_macros/src/build/comp.rs new file mode 100644 index 0000000..81a26e8 --- /dev/null +++ b/mingling_macros/src/build/comp.rs @@ -0,0 +1,158 @@ +use std::path::PathBuf; + +use just_template::tmpl; + +/// Represents the shell environment for which the output format is intended. +/// +/// This is an internal copy of `mingling_core::ShellFlag`, kept private to the +/// build module because the macros crate must not depend on `mingling_core`. +/// Which variants are constructed depends on the target OS (`#[cfg]`), so +/// platform-gated variants may be unused on any given host. +#[allow(dead_code)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub(crate) enum ShellFlag { + /// Represents the Bash shell. + #[default] + Bash, + /// Represents the Zsh shell. + Zsh, + /// Represents the Fish shell. + Fish, + /// Represents `PowerShell`. + Powershell, + /// A custom or unsupported shell type, identified by the provided string. + Other(String), +} + +const TMPL_COMP_BASH: &str = include_str!("../../tmpls/comps/bash.sh"); +const TMPL_COMP_ZSH: &str = include_str!("../../tmpls/comps/zsh.zsh"); +const TMPL_COMP_FISH: &str = include_str!("../../tmpls/comps/fish.fish"); +const TMPL_COMP_PWSH: &str = include_str!("../../tmpls/comps/pwsh.ps1"); + +/// Generate shell completion scripts for a given binary name. +/// +/// On Windows, generates `PowerShell` completion. +/// On Linux, generates Zsh, Bash, and Fish completions. +/// Scripts are written to the `OUT_DIR` (or `target/` if `OUT_DIR` is not set). +/// +/// # Errors +/// +/// Returns an [`std::io::Error`] if a script cannot be written. +pub(crate) fn build_comp_scripts(name: &str) -> Result<(), std::io::Error> { + #[cfg(target_os = "windows")] + { + build_comp_script(&ShellFlag::Powershell, name)?; + Ok(()) + } + + #[cfg(target_os = "linux")] + { + build_comp_script(&ShellFlag::Zsh, name)?; + build_comp_script(&ShellFlag::Bash, name)?; + build_comp_script(&ShellFlag::Fish, name)?; + Ok(()) + } + + #[cfg(target_os = "macos")] + { + build_comp_script(&ShellFlag::Zsh, name)?; + build_comp_script(&ShellFlag::Bash, name)?; + build_comp_script(&ShellFlag::Fish, name)?; + Ok(()) + } +} + +/// Generate a shell completion script for a specific shell. +/// +/// This function takes a shell flag and a binary name, selects the appropriate +/// template, substitutes the binary name into the template, and writes the +/// resulting completion script to the Mingling build directory +/// (`{target_directory}/mingling/`, resolved via `cargo metadata`). +/// +/// # Errors +/// +/// Returns an [`std::io::Error`] if the script cannot be written. +pub(crate) fn build_comp_script( + shell_flag: &ShellFlag, + bin_name: &str, +) -> Result<(), std::io::Error> { + let output_dir = comp_output_dir()?; + build_comp_script_to(shell_flag, bin_name, &output_dir.to_string_lossy()) +} + +/// The directory where completion scripts are written: `{target_directory}/mingling/`. +fn comp_output_dir() -> Result<PathBuf, std::io::Error> { + mingling_pathf::build_output_dir().map_err(|e| std::io::Error::other(e.to_string())) +} + +/// Generate a shell completion script to a specified directory. +/// +/// This function takes a shell flag, a binary name, and a target directory path, +/// selects the appropriate template, substitutes the binary name into the template, +/// and writes the resulting completion script to the specified directory. +/// +/// # Errors +/// +/// Returns an [`std::io::Error`] if the script cannot be written. +pub(crate) fn build_comp_script_to( + shell_flag: &ShellFlag, + bin_name: &str, + target_dir: &str, +) -> Result<(), std::io::Error> { + let (tmpl_str, ext) = get_tmpl(shell_flag); + let mut tmpl = just_template::Template::from(tmpl_str); + tmpl!(bin_name = bin_name); + let target_path = std::path::PathBuf::from(target_dir); + std::fs::create_dir_all(&target_path)?; + let output_path = target_path.join(format!("{bin_name}_comp{ext}")); + std::fs::write(&output_path, tmpl.to_string()) +} + +const fn get_tmpl(shell_flag: &ShellFlag) -> (&'static str, &'static str) { + match shell_flag { + ShellFlag::Bash | ShellFlag::Other(_) => (TMPL_COMP_BASH, ".sh"), + ShellFlag::Zsh => (TMPL_COMP_ZSH, ".zsh"), + ShellFlag::Fish => (TMPL_COMP_FISH, ".fish"), + ShellFlag::Powershell => (TMPL_COMP_PWSH, ".ps1"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_tmpl_bash() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Bash); + assert_eq!(ext, ".sh"); + assert!(!tmpl.is_empty(), "bash template should not be empty"); + } + + #[test] + fn get_tmpl_zsh() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Zsh); + assert_eq!(ext, ".zsh"); + assert!(!tmpl.is_empty(), "zsh template should not be empty"); + } + + #[test] + fn get_tmpl_fish() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Fish); + assert_eq!(ext, ".fish"); + assert!(!tmpl.is_empty(), "fish template should not be empty"); + } + + #[test] + fn get_tmpl_powershell() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Powershell); + assert_eq!(ext, ".ps1"); + assert!(!tmpl.is_empty(), "powershell template should not be empty"); + } + + #[test] + fn get_tmpl_other() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Other("custom".to_string())); + assert_eq!(ext, ".sh"); + assert!(!tmpl.is_empty(), "fallback template should not be empty"); + } +} diff --git a/mingling_macros/src/build/pathf.rs b/mingling_macros/src/build/pathf.rs new file mode 100644 index 0000000..788f527 --- /dev/null +++ b/mingling_macros/src/build/pathf.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +use mingling_pathf::error::MinglingPathfinderError; + +/// The directory where pathf's build artifacts are stored for the current +/// crate: `{target_directory}/mingling/{CARGO_PKG_NAME}`. +pub fn output_dir() -> Result<PathBuf, MinglingPathfinderError> { + Ok(mingling_pathf::build_output_dir()?.join(crate_name())) +} + +/// Runs the pathf type-mapping analysis for the current crate at compile time +/// (replacing the previous `build.rs` call). +pub fn analyze_and_build_type_mapping() -> Result<(), MinglingPathfinderError> { + mingling_pathf::analyze_and_build_type_mapping() +} + +fn crate_name() -> String { + std::env::var("CARGO_PKG_NAME").unwrap_or_default() +} diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs index c0a7ea8..35e8352 100644 --- a/mingling_macros/src/func/gen_program.rs +++ b/mingling_macros/src/func/gen_program.rs @@ -7,6 +7,11 @@ use quote::quote; /// Generates the `Next` type alias, `Routable` impl for `ChainProcess`, /// and delegates to `program_comp_gen!()`, `program_fallback_gen!()`, /// and `program_final_gen!()`. +/// +/// When the `comp` / `pathf` features are enabled, the expansion begins by +/// invoking `build_comp!()` / `build_pathf!()`, which run the build steps +/// (previously done in `build.rs`) as a compile-time side effect and expand +/// to nothing. pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { #[cfg(feature = "comp")] let comp_gen = quote! { @@ -16,18 +21,46 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { #[cfg(not(feature = "comp"))] let comp_gen = quote! {}; - // When pathf is enabled, load the type_using.rs generated by the build script + // `build_pathf!()` / `build_comp!()` are invoked at the very beginning of the + // expansion: they run the build logic at compile time and expand to nothing. + #[cfg(feature = "comp")] + let comp_build = quote! { + ::mingling::macros::build_comp!(); + }; + + #[cfg(not(feature = "comp"))] + let comp_build = quote! {}; + + #[cfg(feature = "pathf")] + let pathf_build = quote! { + ::mingling::macros::build_pathf!(); + }; + + #[cfg(not(feature = "pathf"))] + let pathf_build = quote! {}; + + // When pathf is enabled, load the type_using.rs generated by the build logic // and emit its use statements so types from submodules are in scope. #[cfg(feature = "pathf")] let pathf_uses: Vec<proc_macro2::TokenStream> = { + // The `build_pathf!()` macro emitted above will (re-)run the analysis + // during expansion, but the `use` statements are needed right now, so + // make sure the mapping exists before reading it. + if let Err(e) = crate::build::pathf::analyze_and_build_type_mapping() { + let msg = format!("pathf: type mapping analysis failed: {e}"); + return syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into(); + } let uses = load_pathf_uses(); if uses.is_empty() { - // The file might not exist yet — emit a clear hint + // The analyzer found nothing — emit a clear hint let hint: proc_macro2::TokenStream = syn::parse_quote! { compile_error!( - "pathf: `{}` not found or empty.\n\ - Make sure `build.rs` calls `mingling::build::analyze_and_build_type_mapping().unwrap();`\n\ - with features [\"build\", \"pathf\"] enabled." + "pathf: no types were found by the analyzer.\n\ + Make sure the `pathf` feature is enabled (which also enables\n\ + the `build_pathf!()` macro) and that `gen_program!()` is called\n\ + in a crate with a `src/` directory." ); }; vec![hint] @@ -47,6 +80,8 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { }; TokenStream::from(quote! { + #comp_build + #pathf_build pub use __this_program_impl::*; #[doc(hidden)] @@ -88,24 +123,16 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { }) } -/// Loads `type_using.rs` generated by the pathf build script and returns each +/// Loads `type_using.rs` generated by the pathf build logic and returns each /// `use ...;` line as a token stream, ready to be emitted in the generated output. #[cfg(feature = "pathf")] fn load_pathf_uses() -> Vec<proc_macro2::TokenStream> { - let out_dir = match std::env::var("OUT_DIR") { - Ok(d) => d, - Err(_) => return Vec::new(), - }; - let crate_name = match std::env::var("CARGO_PKG_NAME") { - Ok(n) => n, - Err(_) => return Vec::new(), + let Ok(output_dir) = crate::build::pathf::output_dir() else { + return Vec::new(); }; - let path = std::path::Path::new(&out_dir) - .join(&crate_name) - .join("type_using.rs"); - let content = match std::fs::read_to_string(&path) { - Ok(c) => c, - Err(_) => return Vec::new(), + let path = output_dir.join("type_using.rs"); + let Ok(content) = std::fs::read_to_string(&path) else { + return Vec::new(); }; content .lines() diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs index 8123eaf..ceee66d 100644 --- a/mingling_macros/src/func/program_final_gen.rs +++ b/mingling_macros/src/func/program_final_gen.rs @@ -273,9 +273,25 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { .collect(); let do_chain_fn = if chain_tokens.is_empty() { - quote! { - fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { - ::core::panic!("No chain found for type id") + // An empty chain list is still valid, but the synthesized `do_chain` + // must match the trait signature for the enabled mode. The sync + // branch used unconditionally breaks the `async` feature (E0053), + // so dispatch on `ASYNC_ENABLED` here as well. + if ASYNC_ENABLED { + quote! { + fn do_chain( + _any: ::mingling::AnyOutput<Self::Enum>, + ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> { + ::std::boxed::Box::pin(async { + ::core::panic!("No chain found for type id") + }) + } + } + } else { + quote! { + fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { + ::core::panic!("No chain found for type id") + } } } } else if ASYNC_ENABLED { diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 2271e21..1aaedb1 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -20,6 +20,9 @@ mod derive; mod func; mod systems; +#[cfg(any(feature = "comp", feature = "pathf"))] +mod build; + mod extensions; mod utils; @@ -689,31 +692,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 +727,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 +744,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] @@ -1651,6 +1656,50 @@ pub fn gen_program(input: TokenStream) -> TokenStream { func::gen_program::gen_program_impl(input) } +/// Executes the completion-script build at compile time and expands to nothing. +/// +/// **This macro is only available with the `comp` feature.** +/// +/// The completion scripts are written to `{target_directory}/mingling/` (the +/// target directory is resolved via `cargo metadata`). +/// +/// `gen_program!()` calls this macro automatically when the `comp` feature is +/// enabled. It can also be invoked manually to customize the binary name: +/// +/// - `build_comp!()` — uses the current package name (`CARGO_PKG_NAME`). +/// - `build_comp!("mybin")` — uses the given binary name. +/// +/// ```rust,ignore +/// mingling::macros::build_comp!(); +/// // or: +/// mingling::macros::build_comp!("mybin"); +/// ``` +#[cfg(feature = "comp")] +#[proc_macro] +pub fn build_comp(input: TokenStream) -> TokenStream { + build::comp_build_impl(input) +} + +/// Executes the pathf type-mapping build at compile time and expands to nothing. +/// +/// **This macro is only available with the `pathf` feature.** +/// +/// The mapping files are written to `{target_directory}/mingling/{CARGO_PKG_NAME}/` +/// (the target directory is resolved via `cargo metadata`), and are consumed by +/// `gen_program!()` so that types defined in submodules are resolved automatically. +/// +/// `gen_program!()` calls this macro automatically when the `pathf` feature is +/// enabled. +/// +/// ```rust,ignore +/// mingling::macros::build_pathf!(); +/// ``` +#[cfg(feature = "pathf")] +#[proc_macro] +pub fn build_pathf(input: TokenStream) -> TokenStream { + build::pathf_build_impl(input) +} + /// Internal macro used by `gen_program!` to generate the completion infrastructure for /// shell completion support. /// |
