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 14, 2024
1 parent 65c1927 commit 1ef6976
Show file tree
Hide file tree
Showing 8 changed files with 253 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
160 changes: 157 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.to_string()));
}

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,8 +236,27 @@ macro_rules! cli_examples {
}
}

mod autocomplete {
pub(crate) fn dump() {
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);
}
}
}

static DUMPING_CLI_DEFINITION: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

fn help() -> &'static str {
use std::sync::atomic::Ordering;
if std::env::var("CLI_DUMP_DEFINITION") == Ok("1".to_string()) {
if DUMPING_CLI_DEFINITION.compare_exchange_weak(false, true, Ordering::SeqCst, Ordering::Relaxed) == Ok(false) {
autocomplete::dump();
}
}
Box::leak(format!("{}\n{}", forc_util::ansi_term::Colour::Yellow.paint("EXAMPLES:"), examples()).into_boxed_str())
}

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 1ef6976

Please sign in to comment.