cli/
completion.rs

1use log::{error, info};
2use std::io::BufRead;
3use std::path::Path;
4use std::{fs, io};
5
6/// # Completions Module
7///
8/// This module handles the generation of shell completion scripts for the `cargo-make` tool.
9///
10/// ## Functionality
11/// - `generate_completion_zsh`: Generates a Zsh completion script, creates the necessary directory,
12///   and prompts for overwriting existing files.
13///
14/// ## Improvements to Consider
15/// 1. **Modularity**: Separate the completion logic into different modules for different shells
16///    (e.g., Zsh, Bash, Fish) to improve code organization.
17/// 2. **Cross-Platform Support**: Abstract the completion generation into a trait or interface
18///    to facilitate adding support for other shell types.
19/// 3. **Enhanced Error Handling**: Provide more informative error messages for file operations.
20/// 4. **User Input Handling**: Ensure user input is trimmed and handled correctly.
21/// 5. **Testing**: Implement unit tests to verify the correct behavior of completion generation functions.
22
23#[cfg(test)]
24#[path = "completion_test.rs"]
25mod completion_test;
26
27pub fn generate_completions(shell: &str) -> Result<(), Box<dyn std::error::Error>> {
28    match shell {
29        "zsh" => {
30            generate_completion_zsh(None)?; // Use the `?` operator to propagate errors
31            Ok(()) // Return Ok if no error occurred
32        }
33        _ => {
34            // Return an error for unsupported shell
35            Err(Box::from(format!(
36                "Unsupported shell for completion: {}",
37                shell
38            )))
39        }
40    }
41}
42
43// Modify the function to accept an optional input stream
44fn generate_completion_zsh(
45    input: Option<&mut dyn io::Read>,
46) -> Result<(), Box<dyn std::error::Error>> {
47    let home_dir = std::env::var("HOME")?;
48    let zfunc_dir = format!("{}/.zfunc", home_dir);
49    let completion_file = format!("{}/_cargo-make", zfunc_dir);
50
51    if !Path::new(&zfunc_dir).exists() {
52        if let Err(e) = fs::create_dir_all(&zfunc_dir) {
53            error!("Failed to create directory {}: {}", zfunc_dir, e);
54            return Err(Box::new(e));
55        }
56        info!("Created directory: {}", zfunc_dir);
57    }
58
59    if Path::new(&completion_file).exists() {
60        let mut input_str = String::new();
61        let reader: Box<dyn io::Read> = match input {
62            Some(input) => Box::new(input),
63            None => Box::new(io::stdin()),
64        };
65
66        // Create a BufReader to read from the provided input or stdin
67        let mut buf_reader = io::BufReader::new(reader);
68        println!(
69            "File {} already exists. Overwrite? (y/n): ",
70            completion_file
71        );
72        buf_reader.read_line(&mut input_str)?;
73
74        if input_str.trim().to_lowercase() != "y" {
75            println!("Aborted overwriting the file.");
76            return Ok(());
77        }
78    }
79
80    let completion_script = r#"
81#compdef cargo make cargo-make
82
83_cargo_make() {
84    local tasks
85    local makefile="Makefile.toml"
86    
87    if [[ ! -f $makefile ]]; then
88        return 1
89    fi
90
91    tasks=($(awk -F'[\\[\\.\\]]' '/^\[tasks/ {print $3}' "$makefile"))
92
93    if [[ ${#tasks[@]} -eq 0 ]]; then
94        return 1
95    fi
96
97    _describe -t tasks 'cargo-make tasks' tasks
98}
99
100_cargo_make "$@"
101"#;
102
103    fs::write(&completion_file, completion_script)?;
104    println!("\nWrote tasks completion script to: {}", completion_file);
105
106    println!("To enable Zsh completion, add the following lines to your ~/.zshrc:\n");
107    println!("    fpath=(~/.zfunc $fpath)");
108    println!("    autoload -Uz compinit && compinit");
109    println!("\nThen, restart your terminal or run 'source ~/.zshrc'.");
110
111    Ok(())
112}