// Copyright 2024 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::fmt::Write as _; use std::io::Write as _; use clap::builder::PossibleValue; use clap::builder::StyledStr; use clap::error::ContextKind; use crossterm::style::Stylize as _; use itertools::Itertools as _; use tracing::instrument; use crate::cli_util::CommandHelper; use crate::command_error::CommandError; use crate::command_error::cli_error; use crate::ui::Ui; /// Print this message or the help of the given subcommand(s) #[derive(clap::Args, Clone, Debug)] 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, 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 bin_name = command .string_args() .first() .map_or(command.app().get_name(), |name| name.as_ref()); let mut args_to_get_command = vec![bin_name]; args_to_get_command.extend(args.command.iter().map(|s| s.as_str())); let mut app = command.app().clone(); // This propagates global arguments to subcommand, and generates error if // the subcommand doesn't exist. if let Err(err) = app.try_get_matches_from_mut(args_to_get_command) { if err.get(ContextKind::InvalidSubcommand).is_some() { return Err(err.into()); } else { // `help log -- -r`, etc. shouldn't generate an argument error. } } let command = args .command .iter() .try_fold(&mut app, |cmd, name| cmd.find_subcommand_mut(name)) .ok_or_else(|| cli_error(format!("Unknown command: {}", args.command.join(" "))))?; ui.request_pager(); let help_text = command.render_long_help(); if ui.color() { write!(ui.stdout(), "{}", help_text.ansi())?; } else { write!(ui.stdout(), "{help_text}")?; } Ok(()) } #[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/jj-vcs/jj/pull/3130 const KEYWORDS: &[Keyword] = &[ Keyword { name: "bookmarks", description: "Named pointers to revisions (similar to Git's branches)", content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "bookmarks.md")), }, Keyword { name: "config", description: "How and where to set configuration options", content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "config.md")), }, Keyword { name: "filesets", description: "A functional language for selecting a set of files", content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "filesets.md")), }, Keyword { name: "glossary", description: "Definitions of various terms", content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "glossary.md")), }, Keyword { name: "revsets", description: "A functional language for selecting a set of revision", content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "revsets.md")), }, Keyword { name: "templates", description: "A functional language to customize command output", content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "templates.md")), }, Keyword { name: "tutorial", description: "Show a tutorial to get started with jj", content: include_str!(concat!("../../", env!("JJ_DOCS_DIR"), "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, "{} lists available keywords. Use {} to show help for one of these keywords.", "'jj help --help'".bold(), "'jj help -k'".bold(), ) .unwrap(); ret }