Skip to main content

jj_cli/commands/
help.rs

1// Copyright 2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Write as _;
16use std::io::Write as _;
17
18use clap::builder::PossibleValue;
19use clap::builder::StyledStr;
20use clap::error::ContextKind;
21use crossterm::style::Stylize as _;
22use itertools::Itertools as _;
23use tracing::instrument;
24
25use crate::cli_util::CommandHelper;
26use crate::command_error::CommandError;
27use crate::command_error::cli_error;
28use crate::ui::Ui;
29
30/// Print this message or the help of the given subcommand(s)
31#[derive(clap::Args, Clone, Debug)]
32pub(crate) struct HelpArgs {
33    /// Print help for the subcommand(s)
34    pub(crate) command: Vec<String>,
35
36    /// Show help for keywords instead of commands
37    #[arg(
38        long,
39        short = 'k',
40        conflicts_with = "command",
41        value_parser = KEYWORDS
42            .iter()
43            .map(|k| PossibleValue::new(k.name).help(k.description))
44            .collect_vec()
45    )]
46    pub(crate) keyword: Option<String>,
47}
48
49#[instrument(skip_all)]
50pub(crate) async fn cmd_help(
51    ui: &mut Ui,
52    command: &CommandHelper,
53    args: &HelpArgs,
54) -> Result<(), CommandError> {
55    if let Some(name) = &args.keyword {
56        let keyword = find_keyword(name).expect("clap should check this with `value_parser`");
57        ui.request_pager();
58        write!(ui.stdout(), "{}", keyword.content)?;
59
60        return Ok(());
61    }
62
63    let bin_name = command
64        .string_args()
65        .first()
66        .map_or(command.app().get_name(), |name| name.as_ref());
67    let mut args_to_get_command = vec![bin_name];
68    args_to_get_command.extend(args.command.iter().map(|s| s.as_str()));
69
70    let mut app = command.app().clone();
71    // This propagates global arguments to subcommand, and generates error if
72    // the subcommand doesn't exist.
73    if let Err(err) = app.try_get_matches_from_mut(args_to_get_command) {
74        if err.get(ContextKind::InvalidSubcommand).is_some() {
75            return Err(err.into());
76        } else {
77            // `help log -- -r`, etc. shouldn't generate an argument error.
78        }
79    }
80    let command = args
81        .command
82        .iter()
83        .try_fold(&mut app, |cmd, name| cmd.find_subcommand_mut(name))
84        .ok_or_else(|| cli_error(format!("Unknown command: {}", args.command.join(" "))))?;
85
86    ui.request_pager();
87    let help_text = command.render_long_help();
88    if ui.color() {
89        write!(ui.stdout(), "{}", help_text.ansi())?;
90    } else {
91        write!(ui.stdout(), "{help_text}")?;
92    }
93    Ok(())
94}
95
96#[derive(Clone)]
97struct Keyword {
98    name: &'static str,
99    description: &'static str,
100    content: &'static str,
101}
102
103// TODO: Add all documentation to keywords
104//
105// Maybe adding some code to build.rs to find all the docs files and build the
106// `KEYWORDS` at compile time.
107//
108// It would be cool to follow the docs hierarchy somehow.
109//
110// One of the problems would be `config.md`, as it has the same name as a
111// subcommand.
112//
113// TODO: Find a way to render markdown using ANSI escape codes.
114//
115// Maybe we can steal some ideas from https://github.com/jj-vcs/jj/pull/3130
116const KEYWORDS: &[Keyword] = &[
117    Keyword {
118        name: "bookmarks",
119        description: "Named pointers to revisions (similar to Git's branches)",
120        content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "bookmarks.md")),
121    },
122    Keyword {
123        name: "config",
124        description: "How and where to set configuration options",
125        content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "config.md")),
126    },
127    Keyword {
128        name: "filesets",
129        description: "A functional language for selecting a set of files",
130        content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "filesets.md")),
131    },
132    Keyword {
133        name: "glossary",
134        description: "Definitions of various terms",
135        content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "glossary.md")),
136    },
137    Keyword {
138        name: "revsets",
139        description: "A functional language for selecting a set of revision",
140        content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "revsets.md")),
141    },
142    Keyword {
143        name: "templates",
144        description: "A functional language to customize command output",
145        content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "templates.md")),
146    },
147    Keyword {
148        name: "tutorial",
149        description: "Show a tutorial to get started with jj",
150        content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "tutorial.md")),
151    },
152];
153
154fn find_keyword(name: &str) -> Option<&Keyword> {
155    KEYWORDS.iter().find(|keyword| keyword.name == name)
156}
157
158pub fn show_keyword_hint_after_help() -> StyledStr {
159    let mut ret = StyledStr::new();
160    writeln!(
161        ret,
162        "{} lists available keywords. Use {} to show help for one of these keywords.",
163        "'jj help --help'".bold(),
164        "'jj help -k'".bold(),
165    )
166    .unwrap();
167    ret
168}