Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/autocomplete run tasks #390

Merged
merged 11 commits into from
Oct 19, 2023
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() {
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
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",);
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading