Skip to content

Commit

Permalink
Feat/autocomplete run tasks (#390)
Browse files Browse the repository at this point in the history
Adds auto completion for `pixi run <TAB TAB>` in `bash` and `zsh` .

closes #383 but not yet #372 as it doesn't activate the environment.
  • Loading branch information
ruben-arts authored Oct 19, 2023
1 parent 0da3a27 commit 9561398
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 23 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ repos:
rev: v2.2.5
hooks:
- id: codespell
exclude: ".snap"
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ rattler_repodata_gateway = { version = "0.11.0", default-features = false, featu
rattler_shell = { version = "0.11.0", default-features = false, features = ["sysinfo"] }
rattler_solve = { version = "0.11.0", default-features = false, features = ["resolvo"] }
rattler_virtual_packages = { version = "0.11.0", default-features = false }
regex = "1.9.5"
reqwest = { version = "0.11.22", default-features = false }
serde = "1.0.188"
serde_json = "1.0.107"
Expand Down
181 changes: 181 additions & 0 deletions src/cli/completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use crate::cli::{Args, CompletionCommand};
use clap::CommandFactory;
use miette::IntoDiagnostic;
use regex::Regex;
use std::borrow::Cow;
use std::io::Write;

/// Generate completions for the pixi cli, and print those to the stdout
pub(crate) fn execute(args: CompletionCommand) -> miette::Result<()> {
let clap_shell = args
.shell
.or(clap_complete::Shell::from_env())
.unwrap_or(clap_complete::Shell::Bash);

// Generate the original completion script.
let script = get_completion_script(clap_shell);

// For supported shells, modify the script to include more context sensitive completions.
let script = match clap_shell {
clap_complete::Shell::Bash => replace_bash_completion(&script),
clap_complete::Shell::Zsh => replace_zsh_completion(&script),
_ => Cow::Owned(script),
};

// Write the result to the standard output
std::io::stdout()
.write_all(script.as_bytes())
.into_diagnostic()?;

Ok(())
}

/// Generate the completion script using clap_complete for a specified shell.
fn get_completion_script(shell: clap_complete::Shell) -> String {
let mut buf = vec![];
clap_complete::generate(shell, &mut Args::command(), "pixi", &mut buf);
String::from_utf8(buf).expect("clap_complete did not generate a valid UTF8 script")
}

/// Replace the parts of the bash completion script that need different functionality.
fn replace_bash_completion(script: &str) -> Cow<str> {
let pattern = r#"(?s)pixi__run\).*?opts="(.*?)".*?(if.*?fi)"#;
// Adds tab completion to the pixi run command.
// NOTE THIS IS FORMATTED BY HAND
let replacement = r#"pixi__run)
opts="$1"
if [[ $${cur} == -* ]] ; then
COMPREPLY=( $$(compgen -W "$${opts}" -- "$${cur}") )
return 0
elif [[ $${COMP_CWORD} -eq 2 ]]; then
local tasks=$$(pixi task list --summary 2> /dev/null)
if [[ $$? -eq 0 ]]; then
COMPREPLY=( $$(compgen -W "$${tasks}" -- "$${cur}") )
return 0
fi
fi"#;
let re = Regex::new(pattern).unwrap();
re.replace(script, replacement)
}

/// Replace the parts of the bash completion script that need different functionality.
fn replace_zsh_completion(script: &str) -> Cow<str> {
let pattern = r#"(?ms)(\(run\))(?:.*?)(_arguments.*?)(\*::task)"#;
// Adds tab completion to the pixi run command.
// NOTE THIS IS FORMATTED BY HAND
let zsh_replacement = r#"$1
_values 'task' $$( pixi task list --summary 2> /dev/null )
$2::task"#;

let re = Regex::new(pattern).unwrap();
re.replace(script, zsh_replacement)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
pub fn test_zsh_completion() {
let mut script = r#"
(add)
_arguments "${_arguments_options[@]}" \
'--manifest-path=[The path to '\''pixi.toml'\'']:MANIFEST_PATH:_files' \
'*::specs -- Specify the dependencies you wish to add to the project:' \
&& ret=0
;;
(run)
_arguments "${_arguments_options[@]}" \
'--manifest-path=[The path to '\''pixi.toml'\'']:MANIFEST_PATH:_files' \
'--color=[Whether the log needs to be colored]:COLOR:(always never auto)' \
'(--frozen)--locked[Require pixi.lock is up-to-date]' \
'(--locked)--frozen[Don'\''t check if pixi.lock is up-to-date, install as lockfile states]' \
'*-v[More output per occurrence]' \
'*--verbose[More output per occurrence]' \
'(-v --verbose)*-q[Less output per occurrence]' \
'(-v --verbose)*--quiet[Less output per occurrence]' \
'-h[Print help]' \
'--help[Print help]' \
'*::task -- The task you want to run in the projects environment:' \
&& ret=0
;;
(add)
_arguments "${_arguments_options[@]}" \
&& ret=0
;;
(run)
_arguments "${_arguments_options[@]}" \
&& ret=0
;;
(shell)
_arguments "${_arguments_options[@]}" \
&& ret=0
;;
"#;
let result = replace_zsh_completion(&mut script);
insta::assert_snapshot!(result);
}

#[test]
pub fn test_bash_completion() {
// NOTE THIS IS FORMATTED BY HAND!
let script = r#"
pixi__project__help__help)
opts=""
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
pixi__run)
opts="-v -q -h --manifest-path --locked --frozen --verbose --quiet --color --help [TASK]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--manifest-path)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "always never auto" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
pixi__search)
opts="-c -l -v -q -h --channel --color --help <PACKAGE>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
fi
case "${prev}" in
--channel)
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
;;
"#;
let result = replace_bash_completion(script);
insta::assert_snapshot!(result);
}

