diff --git a/cli/src/complete.rs b/cli/src/complete.rs index 944b6080fb1..3a5287fd4ee 100644 --- a/cli/src/complete.rs +++ b/cli/src/complete.rs @@ -215,14 +215,94 @@ pub fn aliases() -> Vec { }) } -fn revisions(revisions: &str) -> Vec { - with_jj(|jj, _| { +fn revisions(revisions: Option<&str>) -> Vec { + with_jj(|jj, config| { + // display order: + // - 0: local bookmarks (mine) + // - 1: local bookmarks + // - 2: tags + // - 3: change IDs + // - 4: remote bookmarks (mine) + // - 5: remote bookmarks + + let mut candidates = Vec::new(); + + // bookmarks + + let prefix = config.get::("git.push-bookmark-prefix").ok(); + + let mut cmd = jj.build(); + cmd.arg("bookmark") + .arg("list") + .arg("--all-remotes") + .arg("--config-toml") + .arg(BOOKMARK_HELP_TEMPLATE) + .arg("--template") + .arg( + r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#, + ); + if let Some(revs) = revisions { + cmd.arg("--revisions").arg(revs); + } + let output = cmd.output().map_err(user_error)?; + let stdout = String::from_utf8_lossy(&output.stdout); + + candidates.extend(stdout.lines().map(|line| { + let (bookmark, help) = split_help_text(line); + + let local = !bookmark.contains('@'); + let mine = prefix.as_ref().is_some_and(|p| bookmark.starts_with(p)); + + let display_order = match (local, mine) { + (true, true) => 0, + (true, false) => 1, + (false, true) => 4, + (false, false) => 5, + }; + CompletionCandidate::new(bookmark) + .help(help) + .display_order(Some(display_order)) + })); + + // tags + + // Tags cannot be filtered by revisions. In order to avoid suggesting + // immutable tags for mutable revision args, we skip tags entirely if + // revisions is set. This is not a big loss, since tags usually point + // to immutable revisions anyway. + if revisions.is_none() { + let output = jj + .build() + .arg("tag") + .arg("list") + .arg("--config-toml") + .arg(BOOKMARK_HELP_TEMPLATE) + .arg("--template") + .arg(r#"name ++ bookmark_help() ++ "\n""#) + .output() + .map_err(user_error)?; + let stdout = String::from_utf8_lossy(&output.stdout); + + candidates.extend(stdout.lines().map(|line| { + let (name, desc) = split_help_text(line); + CompletionCandidate::new(name) + .help(desc) + .display_order(Some(2)) + })); + } + + // change IDs + + let revisions = revisions + .map(String::from) + .or_else(|| config.get_string("revsets.short-prefixes").ok()) + .or_else(|| config.get_string("revsets.log").ok()) + .unwrap(); + let output = jj .build() .arg("log") .arg("--no-graph") - .arg("--limit") - .arg("100") .arg("--revisions") .arg(revisions) .arg("--template") @@ -231,22 +311,23 @@ fn revisions(revisions: &str) -> Vec { .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); - Ok(stdout - .lines() - .map(|line| { - let (id, desc) = split_help_text(line); - CompletionCandidate::new(id).help(desc) - }) - .collect()) + candidates.extend(stdout.lines().map(|line| { + let (id, desc) = split_help_text(line); + CompletionCandidate::new(id) + .help(desc) + .display_order(Some(3)) + })); + + Ok(candidates) }) } pub fn mutable_revisions() -> Vec { - revisions("mutable()") + revisions(Some("mutable()")) } pub fn all_revisions() -> Vec { - revisions("all()") + revisions(None) } pub fn operations() -> Vec { diff --git a/cli/tests/test_completion.rs b/cli/tests/test_completion.rs index 971222a7cf8..617a60b98cc 100644 --- a/cli/tests/test_completion.rs +++ b/cli/tests/test_completion.rs @@ -355,10 +355,37 @@ fn test_revisions() { test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); + // create remote to test remote branches + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "origin"]); + let origin_path = test_env.env_root().join("origin"); + let origin_git_repo_path = origin_path + .join(".jj") + .join("repo") + .join("store") + .join("git"); + test_env.jj_cmd_ok( + &repo_path, + &[ + "git", + "remote", + "add", + "origin", + origin_git_repo_path.to_str().unwrap(), + ], + ); + test_env.jj_cmd_ok(&origin_path, &["b", "c", "remote_bookmark"]); + test_env.jj_cmd_ok(&origin_path, &["commit", "-m", "remote_commit"]); + test_env.jj_cmd_ok(&origin_path, &["git", "export"]); + test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]); + + test_env.jj_cmd_ok(&repo_path, &["b", "c", "immutable_bookmark"]); test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "immutable"]); + test_env.add_config(r#"revset-aliases."immutable_heads()" = "immutable_bookmark""#); + + test_env.jj_cmd_ok(&repo_path, &["b", "c", "mutable_bookmark"]); test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "mutable"]); - test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "main", "-r", "@--"]); - test_env.add_config(r#"revset-aliases."immutable_heads()" = "main""#); + + test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "working_copy"]); let mut test_env = test_env; test_env.add_env_var("COMPLETE", "fish"); @@ -371,27 +398,37 @@ fn test_revisions() { // complete all revisions let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "diff", "--from", ""]); insta::assert_snapshot!(stdout, @r" - k (no description set) - r mutable + immutable_bookmark immutable + mutable_bookmark mutable + k working_copy + y mutable q immutable - z (no description set) + zq remote_commit + zz (no description set) + remote_bookmark@origin remote_commit "); // complete only mutable revisions let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "squash", "--into", ""]); insta::assert_snapshot!(stdout, @r" - k (no description set) - r mutable + mutable_bookmark mutable + k working_copy + y mutable + zq remote_commit "); // complete args of the default command test_env.add_config("ui.default-command = 'log'"); let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "-r", ""]); insta::assert_snapshot!(stdout, @r" - k (no description set) - r mutable + immutable_bookmark immutable + mutable_bookmark mutable + k working_copy + y mutable q immutable - z (no description set) + zq remote_commit + zz (no description set) + remote_bookmark@origin remote_commit "); }