aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src')
-rw-r--r--mingling_macros/src/attr/completion.rs232
-rw-r--r--mingling_macros/src/build.rs56
-rw-r--r--mingling_macros/src/build/comp.rs158
-rw-r--r--mingling_macros/src/build/pathf.rs19
-rw-r--r--mingling_macros/src/func/gen_program.rs65
-rw-r--r--mingling_macros/src/func/program_final_gen.rs22
-rw-r--r--mingling_macros/src/lib.rs87
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.
///