#[test]
pub fn test_bash_completion_working_regex() {
// Generate the original completion script.
let script = get_completion_script(clap_complete::Shell::Bash);
// Test if there was a replacement done on the clap generated completions
assert_ne!(replace_bash_completion(&script), script);
}

#[test]
pub fn test_zsh_completion_working_regex() {
// Generate the original completion script.
let script = get_completion_script(clap_complete::Shell::Zsh);
// Test if there was a replacement done on the clap generated completions
assert_ne!(replace_zsh_completion(&script), script);
}
}
19 changes: 3 additions & 16 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::util::IndicatifWriter;
use crate::progress;
use clap::{CommandFactory, Parser};
use clap::Parser;
use clap_complete;
use clap_verbosity_flag::Verbosity;
use miette::IntoDiagnostic;
Expand All @@ -9,6 +9,7 @@ use tracing_subscriber::{filter::LevelFilter, util::SubscriberInitExt, EnvFilter

pub mod add;
pub mod auth;
pub mod completion;
pub mod global;
pub mod info;
pub mod init;
Expand Down Expand Up @@ -67,20 +68,6 @@ pub enum Command {
Project(project::Args),
}

fn completion(args: CompletionCommand) -> miette::Result<()> {
let clap_shell = args
.shell
.or(clap_complete::Shell::from_env())
.unwrap_or(clap_complete::Shell::Bash);
clap_complete::generate(
clap_shell,
&mut Args::command(),
"pixi",
&mut std::io::stdout(),
);
Ok(())
}

pub async fn execute() -> miette::Result<()> {
let args = Args::parse();
let use_colors = use_color_output(&args);
Expand Down Expand Up @@ -135,7 +122,7 @@ pub async fn execute() -> miette::Result<()> {
/// Execute the actual command
pub async fn execute_command(command: Command) -> miette::Result<()> {
match command {
Command::Completion(cmd) => completion(cmd),
Command::Completion(cmd) => completion::execute(cmd),
Command::Init(cmd) => init::execute(cmd).await,
Command::Add(cmd) => add::execute(cmd).await,
Command::Run(cmd) => run::execute(cmd).await,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
source: src/cli/completion.rs
expression: result
---

pixi__project__help__help)
opts=""
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
pixi__run)
opts="-v -q -h --manifest-path --locked --frozen --verbose --quiet --color --help [TASK]..."
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
elif [[ ${COMP_CWORD} -eq 2 ]]; then
local tasks=$(pixi task list --summary 2> /dev/null)
if [[ $? -eq 0 ]]; then
COMPREPLY=( $(compgen -W "${tasks}" -- "${cur}") )
return 0
fi
fi
case "${prev}" in
--manifest-path)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "always never auto" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
pixi__search)
opts="-c -l -v -q -h --channel --color --help <PACKAGE>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
fi
case "${prev}" in
--channel)
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )

;;

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
source: src/cli/completion.rs
expression: result
---

(add)
_arguments "${_arguments_options[@]}" \
'--manifest-path=[The path to '\''pixi.toml'\'']:MANIFEST_PATH:_files' \
'*::specs -- Specify the dependencies you wish to add to the project:' \
&& ret=0
;;
(run)
_values 'task' $( pixi task list --summary 2> /dev/null )
_arguments "${_arguments_options[@]}" \
'--manifest-path=[The path to '\''pixi.toml'\'']:MANIFEST_PATH:_files' \
'--color=[Whether the log needs to be colored]:COLOR:(always never auto)' \
'(--frozen)--locked[Require pixi.lock is up-to-date]' \
'(--locked)--frozen[Don'\''t check if pixi.lock is up-to-date, install as lockfile states]' \
'*-v[More output per occurrence]' \
'*--verbose[More output per occurrence]' \
'(-v --verbose)*-q[Less output per occurrence]' \
'(-v --verbose)*--quiet[Less output per occurrence]' \
'-h[Print help]' \
'--help[Print help]' \
'::task -- The task you want to run in the projects environment:' \
&& ret=0
;;
(add)
_arguments "${_arguments_options[@]}" \
&& ret=0
;;
(run)
_arguments "${_arguments_options[@]}" \
&& ret=0
;;
(shell)
_arguments "${_arguments_options[@]}" \
&& ret=0
;;


27 changes: 20 additions & 7 deletions src/cli/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub enum Operation {

/// List all tasks
#[clap(alias = "l")]
List,
List(ListArgs),
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -74,6 +74,12 @@ pub struct AliasArgs {
pub platform: Option<Platform>,
}

#[derive(Parser, Debug, Clone)]
pub struct ListArgs {
#[arg(long, short)]
pub summary: bool,
}

impl From<AddArgs> for Task {
fn from(value: AddArgs) -> Self {
let depends_on = value.depends_on.unwrap_or_default();
Expand Down Expand Up @@ -202,16 +208,23 @@ pub fn execute(args: Args) -> miette::Result<()> {
task,
);
}
Operation::List => {
Operation::List(args) => {
let tasks = project.task_names(Some(Platform::current()));
if tasks.is_empty() {
eprintln!("No tasks found",);
} else {
let mut formatted = String::new();
for name in tasks {
formatted.push_str(&format!("* {}\n", console::style(name).bold(),));
}
eprintln!("{}", formatted);
let formatted: String = tasks
.iter()
.map(|name| {
if args.summary {
format!("{} ", console::style(name))
} else {
format!("* {}\n", console::style(name).bold())
}
})
.collect();

println!("{}", formatted);
}
}
};
Expand Down

0 comments on commit 9561398

Please sign in to comment.