diff --git a/crates/pixi_cli/src/completion.rs b/crates/pixi_cli/src/completion.rs index 024002f36e..ee5212d324 100644 --- a/crates/pixi_cli/src/completion.rs +++ b/crates/pixi_cli/src/completion.rs @@ -7,6 +7,13 @@ use regex::{Captures, Regex}; use std::borrow::Cow; use std::io::Write; +/// `pixi` subcommand emitting the space-delimited workspace environment names +/// used for `run` option-value completion in bash, zsh, and fish. +const ENVIRONMENT_LIST: &str = "workspace environment list --machine-readable"; +/// `pixi` subcommand emitting the space-delimited workspace platform names +/// used for `run` option-value completion in bash, zsh, and fish. +const PLATFORM_LIST: &str = "workspace platform list --machine-readable"; + /// Generates a completion script for a shell. #[derive(Parser, Debug)] pub struct Args { @@ -64,7 +71,7 @@ pub fn execute(args: Args) -> miette::Result<()> { // For supported shells, modify the script to include more context sensitive completions. let script = match args.shell { - Shell::Bash => replace_bash_completion(&script), + Shell::Bash => replace_bash_completion(&script, pixi_utils::executable_name()), Shell::Zsh => replace_zsh_completion(&script), Shell::Fish => replace_fish_completion(&script), Shell::Nushell => replace_nushell_completion(&script), @@ -88,65 +95,143 @@ fn get_completion_script(shell: Shell) -> String { } /// Replace the parts of the bash completion script that need different functionality. -fn replace_bash_completion(script: &str) -> Cow<'_, str> { +fn replace_bash_completion<'a>(script: &'a str, bin_name: &str) -> Cow<'a, str> { // Adds tab completion to the pixi run command. // NOTE THIS IS FORMATTED BY HAND // Replace the '-' with '__' since that's what clap's generator does as well for Bash Shell completion. - let bin_name: &str = pixi_utils::executable_name(); let clap_name = bin_name.replace("-", "__"); // clap_complete >=4.6.2 separates each segment of the bin name from each // subcommand with an explicit `__subcmd__` marker, so the function for the // `run` subcommand of `pixi` is `pixi__subcmd__run`. let func_prefix = clap_name.replace("__", "__subcmd__"); + // Keep the generated `case "${prev}"` block (option-value completion) and + // run task completion after it, so tasks complete at any position. let pattern = format!( - r#"(?s){}__subcmd__run\).*?opts="(.*?)".*?(if.*?fi)"#, + r#"(?s){}__subcmd__run\).*?opts="(?P.*?)".*?if.*?fi\s*(?Pcase.*?esac)"#, func_prefix ); - let replacement = r#"FUNC_PREFIX__subcmd__run) - opts="$1" - if [[ $${cur} == -* ]] ; then - COMPREPLY=( $$(compgen -W "$${opts}" -- "$${cur}") ) - return 0 - elif [[ $${COMP_CWORD} -eq 2 ]]; then - local tasks=$$(BIN_NAME task list --machine-readable 2> /dev/null) - if [[ $$? -eq 0 ]]; then - COMPREPLY=( $$(compgen -W "$${tasks}" -- "$${cur}") ) - return 0 - fi - fi"#; let re = Regex::new(pattern.as_str()).expect("should be able to compile the regex"); - re.replace( - script, - replacement - .replace("BIN_NAME", bin_name) - .replace("FUNC_PREFIX", &func_prefix), - ) + re.replace(script, |caps: &Captures| { + let opts = &caps["opts"]; + // Complete environment and platform names after their options instead + // of files. + let case = complete_bash_option( + &caps["case"], + bin_name, + &BashOptionCompletion { + labels: "--environment|-e", + var: "environments", + command: ENVIRONMENT_LIST, + }, + ); + let case = complete_bash_option( + &case, + bin_name, + &BashOptionCompletion { + labels: "--platform|-p", + var: "platforms", + command: PLATFORM_LIST, + }, + ); + format!( + r#"{func_prefix}__subcmd__run) + opts="{opts}" + if [[ ${{cur}} == -* ]] ; then + COMPREPLY=( $(compgen -W "${{opts}}" -- "${{cur}}") ) + return 0 + fi + {case} + local tasks + if tasks=$({bin_name} task list --machine-readable 2> /dev/null) && [[ -n "${{tasks}}" ]]; then + COMPREPLY=( $(compgen -W "${{tasks}}" -- "${{cur}}") ) + return 0 + fi"# + ) + }) +} + +/// A `run` option whose value bash should complete from the space-delimited +/// output of a `pixi` subcommand instead of file paths. +struct BashOptionCompletion<'a> { + /// Regex alternation matching the option's `case` labels, e.g. + /// `--environment|-e`. + labels: &'a str, + /// Shell variable holding the candidate list. + var: &'a str, + /// `pixi` subcommand emitting the candidates. + command: &'a str, +} + +/// Complete an option's value in the bash `case "${prev}"` block from a `pixi` +/// subcommand's output, falling back to file completion when it yields nothing. +fn complete_bash_option(case_block: &str, bin_name: &str, opt: &BashOptionCompletion) -> String { + let pattern = format!( + r#"(?m)^(?P\s*)(?P