cli completion: complete --tool args for merge tools

Includes diff tools, diff editors, and merge editors.
This commit is contained in:
Ilya Grigoriev 2025-03-31 23:11:50 -07:00
parent 5dc9da3c2b
commit b8cfb1a8c6
10 changed files with 157 additions and 13 deletions

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter; use clap_complete::ArgValueCompleter;
use jj_lib::backend::Signature; use jj_lib::backend::Signature;
use jj_lib::object_id::ObjectId as _; use jj_lib::object_id::ObjectId as _;
@ -35,7 +36,11 @@ pub(crate) struct CommitArgs {
#[arg(short, long)] #[arg(short, long)]
interactive: bool, interactive: bool,
/// Specify diff editor to be used (implies --interactive) /// Specify diff editor to be used (implies --interactive)
#[arg(long, value_name = "NAME")] #[arg(
long,
value_name = "NAME",
add = ArgValueCandidates::new(complete::diff_editors),
)]
tool: Option<String>, tool: Option<String>,
/// The change description to use (don't open editor) /// The change description to use (don't open editor)
#[arg(long = "message", short, value_name = "MESSAGE")] #[arg(long = "message", short, value_name = "MESSAGE")]

View File

@ -78,7 +78,11 @@ pub(crate) struct DiffeditArgs {
)] )]
to: Option<RevisionArg>, to: Option<RevisionArg>,
/// Specify diff editor to be used /// Specify diff editor to be used
#[arg(long, value_name = "NAME")] #[arg(
long,
value_name = "NAME",
add = ArgValueCandidates::new(complete::diff_editors),
)]
tool: Option<String>, tool: Option<String>,
/// Preserve the content (not the diff) when rebasing descendants /// Preserve the content (not the diff) when rebasing descendants
/// ///

View File

@ -62,7 +62,12 @@ pub(crate) struct ResolveArgs {
/// ///
/// The built-in merge tools `:ours` and `:theirs` can be used to choose /// The built-in merge tools `:ours` and `:theirs` can be used to choose
/// side #1 and side #2 of the conflict respectively. /// side #1 and side #2 of the conflict respectively.
#[arg(long, conflicts_with = "list", value_name = "NAME")] #[arg(
long,
conflicts_with = "list",
value_name = "NAME",
add = ArgValueCandidates::new(complete::merge_tools),
)]
tool: Option<String>, tool: Option<String>,
/// Only resolve conflicts in these paths. You can use the `--list` argument /// Only resolve conflicts in these paths. You can use the `--list` argument
/// to find paths to use here. /// to find paths to use here.

View File

@ -97,7 +97,11 @@ pub(crate) struct RestoreArgs {
#[arg(long, short)] #[arg(long, short)]
interactive: bool, interactive: bool,
/// Specify diff editor to be used (implies --interactive) /// Specify diff editor to be used (implies --interactive)
#[arg(long, value_name = "NAME")] #[arg(
long,
value_name = "NAME",
add = ArgValueCandidates::new(complete::diff_editors),
)]
tool: Option<String>, tool: Option<String>,
/// Preserve the content (not the diff) when rebasing descendants /// Preserve the content (not the diff) when rebasing descendants
#[arg(long)] #[arg(long)]

View File

@ -59,7 +59,11 @@ pub(crate) struct SplitArgs {
#[arg(long, short)] #[arg(long, short)]
interactive: bool, interactive: bool,
/// Specify diff editor to be used (implies --interactive) /// Specify diff editor to be used (implies --interactive)
#[arg(long, value_name = "NAME")] #[arg(
long,
value_name = "NAME",
add = ArgValueCandidates::new(complete::diff_editors),
)]
tool: Option<String>, tool: Option<String>,
/// The revision to split /// The revision to split
#[arg( #[arg(

View File

@ -99,7 +99,11 @@ pub(crate) struct SquashArgs {
#[arg(long, short)] #[arg(long, short)]
interactive: bool, interactive: bool,
/// Specify diff editor to be used (implies --interactive) /// Specify diff editor to be used (implies --interactive)
#[arg(long, value_name = "NAME")] #[arg(
long,
value_name = "NAME",
add = ArgValueCandidates::new(complete::diff_editors),
)]
tool: Option<String>, tool: Option<String>,
/// Move only changes to these paths (instead of all paths) /// Move only changes to these paths (instead of all paths)
#[arg( #[arg(

View File

