diff --git a/docs/_includes/content.md b/docs/_includes/content.md index 352ac455..e0d50922 100755 --- a/docs/_includes/content.md +++ b/docs/_includes/content.md @@ -3933,6 +3933,13 @@ source ./extra/shell/makers-completion.bash It will enable auto completion for the **makers** executable. + +#### Zsh Task Completion +By executing `cargo make --completion zsh` the necesary components will be created to enable task autocompletion. A restart of the shell is needed, or `source ~/.zshrc` +Then it will be possible to list the tasks defined in Makefile.toml using tab: `cargo make ` + + + #### Fig / Amazon CodeWhisperer for command line diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index cf1392fd..80ca0420 100755 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -107,6 +107,7 @@ * [Shell Completion](#usage-shell-completion) * [Bash](#usage-shell-completion-bash) * [zsh](#usage-shell-completion-zsh) + * [Zsh Task Completion](usage-task-completion-zsh) * [Fig / Amazon CodeWhisperer for command line](#usage-shell-completion-fig) * [Global Configuration](#cargo-make-global-config) * [Makefile Definition](#descriptor-definition) diff --git a/src/lib/cli_parser.rs b/src/lib/cli_parser.rs index e14f9883..82ab96fe 100644 --- a/src/lib/cli_parser.rs +++ b/src/lib/cli_parser.rs @@ -7,6 +7,8 @@ #[path = "cli_parser_test.rs"] mod cli_parser_test; +use crate::completion::generate_completions; + use crate::cli::{ AUTHOR, DEFAULT_LOG_LEVEL, DEFAULT_OUTPUT_FORMAT, DEFAULT_TASK_NAME, DESCRIPTION, VERSION, }; @@ -42,6 +44,11 @@ fn get_args( None => None, }; + cli_args.completion = match cli_parsed.get_first_value("completion") { + Some(value) => Some(value.to_string()), + None => None, + }; + cli_args.cwd = cli_parsed.get_first_value("cwd"); let default_log_level = match global_config.log_level { @@ -214,6 +221,17 @@ fn add_arguments(spec: CliSpec, default_task_name: &str, default_log_level: &str "PROFILE".to_string(), )), }) + .add_argument(Argument { + name: "completion".to_string(), + key: vec!["--completion".to_string()], + argument_occurrence: ArgumentOccurrence::Single, + value_type: ArgumentValueType::Single, + default_value: None, + help: Some(ArgumentHelp::TextAndParam( + "Will enable completion for the defined tasks for a given shell".to_string(), + "COMPLETION".to_string(), + )), + }) .add_argument(Argument { name: "cwd".to_string(), key: vec!["--cwd".to_string()], @@ -482,6 +500,15 @@ pub fn parse_args( let version_text = cliparser::version(&spec); println!("{}", version_text); Err(CargoMakeError::ExitCode(std::process::ExitCode::SUCCESS)) + } else if let Some(shell) = cli_parsed.get_first_value("completion") { + // Call the function to generate completions + if let Err(e) = generate_completions(&shell) { + error!("Error generating completions: {}", e); + return Err(CargoMakeError::ExitCode(std::process::ExitCode::FAILURE)); + } + Err(crate::error::CargoMakeError::ExitCode( + std::process::ExitCode::SUCCESS, + )) } else { Ok(get_args( &cli_parsed, diff --git a/src/lib/cli_test.rs b/src/lib/cli_test.rs index 0f236b11..d3e7efe9 100644 --- a/src/lib/cli_test.rs +++ b/src/lib/cli_test.rs @@ -18,6 +18,7 @@ fn run_makefile_not_found() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -57,6 +58,7 @@ fn run_empty_task() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -96,6 +98,7 @@ fn print_empty_task() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -135,6 +138,7 @@ fn list_empty_task() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -174,6 +178,7 @@ fn run_file_and_task() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -216,6 +221,7 @@ fn run_cwd_with_file() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: Some("..".to_string()), env: None, env_file: None, @@ -256,6 +262,7 @@ fn run_file_not_go_to_project_root() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -296,6 +303,7 @@ fn run_cwd_go_to_project_root_current_dir() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -339,6 +347,7 @@ fn run_cwd_go_to_project_root_child_dir() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: None, env: None, env_file: None, @@ -382,6 +391,7 @@ fn run_cwd_task_not_found() { profile: None, log_level: "error".to_string(), disable_color: true, + completion: None, cwd: Some("..".to_string()), env: None, env_file: None, diff --git a/src/lib/completion.rs b/src/lib/completion.rs new file mode 100644 index 00000000..75e0d472 --- /dev/null +++ b/src/lib/completion.rs @@ -0,0 +1,112 @@ +use log::{error, info}; +use std::io::BufRead; +use std::path::Path; +use std::{fs, io}; + +/// # Completions Module +/// +/// This module handles the generation of shell completion scripts for the `cargo-make` tool. +/// +/// ## Functionality +/// - `generate_completion_zsh`: Generates a Zsh completion script, creates the necessary directory, +/// and prompts for overwriting existing files. +/// +/// ## Improvements to Consider +/// 1. **Modularity**: Separate the completion logic into different modules for different shells +/// (e.g., Zsh, Bash, Fish) to improve code organization. +/// 2. **Cross-Platform Support**: Abstract the completion generation into a trait or interface +/// to facilitate adding support for other shell types. +/// 3. **Enhanced Error Handling**: Provide more informative error messages for file operations. +/// 4. **User Input Handling**: Ensure user input is trimmed and handled correctly. +/// 5. **Testing**: Implement unit tests to verify the correct behavior of completion generation functions. + +#[cfg(test)] +#[path = "completion_test.rs"] +mod completion_test; + +pub fn generate_completions(shell: &str) -> Result<(), Box> { + match shell { + "zsh" => { + generate_completion_zsh(None)?; // Use the `?` operator to propagate errors + Ok(()) // Return Ok if no error occurred + } + _ => { + // Return an error for unsupported shell + Err(Box::from(format!( + "Unsupported shell for completion: {}", + shell + ))) + } + } +} + +// Modify the function to accept an optional input stream +fn generate_completion_zsh( + input: Option<&mut dyn io::Read>, +) -> Result<(), Box> { + let home_dir = std::env::var("HOME")?; + let zfunc_dir = format!("{}/.zfunc", home_dir); + let completion_file = format!("{}/_cargo-make", zfunc_dir); + + if !Path::new(&zfunc_dir).exists() { + if let Err(e) = fs::create_dir_all(&zfunc_dir) { + error!("Failed to create directory {}: {}", zfunc_dir, e); + return Err(Box::new(e)); + } + info!("Created directory: {}", zfunc_dir); + } + + if Path::new(&completion_file).exists() { + let mut input_str = String::new(); + let reader: Box = match input { + Some(input) => Box::new(input), + None => Box::new(io::stdin()), + }; + + // Create a BufReader to read from the provided input or stdin + let mut buf_reader = io::BufReader::new(reader); + println!( + "File {} already exists. Overwrite? (y/n): ", + completion_file + ); + buf_reader.read_line(&mut input_str)?; + + if input_str.trim().to_lowercase() != "y" { + println!("Aborted overwriting the file."); + return Ok(()); + } + } + + let completion_script = r#" +#compdef cargo make cargo-make + +_cargo_make() { + local tasks + local makefile="Makefile.toml" + + if [[ ! -f $makefile ]]; then + return 1 + fi + + tasks=($(awk -F'[\\[\\.\\]]' '/^\[tasks/ {print $3}' "$makefile")) + + if [[ ${#tasks[@]} -eq 0 ]]; then + return 1 + fi + + _describe -t tasks 'cargo-make tasks' tasks +} + +_cargo_make "$@" +"#; + + fs::write(&completion_file, completion_script)?; + println!("\nWrote tasks completion script to: {}", completion_file); + + println!("To enable Zsh completion, add the following lines to your ~/.zshrc:\n"); + println!(" fpath=(~/.zfunc $fpath)"); + println!(" autoload -Uz compinit && compinit"); + println!("\nThen, restart your terminal or run 'source ~/.zshrc'."); + + Ok(()) +} diff --git a/src/lib/completion_test.rs b/src/lib/completion_test.rs new file mode 100644 index 00000000..99083ea9 --- /dev/null +++ b/src/lib/completion_test.rs @@ -0,0 +1,103 @@ +use std::fs; +use std::io::Cursor; +use std::path::Path; + +mod tests { + use crate::completion::generate_completion_zsh; + + use super::*; + + // Function to clean up test environment by removing the completion file + fn cleanup() { + let home_dir = std::env::var("HOME").expect("Failed to get HOME"); + let completion_file = format!("{}/.zfunc/_cargo-make", home_dir); + println!("\n\n\n\n{}\n\n\n\n", completion_file); + + if Path::new(&completion_file).exists() { + fs::remove_file(&completion_file).expect("Failed to clean up test file"); + } + } + + #[test] + #[ignore] + fn test_generate_completion_zsh_overwrite_prompt_yes() { + cleanup(); // Clean up before the test + + let input = b"y\n"; // Simulate user input of 'y' + let mut reader = Cursor::new(input); + + let result = generate_completion_zsh(Some(&mut reader)); + assert!(result.is_ok()); + } + + #[test] + #[ignore] + fn test_generate_completion_zsh_overwrite_prompt_no() { + cleanup(); // Clean up before the test + let input = b"n\n"; // Simulate user input of 'n' + let mut reader = Cursor::new(input); + + let result = generate_completion_zsh(Some(&mut reader)); + assert!(result.is_ok()); + } + + #[test] + #[ignore] + fn test_generate_completion_zsh_creates_directory() { + cleanup(); // Clean up before the test + + let input = b"y\n"; // Simulate user input of 'y' + let mut reader = Cursor::new(input); + + let result = generate_completion_zsh(Some(&mut reader)); + assert!(result.is_ok(), "Should succeed in generating completions"); + + // Check if the directory was created + let home_dir = std::env::var("HOME").expect("Failed to get HOME"); + let zfunc_dir = format!("{}/.zfunc", home_dir); + assert!( + Path::new(&zfunc_dir).exists(), + "The zfunc directory should exist" + ); + } + + #[test] + #[ignore] + fn test_generate_completion_zsh_creates_file() { + cleanup(); // Clean up before the test + + let input = b"y\n"; // Simulate user input of 'y' + let mut reader = Cursor::new(input); + + let result = generate_completion_zsh(Some(&mut reader)); + assert!(result.is_ok(), "Should succeed in generating completions"); + + // Check if the completion file was created + let home_dir = std::env::var("HOME").expect("Failed to get HOME"); + let completion_file = format!("{}/.zfunc/_cargo-make", home_dir); + assert!( + Path::new(&completion_file).exists(), + "The completion file should exist" + ); + } + + #[test] + #[ignore] + fn test_generate_completion_zsh_overwrite_prompt() { + cleanup(); // Clean up before the test + + // Create the directory and file first + let input = b"y\n"; // Simulate user input of 'y' + let mut reader = Cursor::new(input); + + generate_completion_zsh(Some(&mut reader)) + .expect("Should succeed in generating completions"); + + // Simulate user input for overwrite. + let input = b"y\n"; // Simulate user input of 'y' again + let mut reader = Cursor::new(input); + + let result = generate_completion_zsh(Some(&mut reader)); + assert!(result.is_ok(), "Should handle overwrite prompt gracefully"); + } +} diff --git a/src/lib/mod.rs b/src/lib/mod.rs index f013cada..8053a906 100755 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -56,6 +56,7 @@ pub mod cli; pub mod cli_commands; pub mod cli_parser; mod command; +pub mod completion; mod condition; pub mod config; mod descriptor; diff --git a/src/lib/types.rs b/src/lib/types.rs index 9f4fe0a7..dae47bba 100755 --- a/src/lib/types.rs +++ b/src/lib/types.rs @@ -91,6 +91,8 @@ pub struct CliArgs { pub log_level: String, /// Disables colorful output pub disable_color: bool, + /// Task completion for given shell + pub completion: Option, /// Current working directory pub cwd: Option, /// Environment variables @@ -141,6 +143,7 @@ impl CliArgs { profile: None, log_level: "info".to_string(), disable_color: false, + completion: None, cwd: None, env: None, env_file: None,