From 89bf57f33a9f0f5a96fc50e272c83d926fa35c4a Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 17 Aug 2026 19:21:52 +0800 Subject: refactor!: replace build.rs with compile-time macro build steps BREAKING CHANGE: Replace the `build`/`builds` feature system with compile-time macro-driven generation. `gen_program!()` now automatically invokes `build_comp!()` and `build_pathf!()` when the `comp`/`pathf` features are enabled, eliminating the need for `build.rs` and `[build-dependencies]` blocks. This removes the `build` feature, the `mingling::build` module, and all related build-time API functions. Completion scripts are now written to `{target_directory}/mingling/` instead of `target/release/`. The `mingling_cli` uses `build_comp!("mling")` for its custom binary name. --- mingling_macros/Cargo.toml | 10 +- mingling_macros/src/build.rs | 56 +++++++++++ mingling_macros/src/build/comp.rs | 158 ++++++++++++++++++++++++++++++++ mingling_macros/src/build/pathf.rs | 19 ++++ mingling_macros/src/func/gen_program.rs | 65 +++++++++---- mingling_macros/src/lib.rs | 47 ++++++++++ mingling_macros/tmpls/comps/bash.sh | 67 ++++++++++++++ mingling_macros/tmpls/comps/fish.fish | 120 ++++++++++++++++++++++++ mingling_macros/tmpls/comps/pwsh.ps1 | 136 +++++++++++++++++++++++++++ mingling_macros/tmpls/comps/zsh.zsh | 71 ++++++++++++++ 10 files changed, 728 insertions(+), 21 deletions(-) create mode 100644 mingling_macros/src/build.rs create mode 100644 mingling_macros/src/build/comp.rs create mode 100644 mingling_macros/src/build/pathf.rs create mode 100644 mingling_macros/tmpls/comps/bash.sh create mode 100644 mingling_macros/tmpls/comps/fish.fish create mode 100644 mingling_macros/tmpls/comps/pwsh.ps1 create mode 100644 mingling_macros/tmpls/comps/zsh.zsh (limited to 'mingling_macros') diff --git a/mingling_macros/Cargo.toml b/mingling_macros/Cargo.toml index 137213a..6ae7aeb 100644 --- a/mingling_macros/Cargo.toml +++ b/mingling_macros/Cargo.toml @@ -19,11 +19,11 @@ default = [] async = [] clap = [] -comp = [] +comp = ["dep:just_template", "dep:mingling_pathf"] dispatch_tree = [] structural_renderer = [] repl = [] -pathf = [] +pathf = ["dep:mingling_pathf"] extras = [] @@ -33,3 +33,9 @@ quote.workspace = true proc-macro2.workspace = true just_fmt.workspace = true + +# comp — compile-time completion script generation (build_comp!()) +just_template = { workspace = true, optional = true } + +# pathf — compile-time type path analysis (build_pathf!()) +mingling_pathf = { workspace = true, optional = true } 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::(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 { + 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 { + 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 = { + // 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 { - 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/lib.rs b/mingling_macros/src/lib.rs index d00773c..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; @@ -1653,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. /// diff --git a/mingling_macros/tmpls/comps/bash.sh b/mingling_macros/tmpls/comps/bash.sh new file mode 100644 index 0000000..edec28d --- /dev/null +++ b/mingling_macros/tmpls/comps/bash.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +_<<>>_bash_completion() { + local line="${COMP_LINE:0:COMP_POINT}" + local cur="${line##* }" + local prev="" + local word_index=1 + + local before="${line:0:$(( ${#line} - ${#cur} ))}" + local -a before_words + if [[ -n "$before" ]]; then + read -ra before_words <<< "$before" + word_index=$(( ${#before_words[@]} + 1 )) + if [[ $word_index -gt 1 ]]; then + prev="${before_words[${#before_words[@]}-1]}" + fi + fi + + local args=() + args+=(-f "${COMP_LINE//-/^}") + args+=(-C "$COMP_POINT") + args+=(-w "${cur//-/^}") + args+=(-p "${prev//-/^}") + args+=(-c "${COMP_WORDS[0]//-/^}") + args+=(-i "$word_index") + args+=(-F "bash") + + for word in "${COMP_WORDS[@]}"; do + args+=(-a "${word//-/^}") + done + + local suggestions + if suggestions=$(<<>> __comp "${args[@]}" 2>/dev/null); then + if [ $? -eq 0 ]; then + if [ "$suggestions" = "_file_" ]; then + compopt -o default + COMPREPLY=() + return + fi + + if [ -n "$suggestions" ]; then + local -a all_suggestions filtered + mapfile -t all_suggestions < <(printf '%s\n' "$suggestions") + + for suggestion in "${all_suggestions[@]}"; do + [ -z "$cur" ] || [[ "$suggestion" == "$cur"* ]] && filtered+=("$suggestion") + done + + if [ ${#filtered[@]} -gt 0 ]; then + COMPREPLY=("${filtered[@]}") + if [[ "$cur" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then + local colon_prefix="${cur%"${cur##*:}"}" + local -a ltrimmed=() + for suggestion in "${COMPREPLY[@]}"; do + ltrimmed+=("${suggestion#"$colon_prefix"}") + done + COMPREPLY=("${ltrimmed[@]}") + fi + fi + return + fi + fi + fi + + COMPREPLY=() +} + +complete -F _<<>>_bash_completion <<>> diff --git a/mingling_macros/tmpls/comps/fish.fish b/mingling_macros/tmpls/comps/fish.fish new file mode 100644 index 0000000..64b4ed3 --- /dev/null +++ b/mingling_macros/tmpls/comps/fish.fish @@ -0,0 +1,120 @@ +#!/usr/bin/env fish +function __<<>>_fish_complete + set -l cmdline (commandline -opc) + set -l buffer (commandline -b) + set -l cursor (commandline -C) + set -l current_token (commandline -ct) + + set -l current_word "" + set -l previous_word "" + set -l word_index 0 + set -l char_count 0 + + set -l found false + if test -n "$current_token" + for i in (seq (count $cmdline)) + if test "$cmdline[$i]" = "$current_token" + set word_index $i + set current_word $current_token + if test $i -gt 1 + set previous_word $cmdline[(math $i - 1)] + end + set found true + break + end + end + end + + if not $found + for i in (seq (count $cmdline)) + set word $cmdline[$i] + if test $i -gt 1 + set char_count (math $char_count + 1) + end + set char_count (math $char_count + (string length -- "$word")) + + if test $cursor -le $char_count + set word_index $i + set current_word $word + if test $i -gt 1 + set previous_word $cmdline[(math $i - 1)] + end + break + end + end + end + + if test $word_index -eq 0 -a (count $cmdline) -gt 0 + set word_index (count $cmdline) + if test -n "$current_token" -a "$current_token" != "$cmdline[-1]" + set current_word $current_token + else + set current_word "" + end + set previous_word $cmdline[-1] + end + + if test $word_index -gt (count $cmdline) + set word_index (count $cmdline) + end + + set -l buffer_replaced (string replace -a "-" "^" -- "$buffer") + set -l current_word_replaced (string replace -a "-" "^" -- "$current_word") + set -l previous_word_replaced (string replace -a "-" "^" -- "$previous_word") + + set -l args + set -a args -f "$buffer_replaced" -C "$cursor" -w "$current_word_replaced" -p "$previous_word_replaced" + + if test (count $cmdline) -gt 0 + set -a args -c "$cmdline[1]" + else + set -a args -c "" + end + + set -a args -i "$word_index" + + if test (count $cmdline) -gt 0 + set -l all_words_replaced + for word in $cmdline + set -a all_words_replaced (string replace -a "-" "^" -- "$word") + end + + if test -n "$current_token" -a "$current_word" = "$current_token" + set -l found_in_cmdline false + for word in $cmdline + if test "$word" = "$current_token" + set found_in_cmdline true + break + end + end + if not $found_in_cmdline -a $word_index -eq (math (count $cmdline) + 1) + set -a all_words_replaced (string replace -a "-" "^" -- "$current_token") + end + end + + set -a args -a $all_words_replaced + else + set -a args -a "" + end + + set -a args -F "fish" + + set -l output + if not <<>> __comp $args 2>/dev/null | read -z output + return + end + + set -l trimmed_output (string trim -- "$output") + if test "$trimmed_output" = "_file_" + __fish_complete_path "$current_word" + return 0 + else if test -n "$trimmed_output" + string split -n \n -- "$output" | while read -l line + test -n "$line" && echo "$line" + end + return 0 + end + return 1 +end + +complete -c <<>> -a '(__<<>>_fish_complete)' -f diff --git a/mingling_macros/tmpls/comps/pwsh.ps1 b/mingling_macros/tmpls/comps/pwsh.ps1 new file mode 100644 index 0000000..d72a027 --- /dev/null +++ b/mingling_macros/tmpls/comps/pwsh.ps1 @@ -0,0 +1,136 @@ +# PowerShell completion script for <<>> +Register-ArgumentCompleter -Native -CommandName '<<>>' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $line = $commandAst.ToString() + + $elements = @() + if ($commandAst.CommandElements.Count -gt 0) { + $elements = $commandAst.CommandElements | ForEach-Object { $_.Value } + } + + $commandName = if ($elements.Count -gt 0) { $elements[0] } else { "" } + + $currentWord = $wordToComplete + $previousWord = "" + $wordIndex = 0 + + $found = $false + for ($i = 0; $i -lt $elements.Count; $i++) { + if ($elements[$i] -eq $currentWord) { + $wordIndex = $i + 1 + if ($i -gt 0) { + $previousWord = $elements[$i - 1] + } + $found = $true + break + } + } + + if (-not $found) { + $wordIndex = $elements.Count + 1 + if ($elements.Count -gt 0) { + $previousWord = $elements[-1] + } + } + + $args = @( + "-C", $cursorPosition.ToString() + "-i", $wordIndex.ToString() + "-F", "Powershell" + ) + + if ($line) { + $args += "-f" + $args += ($line -replace '-', '^') + } + if ($currentWord) { + $args += "-w" + $args += ($currentWord -replace '-', '^') + } + if ($previousWord) { + $args += "-p" + $args += ($previousWord -replace '-', '^') + } + if ($commandName) { + $args += "-c" + $args += ($commandName -replace '-', '^') + } + + foreach ($element in $elements) { + if ($element) { + $args += "-a" + $args += ($element -replace '-', '^') + } + } + + $originalEncoding = [Console]::OutputEncoding + $originalPSEncoding = $OutputEncoding + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + $OutputEncoding = [System.Text.Encoding]::UTF8 + + $output = & <<>> __comp $args 2>&1 + + [Console]::OutputEncoding = $originalEncoding + $OutputEncoding = $originalPSEncoding + + $output = $output -replace "`r`n", "`n" -replace "`r", "`n" + + if (-not $output) { + return @() + } + + $lines = $output -split "`n" + + if ($lines.Count -eq 0) { + return @() + } + + $firstLine = $lines[0].Trim() + + if ($firstLine -eq "_file_") { + if ($lines.Count -gt 1) { + $fileSuggestions = $lines[1..($lines.Count-1)] + } else { + $fileSuggestions = @() + } + + $completionResults = @() + $fileSuggestions | ForEach-Object { + $path = $_ + $isDirectory = $path.EndsWith([System.IO.Path]::DirectorySeparatorChar) -or $path.EndsWith('/') + $completionType = if ($isDirectory) { 'ProviderContainer' } else { 'ProviderItem' } + $completionResults += [System.Management.Automation.CompletionResult]::new($path, $path, $completionType, $path) + } + + return $completionResults + } else { + $completionResults = @() + + foreach ($line in $lines) { + $trimmedLine = $line.Trim() + + if ($trimmedLine -match '^([^$]+)\$\((.+)\)$') { + $text = $matches[1] + $description = $matches[2] + $completionResults += [System.Management.Automation.CompletionResult]::new( + $text, + $text, + 'ParameterValue', + $description + ) + } else { + $text = $trimmedLine + $resultType = if ($text.StartsWith('-')) { 'ParameterName' } else { 'ParameterValue' } + $completionResults += [System.Management.Automation.CompletionResult]::new( + $text, + $text, + $resultType, + $text + ) + } + } + + return $completionResults + } +} diff --git a/mingling_macros/tmpls/comps/zsh.zsh b/mingling_macros/tmpls/comps/zsh.zsh new file mode 100644 index 0000000..7cf5f7b --- /dev/null +++ b/mingling_macros/tmpls/comps/zsh.zsh @@ -0,0 +1,71 @@ +#!/usr/bin/env zsh +_<<>>_completion() { + local -a args + local suggestions + + local buffer="$BUFFER" + local cursor="$CURSOR" + local current_word="${words[$CURRENT]}" + local previous_word="" + local command_name="${words[1]}" + local word_index="$CURRENT" + + if [[ $CURRENT -gt 1 ]]; then + previous_word="${words[$((CURRENT-1))]}" + fi + + args=( + -f "${buffer//-/^}" + -C "$cursor" + -w "${current_word//-/^}" + -p "${previous_word//-/^}" + -c "$command_name" + -i "$word_index" + -a "${(@)words//-/^}" + -F "zsh" + ) + + suggestions=$(<<>> __comp "${args[@]}" 2>/dev/null) + + if [[ $? -eq 0 ]] && [[ -n "$suggestions" ]]; then + local -a completions + completions=(${(f)suggestions}) + + if [[ "${completions[1]}" == "_file_" ]]; then + shift completions + _files + else + local -a parsed_completions + for item in "${completions[@]}"; do + if [[ "$item" =~ '^([^$]+)\$\((.+)\)$' ]]; then + parsed_completions+=("${match[1]//:/\\:}:${match[2]}") + else + parsed_completions+=("${item//:/\\:}") + fi + done + + if (( $+functions[_describe] )); then + _describe '<<>> commands' parsed_completions + else + local -a simple_completions + for item in "${completions[@]}"; do + if [[ "$item" =~ '^([^$]+)\$\((.+)\)$' ]]; then + simple_completions+=("${match[1]}") + else + simple_completions+=("$item") + fi + done + compadd -a simple_completions + fi + fi + fi +} + +if (( $+functions[compdef] )); then + compdef _<<>>_completion <<>> + if [[ $? -ne 0 ]]; then + compctl -K _<<>>_completion <<>> + fi +else + compctl -K _<<>>_completion <<>> +fi -- cgit