diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d97601187..cb87fa9e00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * New command `jj config unset` that unsets config values. For example, `jj config unset --user user.name`. +* `jj help` now has the flag `--keyword` (shorthand `-k`), which can give help + for some keywords (e.g. `jj help -k revsets`). To see a list of the available + keywords you can do `jj help --help`. + ### Fixed bugs * Error on `trunk()` revset resolution is now handled gracefully. diff --git a/cli/src/commands/help.rs b/cli/src/commands/help.rs index 364cbe7fca..0def340ed4 100644 --- a/cli/src/commands/help.rs +++ b/cli/src/commands/help.rs @@ -12,6 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Write as _; +use std::io::Write; + +use clap::builder::PossibleValue; +use clap::builder::StyledStr; +use crossterm::style::Stylize; +use itertools::Itertools; use tracing::instrument; use crate::cli_util::CommandHelper; @@ -24,14 +31,33 @@ use crate::ui::Ui; pub(crate) struct HelpArgs { /// Print help for the subcommand(s) pub(crate) command: Vec, + /// Show help for keywords instead of commands + #[arg( + long, + short = 'k', + conflicts_with = "command", + value_parser = KEYWORDS + .iter() + .map(|k| PossibleValue::new(k.name).help(k.description)) + .collect_vec() + )] + pub(crate) keyword: Option, } #[instrument(skip_all)] pub(crate) fn cmd_help( - _ui: &mut Ui, + ui: &mut Ui, command: &CommandHelper, args: &HelpArgs, ) -> Result<(), CommandError> { + if let Some(name) = &args.keyword { + let keyword = find_keyword(name).expect("clap should check this with `value_parser`"); + ui.request_pager(); + write!(ui.stdout(), "{}", keyword.content)?; + + return Ok(()); + } + let mut args_to_show_help = vec![command.app().get_name()]; args_to_show_help.extend(args.command.iter().map(|s| s.as_str())); args_to_show_help.push("--help"); @@ -47,3 +73,52 @@ pub(crate) fn cmd_help( Err(command_error::cli_error(help_err)) } + +#[derive(Clone)] +struct Keyword { + name: &'static str, + description: &'static str, + content: &'static str, +} + +// TODO: Add all documentation to keywords +// +// Maybe adding some code to build.rs to find all the docs files and build the +// `KEYWORDS` at compile time. +// +// It would be cool to follow the docs hierarchy somehow. +// +// One of the problems would be `config.md`, as it has the same name as a +// subcommand. +// +// TODO: Find a way to render markdown using ANSI escape codes. +// +// Maybe we can steal some ideas from https://github.com/martinvonz/jj/pull/3130 +const KEYWORDS: &[Keyword] = &[ + Keyword { + name: "revsets", + description: "A functional language for selecting a set of revision", + content: include_str!("../../../docs/revsets.md"), + }, + Keyword { + name: "tutorial", + description: "Show a tutorial to get started with jj", + content: include_str!("../../../docs/tutorial.md"), + }, +]; + +fn find_keyword(name: &str) -> Option<&Keyword> { + KEYWORDS.iter().find(|keyword| keyword.name == name) +} + +pub fn show_keyword_hint_after_help() -> StyledStr { + let mut ret = StyledStr::new(); + writeln!( + ret, + "{} list available keywords. Use {} to show help for one of these keywords.", + "'jj help --help'".bold(), + "'jj help -k'".bold(), + ) + .unwrap(); + ret +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 25bf0a32f1..7bf08b2b82 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -73,6 +73,7 @@ use crate::ui::Ui; #[derive(clap::Parser, Clone, Debug)] #[command(disable_help_subcommand = true)] +#[command(after_long_help = help::show_keyword_hint_after_help())] enum Command { Abandon(abandon::AbandonArgs), Backout(backout::BackoutArgs), diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 3a3f4f788e..b72045d60c 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -113,6 +113,9 @@ To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutor **Usage:** `jj [OPTIONS] [COMMAND]` +'jj help --help' list available keywords. Use 'jj help -k' to show help for one of these keywords. + + ###### **Subcommands:** * `abandon` — Abandon a revision @@ -1215,12 +1218,23 @@ Set the URL of a Git remote Print this message or the help of the given subcommand(s) -**Usage:** `jj help [COMMAND]...` +**Usage:** `jj help [OPTIONS] [COMMAND]...` ###### **Arguments:** * `` — Print help for the subcommand(s) +###### **Options:** + +* `-k`, `--keyword ` — Show help for keywords instead of commands + + Possible values: + - `revsets`: + A functional language for selecting a set of revision + - `tutorial`: + Show a tutorial to get started with jj + + ## `jj init` diff --git a/cli/tests/test_help_command.rs b/cli/tests/test_help_command.rs index 8c8d0d12f6..c1ee333dd7 100644 --- a/cli/tests/test_help_command.rs +++ b/cli/tests/test_help_command.rs @@ -84,3 +84,62 @@ fn test_help() { For more information, try '--help'. "#); } + +#[test] +fn test_help_keyword() { + let test_env = TestEnvironment::default(); + + // It should show help for a certain keyword if the `--keyword` flag is present + let help_cmd_stdout = + test_env.jj_cmd_success(test_env.env_root(), &["help", "--keyword", "revsets"]); + // It should be equal to the docs + assert_eq!(help_cmd_stdout, include_str!("../../docs/revsets.md")); + + // It should show help for a certain keyword if the `-k` flag is present + let help_cmd_stdout = test_env.jj_cmd_success(test_env.env_root(), &["help", "-k", "revsets"]); + // It should be equal to the docs + assert_eq!(help_cmd_stdout, include_str!("../../docs/revsets.md")); + + // It should give hints if a similar keyword is present + let help_cmd_stderr = test_env.jj_cmd_cli_error(test_env.env_root(), &["help", "-k", "rev"]); + insta::assert_snapshot!(help_cmd_stderr, @r#" + error: invalid value 'rev' for '--keyword ' + [possible values: revsets, tutorial] + + tip: a similar value exists: 'revsets' + + For more information, try '--help'. + "#); + + // It should give error with a hint if no similar keyword is found + let help_cmd_stderr = + test_env.jj_cmd_cli_error(test_env.env_root(), &["help", "-k", ""]); + insta::assert_snapshot!(help_cmd_stderr, @r#" + error: invalid value '' for '--keyword ' + [possible values: revsets, tutorial] + + For more information, try '--help'. + "#); + + // The keyword flag with no argument should error with a hint + let help_cmd_stderr = test_env.jj_cmd_cli_error(test_env.env_root(), &["help", "-k"]); + insta::assert_snapshot!(help_cmd_stderr, @r#" + error: a value is required for '--keyword ' but none was supplied + [possible values: revsets, tutorial] + + For more information, try '--help'. + "#); + + // It shouldn't show help for a certain keyword if the `--keyword` is not + // present + let help_cmd_stderr = test_env.jj_cmd_cli_error(test_env.env_root(), &["help", "revsets"]); + insta::assert_snapshot!(help_cmd_stderr, @r#" + error: unrecognized subcommand 'revsets' + + tip: some similar subcommands exist: 'resolve', 'prev', 'restore', 'rebase', 'revert' + + Usage: jj [OPTIONS] + + For more information, try '--help'. + "#); +}