@ -34,6 +34,8 @@ use crate::config::default_config_layers;
use crate::config::ConfigArgKind; use crate::config::ConfigArgKind;
use crate::config::ConfigEnv; use crate::config::ConfigEnv;
use crate::config::CONFIG_SCHEMA; use crate::config::CONFIG_SCHEMA;
use crate::merge_tools::configured_merge_tools;
use crate::merge_tools::MergeEditor;
use crate::revset_util::load_revset_aliases; use crate::revset_util::load_revset_aliases;
use crate::ui::Ui; use crate::ui::Ui;
@ -412,6 +414,47 @@ pub fn workspaces() -> Vec<CompletionCandidate> {
.collect()) .collect())
}) })
} }
pub fn merge_tools() -> Vec<CompletionCandidate> {
with_jj(|_, settings| {
Ok([":builtin", ":ours", ":theirs"]
.into_iter()
.chain(
configured_merge_tools(settings)
.filter(|name| MergeEditor::dummy_with_name(name, settings).is_ok()),
)
.map(CompletionCandidate::new)
.collect())
})
}
/// Approximate list of known diff editors
///
/// Diff tools can be used without configuration. Some merge tools that are
/// configured for 3-way merging may not work for diffing/diff editing, and we
/// can't tell which these are. So, this not reliable, but probably good enough
/// for command-line completion.
pub fn diff_editors() -> Vec<CompletionCandidate> {
with_jj(|_, settings| {
Ok(std::iter::once(":builtin")
.chain(configured_merge_tools(settings))
.map(CompletionCandidate::new)
.collect())
})
}
/// Approximate list of known diff tools
///
/// Diff tools can be used without configuration. Some merge tools that are
/// configured for 3-way merging may not work for diffing/diff editing, and we
/// can't tell which these are. So, this not reliable, but probably good enough
/// for command-line completion.
pub fn diff_tools() -> Vec<CompletionCandidate> {
with_jj(|_, settings| {
Ok(configured_merge_tools(settings)
.map(CompletionCandidate::new)
.collect())
})
}
fn config_keys_rec( fn config_keys_rec(
prefix: ConfigNamePathBuf, prefix: ConfigNamePathBuf,

View File

@ -23,6 +23,7 @@ use std::path::PathBuf;
use bstr::BStr; use bstr::BStr;
use bstr::BString; use bstr::BString;
use clap_complete::ArgValueCandidates;
use futures::executor::block_on_stream; use futures::executor::block_on_stream;
use futures::stream::BoxStream; use futures::stream::BoxStream;
use futures::StreamExt as _; use futures::StreamExt as _;
@ -117,7 +118,10 @@ pub struct DiffFormatArgs {
#[arg(long)] #[arg(long)]
pub color_words: bool, pub color_words: bool,
/// Generate diff by external command /// Generate diff by external command
#[arg(long)] #[arg(
long,
add = ArgValueCandidates::new(crate::complete::diff_tools),
)]
pub tool: Option<String>, pub tool: Option<String>,
/// Number of lines of context to show /// Number of lines of context to show
#[arg(long)] #[arg(long)]

View File

@ -204,6 +204,11 @@ fn editor_args_from_settings(
} }
} }
/// List configured merge tools (diff editors, diff tools, merge editors)
pub fn configured_merge_tools(settings: &UserSettings) -> impl Iterator<Item = &str> {
settings.table_keys("merge-tools")
}
/// Loads external diff/merge tool options from `[merge-tools.<name>]`. /// Loads external diff/merge tool options from `[merge-tools.<name>]`.
pub fn get_external_tool_config( pub fn get_external_tool_config(
settings: &UserSettings, settings: &UserSettings,
@ -376,6 +381,22 @@ impl MergeEditor {
Self::new_inner(name, tool, path_converter, conflict_marker_style) Self::new_inner(name, tool, path_converter, conflict_marker_style)
} }
/// For the purposes of testing or checking basic config
pub fn dummy_with_name(
name: &str,
settings: &UserSettings,
) -> Result<Self, MergeToolConfigError> {
Self::with_name(
name,
settings,
RepoPathUiConverter::Fs {
cwd: "".into(),
base: "".into(),
},
ConflictMarkerStyle::Diff,
)
}
/// Loads the default 3-way merge editor from the settings. /// Loads the default 3-way merge editor from the settings.
pub fn from_settings( pub fn from_settings(
ui: &Ui, ui: &Ui,
@ -762,12 +783,7 @@ mod tests {
let get = |name, config_text| { let get = |name, config_text| {
let config = config_from_string(config_text); let config = config_from_string(config_text);
let settings = UserSettings::from_config(config).unwrap(); let settings = UserSettings::from_config(config).unwrap();
let path_converter = RepoPathUiConverter::Fs { MergeEditor::dummy_with_name(name, &settings).map(|editor| editor.tool)
cwd: "".into(),
base: "".into(),
};
MergeEditor::with_name(name, &settings, path_converter, ConflictMarkerStyle::Diff)
.map(|editor| editor.tool)
}; };
insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin"); insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");

View File

@ -772,6 +772,61 @@ fn test_template_alias() {
"); ");
} }
#[test]
fn test_merge_tools() {
let mut test_env = TestEnvironment::default();
test_env.add_env_var("COMPLETE", "fish");
let dir = test_env.env_root();
let output = test_env.run_jj_in(dir, ["--", "jj", "diff", "--tool", ""]);
insta::assert_snapshot!(output, @r"
diffedit3
diffedit3-ssh
difft
kdiff3
meld
meld-3
mergiraf
smerge
vimdiff
vscode
vscodium
[EOF]
");
// Includes :builtin
let output = test_env.run_jj_in(dir, ["--", "jj", "diffedit", "--tool", ""]);
insta::assert_snapshot!(output, @r"
:builtin
diffedit3
diffedit3-ssh
difft
kdiff3
meld
meld-3
mergiraf
smerge
vimdiff
vscode
vscodium
[EOF]
");
// Only includes configured merge editors
let output = test_env.run_jj_in(dir, ["--", "jj", "resolve", "--tool", ""]);
insta::assert_snapshot!(output, @r"
:builtin
:ours
:theirs
kdiff3
meld
mergiraf
smerge
vimdiff
vscode
vscodium
[EOF]
");
}
fn create_commit( fn create_commit(
work_dir: &TestWorkDir, work_dir: &TestWorkDir,
name: &str, name: &str,