diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index d5e0832f3e..14eaf8407f 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -3248,8 +3248,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 e473df6687..6efdfeb646 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -64,17 +64,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), Absorb(absorb::AbsorbArgs), diff --git a/cli/src/complete.rs b/cli/src/complete.rs index 6f38acec34..1928ca53f6 100644 --- a/cli/src/complete.rs +++ b/cli/src/complete.rs @@ -144,6 +144,21 @@ pub fn git_remotes() -> 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. diff --git a/cli/tests/test_completion.rs b/cli/tests/test_completion.rs index 8fe396385a..c9484106c9 100644 --- a/cli/tests/test_completion.rs +++ b/cli/tests/test_completion.rs @@ -242,3 +242,57 @@ fn test_remote_names() { ); insta::assert_snapshot!(stdout, @r"origin"); } + +#[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"); + + // 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"); + + // cannot load aliases from --config-toml flag + let stdout = test_env.jj_cmd_success( + test_env.env_root(), + &[ + "--", + "jj", + "--config-toml", + "aliases.cli-alias = ['bookmark']", + "cli-al", + ], + ); + insta::assert_snapshot!(stdout, @""); +}