help: Add a keyword feature

It would be nice to not need to go the documentation website. This aims
to solve that by introducing the concept of keyword to the help
command.

Basically, keywords are things that we want to add help messages to,
but they don't necessarily have an associated subcommand.

For now we only have two keywords:
	- `revsets`: Shows the docs for revsets
	- `tutorial`: Shows the tutorial that is on the documentation

You get the keyword content by tipping `jj help --keyword revsets` or
`jj help -k revsets`.

You can also list the available keywords with `jj help --help`.

It would be nice to have all the documentation on the keywords, maybe
a next commit could do it.
This commit is contained in:
Arthur Grillo 2024-10-12 11:12:38 -03:00 committed by Arthur Grillo
parent c7cd014fe6
commit f2acb98ef2
5 changed files with 155 additions and 2 deletions

View File

@ -84,6 +84,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* New command `jj config unset` that unsets config values. For example, * New command `jj config unset` that unsets config values. For example,
`jj config unset --user user.name`. `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 ### Fixed bugs
* Error on `trunk()` revset resolution is now handled gracefully. * Error on `trunk()` revset resolution is now handled gracefully.

View File

@ -12,6 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 tracing::instrument;
use crate::cli_util::CommandHelper; use crate::cli_util::CommandHelper;
@ -24,14 +31,33 @@ use crate::ui::Ui;
pub(crate) struct HelpArgs { pub(crate) struct HelpArgs {
/// Print help for the subcommand(s) /// Print help for the subcommand(s)
pub(crate) command: Vec<String>, pub(crate) command: Vec<String>,
/// 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<String>,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub(crate) fn cmd_help( pub(crate) fn cmd_help(
_ui: &mut Ui, ui: &mut Ui,
command: &CommandHelper, command: &CommandHelper,
args: &HelpArgs, args: &HelpArgs,
) -> Result<(), CommandError> { ) -> 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()]; 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.extend(args.command.iter().map(|s| s.as_str()));
args_to_show_help.push("--help"); args_to_show_help.push("--help");
@ -47,3 +73,52 @@ pub(crate) fn cmd_help(
Err(command_error::cli_error(help_err)) 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
}

View File

@ -73,6 +73,7 @@ use crate::ui::Ui;
#[derive(clap::Parser, Clone, Debug)] #[derive(clap::Parser, Clone, Debug)]
#[command(disable_help_subcommand = true)] #[command(disable_help_subcommand = true)]
#[command(after_long_help = help::show_keyword_hint_after_help())]
enum Command { enum Command {
Abandon(abandon::AbandonArgs), Abandon(abandon::AbandonArgs),
Backout(backout::BackoutArgs), Backout(backout::BackoutArgs),

View File

@ -113,6 +113,9 @@ To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutor
**Usage:** `jj [OPTIONS] [COMMAND]` **Usage:** `jj [OPTIONS] [COMMAND]`
'jj help --help' list available keywords. Use 'jj help -k' to show help for one of these keywords.
###### **Subcommands:** ###### **Subcommands:**
* `abandon` — Abandon a revision * `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) Print this message or the help of the given subcommand(s)
**Usage:** `jj help [COMMAND]...` **Usage:** `jj help [OPTIONS] [COMMAND]...`
###### **Arguments:** ###### **Arguments:**
* `<COMMAND>` — Print help for the subcommand(s) * `<COMMAND>` — Print help for the subcommand(s)
###### **Options:**
* `-k`, `--keyword <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` ## `jj init`

View File

@ -84,3 +84,62 @@ fn test_help() {
For more information, try '--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 <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", "<no-similar-keyword>"]);
insta::assert_snapshot!(help_cmd_stderr, @r#"
error: invalid value '<no-similar-keyword>' for '--keyword <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 <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] <COMMAND>
For more information, try '--help'.
"#);
}