From 214e27083a3d01b3305bb0f5d5363e0b7ba67f0b Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Thu, 8 Feb 2024 05:13:32 -0500 Subject: [PATCH] feat(cli): upgrade to clap v4 --- Cargo.lock | 143 ++++--- cli/Cargo.toml | 9 +- cli/src/main.rs | 1012 ++++++++++++++++++++++++----------------------- 3 files changed, 613 insertions(+), 551 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37672d72..ce2048ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,54 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.79" @@ -50,17 +98,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -172,19 +209,50 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim", - "textwrap", - "unicode-width", - "vec_map", + "clap_builder", + "clap_derive", ] +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "combine" version = "4.6.6" @@ -482,13 +550,10 @@ dependencies = [ ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "home" @@ -1065,9 +1130,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -1109,15 +1174,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.56" @@ -1246,6 +1302,7 @@ name = "tree-sitter-cli" version = "0.20.9" dependencies = [ "ansi_term", + "anstyle", "anyhow", "clap", "ctor", @@ -1365,12 +1422,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - [[package]] name = "unindent" version = "0.2.3" @@ -1395,10 +1446,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" [[package]] -name = "vec_map" -version = "0.8.2" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "version_check" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3ef1a911..22b002bc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -26,8 +26,15 @@ wasm = ["tree-sitter/wasm", "tree-sitter-loader/wasm"] [dependencies] ansi_term = "0.12.1" +anstyle = "1.0.4" anyhow = "1.0.72" -clap = "2.32" +clap = { version = "4.4.18", features = [ + "cargo", + "derive", + "env", + "help", + "unstable-styles", +] } ctrlc = { version = "3.4.0", features = ["termination"] } difference = "2.0.0" dirs = "5.0.1" diff --git a/cli/src/main.rs b/cli/src/main.rs index 4d7cb395..a93705f4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,6 @@ -use anyhow::{anyhow, Context, Error, Result}; -use clap::{App, AppSettings, Arg, SubCommand}; +use anstyle::{AnsiColor, Color, Style}; +use anyhow::{anyhow, Context, Result}; +use clap::{crate_authors, Args, Command, FromArgMatches as _, Subcommand}; use glob::glob; use regex::Regex; use std::collections::HashSet; @@ -21,6 +22,294 @@ const BUILD_VERSION: &str = env!("CARGO_PKG_VERSION"); const BUILD_SHA: Option<&'static str> = option_env!("BUILD_SHA"); const DEFAULT_GENERATE_ABI_VERSION: usize = 14; +#[derive(Subcommand)] +#[command(about="Generates and tests parsers", author=crate_authors!("\n"), styles=get_styles())] +enum Commands { + InitConfig(InitConfig), + Generate(Generate), + Parse(Parse), + Test(Test), + Query(Query), + Highlight(Highlight), + Tags(Tags), + BuildWasm(BuildWasm), + Playground(Playground), + DumpLanguages(DumpLanguages), +} + +#[derive(Args)] +#[command(about = "Generate a default config file")] +struct InitConfig; + +#[derive(Args)] +#[command(about = "Generate a parser", alias = "gen", alias = "g")] +struct Generate { + #[arg(long, index = 1, help = "The path to the grammar file")] + pub grammar_path: Option, + #[arg(long, short, help = "Show debug log during generation")] + pub log: bool, + #[arg( + long = "abi", + value_name = "VERSION", + help = format!(concat!( + "Select the language ABI version to generate (default {}).\n", + "Use --abi=latest to generate the newest supported version ({}).", + ), + DEFAULT_GENERATE_ABI_VERSION, + tree_sitter::LANGUAGE_VERSION, + ) + )] + pub abi_version: Option, + #[arg(long, help = "Don't generate language bindings")] + pub no_bindings: bool, + #[arg( + long, + short = 'b', + help = "Compile all defined languages in the current dir" + )] + pub build: bool, + #[arg(long, short = '0', help = "Compile a parser in debug mode")] + pub debug_build: bool, + #[arg( + long, + value_name = "PATH", + help = "The path to the directory containing the parser library" + )] + pub libdir: Option, + #[arg( + long, + help = "Produce a report of the states for the given rule, use `-` to report every rule" + )] + pub report_states_for_rule: Option, + + #[arg( + long, + value_name = "EXECUTABLE", + env = "TREE_SITTER_JS_RUNTIME", + help = "The path to the JavaScript runtime to use for generating parsers" + )] + pub js_runtime: Option, +} + +#[derive(Args)] +#[command(about = "Parse files", alias = "p")] +struct Parse { + #[arg( + long = "paths", + help = "The path to a file with paths to source file(s)" + )] + pub paths_file: Option, + #[arg(num_args=1.., help = "The source file(s) to use")] + pub paths: Option>, + #[arg( + long, + help = "Select a language by the scope instead of a file extension" + )] + pub scope: Option, + #[arg(long, short = 'd', help = "Show parsing debug log")] + pub debug: bool, + #[arg(long, short = '0', help = "Compile a parser in debug mode")] + pub debug_build: bool, + #[arg( + long, + short = 'D', + help = "Produce the log.html file with debug graphs" + )] + pub debug_graph: bool, + #[arg( + long, + help = "Compile parsers to wasm instead of native dynamic libraries" + )] + pub wasm: bool, + #[arg(long = "dot", help = "Output the parse data with graphviz dot")] + pub output_dot: bool, + #[arg( + long = "xml", + short = 'x', + help = "Output the parse data in XML format" + )] + pub output_xml: bool, + #[arg(long, short, help = "Show parsing statistic")] + pub stat: bool, + #[arg(long, help = "Interrupt the parsing process by timeout (µs)")] + pub timeout: Option, + #[arg(long, short, help = "Measure execution time")] + pub time: bool, + #[arg(long, short, help = "Suppress main output")] + pub quiet: bool, + #[arg( + long, + num_args = 1.., + help = "Apply edits in the format: \"row, col delcount insert_text\"" + )] + pub edits: Option>, + #[arg(long, help = "The encoding of the input files")] + pub encoding: Option, +} + +#[derive(Args)] +#[command(about = "Run a parser's tests", alias = "t")] +struct Test { + #[arg( + long, + short, + help = "Only run corpus test cases whose name includes the given string" + )] + pub filter: Option, + #[arg( + long, + short, + help = "Only run corpus test cases whose name matches the given regex" + )] + pub include: Option, + #[arg( + long, + short, + help = "Only run corpus test cases whose name does not match the given regex" + )] + pub exclude: Option, + #[arg( + long, + short, + help = "Update all syntax trees in corpus files with current parser output" + )] + pub update: bool, + #[arg(long, short = 'd', help = "Show parsing debug log")] + pub debug: bool, + #[arg(long, short = '0', help = "Compile a parser in debug mode")] + pub debug_build: bool, + #[arg( + long, + short = 'D', + help = "Produce the log.html file with debug graphs" + )] + pub debug_graph: bool, + #[arg( + long, + help = "Compile parsers to wasm instead of native dynamic libraries" + )] + pub wasm: bool, + #[arg(long, help = "Apply all captures to highlights")] + pub apply_all_captures: bool, +} + +#[derive(Args)] +#[command(about = "Search files using a syntax tree query", alias = "q")] +struct Query { + #[arg(help = "Path to a file with queries", index = 1, required = true)] + query_path: String, + #[arg(long, short, help = "Measure execution time")] + pub time: bool, + #[arg(long, short, help = "Suppress main output")] + pub quiet: bool, + #[arg( + long = "paths", + help = "The path to a file with paths to source file(s)" + )] + pub paths_file: Option, + #[arg(index = 2, num_args=1.., help = "The source file(s) to use")] + pub paths: Option>, + #[arg( + long, + help = "The range of byte offsets in which the query will be executed" + )] + pub byte_range: Option, + #[arg(long, help = "The range of rows in which the query will be executed")] + pub row_range: Option, + #[arg( + long, + help = "Select a language by the scope instead of a file extension" + )] + pub scope: Option, + #[arg(long, short, help = "Order by captures instead of matches")] + pub captures: bool, + #[arg(long, help = "Whether to run query tests or not")] + pub test: bool, +} + +#[derive(Args)] +#[command(about = "Highlight a file", alias = "hi")] +struct Highlight { + #[arg(long, short = 'H', help = "Generate highlighting as an HTML document")] + pub html: bool, + #[arg( + long, + help = "Check that highlighting captures conform strictly to standards" + )] + pub check: bool, + #[arg(long, help = "The path to a file with captures")] + pub captures_path: Option, + #[arg(long, num_args = 1.., help = "The paths to files with queries")] + pub query_paths: Option>, + #[arg( + long, + help = "Select a language by the scope instead of a file extension" + )] + pub scope: Option, + #[arg(long, short, help = "Measure execution time")] + pub time: bool, + #[arg(long, short, help = "Suppress main output")] + pub quiet: bool, + #[arg( + long = "paths", + help = "The path to a file with paths to source file(s)" + )] + pub paths_file: Option, + #[arg(num_args = 1.., help = "The source file(s) to use")] + pub paths: Option>, + #[arg(long, help = "Apply all captures to highlights")] + pub apply_all_captures: bool, +} + +#[derive(Args)] +#[command(about = "Generate a list of tags")] +struct Tags { + #[arg( + long, + help = "Select a language by the scope instead of a file extension" + )] + pub scope: Option, + #[arg(long, short, help = "Measure execution time")] + pub time: bool, + #[arg(long, short, help = "Suppress main output")] + pub quiet: bool, + #[arg( + long = "paths", + help = "The path to a file with paths to source file(s)" + )] + pub paths_file: Option, + #[arg(num_args = 1.., help = "The source file(s) to use")] + pub paths: Option>, +} + +#[derive(Args)] +#[command(about = "Compile a parser to WASM", alias = "bw")] +struct BuildWasm { + #[arg( + long, + help = "Run emscripten via docker even if it is installed locally" + )] + pub docker: bool, + #[arg(index = 1, num_args = 1.., help = "The path to output the wasm file")] + pub path: Option, +} + +#[derive(Args)] +#[command( + about = "Start local playground for a parser in the browser", + alias = "play", + alias = "pg", + alias = "web-ui" +)] +struct Playground { + #[arg(long, short, help = "Don't open in default browser")] + pub quiet: bool, +} + +#[derive(Args)] +#[command(about = "Print info about all known language parsers", alias = "langs")] +struct DumpLanguages; + fn main() { let result = run(); if let Err(err) = &result { @@ -42,303 +331,33 @@ fn run() -> Result<()> { || BUILD_VERSION.to_string(), |build_sha| format!("{BUILD_VERSION} ({build_sha})"), ); + let version: &'static str = Box::leak(version.into_boxed_str()); - let debug_arg = Arg::with_name("debug") - .help("Show parsing debug log") - .long("debug") - .short("d"); + let cli = Command::new("tree-sitter") + .help_template( + "\ +{before-help}{name} {version} +{author-with-newline}{about-with-newline} +{usage-heading} {usage} - let debug_graph_arg = Arg::with_name("debug-graph") - .help("Produce the log.html file with debug graphs") - .long("debug-graph") - .short("D"); - - let debug_build_arg = Arg::with_name("debug-build") - .help("Compile a parser in debug mode") - .long("debug-build") - .short("0"); - - let paths_file_arg = Arg::with_name("paths-file") - .help("The path to a file with paths to source file(s)") - .long("paths") - .takes_value(true); - - let paths_arg = Arg::with_name("paths") - .help("The source file(s) to use") - .multiple(true); - - let scope_arg = Arg::with_name("scope") - .help("Select a language by the scope instead of a file extension") - .long("scope") - .takes_value(true); - - let time_arg = Arg::with_name("time") - .help("Measure execution time") - .long("time") - .short("t"); - - let quiet_arg = Arg::with_name("quiet") - .help("Suppress main output") - .long("quiet") - .short("q"); - - let wasm_arg = Arg::with_name("wasm") - .long("wasm") - .help("compile parsers to wasm instead of native dynamic libraries"); - let apply_all_captures_arg = Arg::with_name("apply-all-captures") - .help("Apply all captures to highlights") - .long("apply-all-captures"); - - let matches = App::new("tree-sitter") - .author("Max Brunsfeld ") - .about("Generates and tests parsers") - .version(version.as_str()) - .setting(AppSettings::SubcommandRequiredElseHelp) - .global_setting(AppSettings::ColoredHelp) - .global_setting(AppSettings::DeriveDisplayOrder) - .global_setting(AppSettings::DisableHelpSubcommand) - .subcommand(SubCommand::with_name("init-config").about("Generate a default config file")) - .subcommand( - SubCommand::with_name("generate") - .alias("gen") - .alias("g") - .about("Generate a parser") - .arg(Arg::with_name("grammar-path").index(1)) - .arg(Arg::with_name("log").long("log")) - .arg( - Arg::with_name("abi-version") - .long("abi") - .value_name("version") - .help(&format!( - concat!( - "Select the language ABI version to generate (default {}).\n", - "Use --abi=latest to generate the newest supported version ({}).", - ), - DEFAULT_GENERATE_ABI_VERSION, - tree_sitter::LANGUAGE_VERSION, - )), - ) - .arg(Arg::with_name("no-bindings").long("no-bindings")) - .arg( - Arg::with_name("build") - .long("build") - .short("b") - .help("Compile all defined languages in the current dir"), - ) - .arg(&debug_build_arg) - .arg( - Arg::with_name("libdir") - .long("libdir") - .takes_value(true) - .value_name("path"), - ) - .arg( - Arg::with_name("report-states-for-rule") - .long("report-states-for-rule") - .value_name("rule-name") - .takes_value(true), - ) - .arg( - Arg::with_name("js-runtime") - .long("js-runtime") - .takes_value(true) - .value_name("executable") - .env("TREE_SITTER_JS_RUNTIME") - .help("Use a JavaScript runtime other than node"), - ), +{all-args}{after-help} +", ) - .subcommand( - SubCommand::with_name("parse") - .alias("p") - .about("Parse files") - .arg(&paths_file_arg) - .arg(&paths_arg) - .arg(&scope_arg) - .arg(&debug_arg) - .arg(&debug_build_arg) - .arg(&debug_graph_arg) - .arg(&wasm_arg) - .arg(Arg::with_name("output-dot").long("dot")) - .arg(Arg::with_name("output-xml").long("xml").short("x")) - .arg( - Arg::with_name("stat") - .help("Show parsing statistic") - .long("stat") - .short("s"), - ) - .arg( - Arg::with_name("timeout") - .help("Interrupt the parsing process by timeout (µs)") - .long("timeout") - .takes_value(true), - ) - .arg(&time_arg) - .arg(&quiet_arg) - .arg( - Arg::with_name("edits") - .help("Apply edits in the format: \"row,col del_count insert_text\"") - .long("edit") - .short("edit") - .takes_value(true) - .multiple(true) - .number_of_values(1), - ) - .arg( - Arg::with_name("encoding") - .help("The encoding of the input files") - .long("encoding") - .takes_value(true), - ), - ) - .subcommand( - SubCommand::with_name("query") - .alias("q") - .about("Search files using a syntax tree query") - .arg( - Arg::with_name("query-path") - .help("Path to a file with queries") - .index(1) - .required(true), - ) - .arg(&time_arg) - .arg(&quiet_arg) - .arg(&paths_file_arg) - .arg(&paths_arg.clone().index(2)) - .arg( - Arg::with_name("byte-range") - .help("The range of byte offsets in which the query will be executed") - .long("byte-range") - .takes_value(true), - ) - .arg( - Arg::with_name("row-range") - .help("The range of rows in which the query will be executed") - .long("row-range") - .takes_value(true), - ) - .arg(&scope_arg) - .arg(Arg::with_name("captures").long("captures").short("c")) - .arg(Arg::with_name("test").long("test")), - ) - .subcommand( - SubCommand::with_name("tags") - .about("Generate a list of tags") - .arg(&scope_arg) - .arg(&time_arg) - .arg(&quiet_arg) - .arg(&paths_file_arg) - .arg(&paths_arg), - ) - .subcommand( - SubCommand::with_name("test") - .alias("t") - .about("Run a parser's tests") - .arg( - Arg::with_name("filter") - .long("filter") - .short("f") - .takes_value(true) - .help("[DEPRECATED in favor of include]\nOnly run corpus test cases whose name includes the given string"), - ) - .arg( - Arg::with_name("include") - .long("include") - .short("i") - .takes_value(true) - .help("Only run corpus test cases whose name matches the given regex"), - ) - .arg( - Arg::with_name("exclude") - .long("exclude") - .short("e") - .takes_value(true) - .help( - "Only run corpus test cases whose name does not match the given regex", - ), - ) - .arg( - Arg::with_name("update") - .long("update") - .short("u") - .help("Update all syntax trees in corpus files with current parser output"), - ) - .arg(&debug_arg) - .arg(&debug_build_arg) - .arg(&debug_graph_arg) - .arg(&wasm_arg) - .arg(&apply_all_captures_arg), - ) - .subcommand( - SubCommand::with_name("highlight") - .about("Highlight a file") - .arg( - Arg::with_name("html") - .help("Generate highlighting as an HTML document") - .long("html") - .short("H"), - ) - .arg( - Arg::with_name("check") - .help("Check that highlighting captures conform strictly to standards") - .long("check"), - ) - .arg( - Arg::with_name("captures-path") - .help("Path to a file with captures") - .long("captures-path") - .takes_value(true), - ) - .arg( - Arg::with_name("query-paths") - .help("Paths to files with queries") - .long("query-paths") - .takes_value(true) - .multiple(true) - .number_of_values(1), - ) - .arg(&scope_arg) - .arg(&time_arg) - .arg(&quiet_arg) - .arg(&paths_file_arg) - .arg(&paths_arg) - .arg(&apply_all_captures_arg), - ) - .subcommand( - SubCommand::with_name("build-wasm") - .alias("bw") - .about("Compile a parser to WASM") - .arg( - Arg::with_name("docker").long("docker").help( - "Run emscripten via docker or podman even if it is installed locally", - ), - ) - .arg(Arg::with_name("path").index(1).multiple(true)), - ) - .subcommand( - SubCommand::with_name("playground") - .alias("play") - .alias("pg") - .alias("web-ui") - .about("Start local playground for a parser in the browser") - .arg( - Arg::with_name("quiet") - .long("quiet") - .short("q") - .help("Don't open in default browser"), - ), - ) - .subcommand( - SubCommand::with_name("dump-languages") - .about("Print info about all known language parsers"), - ) - .get_matches(); + .version(version) + .subcommand_required(true) + .arg_required_else_help(true) + .disable_help_subcommand(true) + .disable_colored_help(false); + let cli = Commands::augment_subcommands(cli); + + let command = Commands::from_arg_matches(&cli.get_matches())?; let current_dir = env::current_dir().unwrap(); let config = Config::load()?; let mut loader = loader::Loader::new()?; - match matches.subcommand() { - ("init-config", Some(_)) => { + match command { + Commands::InitConfig(_) => { if let Ok(Some(config_path)) = Config::find_config_file() { return Err(anyhow!( "Remove your existing config file first: {}", @@ -355,175 +374,72 @@ fn run() -> Result<()> { ); } - ("generate", Some(matches)) => { - let grammar_path = matches.value_of("grammar-path"); - let debug_build = matches.is_present("debug-build"); - let build = matches.is_present("build"); - let libdir = matches.value_of("libdir"); - let js_runtime = matches.value_of("js-runtime"); - let report_symbol_name = matches.value_of("report-states-for-rule").or_else(|| { - if matches.is_present("report-states") { - Some("") - } else { - None - } - }); - if matches.is_present("log") { + Commands::Generate(generate_options) => { + if generate_options.log { logger::init(); } - let abi_version = matches.value_of("abi-version").map_or( - Ok::<_, Error>(DEFAULT_GENERATE_ABI_VERSION), - |version| { - Ok(if version == "latest" { + let abi_version = match generate_options.abi_version { + Some(ref version) => { + if version == "latest" { tree_sitter::LANGUAGE_VERSION } else { - version - .parse() - .with_context(|| "invalid abi version flag")? - }) - }, - )?; - let generate_bindings = !matches.is_present("no-bindings"); + version.parse().expect("invalid abi version flag") + } + } + None => DEFAULT_GENERATE_ABI_VERSION, + }; generate::generate_parser_in_directory( ¤t_dir, - grammar_path, + generate_options.grammar_path.as_deref(), abi_version, - generate_bindings, - report_symbol_name, - js_runtime, + !generate_options.no_bindings, + generate_options.report_states_for_rule.as_deref(), + generate_options.js_runtime.as_deref(), )?; - if build { - if let Some(path) = libdir { + if generate_options.build { + if let Some(path) = generate_options.libdir { loader = loader::Loader::with_parser_lib_path(PathBuf::from(path)); } - loader.use_debug_build(debug_build); + loader.use_debug_build(generate_options.debug_build); loader.languages_at_path(¤t_dir)?; } } - ("test", Some(matches)) => { - let debug = matches.is_present("debug"); - let debug_graph = matches.is_present("debug-graph"); - let debug_build = matches.is_present("debug-build"); - let update = matches.is_present("update"); - let filter = matches.value_of("filter"); - let include: Option = - matches.value_of("include").and_then(|s| Regex::new(s).ok()); - let exclude: Option = - matches.value_of("exclude").and_then(|s| Regex::new(s).ok()); - let apply_all_captures = matches.is_present("apply-all-captures"); - - if debug { - // For augmenting debug logging in external scanners - env::set_var("TREE_SITTER_DEBUG", "1"); - } - - loader.use_debug_build(debug_build); - - let mut parser = Parser::new(); - - #[cfg(feature = "wasm")] - if matches.is_present("wasm") { - let engine = tree_sitter::wasmtime::Engine::default(); - parser - .set_wasm_store(tree_sitter::WasmStore::new(engine.clone()).unwrap()) - .unwrap(); - loader.use_wasm(engine); - } - - let languages = loader.languages_at_path(¤t_dir)?; - let language = languages - .first() - .ok_or_else(|| anyhow!("No language found"))?; - parser.set_language(language)?; - - let test_dir = current_dir.join("test"); - - // Run the corpus tests. Look for them at two paths: `test/corpus` and `corpus`. - let mut test_corpus_dir = test_dir.join("corpus"); - if !test_corpus_dir.is_dir() { - test_corpus_dir = current_dir.join("corpus"); - } - if test_corpus_dir.is_dir() { - let mut opts = TestOptions { - path: test_corpus_dir, - debug, - debug_graph, - filter, - include, - exclude, - update, - }; - - test::run_tests_at_path(&mut parser, &mut opts)?; - } - - // Check that all of the queries are valid. - test::check_queries_at_path(language, ¤t_dir.join("queries"))?; - - // Run the syntax highlighting tests. - let test_highlight_dir = test_dir.join("highlight"); - if test_highlight_dir.is_dir() { - let mut highlighter = Highlighter::new(); - highlighter.parser = parser; - test_highlight::test_highlights( - &loader, - &mut highlighter, - &test_highlight_dir, - apply_all_captures, - )?; - parser = highlighter.parser; - } - - let test_tag_dir = test_dir.join("tags"); - if test_tag_dir.is_dir() { - let mut tags_context = TagsContext::new(); - tags_context.parser = parser; - test_tags::test_tags(&loader, &mut tags_context, &test_tag_dir)?; - } - } - - ("parse", Some(matches)) => { - let debug = matches.is_present("debug"); - let debug_graph = matches.is_present("debug-graph"); - let debug_build = matches.is_present("debug-build"); - - let output = if matches.is_present("output-dot") { + Commands::Parse(parse_options) => { + let output = if parse_options.output_dot { ParseOutput::Dot - } else if matches.is_present("output-xml") { + } else if parse_options.output_xml { ParseOutput::Xml - } else if matches.is_present("quiet") { + } else if parse_options.quiet { ParseOutput::Quiet } else { ParseOutput::Normal }; - let encoding = - matches - .values_of("encoding") - .map_or(Ok(None), |mut e| match e.next() { - Some("utf16") => Ok(Some(ffi::TSInputEncodingUTF16)), - Some("utf8") => Ok(Some(ffi::TSInputEncodingUTF8)), - Some(_) => Err(anyhow!("Invalid encoding. Expected one of: utf8, utf16")), - None => Ok(None), - })?; + let encoding = if let Some(encoding) = parse_options.encoding { + match encoding.as_str() { + "utf16" => Some(ffi::TSInputEncodingUTF16), + "utf8" => Some(ffi::TSInputEncodingUTF8), + _ => return Err(anyhow!("Invalid encoding. Expected one of: utf8, utf16")), + } + } else { + None + }; - let time = matches.is_present("time"); - let edits = matches - .values_of("edits") - .map_or(Vec::new(), std::iter::Iterator::collect); + let time = parse_options.time; + let edits = parse_options.edits.unwrap_or_default(); let cancellation_flag = util::cancel_on_signal(); let mut parser = Parser::new(); - if debug { + if parse_options.debug { // For augmenting debug logging in external scanners env::set_var("TREE_SITTER_DEBUG", "1"); } - loader.use_debug_build(debug_build); + loader.use_debug_build(parse_options.debug_build); #[cfg(feature = "wasm")] - if matches.is_present("wasm") { + if parse_options.wasm { let engine = tree_sitter::wasmtime::Engine::default(); parser .set_wasm_store(tree_sitter::WasmStore::new(engine.clone()).unwrap()) @@ -531,25 +447,23 @@ fn run() -> Result<()> { loader.use_wasm(engine); } - let timeout = matches - .value_of("timeout") - .map_or(0, |t| t.parse::().unwrap()); + let timeout = parse_options.timeout.unwrap_or_default(); - let paths = collect_paths(matches.value_of("paths-file"), matches.values_of("paths"))?; + let paths = collect_paths(parse_options.paths_file.as_deref(), parse_options.paths)?; let max_path_length = paths.iter().map(|p| p.chars().count()).max().unwrap_or(0); let mut has_error = false; let loader_config = config.get()?; loader.find_all_languages(&loader_config)?; - let should_track_stats = matches.is_present("stat"); + let should_track_stats = parse_options.stat; let mut stats = parse::Stats::default(); for path in paths { let path = Path::new(&path); let language = - loader.select_language(path, ¤t_dir, matches.value_of("scope"))?; + loader.select_language(path, ¤t_dir, parse_options.scope.as_deref())?; parser .set_language(&language) .context("incompatible language")?; @@ -557,13 +471,13 @@ fn run() -> Result<()> { let opts = ParseFileOptions { language: language.clone(), path, - edits: &edits, + edits: &edits.iter().map(|s| s.as_str()).collect::>(), max_path_length, output, print_time: time, timeout, - debug, - debug_graph, + debug: parse_options.debug, + debug_graph: parse_options.debug_graph, cancellation_flag: Some(&cancellation_flag), encoding, }; @@ -593,70 +507,126 @@ fn run() -> Result<()> { } } - ("query", Some(matches)) => { - let ordered_captures = matches.values_of("captures").is_some(); - let quiet = matches.values_of("quiet").is_some(); - let time = matches.values_of("time").is_some(); - let paths = collect_paths(matches.value_of("paths-file"), matches.values_of("paths"))?; + Commands::Test(test_options) => { + if test_options.debug { + // For augmenting debug logging in external scanners + env::set_var("TREE_SITTER_DEBUG", "1"); + } + + loader.use_debug_build(test_options.debug_build); + + let mut parser = Parser::new(); + + #[cfg(feature = "wasm")] + if test_options.wasm { + let engine = tree_sitter::wasmtime::Engine::default(); + parser + .set_wasm_store(tree_sitter::WasmStore::new(engine.clone()).unwrap()) + .unwrap(); + loader.use_wasm(engine); + } + + let languages = loader.languages_at_path(¤t_dir)?; + let language = languages + .first() + .ok_or_else(|| anyhow!("No language found"))?; + parser.set_language(language)?; + + let test_dir = current_dir.join("test"); + + // Run the corpus tests. Look for them at two paths: `test/corpus` and `corpus`. + let mut test_corpus_dir = test_dir.join("corpus"); + if !test_corpus_dir.is_dir() { + test_corpus_dir = current_dir.join("corpus"); + } + if test_corpus_dir.is_dir() { + let mut opts = TestOptions { + path: test_corpus_dir, + debug: test_options.debug, + debug_graph: test_options.debug_graph, + filter: test_options.filter.as_deref(), + include: test_options.include, + exclude: test_options.exclude, + update: test_options.update, + }; + + test::run_tests_at_path(&mut parser, &mut opts)?; + } + + // Check that all of the queries are valid. + test::check_queries_at_path(language, ¤t_dir.join("queries"))?; + + // Run the syntax highlighting tests. + let test_highlight_dir = test_dir.join("highlight"); + if test_highlight_dir.is_dir() { + let mut highlighter = Highlighter::new(); + highlighter.parser = parser; + test_highlight::test_highlights( + &loader, + &mut highlighter, + &test_highlight_dir, + test_options.apply_all_captures, + )?; + parser = highlighter.parser; + } + + let test_tag_dir = test_dir.join("tags"); + if test_tag_dir.is_dir() { + let mut tags_context = TagsContext::new(); + tags_context.parser = parser; + test_tags::test_tags(&loader, &mut tags_context, &test_tag_dir)?; + } + } + + Commands::Query(query_options) => { + let paths = collect_paths(query_options.paths_file.as_deref(), query_options.paths)?; let loader_config = config.get()?; loader.find_all_languages(&loader_config)?; let language = loader.select_language( Path::new(&paths[0]), ¤t_dir, - matches.value_of("scope"), + query_options.scope.as_deref(), )?; - let query_path = Path::new(matches.value_of("query-path").unwrap()); - let byte_range = matches.value_of("byte-range").and_then(|arg| { - let mut parts = arg.split(':'); + let query_path = Path::new(&query_options.query_path); + + let byte_range = query_options.byte_range.as_ref().and_then(|range| { + let mut parts = range.split(':'); let start = parts.next()?.parse().ok()?; let end = parts.next().unwrap().parse().ok()?; Some(start..end) }); - let point_range = matches.value_of("row-range").and_then(|arg| { - let mut parts = arg.split(':'); + let point_range = query_options.row_range.as_ref().and_then(|range| { + let mut parts = range.split(':'); let start = parts.next()?.parse().ok()?; let end = parts.next().unwrap().parse().ok()?; Some(Point::new(start, 0)..Point::new(end, 0)) }); - let should_test = matches.is_present("test"); + query::query_files_at_paths( &language, paths, query_path, - ordered_captures, + query_options.captures, byte_range, point_range, - should_test, - quiet, - time, + query_options.test, + query_options.quiet, + query_options.time, )?; } - ("tags", Some(matches)) => { - let loader_config = config.get()?; - loader.find_all_languages(&loader_config)?; - let paths = collect_paths(matches.value_of("paths-file"), matches.values_of("paths"))?; - tags::generate_tags( - &loader, - matches.value_of("scope"), - &paths, - matches.is_present("quiet"), - matches.is_present("time"), - )?; - } - - ("highlight", Some(matches)) => { + Commands::Highlight(highlight_options) => { let theme_config: tree_sitter_cli::highlight::ThemeConfig = config.get()?; loader.configure_highlights(&theme_config.theme.highlight_names); let loader_config = config.get()?; loader.find_all_languages(&loader_config)?; - let time = matches.is_present("time"); - let quiet = matches.is_present("quiet"); - let html_mode = quiet || matches.is_present("html"); - let should_check = matches.is_present("check"); - let paths = collect_paths(matches.value_of("paths-file"), matches.values_of("paths"))?; - let apply_all_captures = matches.is_present("apply-all-captures"); + let quiet = highlight_options.quiet; + let html_mode = quiet || highlight_options.html; + let paths = collect_paths( + highlight_options.paths_file.as_deref(), + highlight_options.paths, + )?; if html_mode && !quiet { println!("{}", highlight::HTML_HEADER); @@ -665,20 +635,13 @@ fn run() -> Result<()> { let cancellation_flag = util::cancel_on_signal(); let mut language = None; - if let Some(scope) = matches.value_of("scope") { + if let Some(scope) = highlight_options.scope.as_deref() { language = loader.language_configuration_for_scope(scope)?; if language.is_none() { return Err(anyhow!("Unknown scope '{scope}'")); } } - let query_paths = matches.values_of("query-paths").map(|e| { - e.collect::>() - .into_iter() - .map(std::string::ToString::to_string) - .collect::>() - }); - for path in paths { let path = Path::new(&path); let (language, language_config) = match language.clone() { @@ -695,11 +658,11 @@ fn run() -> Result<()> { if let Some(highlight_config) = language_config.highlight_config( language, - apply_all_captures, - query_paths.as_deref(), + highlight_options.apply_all_captures, + highlight_options.query_paths.as_deref(), )? { - if should_check { - let names = if let Some(path) = matches.value_of("captures-path") { + if highlight_options.check { + let names = if let Some(path) = highlight_options.captures_path.as_deref() { let path = Path::new(path); let file = fs::read_to_string(path)?; let capture_names = file @@ -740,7 +703,7 @@ fn run() -> Result<()> { &source, highlight_config, quiet, - time, + highlight_options.time, Some(&cancellation_flag), )?; } else { @@ -749,7 +712,7 @@ fn run() -> Result<()> { &theme_config.theme, &source, highlight_config, - time, + highlight_options.time, Some(&cancellation_flag), )?; } @@ -763,22 +726,35 @@ fn run() -> Result<()> { } } - ("build-wasm", Some(matches)) => { - let grammar_path = current_dir.join(matches.value_of("path").unwrap_or("")); + Commands::Tags(tags_options) => { + let loader_config = config.get()?; + loader.find_all_languages(&loader_config)?; + let paths = collect_paths(tags_options.paths_file.as_deref(), tags_options.paths)?; + tags::generate_tags( + &loader, + tags_options.scope.as_deref(), + &paths, + tags_options.quiet, + tags_options.time, + )?; + } + + Commands::BuildWasm(wasm_options) => { + let grammar_path = current_dir.join(wasm_options.path.unwrap_or_default()); wasm::compile_language_to_wasm( &loader, &grammar_path, ¤t_dir, - matches.is_present("docker"), + wasm_options.docker, )?; } - ("playground", Some(matches)) => { - let open_in_browser = !matches.is_present("quiet"); + Commands::Playground(playground_options) => { + let open_in_browser = !playground_options.quiet; playground::serve(¤t_dir, open_in_browser)?; } - ("dump-languages", Some(_)) => { + Commands::DumpLanguages(_) => { let loader_config = config.get()?; loader.find_all_languages(&loader_config)?; for (configuration, language_path) in loader.get_all_language_configurations() { @@ -800,17 +776,44 @@ fn run() -> Result<()> { ); } } - - _ => unreachable!(), } Ok(()) } -fn collect_paths<'a>( - paths_file: Option<&str>, - paths: Option>, -) -> Result> { +#[must_use] +const fn get_styles() -> clap::builder::Styles { + clap::builder::Styles::styled() + .usage( + Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::Yellow))), + ) + .header( + Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::Yellow))), + ) + .literal(Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)))) + .invalid( + Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::Red))), + ) + .error( + Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::Red))), + ) + .valid( + Style::new() + .bold() + .fg_color(Some(Color::Ansi(AnsiColor::Green))), + ) + .placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White)))) +} + +fn collect_paths(paths_file: Option<&str>, paths: Option>) -> Result> { if let Some(paths_file) = paths_file { return Ok(fs::read_to_string(paths_file) .with_context(|| format!("Failed to read paths file {paths_file}"))? @@ -835,13 +838,14 @@ fn collect_paths<'a>( let mut positive = true; if path.starts_with('!') { positive = false; - path = path.trim_start_matches('!'); + path = path.trim_start_matches('!').to_string(); } - if Path::new(path).exists() { - incorporate_path(path, positive); + if Path::new(&path).exists() { + incorporate_path(&path, positive); } else { - let paths = glob(path).with_context(|| format!("Invalid glob pattern {path:?}"))?; + let paths = + glob(&path).with_context(|| format!("Invalid glob pattern {path:?}"))?; for path in paths { if let Some(path) = path?.to_str() { incorporate_path(path, positive);