diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index b3babc3c89..d5de606170 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -3079,8 +3079,12 @@ fn handle_shell_completion( let resolved_aliases = expand_args(ui, app, env::args_os().skip(2), config)?; args.extend(resolved_aliases.into_iter().map(OsString::from)); } - let ran_completion = clap_complete::CompleteEnv::with_factory(|| app.clone()) - .try_complete(args.iter(), Some(cwd))?; + let ran_completion = clap_complete::CompleteEnv::with_factory(|| { + app.clone() + // for completing aliases + .allow_external_subcommands(true) + }) + .try_complete(args.iter(), Some(cwd))?; assert!( ran_completion, "This function should not be called without the COMPLETE variable set." diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 7bf08b2b82..0d7cb1f276 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -63,17 +63,20 @@ use std::fmt::Debug; use clap::CommandFactory; use clap::FromArgMatches; use clap::Subcommand; +use clap_complete::engine::SubcommandCandidates; use tracing::instrument; use crate::cli_util::Args; use crate::cli_util::CommandHelper; use crate::command_error::user_error_with_hint; use crate::command_error::CommandError; +use crate::complete; use crate::ui::Ui; #[derive(clap::Parser, Clone, Debug)] #[command(disable_help_subcommand = true)] #[command(after_long_help = help::show_keyword_hint_after_help())] +#[command(add = SubcommandCandidates::new(complete::aliases))] enum Command { Abandon(abandon::AbandonArgs), Backout(backout::BackoutArgs), diff --git a/cli/src/complete.rs b/cli/src/complete.rs index 01f59c6ece..183578cbbe 100644 --- a/cli/src/complete.rs +++ b/cli/src/complete.rs @@ -44,6 +44,21 @@ pub fn local_bookmarks() -> Vec { }) } +pub fn aliases() -> Vec { + with_jj(|_, config| { + Ok(config + .get_table("aliases")? + .into_keys() + // This is opinionated, but many people probably have several + // single- or two-letter aliases they use all the time. These + // aliases don't need to be completed and they would only clutter + // the output of `jj `. + .filter(|alias| alias.len() > 2) + .map(CompletionCandidate::new) + .collect()) + }) +} + /// Shell out to jj during dynamic completion generation /// /// In case of errors, print them and early return an empty vector. @@ -107,6 +122,14 @@ fn get_jj_command() -> Result<(std::process::Command, Config), CommandError> { .disable_version_flag(true) .disable_help_flag(true) .ignore_errors(true) + // Here, allow_external_subcommands fixes a weird issue. Without it, + // parsing GlobalArgs will fail with the message that a required arg + // is missing, where the required arg is a boolean flag. This seems + // unexpected, because missing boolean flags are usually treated as + // false. It is also not clear to me why allow_external_subcommands + // changes this behavior. See the discussion in the clap repo: + // https://github.com/clap-rs/clap/discussions/5812 + .allow_external_subcommands(true) .try_get_matches_from(args)?; let args: GlobalArgs = GlobalArgs::from_arg_matches(&args)?; diff --git a/cli/tests/test_completion.rs b/cli/tests/test_completion.rs index 20d5c341fc..5355fa3d91 100644 --- a/cli/tests/test_completion.rs +++ b/cli/tests/test_completion.rs @@ -115,3 +115,47 @@ fn test_completions_are_generated() { let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--"]); assert!(stdout.starts_with("complete --keep-order --exclusive --command jj --arguments")); } + +#[test] +fn test_aliases_are_completed() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + // user config alias + test_env.add_config(r#"aliases.user-alias = ["bookmark"]"#); + // repo config alias + test_env.jj_cmd_ok( + &repo_path, + &[ + "config", + "set", + "--repo", + "aliases.repo-alias", + "['bookmark']", + ], + ); + + let mut test_env = test_env; + test_env.add_env_var("COMPLETE", "fish"); + let test_env = test_env; + + let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "user-al"]); + insta::assert_snapshot!(stdout, @"user-alias"); + + let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "repo-al"]); + insta::assert_snapshot!(stdout, @"repo-alias"); + + // make sure --repository flag is respected + let stdout = test_env.jj_cmd_success( + test_env.env_root(), + &[ + "--", + "jj", + "--repository", + repo_path.to_str().unwrap(), + "repo-al", + ], + ); + insta::assert_snapshot!(stdout, @"repo-alias"); +}