Skip to content

Commit

Permalink
completion: teach jj about aliases
Browse files Browse the repository at this point in the history
This makes completions suggest aliases from the user or repository
configuration. It's more useful for long aliases that aren't used very often,
but serve the purpose of "executable documentation" of complex and useful jj
commands.

An earlier patch "resolved" aliases, meaning that any arguments following an
alias would be completed as if the normal command had been used. So it only
made sure that using aliases doesn't degrade the rest of the completions.
Commit ID: 325402d
  • Loading branch information
senekor committed Nov 12, 2024
1 parent 4f91bf3 commit d950f8f
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 2 deletions.
8 changes: 6 additions & 2 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
3 changes: 3 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
23 changes: 23 additions & 0 deletions cli/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ pub fn local_bookmarks() -> Vec<CompletionCandidate> {
})
}

pub fn aliases() -> Vec<CompletionCandidate> {
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 <TAB>`.
.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.
Expand Down Expand Up @@ -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)?;

Expand Down
44 changes: 44 additions & 0 deletions cli/tests/test_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

0 comments on commit d950f8f

Please sign in to comment.