arity 0.4.0

An LSP, formatter, and linter for R
use clap::CommandFactory;
use clap_complete::{Shell, generate_to};
use clap_mangen::Man;
use std::env;
use std::fs;
use std::io::Result;
use std::path::PathBuf;

#[path = "src/cli.rs"]
mod cli;

use cli::Cli;

fn generate_completions(outdir: &std::ffi::OsString) -> Result<()> {
    let mut cmd = Cli::command();

    // Generate shell completions to OUT_DIR (for cargo build)
    for shell in [
        Shell::Bash,
        Shell::Fish,
        Shell::Zsh,
        Shell::PowerShell,
        Shell::Elvish,
    ] {
        generate_to(shell, &mut cmd, "arity", outdir)?;
    }

    // Also copy completions to target/completions for packaging
    let completions_dir = PathBuf::from("target/completions");
    fs::create_dir_all(&completions_dir)?;

    let outdir_path = PathBuf::from(outdir);

    // Copy bash, fish, and zsh completions for packaging
    let bash_src = outdir_path.join("arity.bash");
    let fish_src = outdir_path.join("arity.fish");
    let zsh_src = outdir_path.join("_arity");

    if bash_src.exists() {
        fs::copy(&bash_src, completions_dir.join("arity.bash"))?;
    }
    if fish_src.exists() {
        fs::copy(&fish_src, completions_dir.join("arity.fish"))?;
    }
    if zsh_src.exists() {
        fs::copy(&zsh_src, completions_dir.join("_arity"))?;
    }

    Ok(())
}

fn generate_cli_markdown() -> Result<()> {
    // Skip during cargo package/publish - file is committed to git, and
    // packaging runs the build in a temporary directory.
    let is_packaging = env::current_dir()
        .ok()
        .and_then(|p| p.to_str().map(|s| s.contains("/target/package/")))
        .unwrap_or(false);
    if is_packaging {
        return Ok(());
    }

    let cmd = Cli::command();
    let docs_dir = PathBuf::from("docs/reference");

    // Only proceed if the docs directory exists (it isn't shipped in the crate).
    if !docs_dir.exists() {
        return Ok(());
    }

    let opts = clap_markdown::MarkdownOptions::default()
        .show_footer(false)
        .show_table_of_contents(false);

    let markdown = clap_markdown::help_markdown_command_custom(&cmd, &opts);

    let mut document = String::new();
    document.push_str("---\n");
    document.push_str("title: CLI Reference\n");
    document.push_str("description: >-\n  Comprehensive reference for the Arity CLI, including all commands and options.\n");
    document.push_str("---\n\n");
    document.push_str(&markdown);

    let output_path = docs_dir.join("cli.qmd");
    fs::write(&output_path, &document)?;
    println!("Generated CLI markdown: {output_path:?}");

    Ok(())
}

fn format_see_also(refs: &[String]) -> String {
    let formatted: Vec<String> = refs.iter().map(|r| format!("\\fB{}\\fR(1)", r)).collect();
    format!(".SH \"SEE ALSO\"\n{}\n", formatted.join(", "))
}

fn generate_man_pages() -> Result<()> {
    // Create man directory if it doesn't exist
    let out_dir = PathBuf::from("target/man");
    fs::create_dir_all(&out_dir)?;

    // Generate main man page and all subcommand pages (like git/cargo do)
    let cmd = Cli::command();

    // Collect top-level subcommand names (skip "help") for SEE ALSO sections
    let subcommand_names: Vec<String> = cmd
        .get_subcommands()
        .filter(|s| s.get_name() != "help")
        .map(|s| format!("arity-{}", s.get_name()))
        .collect();

    // Generate main page
    let man = Man::new(cmd.clone());
    let mut buffer = Vec::new();
    man.render(&mut buffer)?;
    let main_content =
        String::from_utf8_lossy(&buffer).into_owned() + &format_see_also(&subcommand_names);
    fs::write(out_dir.join("arity.1"), main_content.as_bytes())?;

    // Generate pages for each top-level subcommand
    for subcommand in cmd.get_subcommands() {
        let subcommand_name = subcommand.get_name();
        if subcommand_name == "help" {
            continue; // Skip help command
        }

        let name = format!("arity-{}", subcommand_name);
        let man = Man::new(subcommand.clone().version(env!("CARGO_PKG_VERSION"))).title(&name);
        let mut buffer = Vec::new();
        man.render(&mut buffer)?;

        // Post-process: fix NAME and SYNOPSIS subcommand references
        let content = String::from_utf8_lossy(&buffer);
        let fixed_content = content
            .replace(
                &format!("{} \\-", subcommand_name),
                &format!("{} \\-", name),
            )
            .replace(
                &format!("\\fB{}\\fR", subcommand_name),
                &format!("\\fBarity {}\\fR", subcommand_name),
            )
            .replace(
                &format!("{}\\-", subcommand_name),
                &format!("arity\\-{}\\-", subcommand_name),
            );

        // SEE ALSO: arity(1) plus sibling subcommand pages
        let mut see_also_refs: Vec<String> = vec!["arity".to_string()];
        see_also_refs.extend(subcommand_names.iter().filter(|n| *n != &name).cloned());
        let with_see_also = fixed_content + &format_see_also(&see_also_refs);

        fs::write(
            out_dir.join(format!("{}.1", name)),
            with_see_also.as_bytes(),
        )?;
    }

    Ok(())
}

fn main() -> Result<()> {
    // Generate shell completions
    if let Some(outdir) = env::var_os("OUT_DIR") {
        generate_completions(&outdir)?;
    }

    // Generate man pages
    generate_man_pages()?;

    // Generate CLI reference markdown for the docs site
    generate_cli_markdown()?;

    println!("cargo:rerun-if-changed=src/cli.rs");
    println!("cargo:rerun-if-changed=build.rs");

    Ok(())
}