Skip to content

Commit

Permalink
Enhance autocomplete generation
Browse files Browse the repository at this point in the history
This PR fixes #5474,

This PR enhances the autocomplete feature that is built-in ly forc. The first
enhancement is to add support to generate a script for [fig](https://fig.io).

The second enhancement is to add support to let each plugin (automatically
through cli_examples) dump its clap configuration. This configuration is shared
to the main `forc` binary which creates a single autocomplete script for the
requested shell that is also aware of every `plugin`. If a plugin uses
`cli_examples!` macro this will automatically inherit this feature without any
additional change.

The third improvement, still under development, is to install automatically the
autocomplete feature instead of printing to the stdout (to address FuelLabs/fuelup#548)
  • Loading branch information
crodas committed Feb 22, 2024
1 parent 3697f9e commit 56a8db0
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 39 deletions.
39 changes: 12 additions & 27 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion forc-plugins/forc-doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repository.workspace = true

[dependencies]
anyhow = "1.0.65"
clap = { version = "4.0.18", features = ["derive"] }
clap = { version = "3", features = ["derive"] }
colored = "2.0.0"
comrak = "0.16"
forc-pkg = { version = "0.50.0", path = "../../forc-pkg" }
Expand Down
3 changes: 3 additions & 0 deletions forc-plugins/forc-tx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ use std::path::PathBuf;
use thiserror::Error;

forc_util::cli_examples! {
{
None
}
{
// This parser has a custom parser
super::Command::try_parse_from_args
Expand Down
3 changes: 1 addition & 2 deletions forc-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ hex = "0.4.3"
paste = "1.0.14"
regex = "1.10.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.73"
serial_test = "3.0.0"
serde_json = "1.0"
sway-core = { version = "0.50.0", path = "../sway-core" }
sway-error = { version = "0.50.0", path = "../sway-error" }
sway-types = { version = "0.50.0", path = "../sway-types" }
Expand Down
164 changes: 161 additions & 3 deletions forc-util/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,145 @@
use clap::{ArgAction, Command};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CommandInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub long_help: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub subcommands: Vec<CommandInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub args: Vec<ArgInfo>,
}

impl CommandInfo {
pub fn new(cmd: &Command) -> Self {
CommandInfo {
name: cmd.get_name().to_owned(),
long_help: cmd.get_after_long_help().map(|s| s.to_string()),
description: cmd.get_about().map(|s| s.to_string()),
subcommands: Self::get_subcommands(cmd),
args: Self::get_args(cmd),
}
}

pub fn to_clap(&self) -> clap::App<'_> {
let mut cmd = Command::new(self.name.as_str());
if let Some(desc) = &self.description {
cmd = cmd.about(desc.as_str());
}
if let Some(long_help) = &self.long_help {
cmd = cmd.after_long_help(long_help.as_str());
}
for subcommand in &self.subcommands {
cmd = cmd.subcommand(subcommand.to_clap());
}
for arg in &self.args {
cmd = cmd.arg(arg.to_clap());
}
cmd
}

fn get_subcommands(cmd: &Command) -> Vec<CommandInfo> {
cmd.get_subcommands()
.map(|subcommand| CommandInfo::new(subcommand))
.collect::<Vec<_>>()
}

fn arg_conflicts(cmd: &Command, arg: &clap::Arg) -> Vec<String> {
let mut res = vec![];

for conflict in cmd.get_arg_conflicts_with(arg) {
if let Some(s) = conflict.get_short() {
res.push(format!("-{}", s));
}

if let Some(l) = conflict.get_long() {
res.push(format!("--{}", l));
}
}

res
}

fn get_args(cmd: &Command) -> Vec<ArgInfo> {
cmd.get_arguments()
.map(|opt| ArgInfo {
name: opt.get_name().to_string(),
short: opt.get_short_and_visible_aliases(),
aliases: opt
.get_long_and_visible_aliases()
.map(|c| c.iter().map(|x| x.to_string()).collect::<Vec<_>>())
.unwrap_or_default(),
help: opt.get_help().map(|s| s.to_string()),
long_help: opt.get_long_help().map(|s| s.to_string()),
conflicts: Self::arg_conflicts(cmd, opt),
is_repeatable: matches!(
opt.get_action(),
ArgAction::Set | ArgAction::Append | ArgAction::Count,
),
})
.collect::<Vec<_>>()
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ArgInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub short: Option<Vec<char>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub aliases: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub long_help: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub conflicts: Vec<String>,
pub is_repeatable: bool,
}

impl ArgInfo {
pub fn to_clap(&self) -> clap::Arg<'_> {
let mut arg = clap::Arg::with_name(self.name.as_str());
if let Some(short) = &self.short {
arg = arg.short(short[0]);
}
if let Some(help) = &self.help {
arg = arg.help(help.as_str());
}
if let Some(long_help) = &self.long_help {
arg = arg.long_help(long_help.as_str());
}
if self.is_repeatable {
arg = arg.multiple(true);
}
arg
}
}

#[macro_export]
// Let the user format the help and parse it from that string into arguments to create the unit test
macro_rules! cli_examples {
($st:path { $( [ $($description:ident)* => $command:stmt ] )* }) => {
forc_util::cli_examples! {
{
$crate::paste::paste! {
use clap::IntoApp;
Some($st::into_app())
}
} {
$crate::paste::paste! {
use clap::Parser;
$st::try_parse_from
Expand All @@ -13,15 +149,14 @@ macro_rules! cli_examples {
}
}
};
( $code:block { $( [ $($description:ident)* => $command:stmt ] )* }) => {
( $into_app:block $parser:block { $( [ $($description:ident)* => $command:stmt ] )* }) => {
$crate::paste::paste! {
#[cfg(test)]
mod cli_parsing {
$(
#[test]
fn [<$($description:lower _)*:snake example>] () {

let cli_parser = $code;
let cli_parser = $parser;
let mut args = parse_args($command);
if cli_parser(args.clone()).is_err() {
// Failed to parse, it maybe a plugin. To execute a plugin the first argument needs to be removed, `forc`.
Expand Down Expand Up @@ -101,11 +236,34 @@ macro_rules! cli_examples {
}
}

mod cli_definition {
/// Dump the CLI definition to the stdout
pub(crate) fn dump() {
std::env::set_var("CLI_DUMP_DEFINITION", "");

if let Some(mut cmd) = $into_app {
forc_util::serde_json::to_writer_pretty(
std::io::stdout(),
&forc_util::cli::CommandInfo::new(&cmd)
).unwrap();
std::process::exit(0);
}
}
}

/// Show the long help for the current app
///
/// This function is being called automatically, so if CLI_DUMP_DEFINITION is set to 1, it
/// will dump the definition of the CLI. Otherwise, it would have to be manually invoked by
/// the developer
fn help() -> &'static str {
if std::env::var("CLI_DUMP_DEFINITION") == Ok("1".to_string()) {
cli_definition::dump();
}
Box::leak(format!("{}\n{}", forc_util::ansi_term::Colour::Yellow.paint("EXAMPLES:"), examples()).into_boxed_str())
}

/// Returns the examples for the command
pub fn examples() -> &'static str {
Box::leak( [
$(
Expand Down
2 changes: 1 addition & 1 deletion forc-util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub mod cli;
pub use ansi_term;
pub use paste;
pub use regex::Regex;
pub use serial_test;
pub use serde_json;

pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out";
pub const DEFAULT_ERROR_EXIT_CODE: u8 = 1;
Expand Down
3 changes: 2 additions & 1 deletion forc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ansi_term = "0.12"
anyhow = "1.0.41"
clap = { version = "3.1", features = ["cargo", "derive", "env"] }
clap_complete = "3.1"
clap_complete_fig = "3.1"
forc-pkg = { version = "0.50.0", path = "../forc-pkg" }
forc-test = { version = "0.50.0", path = "../forc-test" }
forc-tracing = { version = "0.50.0", path = "../forc-tracing" }
Expand All @@ -30,7 +31,7 @@ fs_extra = "1.2"
fuel-asm = { workspace = true }
hex = "0.4.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.73"
serde_json = "1"
sway-core = { version = "0.50.0", path = "../sway-core" }
sway-error = { version = "0.50.0", path = "../sway-error" }
sway-types = { version = "0.50.0", path = "../sway-types" }
Expand Down
Loading

0 comments on commit 56a8db0

Please sign in to comment.