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_template_aliases;
35use crate::command_error::CommandError;
36use crate::command_error::user_error;
37use crate::config::CONFIG_SCHEMA;
38use crate::config::ConfigArgKind;
39use crate::config::ConfigEnv;
40use crate::config::config_from_environment;
41use crate::config::default_config_layers;
42use crate::merge_tools::ExternalMergeTool;
43use crate::merge_tools::configured_merge_tools;
44use crate::merge_tools::get_external_tool_config;
45use crate::revset_util::load_revset_aliases;
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(CompletionCandidate::new)
281 .sorted()
282 .collect())
283 })
284}
285
286pub fn aliases() -> Vec<CompletionCandidate> {
287 with_jj(|_, settings| {
288 Ok(settings
289 .table_keys("aliases")
290 .filter(|alias| alias.len() > 2)
295 .map(CompletionCandidate::new)
296 .collect())
297 })
298}
299
300fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> {
301 with_jj(|jj, settings| {
302 const LOCAL_BOOKMARK: usize = 0;
304 const TAG: usize = 1;
305 const CHANGE_ID: usize = 2;
306 const REMOTE_BOOKMARK: usize = 3;
307 const REVSET_ALIAS: usize = 4;
308
309 let mut candidates = Vec::new();
310
311 let mut cmd = jj.build();
314 cmd.arg("bookmark")
315 .arg("list")
316 .arg("--all-remotes")
317 .arg("--config")
318 .arg(BOOKMARK_HELP_TEMPLATE)
319 .arg("--template")
320 .arg(
321 r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,
322 );
323 if let Some(revs) = revset_filter {
324 cmd.arg("--revisions").arg(revs);
325 }
326 let output = cmd.output().map_err(user_error)?;
327 let stdout = String::from_utf8_lossy(&output.stdout);
328
329 candidates.extend(
330 stdout
331 .lines()
332 .map(split_help_text)
333 .filter(|(bookmark, _)| bookmark.starts_with(match_prefix))
334 .map(|(bookmark, help)| {
335 let local = !bookmark.contains('@');
336 let display_order = match local {
337 true => LOCAL_BOOKMARK,
338 false => REMOTE_BOOKMARK,
339 };
340 CompletionCandidate::new(bookmark)
341 .help(help)
342 .display_order(Some(display_order))
343 }),
344 );
345
346 if revset_filter.is_none() {
353 let output = jj
354 .build()
355 .arg("tag")
356 .arg("list")
357 .arg("--config")
358 .arg(BOOKMARK_HELP_TEMPLATE)
359 .arg("--template")
360 .arg(r#"name ++ bookmark_help() ++ "\n""#)
361 .arg(format!("glob:{}*", globset::escape(match_prefix)))
362 .output()
363 .map_err(user_error)?;
364 let stdout = String::from_utf8_lossy(&output.stdout);
365
366 candidates.extend(stdout.lines().map(|line| {
367 let (name, desc) = split_help_text(line);
368 CompletionCandidate::new(name)
369 .help(desc)
370 .display_order(Some(TAG))
371 }));
372 }
373
374 let revisions = revset_filter
377 .map(String::from)
378 .or_else(|| settings.get_string("revsets.short-prefixes").ok())
379 .or_else(|| settings.get_string("revsets.log").ok())
380 .unwrap_or_default();
381
382 let output = jj
383 .build()
384 .arg("log")
385 .arg("--no-graph")
386 .arg("--limit")
387 .arg("100")
388 .arg("--revisions")
389 .arg(revisions)
390 .arg("--template")
391 .arg(
392 r#"
393 join(" ",
394 separate("/",
395 change_id.shortest(),
396 if(hidden || divergent, change_offset),
397 ),
398 if(description, description.first_line(), "(no description set)"),
399 ) ++ "\n""#,
400 )
401 .output()
402 .map_err(user_error)?;
403 let stdout = String::from_utf8_lossy(&output.stdout);
404
405 candidates.extend(
406 stdout
407 .lines()
408 .map(split_help_text)
409 .filter(|(id, _)| id.starts_with(match_prefix))
410 .map(|(id, desc)| {
411 CompletionCandidate::new(id)
412 .help(desc)
413 .display_order(Some(CHANGE_ID))
414 }),
415 );
416
417 let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;
420 let symbol_names = revset_aliases
421 .symbol_names()
422 .sorted_unstable()
423 .collect_vec();
424 candidates.extend(
425 symbol_names
426 .into_iter()
427 .filter(|symbol| symbol.starts_with(match_prefix))
428 .map(|symbol| {
429 let (_, defn) = revset_aliases.get_symbol(symbol).unwrap();
430 CompletionCandidate::new(symbol)
431 .help(Some(defn.into()))
432 .display_order(Some(REVSET_ALIAS))
433 }),
434 );
435
436 Ok(candidates)
437 })
438}
439
440fn revset_expression(
441 current: &std::ffi::OsStr,
442 revset_filter: Option<&str>,
443) -> Vec<CompletionCandidate> {
444 let Some(current) = current.to_str() else {
445 return Vec::new();
446 };
447 let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current));
448 let candidates = revisions(match_prefix, revset_filter);
449 if prepend.is_empty() {
450 candidates
451 } else {
452 candidates
453 .into_iter()
454 .map(|candidate| candidate.add_prefix(prepend))
455 .collect()
456 }
457}
458
459pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
460 revset_expression(current, None)
461}
462
463pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
464 revset_expression(current, Some("mutable()"))
465}
466
467pub fn revset_expression_mutable_conflicts(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
468 revset_expression(current, Some("mutable() & conflicts()"))
469}
470
471fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> {
484 let final_part = incomplete_revset_str
485 .rsplit_once([':', '~', '|', '&', '(', ','])
486 .map(|(_, rest)| rest)
487 .unwrap_or(incomplete_revset_str);
488 let final_part = final_part
489 .rsplit_once("..")
490 .map(|(_, rest)| rest)
491 .unwrap_or(final_part)
492 .trim_ascii_start();
493
494 let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap();
495 re.is_match(final_part)
496 .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len()))
497}
498
499pub fn operations() -> Vec<CompletionCandidate> {
500 with_jj(|jj, _| {
501 let output = jj
502 .build()
503 .arg("operation")
504 .arg("log")
505 .arg("--no-graph")
506 .arg("--limit")
507 .arg("100")
508 .arg("--template")
509 .arg(
510 r#"
511 separate(" ",
512 id.short(),
513 "(" ++ format_timestamp(time.end()) ++ ")",
514 description.first_line(),
515 ) ++ "\n""#,
516 )
517 .output()
518 .map_err(user_error)?;
519
520 Ok(String::from_utf8_lossy(&output.stdout)
521 .lines()
522 .map(|line| {
523 let (id, help) = split_help_text(line);
524 CompletionCandidate::new(id).help(help)
525 })
526 .collect())
527 })
528}
529
530pub fn workspaces() -> Vec<CompletionCandidate> {
531 let template = indoc! {r#"
532 name ++ "\t" ++ if(
533 target.description(),
534 target.description().first_line(),
535 "(no description set)"
536 ) ++ "\n"
537 "#};
538 with_jj(|jj, _| {
539 let output = jj
540 .build()
541 .arg("workspace")
542 .arg("list")
543 .arg("--template")
544 .arg(template)
545 .output()
546 .map_err(user_error)?;
547 let stdout = String::from_utf8_lossy(&output.stdout);
548
549 Ok(stdout
550 .lines()
551 .filter_map(|line| {
552 let res = line.split_once('\t').map(|(name, desc)| {
553 CompletionCandidate::new(name).help(Some(desc.to_string().into()))
554 });
555 if res.is_none() {
556 eprintln!("Error parsing line {line}");
557 }
558 res
559 })
560 .collect())
561 })
562}
563
564fn merge_tools_filtered_by(
565 settings: &UserSettings,
566 condition: impl Fn(ExternalMergeTool) -> bool,
567) -> impl Iterator<Item = &str> {
568 configured_merge_tools(settings).filter(move |name| {
569 let Ok(Some(tool)) = get_external_tool_config(settings, name) else {
570 return false;
571 };
572 condition(tool)
573 })
574}
575
576pub fn merge_editors() -> Vec<CompletionCandidate> {
577 with_jj(|_, settings| {
578 Ok([":builtin", ":ours", ":theirs"]
579 .into_iter()
580 .chain(merge_tools_filtered_by(settings, |tool| {
581 !tool.merge_args.is_empty()
582 }))
583 .map(CompletionCandidate::new)
584 .collect())
585 })
586}
587
588pub fn diff_editors() -> Vec<CompletionCandidate> {
590 with_jj(|_, settings| {
591 Ok(std::iter::once(":builtin")
592 .chain(merge_tools_filtered_by(
593 settings,
594 |tool| !tool.edit_args.is_empty(),
598 ))
599 .map(CompletionCandidate::new)
600 .collect())
601 })
602}
603
604pub fn diff_formatters() -> Vec<CompletionCandidate> {
606 let builtin_format_kinds = crate::diff_util::all_builtin_diff_format_names();
607 with_jj(|_, settings| {
608 Ok(builtin_format_kinds
609 .iter()
610 .map(|s| s.as_str())
611 .chain(merge_tools_filtered_by(
612 settings,
613 |tool| !tool.diff_args.is_empty(),
617 ))
618 .map(CompletionCandidate::new)
619 .collect())
620 })
621}
622
623fn config_keys_rec(
624 prefix: ConfigNamePathBuf,
625 properties: &serde_json::Map<String, serde_json::Value>,
626 acc: &mut Vec<CompletionCandidate>,
627 only_leaves: bool,
628 suffix: &str,
629) {
630 for (key, value) in properties {
631 let mut prefix = prefix.clone();
632 prefix.push(key);
633
634 let value = value.as_object().unwrap();
635 match value.get("type").and_then(|v| v.as_str()) {
636 Some("object") => {
637 if !only_leaves {
638 let help = value
639 .get("description")
640 .map(|desc| desc.as_str().unwrap().to_string().into());
641 let escaped_key = prefix.to_string();
642 acc.push(CompletionCandidate::new(escaped_key).help(help));
643 }
644 let Some(properties) = value.get("properties") else {
645 continue;
646 };
647 let properties = properties.as_object().unwrap();
648 config_keys_rec(prefix, properties, acc, only_leaves, suffix);
649 }
650 _ => {
651 let help = value
652 .get("description")
653 .map(|desc| desc.as_str().unwrap().to_string().into());
654 let escaped_key = format!("{prefix}{suffix}");
655 acc.push(CompletionCandidate::new(escaped_key).help(help));
656 }
657 }
658 }
659}
660
661fn json_keypath<'a>(
662 schema: &'a serde_json::Value,
663 keypath: &str,
664 separator: &str,
665) -> Option<&'a serde_json::Value> {
666 keypath
667 .split(separator)
668 .try_fold(schema, |value, step| value.get(step))
669}
670fn jsonschema_keypath<'a>(
671 schema: &'a serde_json::Value,
672 keypath: &ConfigNamePathBuf,
673) -> Option<&'a serde_json::Value> {
674 keypath.components().try_fold(schema, |value, step| {
675 let value = value.as_object()?;
676 if value.get("type")?.as_str()? != "object" {
677 return None;
678 }
679 let properties = value.get("properties")?.as_object()?;
680 properties.get(step.get())
681 })
682}
683
684fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> {
685 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
686
687 let mut config_entry = jsonschema_keypath(&schema, path)?;
688 if let Some(reference) = config_entry.get("$ref") {
689 let reference = reference.as_str()?.strip_prefix("#/")?;
690 config_entry = json_keypath(&schema, reference, "/")?;
691 }
692
693 if let Some(possible_values) = config_entry.get("enum") {
694 return Some(
695 possible_values
696 .as_array()?
697 .iter()
698 .filter_map(|val| val.as_str())
699 .map(ToOwned::to_owned)
700 .collect(),
701 );
702 }
703
704 Some(match config_entry.get("type")?.as_str()? {
705 "boolean" => vec!["false".into(), "true".into()],
706 _ => vec![],
707 })
708}
709
710fn config_keys_impl(only_leaves: bool, suffix: &str) -> Vec<CompletionCandidate> {
711 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
712 let schema = schema.as_object().unwrap();
713 let properties = schema["properties"].as_object().unwrap();
714
715 let mut candidates = Vec::new();
716 config_keys_rec(
717 ConfigNamePathBuf::root(),
718 properties,
719 &mut candidates,
720 only_leaves,
721 suffix,
722 );
723 candidates
724}
725
726pub fn config_keys() -> Vec<CompletionCandidate> {
727 config_keys_impl(false, "")
728}
729
730pub fn leaf_config_keys() -> Vec<CompletionCandidate> {
731 config_keys_impl(true, "")
732}
733
734pub fn leaf_config_key_value(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
735 let Some(current) = current.to_str() else {
736 return Vec::new();
737 };
738
739 if let Some((key, current_val)) = current.split_once('=') {
740 let Ok(key) = key.parse() else {
741 return Vec::new();
742 };
743 let possible_values = config_values(&key).unwrap_or_default();
744
745 possible_values
746 .into_iter()
747 .filter(|x| x.starts_with(current_val))
748 .map(|x| CompletionCandidate::new(format!("{key}={x}")))
749 .collect()
750 } else {
751 config_keys_impl(true, "=")
752 .into_iter()
753 .filter(|candidate| candidate.get_value().to_str().unwrap().starts_with(current))
754 .collect()
755 }
756}
757
758pub fn config_keys_to_unset() -> Vec<CompletionCandidate> {
759 let Ok(config_level_flag) = std::env::args()
760 .filter(|arg| matches!(arg.as_str(), "--user" | "--repo" | "--workspace"))
761 .at_most_one()
762 else {
763 return Vec::new();
764 };
765
766 with_jj(|jj, _| {
767 const TEMPLATE: &str = r#"name ++ "\t" ++ source ++ "\t" ++ stringify(value).replace(regex:'\n\s*', " ") ++ "\n""#;
768 let list_output = jj
769 .build()
770 .args(["config", "list"])
771 .args(
774 config_level_flag
775 .is_some()
776 .then_some("--include-overridden"),
777 )
778 .args(config_level_flag)
779 .args(["--template", TEMPLATE])
780 .output()
781 .map_err(user_error)?;
782 Ok(String::from_utf8_lossy(&list_output.stdout)
783 .lines()
784 .filter_map(|line| line.split('\t').collect_tuple())
785 .filter(|(_, source, _)| matches!(*source, "user" | "repo" | "workspace"))
786 .map(|(name, source, value)| {
787 CompletionCandidate::new(name)
788 .tag(Some(source.to_string().into()))
789 .help(Some(format!("{source}: {value}").into()))
790 })
791 .collect())
792 })
793}
794
795pub fn branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
796 let Some(current) = current.to_str() else {
797 return Vec::new();
798 };
799
800 let Some((branch_name, revision)) = current.split_once('=') else {
801 return Vec::new();
803 };
804 revset_expression(revision.as_ref(), None)
805 .into_iter()
806 .map(|rev| rev.add_prefix(format!("{branch_name}=")))
807 .collect()
808}
809
810fn path_completion_candidate_from(
811 current_prefix: &str,
812 normalized_prefix_path: &Path,
813 path: &Path,
814 mode: Option<clap::builder::StyledStr>,
815) -> Option<CompletionCandidate> {
816 let normalized_prefix = match normalized_prefix_path.to_str()? {
817 "." => "", normalized_prefix => normalized_prefix,
819 };
820
821 let path = slash_path(path);
822 let mut remainder = path.to_str()?.strip_prefix(normalized_prefix)?;
823
824 if current_prefix.ends_with(std::path::is_separator) {
828 remainder = remainder.strip_prefix('/').unwrap_or(remainder);
829 }
830
831 match remainder.split_inclusive('/').at_most_one() {
832 Ok(file_completion) => Some(
835 CompletionCandidate::new(format!(
836 "{current_prefix}{}",
837 file_completion.unwrap_or_default()
838 ))
839 .help(mode),
840 ),
841
842 Err(mut components) => Some(CompletionCandidate::new(format!(
844 "{current_prefix}{}",
845 components.next().unwrap()
846 ))),
847 }
848}
849
850fn current_prefix_to_fileset(current: &str) -> String {
851 let cur_esc = globset::escape(current);
852 let dir_pat = format!("{cur_esc}*/**");
853 let path_pat = format!("{cur_esc}*");
854 format!("glob:{dir_pat:?} | glob:{path_pat:?}")
855}
856
857fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
858 let Some(current) = current.to_str() else {
859 return Vec::new();
860 };
861
862 let normalized_prefix = normalize_path(Path::new(current));
863 let normalized_prefix = slash_path(&normalized_prefix);
864
865 with_jj(|jj, _| {
866 let mut child = jj
867 .build()
868 .arg("file")
869 .arg("list")
870 .arg("--revision")
871 .arg(rev)
872 .arg("--template")
873 .arg(r#"path.display() ++ "\n""#)
874 .arg(current_prefix_to_fileset(current))
875 .stdout(std::process::Stdio::piped())
876 .stderr(std::process::Stdio::null())
877 .spawn()
878 .map_err(user_error)?;
879 let stdout = child.stdout.take().unwrap();
880
881 Ok(std::io::BufReader::new(stdout)
882 .lines()
883 .take(1_000)
884 .map_while(Result::ok)
885 .filter_map(|path| {
886 path_completion_candidate_from(current, &normalized_prefix, Path::new(&path), None)
887 })
888 .dedup() .collect())
890 })
891}
892
893fn modified_files_from_rev_with_jj_cmd(
894 rev: (String, Option<String>),
895 mut cmd: std::process::Command,
896 current: &std::ffi::OsStr,
897) -> Result<Vec<CompletionCandidate>, CommandError> {
898 let Some(current) = current.to_str() else {
899 return Ok(Vec::new());
900 };
901
902 let normalized_prefix = normalize_path(Path::new(current));
903 let normalized_prefix = slash_path(&normalized_prefix);
904
905 let template = indoc! {r#"
907 concat(
908 status ++ ' ' ++ path.display() ++ "\n",
909 if(status == 'renamed', 'renamed.source ' ++ source.path().display() ++ "\n"),
910 )
911 "#};
912 cmd.arg("diff")
913 .args(["--template", template])
914 .arg(current_prefix_to_fileset(current));
915 match rev {
916 (rev, None) => cmd.arg("--revisions").arg(rev),
917 (from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to),
918 };
919 let output = cmd.output().map_err(user_error)?;
920 let stdout = String::from_utf8_lossy(&output.stdout);
921
922 let mut include_renames = false;
923 let mut candidates: Vec<_> = stdout
924 .lines()
925 .filter_map(|line| line.split_once(' '))
926 .filter_map(|(mode, path)| {
927 let mode = match mode {
928 "modified" => "Modified".into(),
929 "removed" => "Deleted".into(),
930 "added" => "Added".into(),
931 "renamed" => "Renamed".into(),
932 "renamed.source" => {
933 include_renames = true;
934 "Renamed".into()
935 }
936 "copied" => "Copied".into(),
937 _ => format!("unknown mode: '{mode}'").into(),
938 };
939 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), Some(mode))
940 })
941 .collect();
942
943 if include_renames {
944 candidates.sort_unstable_by(|a, b| Path::new(a.get_value()).cmp(Path::new(b.get_value())));
945 }
946 candidates.dedup();
947
948 Ok(candidates)
949}
950
951fn modified_files_from_rev(
952 rev: (String, Option<String>),
953 current: &std::ffi::OsStr,
954) -> Vec<CompletionCandidate> {
955 with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build(), current))
956}
957
958fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
959 let Some(current) = current.to_str() else {
960 return Vec::new();
961 };
962
963 let normalized_prefix = normalize_path(Path::new(current));
964 let normalized_prefix = slash_path(&normalized_prefix);
965
966 with_jj(|jj, _| {
967 let output = jj
968 .build()
969 .arg("resolve")
970 .arg("--list")
971 .arg("--revision")
972 .arg(rev)
973 .arg(current_prefix_to_fileset(current))
974 .output()
975 .map_err(user_error)?;
976 let stdout = String::from_utf8_lossy(&output.stdout);
977
978 Ok(stdout
979 .lines()
980 .filter_map(|line| {
981 let path = line
982 .split_whitespace()
983 .next()
984 .expect("resolve --list should contain whitespace after path");
985
986 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), None)
987 })
988 .dedup() .collect())
990 })
991}
992
993pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
994 modified_files_from_rev(("@".into(), None), current)
995}
996
997pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
998 all_files_from_rev(parse::revision_or_wc(), current)
999}
1000
1001pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1002 modified_files_from_rev((parse::revision_or_wc(), None), current)
1003}
1004
1005pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1006 match parse::range() {
1007 Some((from, to)) => modified_files_from_rev((from, Some(to)), current),
1008 None => modified_files_from_rev(("@".into(), None), current),
1009 }
1010}
1011
1012pub fn modified_from_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1015 modified_files_from_rev((parse::from_or_wc(), None), current)
1016}
1017
1018pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1019 if let Some(rev) = parse::revision() {
1020 return modified_files_from_rev((rev, None), current);
1021 }
1022 modified_range_files(current)
1023}
1024
1025pub fn modified_changes_in_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1026 if let Some(rev) = parse::changes_in() {
1027 return modified_files_from_rev((rev, None), current);
1028 }
1029 modified_range_files(current)
1030}
1031
1032pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1033 conflicted_files_from_rev(&parse::revision_or_wc(), current)
1034}
1035
1036pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1038 let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
1039 modified_files_from_rev((rev, None), current)
1040}
1041
1042pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1044 let Some((from, to)) = parse::range() else {
1045 return Vec::new();
1046 };
1047 with_jj(|jj, _| {
1051 let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build(), current)?;
1052 res.extend(modified_files_from_rev_with_jj_cmd(
1053 (to, None),
1054 jj.build(),
1055 current,
1056 )?);
1057 Ok(res)
1058 })
1059}
1060
1061pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1063 let mut rev = parse::log_revisions().join(")|(");
1064 if rev.is_empty() {
1065 rev = "@".into();
1066 } else {
1067 rev = format!("latest(heads(({rev})))"); }
1069 all_files_from_rev(rev, current)
1070}
1071
1072fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate>
1076where
1077 F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>,
1078{
1079 get_jj_command()
1080 .and_then(|(jj, settings)| completion_fn(jj, &settings))
1081 .unwrap_or_else(|e| {
1082 eprintln!("{}", e.error);
1083 Vec::new()
1084 })
1085}
1086
1087fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
1097 let current_exe = std::env::current_exe().map_err(user_error)?;
1098 let mut cmd_args = Vec::<String>::new();
1099
1100 cmd_args.push("--ignore-working-copy".into());
1103 cmd_args.push("--color=never".into());
1104 cmd_args.push("--no-pager".into());
1105
1106 let app = crate::commands::default_app();
1110 let mut raw_config = config_from_environment(default_config_layers());
1111 let ui = Ui::null();
1112 let cwd = std::env::current_dir()
1113 .and_then(dunce::canonicalize)
1114 .map_err(user_error)?;
1115 let mut config_env = ConfigEnv::from_environment();
1117 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
1118 config_env.reload_user_config(&mut raw_config).ok();
1119 if let Ok(loader) = &maybe_cwd_workspace_loader {
1120 config_env.reset_repo_path(loader.repo_path());
1121 config_env.reload_repo_config(&ui, &mut raw_config).ok();
1122 config_env.reset_workspace_path(loader.workspace_root());
1123 config_env
1124 .reload_workspace_config(&ui, &mut raw_config)
1125 .ok();
1126 }
1127 let mut config = config_env.resolve_config(&raw_config)?;
1128 let args = std::env::args_os().skip(2);
1130 let args = expand_args(&ui, &app, args, &config)?;
1131 let arg_matches = app
1132 .clone()
1133 .disable_version_flag(true)
1134 .disable_help_flag(true)
1135 .ignore_errors(true)
1136 .try_get_matches_from(args)?;
1137 let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?;
1138
1139 if let Some(repository) = args.repository {
1140 if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) {
1142 config_env.reset_repo_path(loader.repo_path());
1143 config_env.reload_repo_config(&ui, &mut raw_config).ok();
1144 config_env.reset_workspace_path(loader.workspace_root());
1145 config_env
1146 .reload_workspace_config(&ui, &mut raw_config)
1147 .ok();
1148 if let Ok(new_config) = config_env.resolve_config(&raw_config) {
1149 config = new_config;
1150 }
1151 }
1152 cmd_args.push("--repository".into());
1153 cmd_args.push(repository);
1154 }
1155 if let Some(at_operation) = args.at_operation {
1156 let mut canary_cmd = std::process::Command::new(¤t_exe);
1167 canary_cmd.args(&cmd_args);
1168 canary_cmd.arg("--at-operation");
1169 canary_cmd.arg(&at_operation);
1170 canary_cmd.arg("debug");
1171 canary_cmd.arg("snapshot");
1172
1173 match canary_cmd.output() {
1174 Ok(output) if output.status.success() => {
1175 cmd_args.push("--at-operation".into());
1177 cmd_args.push(at_operation);
1178 }
1179 _ => {} }
1181 }
1182 for (kind, value) in args.early_args.merged_config_args(&arg_matches) {
1183 let arg = match kind {
1184 ConfigArgKind::Item => format!("--config={value}"),
1185 ConfigArgKind::File => format!("--config-file={value}"),
1186 };
1187 cmd_args.push(arg);
1188 }
1189
1190 let builder = JjBuilder {
1191 cmd: current_exe,
1192 args: cmd_args,
1193 };
1194 let settings = UserSettings::from_config(config)?;
1195
1196 Ok((builder, settings))
1197}
1198
1199struct JjBuilder {
1202 cmd: std::path::PathBuf,
1203 args: Vec<String>,
1204}
1205
1206impl JjBuilder {
1207 fn build(&self) -> std::process::Command {
1208 let mut cmd = std::process::Command::new(&self.cmd);
1209 cmd.args(&self.args);
1210 cmd
1211 }
1212}
1213
1214mod parse {
1223 pub(super) fn parse_flag(
1224 candidates: &[&str],
1225 mut args: impl Iterator<Item = String>,
1226 ) -> impl Iterator<Item = String> {
1227 std::iter::from_fn(move || {
1228 for arg in args.by_ref() {
1229 if candidates.contains(&arg.as_ref()) {
1231 match args.next() {
1232 Some(val) if !val.starts_with('-') => {
1233 return Some(strip_shell_quotes(&val).into());
1234 }
1235 _ => return None,
1236 }
1237 }
1238
1239 if let Some(value) = candidates.iter().find_map(|candidate| {
1241 let rest = arg.strip_prefix(candidate)?;
1242 match rest.strip_prefix('=') {
1243 Some(value) => Some(value),
1244
1245 None if candidate.len() == 2 => Some(rest),
1247
1248 None => None,
1249 }
1250 }) {
1251 return Some(strip_shell_quotes(value).into());
1252 }
1253 }
1254 None
1255 })
1256 }
1257
1258 pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> {
1259 parse_flag(&["-r", "--revision"], args).next()
1260 }
1261
1262 pub fn revision() -> Option<String> {
1263 parse_revision_impl(std::env::args())
1264 }
1265
1266 pub fn parse_changes_in_impl(args: impl Iterator<Item = String>) -> Option<String> {
1267 parse_flag(&["-c", "--changes-in"], args).next()
1268 }
1269
1270 pub fn changes_in() -> Option<String> {
1271 parse_changes_in_impl(std::env::args())
1272 }
1273
1274 pub fn revision_or_wc() -> String {
1275 revision().unwrap_or_else(|| "@".into())
1276 }
1277
1278 pub fn from_or_wc() -> String {
1279 parse_flag(&["-f", "--from"], std::env::args())
1280 .next()
1281 .unwrap_or_else(|| "@".into())
1282 }
1283
1284 pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)>
1285 where
1286 T: Iterator<Item = String>,
1287 {
1288 let from = parse_flag(&["-f", "--from"], args()).next()?;
1289 let to = parse_flag(&["-t", "--to"], args())
1290 .next()
1291 .unwrap_or_else(|| "@".into());
1292
1293 Some((from, to))
1294 }
1295
1296 pub fn range() -> Option<(String, String)> {
1297 parse_range_impl(std::env::args)
1298 }
1299
1300 pub fn squash_revision() -> Option<String> {
1305 if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() {
1306 return Some(rev);
1307 }
1308 parse_flag(&["-f", "--from"], std::env::args()).next()
1309 }
1310
1311 pub fn log_revisions() -> Vec<String> {
1314 let candidates = &["-r", "--revisions"];
1315 parse_flag(candidates, std::env::args()).collect()
1316 }
1317
1318 fn strip_shell_quotes(s: &str) -> &str {
1319 if s.len() >= 2
1320 && (s.starts_with('"') && s.ends_with('"') || s.starts_with('\'') && s.ends_with('\''))
1321 {
1322 &s[1..s.len() - 1]
1323 } else {
1324 s
1325 }
1326 }
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331 use super::*;
1332
1333 #[test]
1334 fn test_split_revset_trailing_name() {
1335 assert_eq!(split_revset_trailing_name(""), Some(("", "")));
1336 assert_eq!(split_revset_trailing_name(" "), Some((" ", "")));
1337 assert_eq!(split_revset_trailing_name("foo"), Some(("", "foo")));
1338 assert_eq!(split_revset_trailing_name(" foo"), Some((" ", "foo")));
1339 assert_eq!(split_revset_trailing_name("foo "), None);
1340 assert_eq!(split_revset_trailing_name("foo_"), Some(("", "foo_")));
1341 assert_eq!(split_revset_trailing_name("foo/"), Some(("", "foo/")));
1342 assert_eq!(split_revset_trailing_name("foo/b"), Some(("", "foo/b")));
1343
1344 assert_eq!(split_revset_trailing_name("foo-"), Some(("", "foo-")));
1345 assert_eq!(split_revset_trailing_name("foo+"), Some(("", "foo+")));
1346 assert_eq!(
1347 split_revset_trailing_name("foo-bar-"),
1348 Some(("", "foo-bar-"))
1349 );
1350 assert_eq!(
1351 split_revset_trailing_name("foo-bar-b"),
1352 Some(("", "foo-bar-b"))
1353 );
1354
1355 assert_eq!(split_revset_trailing_name("foo."), Some(("", "foo.")));
1356 assert_eq!(split_revset_trailing_name("foo..b"), Some(("foo..", "b")));
1357 assert_eq!(split_revset_trailing_name("..foo"), Some(("..", "foo")));
1358
1359 assert_eq!(split_revset_trailing_name("foo(bar"), Some(("foo(", "bar")));
1360 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1361 assert_eq!(split_revset_trailing_name("(f"), Some(("(", "f")));
1362
1363 assert_eq!(split_revset_trailing_name("foo@"), Some(("", "foo@")));
1364 assert_eq!(split_revset_trailing_name("foo@b"), Some(("", "foo@b")));
1365 assert_eq!(split_revset_trailing_name("..foo@"), Some(("..", "foo@")));
1366 assert_eq!(
1367 split_revset_trailing_name("::F(foo@origin.1..bar@origin."),
1368 Some(("::F(foo@origin.1..", "bar@origin."))
1369 );
1370 }
1371
1372 #[test]
1373 fn test_split_revset_trailing_name_with_trailing_operator() {
1374 assert_eq!(split_revset_trailing_name("foo|"), Some(("foo|", "")));
1375 assert_eq!(split_revset_trailing_name("foo | "), Some(("foo | ", "")));
1376 assert_eq!(split_revset_trailing_name("foo&"), Some(("foo&", "")));
1377 assert_eq!(split_revset_trailing_name("foo~"), Some(("foo~", "")));
1378
1379 assert_eq!(split_revset_trailing_name(".."), Some(("..", "")));
1380 assert_eq!(split_revset_trailing_name("foo.."), Some(("foo..", "")));
1381 assert_eq!(split_revset_trailing_name("::"), Some(("::", "")));
1382 assert_eq!(split_revset_trailing_name("foo::"), Some(("foo::", "")));
1383
1384 assert_eq!(split_revset_trailing_name("("), Some(("(", "")));
1385 assert_eq!(split_revset_trailing_name("foo("), Some(("foo(", "")));
1386 assert_eq!(split_revset_trailing_name("foo()"), None);
1387 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1388 }
1389
1390 #[test]
1391 fn test_config_keys() {
1392 config_keys();
1394 }
1395
1396 #[test]
1397 fn test_parse_revision_impl() {
1398 let good_cases: &[&[&str]] = &[
1399 &["-r", "foo"],
1400 &["-r", "'foo'"],
1401 &["-r", "\"foo\""],
1402 &["-rfoo"],
1403 &["-r'foo'"],
1404 &["-r\"foo\""],
1405 &["--revision", "foo"],
1406 &["-r=foo"],
1407 &["-r='foo'"],
1408 &["-r=\"foo\""],
1409 &["--revision=foo"],
1410 &["--revision='foo'"],
1411 &["--revision=\"foo\""],
1412 &["preceding_arg", "-r", "foo"],
1413 &["-r", "foo", "following_arg"],
1414 ];
1415 for case in good_cases {
1416 let args = case.iter().map(|s| s.to_string());
1417 assert_eq!(
1418 parse::parse_revision_impl(args),
1419 Some("foo".into()),
1420 "case: {case:?}",
1421 );
1422 }
1423 let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1424 for case in bad_cases {
1425 let args = case.iter().map(|s| s.to_string());
1426 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1427 }
1428 }
1429
1430 #[test]
1431 fn test_parse_changes_in_impl() {
1432 let good_cases: &[&[&str]] = &[
1433 &["-c", "foo"],
1434 &["--changes-in", "foo"],
1435 &["-cfoo"],
1436 &["--changes-in=foo"],
1437 ];
1438 for case in good_cases {
1439 let args = case.iter().map(|s| s.to_string());
1440 assert_eq!(
1441 parse::parse_changes_in_impl(args),
1442 Some("foo".into()),
1443 "case: {case:?}",
1444 );
1445 }
1446 let bad_cases: &[&[&str]] = &[&[], &["-c"], &["-r"], &["foo"]];
1447 for case in bad_cases {
1448 let args = case.iter().map(|s| s.to_string());
1449 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1450 }
1451 }
1452
1453 #[test]
1454 fn test_parse_range_impl() {
1455 let wc_cases: &[&[&str]] = &[
1456 &["-f", "foo"],
1457 &["--from", "foo"],
1458 &["-f=foo"],
1459 &["preceding_arg", "-f", "foo"],
1460 &["-f", "foo", "following_arg"],
1461 ];
1462 for case in wc_cases {
1463 let args = case.iter().map(|s| s.to_string());
1464 assert_eq!(
1465 parse::parse_range_impl(|| args.clone()),
1466 Some(("foo".into(), "@".into())),
1467 "case: {case:?}",
1468 );
1469 }
1470 let to_cases: &[&[&str]] = &[
1471 &["-f", "foo", "-t", "bar"],
1472 &["-f", "foo", "--to", "bar"],
1473 &["-f=foo", "-t=bar"],
1474 &["-t=bar", "-f=foo"],
1475 ];
1476 for case in to_cases {
1477 let args = case.iter().map(|s| s.to_string());
1478 assert_eq!(
1479 parse::parse_range_impl(|| args.clone()),
1480 Some(("foo".into(), "bar".into())),
1481 "case: {case:?}",
1482 );
1483 }
1484 let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1485 for case in bad_cases {
1486 let args = case.iter().map(|s| s.to_string());
1487 assert_eq!(
1488 parse::parse_range_impl(|| args.clone()),
1489 None,
1490 "case: {case:?}"
1491 );
1492 }
1493 }
1494
1495 #[test]
1496 fn test_parse_multiple_flags() {
1497 let candidates = &["-r", "--revisions"];
1498 let args = &[
1499 "unrelated_arg_at_the_beginning",
1500 "-r",
1501 "1",
1502 "--revisions",
1503 "2",
1504 "-r=3",
1505 "--revisions=4",
1506 "unrelated_arg_in_the_middle",
1507 "-r5",
1508 "unrelated_arg_at_the_end",
1509 ];
1510 let flags: Vec<_> =
1511 parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect();
1512 let expected = ["1", "2", "3", "4", "5"];
1513 assert_eq!(flags, expected);
1514 }
1515}