1use std::collections::HashSet;
16use std::io::BufRead as _;
17use std::path::Path;
18
19use clap::FromArgMatches as _;
20use clap::builder::StyledStr;
21use clap_complete::CompletionCandidate;
22use indoc::indoc;
23use itertools::Itertools as _;
24use jj_lib::config::ConfigNamePathBuf;
25use jj_lib::file_util::normalize_path;
26use jj_lib::file_util::slash_path;
27use jj_lib::settings::UserSettings;
28use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
29use jj_lib::workspace::WorkspaceLoaderFactory as _;
30
31use crate::cli_util::GlobalArgs;
32use crate::cli_util::expand_args;
33use crate::cli_util::find_workspace_dir;
34use crate::cli_util::load_revset_aliases;
35use crate::cli_util::load_template_aliases;
36use crate::command_error::CommandError;
37use crate::command_error::user_error;
38use crate::config::CONFIG_SCHEMA;
39use crate::config::ConfigArgKind;
40use crate::config::ConfigEnv;
41use crate::config::config_from_environment;
42use crate::config::default_config_layers;
43use crate::merge_tools::ExternalMergeTool;
44use crate::merge_tools::configured_merge_tools;
45use crate::merge_tools::get_external_tool_config;
46use crate::ui::Ui;
47
48const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''
49" " ++
50coalesce(
51 if(!present, "(deleted bookmark)"),
52 if(!normal_target, "(conflicted bookmark)"),
53 if(!normal_target.description(), "(no description set)"),
54 normal_target.description().first_line(),
55)
56'''"#;
57const TAG_HELP_TEMPLATE: &str = r#"template-aliases.'tag_help()'='''
58" " ++
59coalesce(
60 if(!present, "(deleted tag)"),
61 if(!normal_target, "(conflicted tag)"),
62 if(!normal_target.description(), "(no description set)"),
63 normal_target.description().first_line(),
64)
65'''"#;
66
67fn split_help_text(line: &str) -> (&str, Option<StyledStr>) {
70 match line.split_once(' ') {
71 Some((name, help)) => (name, Some(help.to_string().into())),
72 None => (line, None),
73 }
74}
75
76pub fn local_bookmarks() -> Vec<CompletionCandidate> {
77 with_jj(|jj, _| {
78 let output = jj
79 .build()
80 .arg("bookmark")
81 .arg("list")
82 .arg("--config")
83 .arg(BOOKMARK_HELP_TEMPLATE)
84 .arg("--template")
85 .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#)
86 .output()
87 .map_err(user_error)?;
88
89 Ok(String::from_utf8_lossy(&output.stdout)
90 .lines()
91 .map(split_help_text)
92 .map(|(name, help)| CompletionCandidate::new(name).help(help))
93 .collect())
94 })
95}
96
97pub fn tracked_bookmarks() -> Vec<CompletionCandidate> {
98 with_jj(|jj, _| {
99 let output = jj
100 .build()
101 .arg("bookmark")
102 .arg("list")
103 .arg("--tracked")
104 .arg("--config")
105 .arg(BOOKMARK_HELP_TEMPLATE)
106 .arg("--template")
107 .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#)
108 .output()
109 .map_err(user_error)?;
110
111 Ok(String::from_utf8_lossy(&output.stdout)
112 .lines()
113 .map(split_help_text)
114 .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))
115 .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)
118 .map(|((name, _remote), help)| CompletionCandidate::new(name).help(help))
119 .collect())
120 })
121}
122
123pub fn untracked_bookmarks() -> Vec<CompletionCandidate> {
124 with_jj(|jj, _settings| {
125 let remotes = jj
126 .build()
127 .arg("git")
128 .arg("remote")
129 .arg("list")
130 .output()
131 .map_err(user_error)?;
132 let remotes = String::from_utf8_lossy(&remotes.stdout);
133 let remotes = remotes
134 .lines()
135 .filter_map(|l| l.split_whitespace().next())
136 .collect_vec();
137
138 let bookmark_table = jj
139 .build()
140 .arg("bookmark")
141 .arg("list")
142 .arg("--all-remotes")
143 .arg("--config")
144 .arg(BOOKMARK_HELP_TEMPLATE)
145 .arg("--template")
146 .arg(
147 r#"
148 if(remote != "git",
149 if(!remote, name) ++ "\t" ++
150 if(remote, name ++ "@" ++ remote) ++ "\t" ++
151 if(tracked, "tracked") ++ "\t" ++
152 bookmark_help() ++ "\n"
153 )"#,
154 )
155 .output()
156 .map_err(user_error)?;
157 let bookmark_table = String::from_utf8_lossy(&bookmark_table.stdout);
158
159 let mut possible_bookmarks_to_track = Vec::new();
160 let mut already_tracked_bookmarks = HashSet::new();
161
162 for line in bookmark_table.lines() {
163 let [local, remote, tracked, help] =
164 line.split('\t').collect_array().unwrap_or_default();
165
166 if !local.is_empty() {
167 possible_bookmarks_to_track.extend(
168 remotes
169 .iter()
170 .map(|remote| (format!("{local}@{remote}"), help)),
171 );
172 } else if tracked.is_empty() {
173 possible_bookmarks_to_track.push((remote.to_owned(), help));
174 } else {
175 already_tracked_bookmarks.insert(remote);
176 }
177 }
178 possible_bookmarks_to_track
179 .retain(|(bookmark, _help)| !already_tracked_bookmarks.contains(&bookmark.as_str()));
180
181 Ok(possible_bookmarks_to_track
182 .iter()
183 .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))
184 .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)
187 .map(|((name, _remote), help)| {
188 CompletionCandidate::new(name).help(Some(help.to_string().into()))
189 })
190 .collect())
191 })
192}
193
194pub fn bookmarks() -> Vec<CompletionCandidate> {
195 with_jj(|jj, _settings| {
196 let output = jj
197 .build()
198 .arg("bookmark")
199 .arg("list")
200 .arg("--all-remotes")
201 .arg("--config")
202 .arg(BOOKMARK_HELP_TEMPLATE)
203 .arg("--template")
204 .arg(
205 r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#,
207 )
208 .output()
209 .map_err(user_error)?;
210 let stdout = String::from_utf8_lossy(&output.stdout);
211
212 Ok((&stdout
213 .lines()
214 .map(split_help_text)
215 .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name)))
216 .into_iter()
217 .map(|(bookmark, mut refs)| {
218 let help = refs.find_map(|(_, help)| help);
219 let local = help.is_some();
220 let display_order = match local {
221 true => 0,
222 false => 1,
223 };
224 CompletionCandidate::new(bookmark)
225 .help(help)
226 .display_order(Some(display_order))
227 })
228 .collect())
229 })
230}
231
232pub fn local_tags() -> Vec<CompletionCandidate> {
233 with_jj(|jj, _| {
234 let output = jj
235 .build()
236 .arg("tag")
237 .arg("list")
238 .arg("--config")
239 .arg(TAG_HELP_TEMPLATE)
240 .arg("--template")
241 .arg(r#"if(!remote, name ++ tag_help()) ++ "\n""#)
242 .output()
243 .map_err(user_error)?;
244
245 Ok(String::from_utf8_lossy(&output.stdout)
246 .lines()
247 .map(split_help_text)
248 .map(|(name, help)| CompletionCandidate::new(name).help(help))
249 .collect())
250 })
251}
252
253pub fn git_remotes() -> Vec<CompletionCandidate> {
254 with_jj(|jj, _| {
255 let output = jj
256 .build()
257 .arg("git")
258 .arg("remote")
259 .arg("list")
260 .output()
261 .map_err(user_error)?;
262
263 let stdout = String::from_utf8_lossy(&output.stdout);
264
265 Ok(stdout
266 .lines()
267 .filter_map(|line| line.split_once(' ').map(|(name, _url)| name))
268 .map(CompletionCandidate::new)
269 .collect())
270 })
271}
272
273pub fn template_aliases() -> Vec<CompletionCandidate> {
274 with_jj(|_, settings| {
275 let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else {
276 return Ok(Vec::new());
277 };
278 Ok(template_aliases
279 .symbol_names()
280 .map(|name| {
281 let doc = template_aliases.get_symbol(name).unwrap().2;
282 CompletionCandidate::new(name).help(doc.map(|doc| doc.to_owned().into()))
283 })
284 .sorted()
285 .collect())
286 })
287}
288
289pub fn aliases() -> Vec<CompletionCandidate> {
290 with_jj(|_, settings| {
291 Ok(settings
292 .table_keys("aliases")
293 .filter(|alias| alias.len() > 2)
298 .map(|alias| {
299 CompletionCandidate::new(alias).help(
300 settings
301 .get_string(["aliases", alias, "doc"])
302 .ok()
303 .map(|doc| doc.into()),
304 )
305 })
306 .collect())
307 })
308}
309
310fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> {
311 with_jj(|jj, settings| {
312 const LOCAL_BOOKMARK: usize = 0;
314 const TAG: usize = 1;
315 const CHANGE_ID: usize = 2;
316 const REMOTE_BOOKMARK: usize = 3;
317 const REVSET_ALIAS: usize = 4;
318
319 let mut candidates = Vec::new();
320
321 let mut cmd = jj.build();
324 cmd.arg("bookmark")
325 .arg("list")
326 .arg("--all-remotes")
327 .arg("--config")
328 .arg(BOOKMARK_HELP_TEMPLATE)
329 .arg("--template")
330 .arg(
331 r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,
332 );
333 if let Some(revs) = revset_filter {
334 cmd.arg("--revisions").arg(revs);
335 }
336 let output = cmd.output().map_err(user_error)?;
337 let stdout = String::from_utf8_lossy(&output.stdout);
338
339 candidates.extend(
340 stdout
341 .lines()
342 .map(split_help_text)
343 .filter(|(bookmark, _)| bookmark.starts_with(match_prefix))
344 .map(|(bookmark, help)| {
345 let local = !bookmark.contains('@');
346 let display_order = match local {
347 true => LOCAL_BOOKMARK,
348 false => REMOTE_BOOKMARK,
349 };
350 CompletionCandidate::new(bookmark)
351 .help(help)
352 .display_order(Some(display_order))
353 }),
354 );
355
356 if revset_filter.is_none() {
363 let output = jj
364 .build()
365 .arg("tag")
366 .arg("list")
367 .arg("--config")
368 .arg(BOOKMARK_HELP_TEMPLATE)
369 .arg("--template")
370 .arg(r#"name ++ bookmark_help() ++ "\n""#)
371 .arg(format!("glob:{}*", globset::escape(match_prefix)))
372 .output()
373 .map_err(user_error)?;
374 let stdout = String::from_utf8_lossy(&output.stdout);
375
376 candidates.extend(stdout.lines().map(|line| {
377 let (name, desc) = split_help_text(line);
378 CompletionCandidate::new(name)
379 .help(desc)
380 .display_order(Some(TAG))
381 }));
382 }
383
384 let revisions = revset_filter
387 .map(String::from)
388 .or_else(|| settings.get_string("revsets.short-prefixes").ok())
389 .or_else(|| settings.get_string("revsets.log").ok())
390 .unwrap_or_default();
391
392 let output = jj
393 .build()
394 .arg("log")
395 .arg("--no-graph")
396 .arg("--limit")
397 .arg("100")
398 .arg("--revisions")
399 .arg(revisions)
400 .arg("--template")
401 .arg(
402 r#"
403 join(" ",
404 separate("/",
405 change_id.shortest(),
406 if(hidden || divergent, change_offset),
407 ),
408 if(description, description.first_line(), "(no description set)"),
409 ) ++ "\n""#,
410 )
411 .output()
412 .map_err(user_error)?;
413 let stdout = String::from_utf8_lossy(&output.stdout);
414
415 candidates.extend(
416 stdout
417 .lines()
418 .map(split_help_text)
419 .filter(|(id, _)| id.starts_with(match_prefix))
420 .map(|(id, desc)| {
421 CompletionCandidate::new(id)
422 .help(desc)
423 .display_order(Some(CHANGE_ID))
424 }),
425 );
426
427 let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;
430 let symbol_names = revset_aliases
431 .symbol_names()
432 .sorted_unstable()
433 .collect_vec();
434 candidates.extend(
435 symbol_names
436 .into_iter()
437 .filter(|symbol| symbol.starts_with(match_prefix))
438 .map(|symbol| {
439 let (_, defn, doc) = revset_aliases.get_symbol(symbol).unwrap();
440 let help: String = doc.map(|s| s.to_owned()).unwrap_or_else(|| defn.clone());
442 CompletionCandidate::new(symbol)
443 .help(Some(help.into()))
444 .display_order(Some(REVSET_ALIAS))
445 }),
446 );
447
448 Ok(candidates)
449 })
450}
451
452fn revset_expression(
453 current: &std::ffi::OsStr,
454 revset_filter: Option<&str>,
455) -> Vec<CompletionCandidate> {
456 let Some(current) = current.to_str() else {
457 return Vec::new();
458 };
459 let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current));
460 let candidates = revisions(match_prefix, revset_filter);
461 if prepend.is_empty() {
462 candidates
463 } else {
464 candidates
465 .into_iter()
466 .map(|candidate| candidate.add_prefix(prepend))
467 .collect()
468 }
469}
470
471pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
472 revset_expression(current, None)
473}
474
475pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
476 revset_expression(current, Some("mutable()"))
477}
478
479pub fn revset_expression_mutable_conflicts(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
480 revset_expression(current, Some("mutable() & conflicts()"))
481}
482
483fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> {
496 let final_part = incomplete_revset_str
497 .rsplit_once([':', '~', '|', '&', '(', ','])
498 .map(|(_, rest)| rest)
499 .unwrap_or(incomplete_revset_str);
500 let final_part = final_part
501 .rsplit_once("..")
502 .map(|(_, rest)| rest)
503 .unwrap_or(final_part)
504 .trim_ascii_start();
505
506 let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap();
507 re.is_match(final_part)
508 .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len()))
509}
510
511pub fn operations() -> Vec<CompletionCandidate> {
512 with_jj(|jj, _| {
513 let output = jj
514 .build()
515 .arg("operation")
516 .arg("log")
517 .arg("--no-graph")
518 .arg("--limit")
519 .arg("100")
520 .arg("--template")
521 .arg(
522 r#"
523 separate(" ",
524 id.short(),
525 "(" ++ format_timestamp(time.end()) ++ ")",
526 description.first_line(),
527 ) ++ "\n""#,
528 )
529 .output()
530 .map_err(user_error)?;
531
532 Ok(String::from_utf8_lossy(&output.stdout)
533 .lines()
534 .map(|line| {
535 let (id, help) = split_help_text(line);
536 CompletionCandidate::new(id).help(help)
537 })
538 .collect())
539 })
540}
541
542pub fn workspaces() -> Vec<CompletionCandidate> {
543 let template = indoc! {r#"
544 name ++ "\t" ++ if(
545 target.description(),
546 target.description().first_line(),
547 "(no description set)"
548 ) ++ "\n"
549 "#};
550 with_jj(|jj, _| {
551 let output = jj
552 .build()
553 .arg("workspace")
554 .arg("list")
555 .arg("--template")
556 .arg(template)
557 .output()
558 .map_err(user_error)?;
559 let stdout = String::from_utf8_lossy(&output.stdout);
560
561 Ok(stdout
562 .lines()
563 .filter_map(|line| {
564 let res = line.split_once('\t').map(|(name, desc)| {
565 CompletionCandidate::new(name).help(Some(desc.to_string().into()))
566 });
567 if res.is_none() {
568 eprintln!("Error parsing line {line}");
569 }
570 res
571 })
572 .collect())
573 })
574}
575
576fn merge_tools_filtered_by(
577 settings: &UserSettings,
578 condition: impl Fn(ExternalMergeTool) -> bool,
579) -> impl Iterator<Item = &str> {
580 configured_merge_tools(settings).filter(move |name| {
581 let Ok(Some(tool)) = get_external_tool_config(settings, name) else {
582 return false;
583 };
584 condition(tool)
585 })
586}
587
588pub fn merge_editors() -> Vec<CompletionCandidate> {
589 with_jj(|_, settings| {
590 Ok([":builtin", ":ours", ":theirs"]
591 .into_iter()
592 .chain(merge_tools_filtered_by(settings, |tool| {
593 !tool.merge_args.is_empty()
594 }))
595 .map(CompletionCandidate::new)
596 .collect())
597 })
598}
599
600pub fn diff_editors() -> Vec<CompletionCandidate> {
602 with_jj(|_, settings| {
603 Ok(std::iter::once(":builtin")
604 .chain(merge_tools_filtered_by(
605 settings,
606 |tool| !tool.edit_args.is_empty(),
610 ))
611 .map(CompletionCandidate::new)
612 .collect())
613 })
614}
615
616pub fn diff_formatters() -> Vec<CompletionCandidate> {
618 let builtin_format_kinds = crate::diff_util::all_builtin_diff_format_names();
619 with_jj(|_, settings| {
620 Ok(builtin_format_kinds
621 .iter()
622 .map(|s| s.as_str())
623 .chain(merge_tools_filtered_by(
624 settings,
625 |tool| !tool.diff_args.is_empty(),
629 ))
630 .map(CompletionCandidate::new)
631 .collect())
632 })
633}
634
635fn config_keys_rec(
636 prefix: ConfigNamePathBuf,
637 properties: &serde_json::Map<String, serde_json::Value>,
638 acc: &mut Vec<CompletionCandidate>,
639 only_leaves: bool,
640 suffix: &str,
641) {
642 for (key, value) in properties {
643 let mut prefix = prefix.clone();
644 prefix.push(key);
645
646 let value = value.as_object().unwrap();
647 match value.get("type").and_then(|v| v.as_str()) {
648 Some("object") => {
649 if !only_leaves {
650 let help = value
651 .get("description")
652 .map(|desc| desc.as_str().unwrap().to_string().into());
653 let escaped_key = prefix.to_string();
654 acc.push(CompletionCandidate::new(escaped_key).help(help));
655 }
656 let Some(properties) = value.get("properties") else {
657 continue;
658 };
659 let properties = properties.as_object().unwrap();
660 config_keys_rec(prefix, properties, acc, only_leaves, suffix);
661 }
662 _ => {
663 let help = value
664 .get("description")
665 .map(|desc| desc.as_str().unwrap().to_string().into());
666 let escaped_key = format!("{prefix}{suffix}");
667 acc.push(CompletionCandidate::new(escaped_key).help(help));
668 }
669 }
670 }
671}
672
673fn json_keypath<'a>(
674 schema: &'a serde_json::Value,
675 keypath: &str,
676 separator: &str,
677) -> Option<&'a serde_json::Value> {
678 keypath
679 .split(separator)
680 .try_fold(schema, |value, step| value.get(step))
681}
682fn jsonschema_keypath<'a>(
683 schema: &'a serde_json::Value,
684 keypath: &ConfigNamePathBuf,
685) -> Option<&'a serde_json::Value> {
686 keypath.components().try_fold(schema, |value, step| {
687 let value = value.as_object()?;
688 if value.get("type")?.as_str()? != "object" {
689 return None;
690 }
691 let properties = value.get("properties")?.as_object()?;
692 properties.get(step.get())
693 })
694}
695
696fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> {
697 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
698
699 let mut config_entry = jsonschema_keypath(&schema, path)?;
700 if let Some(reference) = config_entry.get("$ref") {
701 let reference = reference.as_str()?.strip_prefix("#/")?;
702 config_entry = json_keypath(&schema, reference, "/")?;
703 }
704
705 if let Some(possible_values) = config_entry.get("enum") {
706 return Some(
707 possible_values
708 .as_array()?
709 .iter()
710 .filter_map(|val| val.as_str())
711 .map(ToOwned::to_owned)
712 .collect(),
713 );
714 }
715
716 Some(match config_entry.get("type")?.as_str()? {
717 "boolean" => vec!["false".into(), "true".into()],
718 _ => vec![],
719 })
720}
721
722fn config_keys_impl(only_leaves: bool, suffix: &str) -> Vec<CompletionCandidate> {
723 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
724 let schema = schema.as_object().unwrap();
725 let properties = schema["properties"].as_object().unwrap();
726
727 let mut candidates = Vec::new();
728 config_keys_rec(
729 ConfigNamePathBuf::root(),
730 properties,
731 &mut candidates,
732 only_leaves,
733 suffix,
734 );
735 candidates
736}
737
738pub fn config_keys() -> Vec<CompletionCandidate> {
739 config_keys_impl(false, "")
740}
741
742pub fn leaf_config_keys() -> Vec<CompletionCandidate> {
743 config_keys_impl(true, "")
744}
745
746pub fn leaf_config_key_value(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
747 let Some(current) = current.to_str() else {
748 return Vec::new();
749 };
750
751 if let Some((key, current_val)) = current.split_once('=') {
752 let Ok(key) = key.parse() else {
753 return Vec::new();
754 };
755 let possible_values = config_values(&key).unwrap_or_default();
756
757 possible_values
758 .into_iter()
759 .filter(|x| x.starts_with(current_val))
760 .map(|x| CompletionCandidate::new(format!("{key}={x}")))
761 .collect()
762 } else {
763 config_keys_impl(true, "=")
764 .into_iter()
765 .filter(|candidate| candidate.get_value().to_str().unwrap().starts_with(current))
766 .collect()
767 }
768}
769
770pub fn config_keys_to_unset() -> Vec<CompletionCandidate> {
771 let Ok(config_level_flag) = std::env::args()
772 .filter(|arg| matches!(arg.as_str(), "--user" | "--repo" | "--workspace"))
773 .at_most_one()
774 else {
775 return Vec::new();
776 };
777
778 with_jj(|jj, _| {
779 const TEMPLATE: &str = r#"name ++ "\t" ++ source ++ "\t" ++ stringify(value).replace(regex:'\n\s*', " ") ++ "\n""#;
780 let list_output = jj
781 .build()
782 .args(["config", "list"])
783 .args(
786 config_level_flag
787 .is_some()
788 .then_some("--include-overridden"),
789 )
790 .args(config_level_flag)
791 .args(["--template", TEMPLATE])
792 .output()
793 .map_err(user_error)?;
794 Ok(String::from_utf8_lossy(&list_output.stdout)
795 .lines()
796 .filter_map(|line| line.split('\t').collect_tuple())
797 .filter(|(_, source, _)| matches!(*source, "user" | "repo" | "workspace"))
798 .map(|(name, source, value)| {
799 CompletionCandidate::new(name)
800 .tag(Some(source.to_string().into()))
801 .help(Some(format!("{source}: {value}").into()))
802 })
803 .collect())
804 })
805}
806
807pub fn branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
808 let Some(current) = current.to_str() else {
809 return Vec::new();
810 };
811
812 let Some((branch_name, revision)) = current.split_once('=') else {
813 return Vec::new();
815 };
816 revset_expression(revision.as_ref(), None)
817 .into_iter()
818 .map(|rev| rev.add_prefix(format!("{branch_name}=")))
819 .collect()
820}
821
822fn path_completion_candidate_from(
823 current_prefix: &str,
824 normalized_prefix_path: &Path,
825 path: &Path,
826 mode: Option<clap::builder::StyledStr>,
827) -> Option<CompletionCandidate> {
828 let normalized_prefix = match normalized_prefix_path.to_str()? {
829 "." => "", normalized_prefix => normalized_prefix,
831 };
832
833 let path = slash_path(path);
834 let mut remainder = path.to_str()?.strip_prefix(normalized_prefix)?;
835
836 if current_prefix.ends_with(std::path::is_separator) {
840 remainder = remainder.strip_prefix('/').unwrap_or(remainder);
841 }
842
843 match remainder.split_inclusive('/').at_most_one() {
844 Ok(file_completion) => Some(
847 CompletionCandidate::new(format!(
848 "{current_prefix}{}",
849 file_completion.unwrap_or_default()
850 ))
851 .help(mode),
852 ),
853
854 Err(mut components) => Some(CompletionCandidate::new(format!(
856 "{current_prefix}{}",
857 components.next().unwrap()
858 ))),
859 }
860}
861
862fn current_prefix_to_fileset(current: &str) -> String {
863 let cur_esc = globset::escape(current);
864 let dir_pat = format!("{cur_esc}*/**");
865 let path_pat = format!("{cur_esc}*");
866 format!("glob:{dir_pat:?} | glob:{path_pat:?}")
867}
868
869fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
870 let Some(current) = current.to_str() else {
871 return Vec::new();
872 };
873
874 let normalized_prefix = normalize_path(Path::new(current));
875 let normalized_prefix = slash_path(&normalized_prefix);
876
877 with_jj(|jj, _| {
878 let mut child = jj
879 .build()
880 .arg("file")
881 .arg("list")
882 .arg("--revision")
883 .arg(rev)
884 .arg("--template")
885 .arg(r#"path.display() ++ "\n""#)
886 .arg(current_prefix_to_fileset(current))
887 .stdout(std::process::Stdio::piped())
888 .stderr(std::process::Stdio::null())
889 .spawn()
890 .map_err(user_error)?;
891 let stdout = child.stdout.take().unwrap();
892
893 Ok(std::io::BufReader::new(stdout)
894 .lines()
895 .take(1_000)
896 .map_while(Result::ok)
897 .filter_map(|path| {
898 path_completion_candidate_from(current, &normalized_prefix, Path::new(&path), None)
899 })
900 .dedup() .collect())
902 })
903}
904
905fn modified_files_from_rev_with_jj_cmd(
906 rev: (String, Option<String>),
907 mut cmd: std::process::Command,
908 current: &std::ffi::OsStr,
909) -> Result<Vec<CompletionCandidate>, CommandError> {
910 let Some(current) = current.to_str() else {
911 return Ok(Vec::new());
912 };
913
914 let normalized_prefix = normalize_path(Path::new(current));
915 let normalized_prefix = slash_path(&normalized_prefix);
916
917 let template = indoc! {r#"
919 concat(
920 status ++ ' ' ++ path.display() ++ "\n",
921 if(status == 'renamed', 'renamed.source ' ++ source.path().display() ++ "\n"),
922 )
923 "#};
924 cmd.arg("diff")
925 .args(["--template", template])
926 .arg(current_prefix_to_fileset(current));
927 match rev {
928 (rev, None) => cmd.arg("--revisions").arg(rev),
929 (from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to),
930 };
931 let output = cmd.output().map_err(user_error)?;
932 let stdout = String::from_utf8_lossy(&output.stdout);
933
934 let mut include_renames = false;
935 let mut candidates: Vec<_> = stdout
936 .lines()
937 .filter_map(|line| line.split_once(' '))
938 .filter_map(|(mode, path)| {
939 let mode = match mode {
940 "modified" => "Modified".into(),
941 "removed" => "Deleted".into(),
942 "added" => "Added".into(),
943 "renamed" => "Renamed".into(),
944 "renamed.source" => {
945 include_renames = true;
946 "Renamed".into()
947 }
948 "copied" => "Copied".into(),
949 _ => format!("unknown mode: '{mode}'").into(),
950 };
951 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), Some(mode))
952 })
953 .collect();
954
955 if include_renames {
956 candidates.sort_unstable_by(|a, b| Path::new(a.get_value()).cmp(Path::new(b.get_value())));
957 }
958 candidates.dedup();
959
960 Ok(candidates)
961}
962
963fn modified_files_from_rev(
964 rev: (String, Option<String>),
965 current: &std::ffi::OsStr,
966) -> Vec<CompletionCandidate> {
967 with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build(), current))
968}
969
970fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
971 let Some(current) = current.to_str() else {
972 return Vec::new();
973 };
974
975 let normalized_prefix = normalize_path(Path::new(current));
976 let normalized_prefix = slash_path(&normalized_prefix);
977
978 with_jj(|jj, _| {
979 let output = jj
980 .build()
981 .arg("resolve")
982 .arg("--list")
983 .arg("--revision")
984 .arg(rev)
985 .arg(current_prefix_to_fileset(current))
986 .output()
987 .map_err(user_error)?;
988 let stdout = String::from_utf8_lossy(&output.stdout);
989
990 Ok(stdout
991 .lines()
992 .filter_map(|line| {
993 let path = line
994 .split_whitespace()
995 .next()
996 .expect("resolve --list should contain whitespace after path");
997
998 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), None)
999 })
1000 .dedup() .collect())
1002 })
1003}
1004
1005pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1006 modified_files_from_rev(("@".into(), None), current)
1007}
1008
1009pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1010 all_files_from_rev(parse::revision_or_wc(), current)
1011}
1012
1013pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1014 modified_files_from_rev((parse::revision_or_wc(), None), current)
1015}
1016
1017pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1018 match parse::range() {
1019 Some((from, to)) => modified_files_from_rev((from, Some(to)), current),
1020 None => modified_files_from_rev(("@".into(), None), current),
1021 }
1022}
1023
1024pub fn modified_from_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1027 modified_files_from_rev((parse::from_or_wc(), None), current)
1028}
1029
1030pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1031 if let Some(rev) = parse::revision() {
1032 return modified_files_from_rev((rev, None), current);
1033 }
1034 modified_range_files(current)
1035}
1036
1037pub fn modified_changes_in_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1038 if let Some(rev) = parse::changes_in() {
1039 return modified_files_from_rev((rev, None), current);
1040 }
1041 modified_range_files(current)
1042}
1043
1044pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1045 conflicted_files_from_rev(&parse::revision_or_wc(), current)
1046}
1047
1048pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1050 let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
1051 modified_files_from_rev((rev, None), current)
1052}
1053
1054pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1056 let Some((from, to)) = parse::range() else {
1057 return Vec::new();
1058 };
1059 with_jj(|jj, _| {
1063 let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build(), current)?;
1064 res.extend(modified_files_from_rev_with_jj_cmd(
1065 (to, None),
1066 jj.build(),
1067 current,
1068 )?);
1069 Ok(res)
1070 })
1071}
1072
1073pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1075 let mut rev = parse::log_revisions().join(")|(");
1076 if rev.is_empty() {
1077 rev = "@".into();
1078 } else {
1079 rev = format!("latest(heads(({rev})))"); }
1081 all_files_from_rev(rev, current)
1082}
1083
1084fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate>
1088where
1089 F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>,
1090{
1091 get_jj_command()
1092 .and_then(|(jj, settings)| completion_fn(jj, &settings))
1093 .unwrap_or_else(|e| {
1094 eprintln!("{}", e.error);
1095 Vec::new()
1096 })
1097}
1098
1099fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
1109 let current_exe = std::env::current_exe().map_err(user_error)?;
1110 let mut cmd_args = Vec::<String>::new();
1111
1112 cmd_args.push("--ignore-working-copy".into());
1115 cmd_args.push("--color=never".into());
1116 cmd_args.push("--no-pager".into());
1117
1118 let app = crate::commands::default_app();
1122 let mut raw_config = config_from_environment(default_config_layers());
1123 let ui = Ui::null();
1124 let cwd = std::env::current_dir()
1125 .and_then(dunce::canonicalize)
1126 .map_err(user_error)?;
1127 let mut config_env = ConfigEnv::from_environment();
1129 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
1130 config_env.reload_user_config(&mut raw_config).ok();
1131 if let Ok(loader) = &maybe_cwd_workspace_loader {
1132 config_env.reset_repo_path(loader.repo_path());
1133 config_env.reload_repo_config(&ui, &mut raw_config).ok();
1134 config_env.reset_workspace_path(loader.workspace_root());
1135 config_env
1136 .reload_workspace_config(&ui, &mut raw_config)
1137 .ok();
1138 }
1139 let mut config = config_env.resolve_config(&raw_config)?;
1140 let args = std::env::args_os().skip(2);
1142 let args = expand_args(&ui, &app, args, &config)?;
1143 let arg_matches = app
1144 .clone()
1145 .disable_version_flag(true)
1146 .disable_help_flag(true)
1147 .ignore_errors(true)
1148 .try_get_matches_from(args)?;
1149 let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?;
1150
1151 if let Some(repository) = args.repository {
1152 if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) {
1154 config_env.reset_repo_path(loader.repo_path());
1155 config_env.reload_repo_config(&ui, &mut raw_config).ok();
1156 config_env.reset_workspace_path(loader.workspace_root());
1157 config_env
1158 .reload_workspace_config(&ui, &mut raw_config)
1159 .ok();
1160 if let Ok(new_config) = config_env.resolve_config(&raw_config) {
1161 config = new_config;
1162 }
1163 }
1164 cmd_args.push("--repository".into());
1165 cmd_args.push(repository);
1166 }
1167 if let Some(at_operation) = args.at_operation {
1168 let mut canary_cmd = std::process::Command::new(¤t_exe);
1179 canary_cmd.args(&cmd_args);
1180 canary_cmd.arg("--at-operation");
1181 canary_cmd.arg(&at_operation);
1182 canary_cmd.arg("debug");
1183 canary_cmd.arg("snapshot");
1184
1185 match canary_cmd.output() {
1186 Ok(output) if output.status.success() => {
1187 cmd_args.push("--at-operation".into());
1189 cmd_args.push(at_operation);
1190 }
1191 _ => {} }
1193 }
1194 for (kind, value) in args.early_args.merged_config_args(&arg_matches) {
1195 let arg = match kind {
1196 ConfigArgKind::Item => format!("--config={value}"),
1197 ConfigArgKind::File => format!("--config-file={value}"),
1198 };
1199 cmd_args.push(arg);
1200 }
1201
1202 let builder = JjBuilder {
1203 cmd: current_exe,
1204 args: cmd_args,
1205 };
1206 let settings = UserSettings::from_config(config)?;
1207
1208 Ok((builder, settings))
1209}
1210
1211struct JjBuilder {
1214 cmd: std::path::PathBuf,
1215 args: Vec<String>,
1216}
1217
1218impl JjBuilder {
1219 fn build(&self) -> std::process::Command {
1220 let mut cmd = std::process::Command::new(&self.cmd);
1221 cmd.args(&self.args);
1222 cmd
1223 }
1224}
1225
1226mod parse {
1235 pub(super) fn parse_flag(
1236 candidates: &[&str],
1237 mut args: impl Iterator<Item = String>,
1238 ) -> impl Iterator<Item = String> {
1239 std::iter::from_fn(move || {
1240 for arg in args.by_ref() {
1241 if candidates.contains(&arg.as_ref()) {
1243 match args.next() {
1244 Some(val) if !val.starts_with('-') => {
1245 return Some(strip_shell_quotes(&val).into());
1246 }
1247 _ => return None,
1248 }
1249 }
1250
1251 if let Some(value) = candidates.iter().find_map(|candidate| {
1253 let rest = arg.strip_prefix(candidate)?;
1254 match rest.strip_prefix('=') {
1255 Some(value) => Some(value),
1256
1257 None if candidate.len() == 2 => Some(rest),
1259
1260 None => None,
1261 }
1262 }) {
1263 return Some(strip_shell_quotes(value).into());
1264 }
1265 }
1266 None
1267 })
1268 }
1269
1270 pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> {
1271 parse_flag(&["-r", "--revision"], args).next()
1272 }
1273
1274 pub fn revision() -> Option<String> {
1275 parse_revision_impl(std::env::args())
1276 }
1277
1278 pub fn parse_changes_in_impl(args: impl Iterator<Item = String>) -> Option<String> {
1279 parse_flag(&["-c", "--changes-in"], args).next()
1280 }
1281
1282 pub fn changes_in() -> Option<String> {
1283 parse_changes_in_impl(std::env::args())
1284 }
1285
1286 pub fn revision_or_wc() -> String {
1287 revision().unwrap_or_else(|| "@".into())
1288 }
1289
1290 pub fn from_or_wc() -> String {
1291 parse_flag(&["-f", "--from"], std::env::args())
1292 .next()
1293 .unwrap_or_else(|| "@".into())
1294 }
1295
1296 pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)>
1297 where
1298 T: Iterator<Item = String>,
1299 {
1300 let from = parse_flag(&["-f", "--from"], args()).next()?;
1301 let to = parse_flag(&["-t", "--to"], args())
1302 .next()
1303 .unwrap_or_else(|| "@".into());
1304
1305 Some((from, to))
1306 }
1307
1308 pub fn range() -> Option<(String, String)> {
1309 parse_range_impl(std::env::args)
1310 }
1311
1312 pub fn squash_revision() -> Option<String> {
1317 if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() {
1318 return Some(rev);
1319 }
1320 parse_flag(&["-f", "--from"], std::env::args()).next()
1321 }
1322
1323 pub fn log_revisions() -> Vec<String> {
1326 let candidates = &["-r", "--revisions"];
1327 parse_flag(candidates, std::env::args()).collect()
1328 }
1329
1330 fn strip_shell_quotes(s: &str) -> &str {
1331 if s.len() >= 2
1332 && (s.starts_with('"') && s.ends_with('"') || s.starts_with('\'') && s.ends_with('\''))
1333 {
1334 &s[1..s.len() - 1]
1335 } else {
1336 s
1337 }
1338 }
1339}
1340
1341#[cfg(test)]
1342mod tests {
1343 use super::*;
1344
1345 #[test]
1346 fn test_split_revset_trailing_name() {
1347 assert_eq!(split_revset_trailing_name(""), Some(("", "")));
1348 assert_eq!(split_revset_trailing_name(" "), Some((" ", "")));
1349 assert_eq!(split_revset_trailing_name("foo"), Some(("", "foo")));
1350 assert_eq!(split_revset_trailing_name(" foo"), Some((" ", "foo")));
1351 assert_eq!(split_revset_trailing_name("foo "), None);
1352 assert_eq!(split_revset_trailing_name("foo_"), Some(("", "foo_")));
1353 assert_eq!(split_revset_trailing_name("foo/"), Some(("", "foo/")));
1354 assert_eq!(split_revset_trailing_name("foo/b"), Some(("", "foo/b")));
1355
1356 assert_eq!(split_revset_trailing_name("foo-"), Some(("", "foo-")));
1357 assert_eq!(split_revset_trailing_name("foo+"), Some(("", "foo+")));
1358 assert_eq!(
1359 split_revset_trailing_name("foo-bar-"),
1360 Some(("", "foo-bar-"))
1361 );
1362 assert_eq!(
1363 split_revset_trailing_name("foo-bar-b"),
1364 Some(("", "foo-bar-b"))
1365 );
1366
1367 assert_eq!(split_revset_trailing_name("foo."), Some(("", "foo.")));
1368 assert_eq!(split_revset_trailing_name("foo..b"), Some(("foo..", "b")));
1369 assert_eq!(split_revset_trailing_name("..foo"), Some(("..", "foo")));
1370
1371 assert_eq!(split_revset_trailing_name("foo(bar"), Some(("foo(", "bar")));
1372 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1373 assert_eq!(split_revset_trailing_name("(f"), Some(("(", "f")));
1374
1375 assert_eq!(split_revset_trailing_name("foo@"), Some(("", "foo@")));
1376 assert_eq!(split_revset_trailing_name("foo@b"), Some(("", "foo@b")));
1377 assert_eq!(split_revset_trailing_name("..foo@"), Some(("..", "foo@")));
1378 assert_eq!(
1379 split_revset_trailing_name("::F(foo@origin.1..bar@origin."),
1380 Some(("::F(foo@origin.1..", "bar@origin."))
1381 );
1382 }
1383
1384 #[test]
1385 fn test_split_revset_trailing_name_with_trailing_operator() {
1386 assert_eq!(split_revset_trailing_name("foo|"), Some(("foo|", "")));
1387 assert_eq!(split_revset_trailing_name("foo | "), Some(("foo | ", "")));
1388 assert_eq!(split_revset_trailing_name("foo&"), Some(("foo&", "")));
1389 assert_eq!(split_revset_trailing_name("foo~"), Some(("foo~", "")));
1390
1391 assert_eq!(split_revset_trailing_name(".."), Some(("..", "")));
1392 assert_eq!(split_revset_trailing_name("foo.."), Some(("foo..", "")));
1393 assert_eq!(split_revset_trailing_name("::"), Some(("::", "")));
1394 assert_eq!(split_revset_trailing_name("foo::"), Some(("foo::", "")));
1395
1396 assert_eq!(split_revset_trailing_name("("), Some(("(", "")));
1397 assert_eq!(split_revset_trailing_name("foo("), Some(("foo(", "")));
1398 assert_eq!(split_revset_trailing_name("foo()"), None);
1399 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1400 }
1401
1402 #[test]
1403 fn test_config_keys() {
1404 config_keys();
1406 }
1407
1408 #[test]
1409 fn test_parse_revision_impl() {
1410 let good_cases: &[&[&str]] = &[
1411 &["-r", "foo"],
1412 &["-r", "'foo'"],
1413 &["-r", "\"foo\""],
1414 &["-rfoo"],
1415 &["-r'foo'"],
1416 &["-r\"foo\""],
1417 &["--revision", "foo"],
1418 &["-r=foo"],
1419 &["-r='foo'"],
1420 &["-r=\"foo\""],
1421 &["--revision=foo"],
1422 &["--revision='foo'"],
1423 &["--revision=\"foo\""],
1424 &["preceding_arg", "-r", "foo"],
1425 &["-r", "foo", "following_arg"],
1426 ];
1427 for case in good_cases {
1428 let args = case.iter().map(|s| s.to_string());
1429 assert_eq!(
1430 parse::parse_revision_impl(args),
1431 Some("foo".into()),
1432 "case: {case:?}",
1433 );
1434 }
1435 let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1436 for case in bad_cases {
1437 let args = case.iter().map(|s| s.to_string());
1438 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1439 }
1440 }
1441
1442 #[test]
1443 fn test_parse_changes_in_impl() {
1444 let good_cases: &[&[&str]] = &[
1445 &["-c", "foo"],
1446 &["--changes-in", "foo"],
1447 &["-cfoo"],
1448 &["--changes-in=foo"],
1449 ];
1450 for case in good_cases {
1451 let args = case.iter().map(|s| s.to_string());
1452 assert_eq!(
1453 parse::parse_changes_in_impl(args),
1454 Some("foo".into()),
1455 "case: {case:?}",
1456 );
1457 }
1458 let bad_cases: &[&[&str]] = &[&[], &["-c"], &["-r"], &["foo"]];
1459 for case in bad_cases {
1460 let args = case.iter().map(|s| s.to_string());
1461 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1462 }
1463 }
1464
1465 #[test]
1466 fn test_parse_range_impl() {
1467 let wc_cases: &[&[&str]] = &[
1468 &["-f", "foo"],
1469 &["--from", "foo"],
1470 &["-f=foo"],
1471 &["preceding_arg", "-f", "foo"],
1472 &["-f", "foo", "following_arg"],
1473 ];
1474 for case in wc_cases {
1475 let args = case.iter().map(|s| s.to_string());
1476 assert_eq!(
1477 parse::parse_range_impl(|| args.clone()),
1478 Some(("foo".into(), "@".into())),
1479 "case: {case:?}",
1480 );
1481 }
1482 let to_cases: &[&[&str]] = &[
1483 &["-f", "foo", "-t", "bar"],
1484 &["-f", "foo", "--to", "bar"],
1485 &["-f=foo", "-t=bar"],
1486 &["-t=bar", "-f=foo"],
1487 ];
1488 for case in to_cases {
1489 let args = case.iter().map(|s| s.to_string());
1490 assert_eq!(
1491 parse::parse_range_impl(|| args.clone()),
1492 Some(("foo".into(), "bar".into())),
1493 "case: {case:?}",
1494 );
1495 }
1496 let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1497 for case in bad_cases {
1498 let args = case.iter().map(|s| s.to_string());
1499 assert_eq!(
1500 parse::parse_range_impl(|| args.clone()),
1501 None,
1502 "case: {case:?}"
1503 );
1504 }
1505 }
1506
1507 #[test]
1508 fn test_parse_multiple_flags() {
1509 let candidates = &["-r", "--revisions"];
1510 let args = &[
1511 "unrelated_arg_at_the_beginning",
1512 "-r",
1513 "1",
1514 "--revisions",
1515 "2",
1516 "-r=3",
1517 "--revisions=4",
1518 "unrelated_arg_in_the_middle",
1519 "-r5",
1520 "unrelated_arg_at_the_end",
1521 ];
1522 let flags: Vec<_> =
1523 parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect();
1524 let expected = ["1", "2", "3", "4", "5"];
1525 assert_eq!(flags, expected);
1526 }
1527}