dprint 0.9.1

Binary for dprint.
use dprint_core::types::ErrBox;
use super::StdInReader;

pub struct CliArgs {
    pub sub_command: SubCommand,
    pub allow_node_modules: bool,
    pub verbose: bool,
    pub incremental: bool,
    pub file_patterns: Vec<String>,
    pub exclude_file_patterns: Vec<String>,
    pub plugins: Vec<String>,
    pub config: Option<String>,
}

impl CliArgs {
    pub fn is_silent_output(&self) -> bool {
        match self.sub_command {
            SubCommand::EditorInfo | SubCommand::EditorService(..) | SubCommand::StdInFmt(..) => true,
            _ => false
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum SubCommand {
    Check,
    Fmt,
    Init,
    ClearCache,
    OutputFilePaths,
    OutputResolvedConfig,
    OutputFormatTimes,
    Version,
    License,
    Help(String),
    EditorInfo, // todo: deprecate
    EditorService(EditorServiceInfo),
    StdInFmt(StdInFmtInfo),
}

#[derive(Debug, PartialEq)]
pub struct EditorServiceInfo {
    pub parent_pid: u32,
}

#[derive(Debug, PartialEq)]
pub struct StdInFmtInfo {
    pub file_name: String,
    pub file_text: String,
}

pub fn parse_args<TStdInReader: StdInReader>(args: Vec<String>, std_in_reader: &TStdInReader) -> Result<CliArgs, ErrBox> {
    let mut cli_parser = create_cli_parser();
    let matches = match cli_parser.get_matches_from_safe_borrow(args) {
        Ok(result) => result,
        Err(err) => return err!("{}", err.to_string()),
    };

    let sub_command = if matches.is_present("fmt") {
        SubCommand::Fmt
    } else if matches.is_present("check") {
        SubCommand::Check
    } else if matches.is_present("init") {
        SubCommand::Init
    } else if matches.is_present("clear-cache") {
        SubCommand::ClearCache
    } else if matches.is_present("output-file-paths") {
        SubCommand::OutputFilePaths
    } else if matches.is_present("output-resolved-config") {
        SubCommand::OutputResolvedConfig
    } else if matches.is_present("output-format-times") {
        SubCommand::OutputFormatTimes
    } else if matches.is_present("version") {
        SubCommand::Version
    } else if matches.is_present("license") {
        SubCommand::License
    } else if matches.is_present("editor-info") {
        SubCommand::EditorInfo
    } else if matches.is_present("editor-service") {
        let matches = match matches.subcommand_matches("editor-service") {
            Some(matches) => matches,
            None => return err!("Could not find stdin-fmt subcommand matches."),
        };
        SubCommand::EditorService(EditorServiceInfo {
            parent_pid: matches.value_of("parent-pid").map(|v| v.parse::<u32>().ok()).flatten().unwrap()
        })
    } else if matches.is_present("stdin-fmt") {
        let stdin_fmt_matches = match matches.subcommand_matches("stdin-fmt") {
            Some(matches) => matches,
            None => return err!("Could not find stdin-fmt subcommand matches."),
        };
        SubCommand::StdInFmt(StdInFmtInfo {
            file_name: stdin_fmt_matches.value_of("file-name").map(String::from).unwrap(),
            file_text: std_in_reader.read()?,
        })
    } else {
        SubCommand::Help({
            let mut text = Vec::new();
            cli_parser.write_help(&mut text).unwrap();
            String::from_utf8(text).unwrap()
        })
    };

    Ok(CliArgs {
        sub_command,
        verbose: matches.is_present("verbose"),
        incremental: matches.is_present("incremental"),
        allow_node_modules: matches.is_present("allow-node-modules"),
        config: matches.value_of("config").map(String::from),
        file_patterns: values_to_vec(matches.values_of("files")),
        exclude_file_patterns: values_to_vec(matches.values_of("excludes")),
        plugins: values_to_vec(matches.values_of("plugins")),
    })
}

fn create_cli_parser<'a, 'b>() -> clap::App<'a, 'b> {
    use clap::{App, Arg, SubCommand, AppSettings};
    App::new("dprint")
        .setting(AppSettings::UnifiedHelpMessage)
        .setting(AppSettings::DisableHelpFlags)
        .setting(AppSettings::DisableHelpSubcommand)
        .setting(AppSettings::DeriveDisplayOrder)
        .bin_name("dprint")
        .version(env!("CARGO_PKG_VERSION"))
        .author("Copyright 2020 by David Sherret")
        .about("Auto-formats source code based on the specified plugins.")
        .usage("dprint <SUBCOMMAND> [OPTIONS] [--] [files]...")
        .template(r#"{bin} {version}
{author}

{about}

USAGE:
    {usage}

SUBCOMMANDS:
{subcommands}

OPTIONS:
{unified}

ARGS:
{positionals}

{after-help}"#)
        .after_help(
            r#"GETTING STARTED:
    1. Navigate to the root directory of a code repository.
    2. Run `dprint init` to create a .dprintrc.json file in that directory.
    3. Modify configuration file if necessary.
    4. Run `dprint fmt` or `dprint check`.

EXAMPLES:
    Write formatted files to file system:

      dprint fmt

    Check for files that haven't been formatted:

      dprint check

    Specify path to config file other than the default:

      dprint fmt --config path/to/config/.dprintrc.json

    Search for files using the specified file patterns:

      dprint fmt "**/*.{ts,tsx,js,jsx,json}""#,
        )
        .subcommand(
            SubCommand::with_name("init")
                .about("Initializes a configuration file in the current directory.")
        )
        .subcommand(
            SubCommand::with_name("fmt")
                .about("Formats the source files and writes the result to the file system.")
        )
        .subcommand(
            SubCommand::with_name("check")
                .about("Checks for any files that haven't been formatted.")
        )
        .subcommand(
            SubCommand::with_name("output-file-paths")
                .about("Prints the resolved file paths for the plugins based on the args and configuration.")
        )
        .subcommand(
            SubCommand::with_name("output-resolved-config")
                .about("Prints the resolved configuration for the plugins based on the args and configuration.")
        )
        .subcommand(
            SubCommand::with_name("output-format-times")
                .about("Prints the amount of time it takes to format each file. Use this for debugging.")
        )
        .subcommand(
            SubCommand::with_name("clear-cache")
                .about("Deletes the plugin cache directory.")
        )
        .subcommand(
            SubCommand::with_name("license")
                .about("Outputs the software license.")
        )
        .subcommand(
            SubCommand::with_name("editor-info")
                .setting(AppSettings::Hidden)
        )
        .subcommand(
            SubCommand::with_name("editor-service")
                .setting(AppSettings::Hidden)
                .arg(
                    Arg::with_name("parent-pid")
                        .long("parent-pid")
                        .required(true)
                        .takes_value(true)
                )
        )
        .subcommand(
            SubCommand::with_name("stdin-fmt")
                .setting(AppSettings::Hidden)
                .arg(
                    Arg::with_name("file-name")
                        .long("file-name")
                        .required(true)
                        .takes_value(true)
                )
        )
        .arg(
            Arg::with_name("files")
                .help("List of files or globs in quotes to format. This overrides what is specified in the config file.")
                .takes_value(true)
                .global(true)
                .conflicts_with("stdin-fmt")
                .multiple(true),
        )
        .arg(
            Arg::with_name("config")
                .long("config")
                .short("c")
                .help("Path or url to JSON configuration file. Defaults to .dprintrc.json in current or ancestor directory when not provided.")
                .global(true)
                .takes_value(true),
        )
        .arg(
            Arg::with_name("help")
                .long("help")
                .short("h")
                .hidden(true)
                .takes_value(false),
        )
        .arg(
            Arg::with_name("excludes")
                .long("excludes")
                .global(true)
                .value_name("patterns")
                .help("List of files or directories or globs in quotes to exclude when formatting. This overrides what is specified in the config file.")
                .takes_value(true)
                .multiple(true),
        )
        .arg(
            Arg::with_name("allow-node-modules")
                .long("allow-node-modules")
                .help("Allows traversing node module directories (unstable - This flag will be renamed to be non-node specific in the future).")
                .global(true)
                .takes_value(false),
        )
        .arg(
            Arg::with_name("incremental")
                .long("incremental")
                .help("Only format files only when they change. This may alternatively be specified in the configuration file.")
                .global(true)
                .takes_value(false),
        )
        .arg(
            Arg::with_name("plugins")
                .long("plugins")
                .value_name("urls/files")
                .help("List of urls or file paths of plugins to use. This overrides what is specified in the config file.")
                .global(true)
                .takes_value(true)
                .multiple(true),
        )
        .arg(
            Arg::with_name("verbose")
                .long("verbose")
                .help("Prints additional diagnostic information.")
                .global(true)
                .takes_value(false),
        )
        .arg(
            Arg::with_name("version")
                .short("v")
                .long("version")
                .help("Prints the version.")
                .takes_value(false),
        )
}

fn values_to_vec(values: Option<clap::Values>) -> Vec<String> {
    values.map(|x| x.map(std::string::ToString::to_string).collect()).unwrap_or(Vec::new())
}