diff --git a/Cargo.lock b/Cargo.lock index e106b5c3..065b737a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,15 @@ dependencies = [ "terminal_size", ] +[[package]] +name = "clap_complete" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4110a1e6af615a9e6d0a36f805d5c99099f8bab9b8042f5bc1fa220a4a89e36f" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.3.12" @@ -1346,6 +1355,7 @@ dependencies = [ "assert_cmd", "async-scoped", "clap", + "clap_complete", "directories", "env_logger", "futures", diff --git a/Cargo.toml b/Cargo.toml index 8661b192..f47120cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ assert_cmd = "2.0" async-scoped = { version = "0.7.1", features = ["use-tokio"] } cfg-if = "1.0.0" clap = { version = "4.3", features = [ "env" ] } +clap_complete = "4.4.1" criterion = "0.5" directories = "5.0" env_logger = "0.10" diff --git a/README.md b/README.md index d510febc..0448b4c5 100644 --- a/README.md +++ b/README.md @@ -171,10 +171,11 @@ CLI app for Topiary, the universal code formatter. Usage: topiary [OPTIONS] Commands: - fmt Format inputs - vis Visualise the input's Tree-sitter parse tree - cfg Print the current configuration - help Print this message or the help of the given subcommand(s) + format Format inputs + visualise Visualise the input's Tree-sitter parse tree + config Print the current configuration + completion Generate shell completion script + help Print this message or the help of the given subcommand(s) Options: -C, --configuration @@ -210,11 +211,11 @@ Options: #### Format - + ``` Format inputs -Usage: topiary fmt [OPTIONS] <--language |FILES> +Usage: topiary format [OPTIONS] <--language |FILES> Arguments: [FILES]... @@ -263,23 +264,23 @@ Options: -h, --help Print help (see a summary with '-h') ``` - + When formatting inputs from disk, language selection is detected from the input files' extensions. To format standard input, you must specify the `--language` and, optionally, `--query` arguments, omitting any input files. -Note: `format` is a recognised alias of the `fmt` subcommand. +Note: `fmt` is a recognised alias of the `format` subcommand. #### Visualise - + ``` Visualise the input's Tree-sitter parse tree -Usage: topiary vis [OPTIONS] <--language |FILE> +Usage: topiary visualise [OPTIONS] <--language |FILE> Arguments: [FILE] @@ -331,24 +332,24 @@ Options: -h, --help Print help (see a summary with '-h') ``` - + When visualising inputs from disk, language selection is detected from the input file's extension. To visualise standard input, you must specify the `--language` and, optionally, `--query` arguments, omitting the input file. The visualisation output is written to standard out. -Note: `visualise`, `visualize` and `view` are recognised aliases of the -`vis` subcommand. +Note: `vis`, `visualize` and `view` are recognised aliases of the +`visualise` subcommand. #### Configuration - + ``` Print the current configuration -Usage: topiary cfg [OPTIONS] +Usage: topiary config [OPTIONS] Options: -C, --configuration @@ -376,12 +377,65 @@ Options: -h, --help Print help (see a summary with '-h') ``` - + Please refer to the [Configuration](#configuration-1) section below to understand the different sources of configuration and collation modes. -Note: `config` is a recognised alias of the `cfg` subcommand. +Note: `cfg` is a recognised alias of the `config` subcommand. + +#### Shell Completion + +Shell completion scripts for Topiary can be generated with the +`completion` subcommand. The output of which can be sourced into your +shell session or profile, as required. + + + +``` +Generate shell completion script + +Usage: topiary completion [OPTIONS] [SHELL] + +Arguments: + [SHELL] + Shell (omit to detect from the environment) + + [possible values: bash, elvish, fish, powershell, zsh] + +Options: + -C, --configuration + Configuration file + + [env: TOPIARY_CONFIG_FILE] + + --configuration-collation + Configuration collation mode + + [env: TOPIARY_CONFIG_COLLATION] + [default: merge] + + Possible values: + - merge: When multiple sources of configuration are available, matching items are + updated from the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items + (including collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority + source is taken. All values from lower priority sources are discarded + + -v, --verbose... + Logging verbosity (increased per occurrence) + + -h, --help + Print help (see a summary with '-h') +``` + + +For example, in Bash: + +```bash +source <(topiary completion) +``` #### Logging diff --git a/default.nix b/default.nix index 25aa41f1..396a3475 100644 --- a/default.nix +++ b/default.nix @@ -1,16 +1,17 @@ -{ - pkgs, - system, - advisory-db, - crane, - rust-overlay, - nix-filter, -}: let - wasmRustVersion = "1.67.1"; +{ pkgs +, system +, advisory-db +, crane +, rust-overlay +, nix-filter +, +}: +let + wasmRustVersion = "1.70.0"; wasmTarget = "wasm32-unknown-unknown"; rustWithWasmTarget = pkgs.rust-bin.stable.${wasmRustVersion}.default.override { - targets = [wasmTarget]; + targets = [ wasmTarget ]; }; craneLib = crane.mkLib pkgs; @@ -50,83 +51,84 @@ # Instead, we just want to update the scope that crane will use by appending # our specific toolchain there. craneLibWasm = craneLib.overrideToolchain rustWithWasmTarget; -in { +in +{ clippy = craneLib.cargoClippy (commonArgs // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "-- --deny warnings"; - }); + inherit cargoArtifacts; + cargoClippyExtraArgs = "-- --deny warnings"; + }); clippy-wasm = craneLibWasm.cargoClippy (commonArgs // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "-p topiary-playground --target ${wasmTarget} -- --deny warnings"; - }); + inherit cargoArtifacts; + cargoClippyExtraArgs = "-p topiary-playground --target ${wasmTarget} -- --deny warnings"; + }); fmt = craneLib.cargoFmt commonArgs; audit = craneLib.cargoAudit (commonArgs // { - inherit advisory-db; - }); + inherit advisory-db; + }); benchmark = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; - cargoTestCommand = "cargo bench --profile release"; - }); + inherit cargoArtifacts; + cargoTestCommand = "cargo bench --profile release"; + }); topiary-lib = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; - pname = "topiary-lib"; - cargoExtraArgs = "-p topiary"; - }); + inherit cargoArtifacts; + pname = "topiary-lib"; + cargoExtraArgs = "-p topiary"; + }); topiary-cli = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; - pname = "topiary"; - cargoExtraArgs = "-p topiary-cli"; - postInstall = '' - install -Dm444 languages/* -t $out/share/languages - ''; - - # Set TOPIARY_LANGUAGE_DIR to the Nix store - # for the build - TOPIARY_LANGUAGE_DIR = "${placeholder "out"}/share/languages"; - - # Set TOPIARY_LANGUAGE_DIR to the working directory - # in a development shell - shellHook = '' - export TOPIARY_LANGUAGE_DIR=$PWD/languages - ''; - }); + inherit cargoArtifacts; + pname = "topiary"; + cargoExtraArgs = "-p topiary-cli"; + postInstall = '' + install -Dm444 languages/* -t $out/share/languages + ''; + + # Set TOPIARY_LANGUAGE_DIR to the Nix store + # for the build + TOPIARY_LANGUAGE_DIR = "${placeholder "out"}/share/languages"; + + # Set TOPIARY_LANGUAGE_DIR to the working directory + # in a development shell + shellHook = '' + export TOPIARY_LANGUAGE_DIR=$PWD/languages + ''; + }); topiary-playground = craneLibWasm.buildPackage (commonArgs // { - inherit cargoArtifacts; - pname = "topiary-playground"; - cargoExtraArgs = "-p topiary-playground --no-default-features --target ${wasmTarget}"; - - # Tests currently need to be run via `cargo wasi` which - # isn't packaged in nixpkgs yet... - doCheck = false; - - postInstall = '' - echo 'Removing unneeded dir' - rm -rf $out/lib - echo 'Running wasm-bindgen' - wasm-bindgen --version - wasm-bindgen --target web --out-dir $out target/wasm32-unknown-unknown/release/topiary_playground.wasm; - echo 'Running wasm-opt' - wasm-opt --version - wasm-opt -Oz -o $out/output.wasm $out/topiary_playground_bg.wasm - echo 'Overwriting topiary_playground_bg.wasm with the optimized file' - mv $out/output.wasm $out/topiary_playground_bg.wasm - echo 'Extracting custom build outputs' - export LANGUAGES_EXPORT="$(ls -t target/wasm32-unknown-unknown/release/build/topiary-playground-*/out/languages_export.ts | head -1)" - cp $LANGUAGES_EXPORT $out/ - ''; - }); + inherit cargoArtifacts; + pname = "topiary-playground"; + cargoExtraArgs = "-p topiary-playground --no-default-features --target ${wasmTarget}"; + + # Tests currently need to be run via `cargo wasi` which + # isn't packaged in nixpkgs yet... + doCheck = false; + + postInstall = '' + echo 'Removing unneeded dir' + rm -rf $out/lib + echo 'Running wasm-bindgen' + wasm-bindgen --version + wasm-bindgen --target web --out-dir $out target/wasm32-unknown-unknown/release/topiary_playground.wasm; + echo 'Running wasm-opt' + wasm-opt --version + wasm-opt -Oz -o $out/output.wasm $out/topiary_playground_bg.wasm + echo 'Overwriting topiary_playground_bg.wasm with the optimized file' + mv $out/output.wasm $out/topiary_playground_bg.wasm + echo 'Extracting custom build outputs' + export LANGUAGES_EXPORT="$(ls -t target/wasm32-unknown-unknown/release/build/topiary-playground-*/out/languages_export.ts | head -1)" + cp $LANGUAGES_EXPORT $out/ + ''; + }); } diff --git a/topiary-cli/Cargo.toml b/topiary-cli/Cargo.toml index dd144209..365118c1 100644 --- a/topiary-cli/Cargo.toml +++ b/topiary-cli/Cargo.toml @@ -28,6 +28,7 @@ path = "src/main.rs" # Eventually we will want to dynamically load them, like Helix does. async-scoped = { workspace = true } clap = { workspace = true, features = ["derive", "env", "wrap_help"] } +clap_complete = { workspace = true } directories = { workspace = true } env_logger = { workspace = true } futures = { workspace = true } diff --git a/topiary-cli/src/cli.rs b/topiary-cli/src/cli.rs index 7f15e650..4df9ced5 100644 --- a/topiary-cli/src/cli.rs +++ b/topiary-cli/src/cli.rs @@ -1,7 +1,8 @@ //! Command line interface argument parsing. -use clap::{ArgAction, ArgGroup, Args, Parser, Subcommand}; -use std::path::PathBuf; +use clap::{ArgAction, ArgGroup, Args, CommandFactory, Parser, Subcommand}; +use clap_complete::{generate, shells::Shell}; +use std::{io::stdout, path::PathBuf}; use log::LevelFilter; use topiary::SupportedLanguage; @@ -119,11 +120,12 @@ pub struct AtLeastOneInput { pub files: Vec, } +// NOTE When changing the subcommands, please update verify-documented-usage.sh respectively. #[derive(Debug, Subcommand)] pub enum Commands { /// Format inputs - #[command(alias = "format", display_order = 1)] - Fmt { + #[command(alias = "fmt", display_order = 1)] + Format { /// Consume as much as possible in the presence of parsing errors #[arg(short, long)] tolerate_parsing_errors: bool, @@ -137,8 +139,8 @@ pub enum Commands { }, /// Visualise the input's Tree-sitter parse tree - #[command(aliases = &["visualise", "visualize", "view"], display_order = 2)] - Vis { + #[command(aliases = &["vis", "visualize", "view"], display_order = 2)] + Visualise { /// Visualisation format #[arg(short, long, default_value = "dot")] format: visualisation::Format, @@ -148,8 +150,15 @@ pub enum Commands { }, /// Print the current configuration - #[command(alias = "config", display_order = 3)] - Cfg, + #[command(alias = "cfg", display_order = 3)] + Config, + + /// Generate shell completion script + #[command(display_order = 100)] + Completion { + /// Shell (omit to detect from the environment) + shell: Option, + }, } /// Given a vector of paths, recursively expand those that identify as directories, in place @@ -192,7 +201,7 @@ pub fn get_args() -> CLIResult { // file, but that's going to be done sooner-or-later by Topiary, so there's no need. match &mut args.command { - Commands::Fmt { + Commands::Format { inputs: AtLeastOneInput { files, .. }, .. } => { @@ -206,7 +215,7 @@ pub fn get_args() -> CLIResult { traverse_fs(files)?; } - Commands::Vis { + Commands::Visualise { input: ExactlyOneInput { file: Some(file), .. }, @@ -221,8 +230,25 @@ pub fn get_args() -> CLIResult { } } + // Attempt to detect shell from environment, when omitted + Commands::Completion { shell: None } => { + let detected_shell = Shell::from_env().ok_or(TopiaryError::Bin( + "Cannot detect shell from environment".into(), + None, + ))?; + + args.command = Commands::Completion { + shell: Some(detected_shell), + }; + } + _ => {} } Ok(args) } + +/// Generate shell completion script, for the given shell, and output to stdout +pub fn completion(shell: Shell) { + generate(shell, &mut Cli::command(), "topiary", &mut stdout()); +} diff --git a/topiary-cli/src/main.rs b/topiary-cli/src/main.rs index 32ef72e0..c3f733cd 100644 --- a/topiary-cli/src/main.rs +++ b/topiary-cli/src/main.rs @@ -41,7 +41,7 @@ async fn run() -> CLIResult<()> { // Delegate by subcommand match args.command { - Commands::Fmt { + Commands::Format { tolerate_parsing_errors, skip_idempotence, inputs, @@ -116,7 +116,7 @@ async fn run() -> CLIResult<()> { } } - Commands::Vis { format, input } => { + Commands::Visualise { format, input } => { // We are guaranteed (by clap) to have exactly one input, so it's safe to unwrap let input = Inputs::new(&config, &input).next().unwrap()?; let output = OutputFile::Stdout; @@ -147,10 +147,15 @@ async fn run() -> CLIResult<()> { )?; } - Commands::Cfg => { - // Output collated configuration as TOML, with annotations about how we got there + Commands::Config => { + // Output collated configuration gtas TOML, with annotations about how we got there print!("{annotations}\n{config}"); } + + Commands::Completion { shell } => { + // The CLI parser fails if no shell is provided/detected, so it's safe to unwrap here + cli::completion(shell.unwrap()); + } } Ok(()) diff --git a/verify-documented-usage.sh b/verify-documented-usage.sh index a7945225..fd0e922d 100755 --- a/verify-documented-usage.sh +++ b/verify-documented-usage.sh @@ -40,7 +40,7 @@ diff-usage() { } main() { - local -a subcommands=(ROOT fmt vis cfg) + local -a subcommands=(ROOT format visualise config completion) local _diff local _subcommand