// Copyright 2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::io::BufRead as _;use std::path::Path; use clap::FromArgMatches as _;use clap::builder::StyledStr;use clap_complete::CompletionCandidate;use indoc::indoc;use itertools::Itertools as _;use jj_lib::config::ConfigNamePathBuf;use jj_lib::settings::UserSettings;use jj_lib::workspace::DefaultWorkspaceLoaderFactory;use jj_lib::workspace::WorkspaceLoaderFactory as _; use crate::cli_util::GlobalArgs;use crate::cli_util::expand_args;use crate::cli_util::find_workspace_dir;use crate::cli_util::load_template_aliases;use crate::command_error::CommandError;use crate::command_error::user_error;use crate::config::CONFIG_SCHEMA;use crate::config::ConfigArgKind;use crate::config::ConfigEnv;use crate::config::config_from_environment;use crate::config::default_config_layers;use crate::merge_tools::ExternalMergeTool;use crate::merge_tools::configured_merge_tools;use crate::merge_tools::get_external_tool_config;use crate::revset_util::load_revset_aliases;use crate::ui::Ui; const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''" " ++if(normal_target, if(normal_target.description(), normal_target.description().first_line(), "(no description set)", ), "(conflicted bookmark)",)'''"#; /// A helper function for various completer functions. It returns/// (candidate, help) assuming they are separated by a space.fn split_help_text(line: &str) -> (&str, Option<StyledStr>) { match line.split_once(' ') { Some((name, help)) => (name, Some(help.to_string().into())), None => (line, None), }} pub fn local_bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(split_help_text) .map(|(name, help)| CompletionCandidate::new(name).help(help)) .collect()) })} pub fn tracked_bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--tracked") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(split_help_text) .map(|(name, help)| CompletionCandidate::new(name).help(help)) .collect()) })} pub fn untracked_bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _settings| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--all-remotes") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg( r#"if(remote && !tracked && remote != "git", name ++ '@' ++ remote ++ bookmark_help() ++ "\n" )"#, ) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(|line| { let (name, help) = split_help_text(line); CompletionCandidate::new(name).help(help) }) .collect()) })} pub fn bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _settings| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--all-remotes") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg( // only provide help for local refs, remote could be ambiguous r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#, ) .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); Ok((&stdout .lines() .map(split_help_text) .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name))) .into_iter() .map(|(bookmark, mut refs)| { let help = refs.find_map(|(_, help)| help); let local = help.is_some(); let display_order = match local { true => 0, false => 1, }; CompletionCandidate::new(bookmark) .help(help) .display_order(Some(display_order)) }) .collect()) })} pub fn git_remotes() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("git") .arg("remote") .arg("list") .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); Ok(stdout .lines() .filter_map(|line| line.split_once(' ').map(|(name, _url)| name)) .map(CompletionCandidate::new) .collect()) })} pub fn template_aliases() -> Vec<CompletionCandidate> { with_jj(|_, settings| { let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else { return Ok(Vec::new()); }; Ok(template_aliases .symbol_names() .map(CompletionCandidate::new) .sorted() .collect()) })} pub fn aliases() -> Vec<CompletionCandidate> { with_jj(|_, settings| { Ok(settings .table_keys("aliases") // 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()) })} fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> { with_jj(|jj, settings| { // display order const LOCAL_BOOKMARK: usize = 0; const TAG: usize = 1; const CHANGE_ID: usize = 2; const REMOTE_BOOKMARK: usize = 3; const REVSET_ALIAS: usize = 4; let mut candidates = Vec::new(); // bookmarks let mut cmd = jj.build(); cmd.arg("bookmark") .arg("list") .arg("--all-remotes") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg( r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#, ); if let Some(revs) = revset_filter { 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(split_help_text) .filter(|(bookmark, _)| bookmark.starts_with(match_prefix)) .map(|(bookmark, help)| { let local = !bookmark.contains('@'); let display_order = match local { true => LOCAL_BOOKMARK, false => REMOTE_BOOKMARK, }; 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 // revset_filter is set. This is not a big loss, since tags usually point // to immutable revisions anyway. if revset_filter.is_none() { let output = jj .build() .arg("tag") .arg("list") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg(r#"name ++ bookmark_help() ++ "\n""#) .arg(format!("glob:{}*", globset::escape(match_prefix))) .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(TAG)) })); } // change IDs let revisions = revset_filter .map(String::from) .or_else(|| settings.get_string("revsets.short-prefixes").ok()) .or_else(|| settings.get_string("revsets.log").ok()) .unwrap_or_default(); let output = jj .build() .arg("log") .arg("--no-graph") .arg("--limit") .arg("100") .arg("--revisions") .arg(revisions) .arg("--template") .arg(r#"change_id.shortest() ++ " " ++ if(description, description.first_line(), "(no description set)") ++ "\n""#) .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); candidates.extend( stdout .lines() .map(split_help_text) .filter(|(id, _)| id.starts_with(match_prefix)) .map(|(id, desc)| { CompletionCandidate::new(id) .help(desc) .display_order(Some(CHANGE_ID)) }), ); // revset aliases let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?; let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect(); symbol_names.sort(); candidates.extend( symbol_names .into_iter() .filter(|symbol| symbol.starts_with(match_prefix)) .map(|symbol| { let (_, defn) = revset_aliases.get_symbol(symbol).unwrap(); CompletionCandidate::new(symbol) .help(Some(defn.into())) .display_order(Some(REVSET_ALIAS)) }), ); Ok(candidates) })} fn revset_expression( current: &std::ffi::OsStr, revset_filter: Option<&str>,) -> Vec<CompletionCandidate> { let Some(current) = current.to_str() else { return Vec::new(); }; let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current)); let candidates = revisions(match_prefix, revset_filter); if prepend.is_empty() { candidates } else { candidates .into_iter() .map(|candidate| candidate.add_prefix(prepend)) .collect() }} pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { revset_expression(current, None)} pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { revset_expression(current, Some("mutable()"))} /// Identifies if an incomplete expression ends with a name, or may be continued/// with a name.////// If the expression ends with an name or a partial name, returns a tuple that/// splits the string at the point the name starts./// If the expression is empty or ends with a prefix or infix operator that/// could plausibly be followed by a name, returns a tuple where the first/// item is the entire input string, and the second item is empty./// Otherwise, returns `None`.////// The input expression may be incomplete (e.g. missing closing parentheses),/// and the ability to reject invalid expressions is limited.fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> { let final_part = incomplete_revset_str .rsplit_once([':', '~', '|', '&', '(', ',']) .map(|(_, rest)| rest) .unwrap_or(incomplete_revset_str); let final_part = final_part .rsplit_once("..") .map(|(_, rest)| rest) .unwrap_or(final_part) .trim_ascii_start(); let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap(); re.is_match(final_part) .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len()))} pub fn operations() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("operation") .arg("log") .arg("--no-graph") .arg("--limit") .arg("100") .arg("--template") .arg( r#" separate(" ", id.short(), "(" ++ format_timestamp(time.end()) ++ ")", description.first_line(), ) ++ "\n""#, ) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(|line| { let (id, help) = split_help_text(line); CompletionCandidate::new(id).help(help) }) .collect()) })} pub fn workspaces() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("workspace") .arg("list") .arg("-T") .arg(r#"name ++ "\t" ++ if(target.description(), target.description().first_line(), "(no description set)") ++ "\n""#) .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); Ok(stdout .lines() .map(|line| { let (name, desc) = line.split_once(": ").unwrap_or((line, "")); CompletionCandidate::new(name).help(Some(desc.to_string().into())) }) .collect()) })} fn merge_tools_filtered_by( settings: &UserSettings, condition: impl Fn(ExternalMergeTool) -> bool,) -> impl Iterator<Item = &str> { configured_merge_tools(settings).filter(move |name| { let Ok(Some(tool)) = get_external_tool_config(settings, name) else { return false; }; condition(tool) })} pub fn merge_editors() -> Vec<CompletionCandidate> { with_jj(|_, settings| { Ok([":builtin", ":ours", ":theirs"] .into_iter() .chain(merge_tools_filtered_by(settings, |tool| { !tool.merge_args.is_empty() })) .map(CompletionCandidate::new) .collect()) })} /// Approximate list of known diff editorspub fn diff_editors() -> Vec<CompletionCandidate> { with_jj(|_, settings| { Ok(std::iter::once(":builtin") .chain(merge_tools_filtered_by( settings, // The args are empty only if `edit-args` are explicitly set to // `[]` in TOML. If they are not specified, the default // `["$left", "$right"]` value would be used. |tool| !tool.edit_args.is_empty(), )) .map(CompletionCandidate::new) .collect()) })} /// Approximate list of known diff toolspub fn diff_formatters() -> Vec<CompletionCandidate> { let builtin_format_kinds = crate::diff_util::all_builtin_diff_format_names();Lines 1–500 of 1281. Load more
// Copyright 2024 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::io::BufRead as _; use std::path::Path; use clap::FromArgMatches as _; use clap::builder::StyledStr; use clap_complete::CompletionCandidate; use indoc::indoc; use itertools::Itertools as _; use jj_lib::config::ConfigNamePathBuf; use jj_lib::settings::UserSettings; use jj_lib::workspace::DefaultWorkspaceLoaderFactory; use jj_lib::workspace::WorkspaceLoaderFactory as _; use crate::cli_util::GlobalArgs; use crate::cli_util::expand_args; use crate::cli_util::find_workspace_dir; use crate::cli_util::load_template_aliases; use crate::command_error::CommandError; use crate::command_error::user_error; use crate::config::CONFIG_SCHEMA; use crate::config::ConfigArgKind; use crate::config::ConfigEnv; use crate::config::config_from_environment; use crate::config::default_config_layers; use crate::merge_tools::ExternalMergeTool; use crate::merge_tools::configured_merge_tools; use crate::merge_tools::get_external_tool_config; use crate::revset_util::load_revset_aliases; use crate::ui::Ui; const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'=''' " " ++ if(normal_target, if(normal_target.description(), normal_target.description().first_line(), "(no description set)", ), "(conflicted bookmark)", ) '''"#; /// A helper function for various completer functions. It returns /// (candidate, help) assuming they are separated by a space. fn split_help_text(line: &str) -> (&str, Option<StyledStr>) { match line.split_once(' ') { Some((name, help)) => (name, Some(help.to_string().into())), None => (line, None), } } pub fn local_bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(split_help_text) .map(|(name, help)| CompletionCandidate::new(name).help(help)) .collect()) }) } pub fn tracked_bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--tracked") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(split_help_text) .map(|(name, help)| CompletionCandidate::new(name).help(help)) .collect()) }) } pub fn untracked_bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _settings| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--all-remotes") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg( r#"if(remote && !tracked && remote != "git", name ++ '@' ++ remote ++ bookmark_help() ++ "\n" )"#, ) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(|line| { let (name, help) = split_help_text(line); CompletionCandidate::new(name).help(help) }) .collect()) }) } pub fn bookmarks() -> Vec<CompletionCandidate> { with_jj(|jj, _settings| { let output = jj .build() .arg("bookmark") .arg("list") .arg("--all-remotes") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg( // only provide help for local refs, remote could be ambiguous r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#, ) .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); Ok((&stdout .lines() .map(split_help_text) .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name))) .into_iter() .map(|(bookmark, mut refs)| { let help = refs.find_map(|(_, help)| help); let local = help.is_some(); let display_order = match local { true => 0, false => 1, }; CompletionCandidate::new(bookmark) .help(help) .display_order(Some(display_order)) }) .collect()) }) } pub fn git_remotes() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("git") .arg("remote") .arg("list") .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); Ok(stdout .lines() .filter_map(|line| line.split_once(' ').map(|(name, _url)| name)) .map(CompletionCandidate::new) .collect()) }) } pub fn template_aliases() -> Vec<CompletionCandidate> { with_jj(|_, settings| { let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else { return Ok(Vec::new()); }; Ok(template_aliases .symbol_names() .map(CompletionCandidate::new) .sorted() .collect()) }) } pub fn aliases() -> Vec<CompletionCandidate> { with_jj(|_, settings| { Ok(settings .table_keys("aliases") // 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()) }) } fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> { with_jj(|jj, settings| { // display order const LOCAL_BOOKMARK: usize = 0; const TAG: usize = 1; const CHANGE_ID: usize = 2; const REMOTE_BOOKMARK: usize = 3; const REVSET_ALIAS: usize = 4; let mut candidates = Vec::new(); // bookmarks let mut cmd = jj.build(); cmd.arg("bookmark") .arg("list") .arg("--all-remotes") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg( r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#, ); if let Some(revs) = revset_filter { 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(split_help_text) .filter(|(bookmark, _)| bookmark.starts_with(match_prefix)) .map(|(bookmark, help)| { let local = !bookmark.contains('@'); let display_order = match local { true => LOCAL_BOOKMARK, false => REMOTE_BOOKMARK, }; 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 // revset_filter is set. This is not a big loss, since tags usually point // to immutable revisions anyway. if revset_filter.is_none() { let output = jj .build() .arg("tag") .arg("list") .arg("--config") .arg(BOOKMARK_HELP_TEMPLATE) .arg("--template") .arg(r#"name ++ bookmark_help() ++ "\n""#) .arg(format!("glob:{}*", globset::escape(match_prefix))) .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(TAG)) })); } // change IDs let revisions = revset_filter .map(String::from) .or_else(|| settings.get_string("revsets.short-prefixes").ok()) .or_else(|| settings.get_string("revsets.log").ok()) .unwrap_or_default(); let output = jj .build() .arg("log") .arg("--no-graph") .arg("--limit") .arg("100") .arg("--revisions") .arg(revisions) .arg("--template") .arg(r#"change_id.shortest() ++ " " ++ if(description, description.first_line(), "(no description set)") ++ "\n""#) .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); candidates.extend( stdout .lines() .map(split_help_text) .filter(|(id, _)| id.starts_with(match_prefix)) .map(|(id, desc)| { CompletionCandidate::new(id) .help(desc) .display_order(Some(CHANGE_ID)) }), ); // revset aliases let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?; let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect(); symbol_names.sort(); candidates.extend( symbol_names .into_iter() .filter(|symbol| symbol.starts_with(match_prefix)) .map(|symbol| { let (_, defn) = revset_aliases.get_symbol(symbol).unwrap(); CompletionCandidate::new(symbol) .help(Some(defn.into())) .display_order(Some(REVSET_ALIAS)) }), ); Ok(candidates) }) } fn revset_expression( current: &std::ffi::OsStr, revset_filter: Option<&str>, ) -> Vec<CompletionCandidate> { let Some(current) = current.to_str() else { return Vec::new(); }; let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current)); let candidates = revisions(match_prefix, revset_filter); if prepend.is_empty() { candidates } else { candidates .into_iter() .map(|candidate| candidate.add_prefix(prepend)) .collect() } } pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { revset_expression(current, None) } pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { revset_expression(current, Some("mutable()")) } /// Identifies if an incomplete expression ends with a name, or may be continued /// with a name. /// /// If the expression ends with an name or a partial name, returns a tuple that /// splits the string at the point the name starts. /// If the expression is empty or ends with a prefix or infix operator that /// could plausibly be followed by a name, returns a tuple where the first /// item is the entire input string, and the second item is empty. /// Otherwise, returns `None`. /// /// The input expression may be incomplete (e.g. missing closing parentheses), /// and the ability to reject invalid expressions is limited. fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> { let final_part = incomplete_revset_str .rsplit_once([':', '~', '|', '&', '(', ',']) .map(|(_, rest)| rest) .unwrap_or(incomplete_revset_str); let final_part = final_part .rsplit_once("..") .map(|(_, rest)| rest) .unwrap_or(final_part) .trim_ascii_start(); let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap(); re.is_match(final_part) .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len())) } pub fn operations() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("operation") .arg("log") .arg("--no-graph") .arg("--limit") .arg("100") .arg("--template") .arg( r#" separate(" ", id.short(), "(" ++ format_timestamp(time.end()) ++ ")", description.first_line(), ) ++ "\n""#, ) .output() .map_err(user_error)?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .map(|line| { let (id, help) = split_help_text(line); CompletionCandidate::new(id).help(help) }) .collect()) }) } pub fn workspaces() -> Vec<CompletionCandidate> { with_jj(|jj, _| { let output = jj .build() .arg("workspace") .arg("list") .arg("-T") .arg(r#"name ++ "\t" ++ if(target.description(), target.description().first_line(), "(no description set)") ++ "\n""#) .output() .map_err(user_error)?; let stdout = String::from_utf8_lossy(&output.stdout); Ok(stdout .lines() .map(|line| { let (name, desc) = line.split_once(": ").unwrap_or((line, "")); CompletionCandidate::new(name).help(Some(desc.to_string().into())) }) .collect()) }) } fn merge_tools_filtered_by( settings: &UserSettings, condition: impl Fn(ExternalMergeTool) -> bool, ) -> impl Iterator<Item = &str> { configured_merge_tools(settings).filter(move |name| { let Ok(Some(tool)) = get_external_tool_config(settings, name) else { return false; }; condition(tool) }) } pub fn merge_editors() -> Vec<CompletionCandidate> { with_jj(|_, settings| { Ok([":builtin", ":ours", ":theirs"] .into_iter() .chain(merge_tools_filtered_by(settings, |tool| { !tool.merge_args.is_empty() })) .map(CompletionCandidate::new) .collect()) }) } /// Approximate list of known diff editors pub fn diff_editors() -> Vec<CompletionCandidate> { with_jj(|_, settings| { Ok(std::iter::once(":builtin") .chain(merge_tools_filtered_by( settings, // The args are empty only if `edit-args` are explicitly set to // `[]` in TOML. If they are not specified, the default // `["$left", "$right"]` value would be used. |tool| !tool.edit_args.is_empty(), )) .map(CompletionCandidate::new) .collect()) }) } /// Approximate list of known diff tools pub fn diff_formatters() -> Vec<CompletionCandidate> { let builtin_format_kinds = crate::diff_util::all_builtin_diff_format_names();