1use std::io::BufRead as _;
16use std::path::Path;
17
18use clap::FromArgMatches as _;
19use clap::builder::StyledStr;
20use clap_complete::CompletionCandidate;
21use indoc::indoc;
22use itertools::Itertools as _;
23use jj_lib::config::ConfigNamePathBuf;
24use jj_lib::settings::UserSettings;
25use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
26use jj_lib::workspace::WorkspaceLoaderFactory as _;
27
28use crate::cli_util::GlobalArgs;
29use crate::cli_util::expand_args;
30use crate::cli_util::find_workspace_dir;
31use crate::cli_util::load_template_aliases;
32use crate::command_error::CommandError;
33use crate::command_error::user_error;
34use crate::config::CONFIG_SCHEMA;
35use crate::config::ConfigArgKind;
36use crate::config::ConfigEnv;
37use crate::config::config_from_environment;
38use crate::config::default_config_layers;
39use crate::merge_tools::ExternalMergeTool;
40use crate::merge_tools::configured_merge_tools;
41use crate::merge_tools::get_external_tool_config;
42use crate::revset_util::load_revset_aliases;
43use crate::ui::Ui;
44
45const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''
46" " ++
47if(normal_target,
48 if(normal_target.description(),
49 normal_target.description().first_line(),
50 "(no description set)",
51 ),
52 "(conflicted bookmark)",
53)
54'''"#;
55
56fn split_help_text(line: &str) -> (&str, Option<StyledStr>) {
59 match line.split_once(' ') {
60 Some((name, help)) => (name, Some(help.to_string().into())),
61 None => (line, None),
62 }
63}
64
65pub fn local_bookmarks() -> Vec<CompletionCandidate> {
66 with_jj(|jj, _| {
67 let output = jj
68 .build()
69 .arg("bookmark")
70 .arg("list")
71 .arg("--config")
72 .arg(BOOKMARK_HELP_TEMPLATE)
73 .arg("--template")
74 .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#)
75 .output()
76 .map_err(user_error)?;
77
78 Ok(String::from_utf8_lossy(&output.stdout)
79 .lines()
80 .map(split_help_text)
81 .map(|(name, help)| CompletionCandidate::new(name).help(help))
82 .collect())
83 })
84}
85
86pub fn tracked_bookmarks() -> Vec<CompletionCandidate> {
87 with_jj(|jj, _| {
88 let output = jj
89 .build()
90 .arg("bookmark")
91 .arg("list")
92 .arg("--tracked")
93 .arg("--config")
94 .arg(BOOKMARK_HELP_TEMPLATE)
95 .arg("--template")
96 .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#)
97 .output()
98 .map_err(user_error)?;
99
100 Ok(String::from_utf8_lossy(&output.stdout)
101 .lines()
102 .map(split_help_text)
103 .map(|(name, help)| CompletionCandidate::new(name).help(help))
104 .collect())
105 })
106}
107
108pub fn untracked_bookmarks() -> Vec<CompletionCandidate> {
109 with_jj(|jj, _settings| {
110 let output = jj
111 .build()
112 .arg("bookmark")
113 .arg("list")
114 .arg("--all-remotes")
115 .arg("--config")
116 .arg(BOOKMARK_HELP_TEMPLATE)
117 .arg("--template")
118 .arg(
119 r#"if(remote && !tracked && remote != "git",
120 name ++ '@' ++ remote ++ bookmark_help() ++ "\n"
121 )"#,
122 )
123 .output()
124 .map_err(user_error)?;
125
126 Ok(String::from_utf8_lossy(&output.stdout)
127 .lines()
128 .map(|line| {
129 let (name, help) = split_help_text(line);
130 CompletionCandidate::new(name).help(help)
131 })
132 .collect())
133 })
134}
135
136pub fn bookmarks() -> Vec<CompletionCandidate> {
137 with_jj(|jj, _settings| {
138 let output = 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#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#,
149 )
150 .output()
151 .map_err(user_error)?;
152 let stdout = String::from_utf8_lossy(&output.stdout);
153
154 Ok((&stdout
155 .lines()
156 .map(split_help_text)
157 .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name)))
158 .into_iter()
159 .map(|(bookmark, mut refs)| {
160 let help = refs.find_map(|(_, help)| help);
161 let local = help.is_some();
162 let display_order = match local {
163 true => 0,
164 false => 1,
165 };
166 CompletionCandidate::new(bookmark)
167 .help(help)
168 .display_order(Some(display_order))
169 })
170 .collect())
171 })
172}
173
174pub fn git_remotes() -> Vec<CompletionCandidate> {
175 with_jj(|jj, _| {
176 let output = jj
177 .build()
178 .arg("git")
179 .arg("remote")
180 .arg("list")
181 .output()
182 .map_err(user_error)?;
183
184 let stdout = String::from_utf8_lossy(&output.stdout);
185
186 Ok(stdout
187 .lines()
188 .filter_map(|line| line.split_once(' ').map(|(name, _url)| name))
189 .map(CompletionCandidate::new)
190 .collect())
191 })
192}
193
194pub fn template_aliases() -> Vec<CompletionCandidate> {
195 with_jj(|_, settings| {
196 let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else {
197 return Ok(Vec::new());
198 };
199 Ok(template_aliases
200 .symbol_names()
201 .map(CompletionCandidate::new)
202 .sorted()
203 .collect())
204 })
205}
206
207pub fn aliases() -> Vec<CompletionCandidate> {
208 with_jj(|_, settings| {
209 Ok(settings
210 .table_keys("aliases")
211 .filter(|alias| alias.len() > 2)
216 .map(CompletionCandidate::new)
217 .collect())
218 })
219}
220
221fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> {
222 with_jj(|jj, settings| {
223 const LOCAL_BOOKMARK: usize = 0;
225 const TAG: usize = 1;
226 const CHANGE_ID: usize = 2;
227 const REMOTE_BOOKMARK: usize = 3;
228 const REVSET_ALIAS: usize = 4;
229
230 let mut candidates = Vec::new();
231
232 let mut cmd = jj.build();
235 cmd.arg("bookmark")
236 .arg("list")
237 .arg("--all-remotes")
238 .arg("--config")
239 .arg(BOOKMARK_HELP_TEMPLATE)
240 .arg("--template")
241 .arg(
242 r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,
243 );
244 if let Some(revs) = revset_filter {
245 cmd.arg("--revisions").arg(revs);
246 }
247 let output = cmd.output().map_err(user_error)?;
248 let stdout = String::from_utf8_lossy(&output.stdout);
249
250 candidates.extend(
251 stdout
252 .lines()
253 .map(split_help_text)
254 .filter(|(bookmark, _)| bookmark.starts_with(match_prefix))
255 .map(|(bookmark, help)| {
256 let local = !bookmark.contains('@');
257 let display_order = match local {
258 true => LOCAL_BOOKMARK,
259 false => REMOTE_BOOKMARK,
260 };
261 CompletionCandidate::new(bookmark)
262 .help(help)
263 .display_order(Some(display_order))
264 }),
265 );
266
267 if revset_filter.is_none() {
274 let output = jj
275 .build()
276 .arg("tag")
277 .arg("list")
278 .arg("--config")
279 .arg(BOOKMARK_HELP_TEMPLATE)
280 .arg("--template")
281 .arg(r#"name ++ bookmark_help() ++ "\n""#)
282 .arg(format!("glob:{}*", globset::escape(match_prefix)))
283 .output()
284 .map_err(user_error)?;
285 let stdout = String::from_utf8_lossy(&output.stdout);
286
287 candidates.extend(stdout.lines().map(|line| {
288 let (name, desc) = split_help_text(line);
289 CompletionCandidate::new(name)
290 .help(desc)
291 .display_order(Some(TAG))
292 }));
293 }
294
295 let revisions = revset_filter
298 .map(String::from)
299 .or_else(|| settings.get_string("revsets.short-prefixes").ok())
300 .or_else(|| settings.get_string("revsets.log").ok())
301 .unwrap_or_default();
302
303 let output = jj
304 .build()
305 .arg("log")
306 .arg("--no-graph")
307 .arg("--limit")
308 .arg("100")
309 .arg("--revisions")
310 .arg(revisions)
311 .arg("--template")
312 .arg(r#"change_id.shortest() ++ " " ++ if(description, description.first_line(), "(no description set)") ++ "\n""#)
313 .output()
314 .map_err(user_error)?;
315 let stdout = String::from_utf8_lossy(&output.stdout);
316
317 candidates.extend(
318 stdout
319 .lines()
320 .map(split_help_text)
321 .filter(|(id, _)| id.starts_with(match_prefix))
322 .map(|(id, desc)| {
323 CompletionCandidate::new(id)
324 .help(desc)
325 .display_order(Some(CHANGE_ID))
326 }),
327 );
328
329 let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;
332 let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect();
333 symbol_names.sort();
334 candidates.extend(
335 symbol_names
336 .into_iter()
337 .filter(|symbol| symbol.starts_with(match_prefix))
338 .map(|symbol| {
339 let (_, defn) = revset_aliases.get_symbol(symbol).unwrap();
340 CompletionCandidate::new(symbol)
341 .help(Some(defn.into()))
342 .display_order(Some(REVSET_ALIAS))
343 }),
344 );
345
346 Ok(candidates)
347 })
348}
349
350fn revset_expression(
351 current: &std::ffi::OsStr,
352 revset_filter: Option<&str>,
353) -> Vec<CompletionCandidate> {
354 let Some(current) = current.to_str() else {
355 return Vec::new();
356 };
357 let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current));
358 let candidates = revisions(match_prefix, revset_filter);
359 if prepend.is_empty() {
360 candidates
361 } else {
362 candidates
363 .into_iter()
364 .map(|candidate| candidate.add_prefix(prepend))
365 .collect()
366 }
367}
368
369pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
370 revset_expression(current, None)
371}
372
373pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
374 revset_expression(current, Some("mutable()"))
375}
376
377pub fn revset_expression_mutable_conflicts(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
378 revset_expression(current, Some("mutable() & conflicts()"))
379}
380
381fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> {
394 let final_part = incomplete_revset_str
395 .rsplit_once([':', '~', '|', '&', '(', ','])
396 .map(|(_, rest)| rest)
397 .unwrap_or(incomplete_revset_str);
398 let final_part = final_part
399 .rsplit_once("..")
400 .map(|(_, rest)| rest)
401 .unwrap_or(final_part)
402 .trim_ascii_start();
403
404 let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap();
405 re.is_match(final_part)
406 .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len()))
407}
408
409pub fn operations() -> Vec<CompletionCandidate> {
410 with_jj(|jj, _| {
411 let output = jj
412 .build()
413 .arg("operation")
414 .arg("log")
415 .arg("--no-graph")
416 .arg("--limit")
417 .arg("100")
418 .arg("--template")
419 .arg(
420 r#"
421 separate(" ",
422 id.short(),
423 "(" ++ format_timestamp(time.end()) ++ ")",
424 description.first_line(),
425 ) ++ "\n""#,
426 )
427 .output()
428 .map_err(user_error)?;
429
430 Ok(String::from_utf8_lossy(&output.stdout)
431 .lines()
432 .map(|line| {
433 let (id, help) = split_help_text(line);
434 CompletionCandidate::new(id).help(help)
435 })
436 .collect())
437 })
438}
439
440pub fn workspaces() -> Vec<CompletionCandidate> {
441 with_jj(|jj, _| {
442 let output = jj
443 .build()
444 .arg("workspace")
445 .arg("list")
446 .arg("-T")
447 .arg(r#"name ++ "\t" ++ if(target.description(), target.description().first_line(), "(no description set)") ++ "\n""#)
448 .output()
449 .map_err(user_error)?;
450 let stdout = String::from_utf8_lossy(&output.stdout);
451
452 Ok(stdout
453 .lines()
454 .map(|line| {
455 let (name, desc) = line.split_once(": ").unwrap_or((line, ""));
456 CompletionCandidate::new(name).help(Some(desc.to_string().into()))
457 })
458 .collect())
459 })
460}
461
462fn merge_tools_filtered_by(
463 settings: &UserSettings,
464 condition: impl Fn(ExternalMergeTool) -> bool,
465) -> impl Iterator<Item = &str> {
466 configured_merge_tools(settings).filter(move |name| {
467 let Ok(Some(tool)) = get_external_tool_config(settings, name) else {
468 return false;
469 };
470 condition(tool)
471 })
472}
473
474pub fn merge_editors() -> Vec<CompletionCandidate> {
475 with_jj(|_, settings| {
476 Ok([":builtin", ":ours", ":theirs"]
477 .into_iter()
478 .chain(merge_tools_filtered_by(settings, |tool| {
479 !tool.merge_args.is_empty()
480 }))
481 .map(CompletionCandidate::new)
482 .collect())
483 })
484}
485
486pub fn diff_editors() -> Vec<CompletionCandidate> {
488 with_jj(|_, settings| {
489 Ok(std::iter::once(":builtin")
490 .chain(merge_tools_filtered_by(
491 settings,
492 |tool| !tool.edit_args.is_empty(),
496 ))
497 .map(CompletionCandidate::new)
498 .collect())
499 })
500}
501
502pub fn diff_formatters() -> Vec<CompletionCandidate> {
504 let builtin_format_kinds = crate::diff_util::all_builtin_diff_format_names();
505 with_jj(|_, settings| {
506 Ok(builtin_format_kinds
507 .iter()
508 .map(|s| s.as_str())
509 .chain(merge_tools_filtered_by(
510 settings,
511 |tool| !tool.diff_args.is_empty(),
515 ))
516 .map(CompletionCandidate::new)
517 .collect())
518 })
519}
520
521fn config_keys_rec(
522 prefix: ConfigNamePathBuf,
523 properties: &serde_json::Map<String, serde_json::Value>,
524 acc: &mut Vec<CompletionCandidate>,
525 only_leaves: bool,
526 suffix: &str,
527) {
528 for (key, value) in properties {
529 let mut prefix = prefix.clone();
530 prefix.push(key);
531
532 let value = value.as_object().unwrap();
533 match value.get("type").and_then(|v| v.as_str()) {
534 Some("object") => {
535 if !only_leaves {
536 let help = value
537 .get("description")
538 .map(|desc| desc.as_str().unwrap().to_string().into());
539 let escaped_key = prefix.to_string();
540 acc.push(CompletionCandidate::new(escaped_key).help(help));
541 }
542 let Some(properties) = value.get("properties") else {
543 continue;
544 };
545 let properties = properties.as_object().unwrap();
546 config_keys_rec(prefix, properties, acc, only_leaves, suffix);
547 }
548 _ => {
549 let help = value
550 .get("description")
551 .map(|desc| desc.as_str().unwrap().to_string().into());
552 let escaped_key = format!("{prefix}{suffix}");
553 acc.push(CompletionCandidate::new(escaped_key).help(help));
554 }
555 }
556 }
557}
558
559fn json_keypath<'a>(
560 schema: &'a serde_json::Value,
561 keypath: &str,
562 separator: &str,
563) -> Option<&'a serde_json::Value> {
564 keypath
565 .split(separator)
566 .try_fold(schema, |value, step| value.get(step))
567}
568fn jsonschema_keypath<'a>(
569 schema: &'a serde_json::Value,
570 keypath: &ConfigNamePathBuf,
571) -> Option<&'a serde_json::Value> {
572 keypath.components().try_fold(schema, |value, step| {
573 let value = value.as_object()?;
574 if value.get("type")?.as_str()? != "object" {
575 return None;
576 }
577 let properties = value.get("properties")?.as_object()?;
578 properties.get(step.get())
579 })
580}
581
582fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> {
583 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
584
585 let mut config_entry = jsonschema_keypath(&schema, path)?;
586 if let Some(reference) = config_entry.get("$ref") {
587 let reference = reference.as_str()?.strip_prefix("#/")?;
588 config_entry = json_keypath(&schema, reference, "/")?;
589 };
590
591 if let Some(possible_values) = config_entry.get("enum") {
592 return Some(
593 possible_values
594 .as_array()?
595 .iter()
596 .filter_map(|val| val.as_str())
597 .map(ToOwned::to_owned)
598 .collect(),
599 );
600 }
601
602 Some(match config_entry.get("type")?.as_str()? {
603 "boolean" => vec!["false".into(), "true".into()],
604 _ => vec![],
605 })
606}
607
608fn config_keys_impl(only_leaves: bool, suffix: &str) -> Vec<CompletionCandidate> {
609 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();
610 let schema = schema.as_object().unwrap();
611 let properties = schema["properties"].as_object().unwrap();
612
613 let mut candidates = Vec::new();
614 config_keys_rec(
615 ConfigNamePathBuf::root(),
616 properties,
617 &mut candidates,
618 only_leaves,
619 suffix,
620 );
621 candidates
622}
623
624pub fn config_keys() -> Vec<CompletionCandidate> {
625 config_keys_impl(false, "")
626}
627
628pub fn leaf_config_keys() -> Vec<CompletionCandidate> {
629 config_keys_impl(true, "")
630}
631
632pub fn leaf_config_key_value(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
633 let Some(current) = current.to_str() else {
634 return Vec::new();
635 };
636
637 if let Some((key, current_val)) = current.split_once('=') {
638 let Ok(key) = key.parse() else {
639 return Vec::new();
640 };
641 let possible_values = config_values(&key).unwrap_or_default();
642
643 possible_values
644 .into_iter()
645 .filter(|x| x.starts_with(current_val))
646 .map(|x| CompletionCandidate::new(format!("{key}={x}")))
647 .collect()
648 } else {
649 config_keys_impl(true, "=")
650 .into_iter()
651 .filter(|candidate| candidate.get_value().to_str().unwrap().starts_with(current))
652 .collect()
653 }
654}
655
656pub fn branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
657 let Some(current) = current.to_str() else {
658 return Vec::new();
659 };
660
661 let Some((branch_name, revision)) = current.split_once('=') else {
662 return Vec::new();
664 };
665 revset_expression(revision.as_ref(), None)
666 .into_iter()
667 .map(|rev| rev.add_prefix(format!("{branch_name}=")))
668 .collect()
669}
670
671fn dir_prefix_from<'a>(path: &'a str, current: &str) -> Option<&'a str> {
672 path[current.len()..]
673 .split_once(std::path::MAIN_SEPARATOR)
674 .map(|(next, _)| path.split_at(current.len() + next.len() + 1).0)
675}
676
677fn current_prefix_to_fileset(current: &str) -> String {
678 let cur_esc = globset::escape(current);
679 let dir_pat = format!("{cur_esc}*/**");
680 let path_pat = format!("{cur_esc}*");
681 format!("glob:{dir_pat:?} | glob:{path_pat:?}")
682}
683
684fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
685 let Some(current) = current.to_str() else {
686 return Vec::new();
687 };
688 with_jj(|jj, _| {
689 let mut child = jj
690 .build()
691 .arg("file")
692 .arg("list")
693 .arg("--revision")
694 .arg(rev)
695 .arg(current_prefix_to_fileset(current))
696 .stdout(std::process::Stdio::piped())
697 .stderr(std::process::Stdio::null())
698 .spawn()
699 .map_err(user_error)?;
700 let stdout = child.stdout.take().unwrap();
701
702 Ok(std::io::BufReader::new(stdout)
703 .lines()
704 .take(1_000)
705 .map_while(Result::ok)
706 .map(|path| {
707 if let Some(dir_path) = dir_prefix_from(&path, current) {
708 return CompletionCandidate::new(dir_path);
709 }
710 CompletionCandidate::new(path)
711 })
712 .dedup() .collect())
714 })
715}
716
717fn modified_files_from_rev_with_jj_cmd(
718 rev: (String, Option<String>),
719 mut cmd: std::process::Command,
720 current: &std::ffi::OsStr,
721) -> Result<Vec<CompletionCandidate>, CommandError> {
722 let Some(current) = current.to_str() else {
723 return Ok(Vec::new());
724 };
725
726 let template = indoc! {r#"
728 concat(
729 status ++ ' ' ++ path.display() ++ "\n",
730 if(status == 'renamed', 'renamed.source ' ++ source.path().display() ++ "\n"),
731 )
732 "#};
733 cmd.arg("diff")
734 .args(["--template", template])
735 .arg(current_prefix_to_fileset(current));
736 match rev {
737 (rev, None) => cmd.arg("--revisions").arg(rev),
738 (from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to),
739 };
740 let output = cmd.output().map_err(user_error)?;
741 let stdout = String::from_utf8_lossy(&output.stdout);
742
743 let mut candidates = Vec::new();
744 let mut include_renames = false;
745
746 for (mode, path) in stdout.lines().filter_map(|line| line.split_once(' ')) {
747 fn path_to_candidate(current: &str, mode: &str, path: &str) -> CompletionCandidate {
748 if let Some(dir_path) = dir_prefix_from(path, current) {
749 return CompletionCandidate::new(dir_path);
750 }
751
752 let help = match mode {
753 "modified" => "Modified".into(),
754 "removed" => "Deleted".into(),
755 "added" => "Added".into(),
756 "renamed" => "Renamed".into(),
757 "copied" => "Copied".into(),
758 _ => format!("unknown mode: '{mode}'"),
759 };
760 CompletionCandidate::new(path).help(Some(help.into()))
761 }
762
763 if mode == "renamed.source" {
764 if !path.starts_with(current) {
765 continue;
766 }
767 candidates.push(path_to_candidate(current, "renamed", path));
768 include_renames |= true;
769 } else {
770 candidates.push(path_to_candidate(current, mode, path));
771 }
772 }
773 if include_renames {
774 candidates.sort_unstable_by(|a, b| Path::new(a.get_value()).cmp(Path::new(b.get_value())));
775 }
776 candidates.dedup();
777
778 Ok(candidates)
779}
780
781fn modified_files_from_rev(
782 rev: (String, Option<String>),
783 current: &std::ffi::OsStr,
784) -> Vec<CompletionCandidate> {
785 with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build(), current))
786}
787
788fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
789 let Some(current) = current.to_str() else {
790 return Vec::new();
791 };
792 with_jj(|jj, _| {
793 let output = jj
794 .build()
795 .arg("resolve")
796 .arg("--list")
797 .arg("--revision")
798 .arg(rev)
799 .arg(current_prefix_to_fileset(current))
800 .output()
801 .map_err(user_error)?;
802 let stdout = String::from_utf8_lossy(&output.stdout);
803
804 Ok(stdout
805 .lines()
806 .map(|line| {
807 let path = line
808 .split_whitespace()
809 .next()
810 .expect("resolve --list should contain whitespace after path");
811
812 if let Some(dir_path) = dir_prefix_from(path, current) {
813 return CompletionCandidate::new(dir_path);
814 }
815 CompletionCandidate::new(path)
816 })
817 .dedup() .collect())
819 })
820}
821
822pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
823 modified_files_from_rev(("@".into(), None), current)
824}
825
826pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
827 all_files_from_rev(parse::revision_or_wc(), current)
828}
829
830pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
831 modified_files_from_rev((parse::revision_or_wc(), None), current)
832}
833
834pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
835 match parse::range() {
836 Some((from, to)) => modified_files_from_rev((from, Some(to)), current),
837 None => modified_files_from_rev(("@".into(), None), current),
838 }
839}
840
841pub fn modified_from_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
844 modified_files_from_rev((parse::from_or_wc(), None), current)
845}
846
847pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
848 if let Some(rev) = parse::revision() {
849 return modified_files_from_rev((rev, None), current);
850 }
851 modified_range_files(current)
852}
853
854pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
855 conflicted_files_from_rev(&parse::revision_or_wc(), current)
856}
857
858pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
860 let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
861 modified_files_from_rev((rev, None), current)
862}
863
864pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
866 let Some((from, to)) = parse::range() else {
867 return Vec::new();
868 };
869 with_jj(|jj, _| {
873 let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build(), current)?;
874 res.extend(modified_files_from_rev_with_jj_cmd(
875 (to, None),
876 jj.build(),
877 current,
878 )?);
879 Ok(res)
880 })
881}
882
883pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
885 let mut rev = parse::log_revisions().join(")|(");
886 if rev.is_empty() {
887 rev = "@".into();
888 } else {
889 rev = format!("latest(heads(({rev})))"); };
891 all_files_from_rev(rev, current)
892}
893
894fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate>
898where
899 F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>,
900{
901 get_jj_command()
902 .and_then(|(jj, settings)| completion_fn(jj, &settings))
903 .unwrap_or_else(|e| {
904 eprintln!("{}", e.error);
905 Vec::new()
906 })
907}
908
909fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
919 let current_exe = std::env::current_exe().map_err(user_error)?;
920 let mut cmd_args = Vec::<String>::new();
921
922 cmd_args.push("--ignore-working-copy".into());
925 cmd_args.push("--color=never".into());
926 cmd_args.push("--no-pager".into());
927
928 let app = crate::commands::default_app();
932 let mut raw_config = config_from_environment(default_config_layers());
933 let ui = Ui::null();
934 let cwd = std::env::current_dir()
935 .and_then(dunce::canonicalize)
936 .map_err(user_error)?;
937 let mut config_env = ConfigEnv::from_environment(&ui);
939 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
940 let _ = config_env.reload_user_config(&mut raw_config);
941 if let Ok(loader) = &maybe_cwd_workspace_loader {
942 config_env.reset_repo_path(loader.repo_path());
943 let _ = config_env.reload_repo_config(&mut raw_config);
944 }
945 let mut config = config_env.resolve_config(&raw_config)?;
946 let args = std::env::args_os().skip(2);
948 let args = expand_args(&ui, &app, args, &config)?;
949 let arg_matches = app
950 .clone()
951 .disable_version_flag(true)
952 .disable_help_flag(true)
953 .ignore_errors(true)
954 .try_get_matches_from(args)?;
955 let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?;
956
957 if let Some(repository) = args.repository {
958 if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) {
960 config_env.reset_repo_path(loader.repo_path());
961 let _ = config_env.reload_repo_config(&mut raw_config);
962 if let Ok(new_config) = config_env.resolve_config(&raw_config) {
963 config = new_config;
964 }
965 }
966 cmd_args.push("--repository".into());
967 cmd_args.push(repository);
968 }
969 if let Some(at_operation) = args.at_operation {
970 let mut canary_cmd = std::process::Command::new(¤t_exe);
981 canary_cmd.args(&cmd_args);
982 canary_cmd.arg("--at-operation");
983 canary_cmd.arg(&at_operation);
984 canary_cmd.arg("debug");
985 canary_cmd.arg("snapshot");
986
987 match canary_cmd.output() {
988 Ok(output) if output.status.success() => {
989 cmd_args.push("--at-operation".into());
991 cmd_args.push(at_operation);
992 }
993 _ => {} }
995 }
996 for (kind, value) in args.early_args.merged_config_args(&arg_matches) {
997 let arg = match kind {
998 ConfigArgKind::Item => format!("--config={value}"),
999 ConfigArgKind::File => format!("--config-file={value}"),
1000 };
1001 cmd_args.push(arg);
1002 }
1003
1004 let builder = JjBuilder {
1005 cmd: current_exe,
1006 args: cmd_args,
1007 };
1008 let settings = UserSettings::from_config(config)?;
1009
1010 Ok((builder, settings))
1011}
1012
1013struct JjBuilder {
1016 cmd: std::path::PathBuf,
1017 args: Vec<String>,
1018}
1019
1020impl JjBuilder {
1021 fn build(&self) -> std::process::Command {
1022 let mut cmd = std::process::Command::new(&self.cmd);
1023 cmd.args(&self.args);
1024 cmd
1025 }
1026}
1027
1028mod parse {
1037 pub(super) fn parse_flag<'a, I: Iterator<Item = String>>(
1038 candidates: &'a [&str],
1039 mut args: I,
1040 ) -> impl Iterator<Item = String> + use<'a, I> {
1041 std::iter::from_fn(move || {
1042 for arg in args.by_ref() {
1043 if candidates.contains(&arg.as_ref()) {
1045 match args.next() {
1046 Some(val) if !val.starts_with('-') => {
1047 return Some(strip_shell_quotes(&val).into());
1048 }
1049 _ => return None,
1050 }
1051 }
1052
1053 if let Some(value) = candidates.iter().find_map(|candidate| {
1055 let rest = arg.strip_prefix(candidate)?;
1056 match rest.strip_prefix('=') {
1057 Some(value) => Some(value),
1058
1059 None if candidate.len() == 2 => Some(rest),
1061
1062 None => None,
1063 }
1064 }) {
1065 return Some(strip_shell_quotes(value).into());
1066 };
1067 }
1068 None
1069 })
1070 }
1071
1072 pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> {
1073 parse_flag(&["-r", "--revision"], args).next()
1074 }
1075
1076 pub fn revision() -> Option<String> {
1077 parse_revision_impl(std::env::args())
1078 }
1079
1080 pub fn revision_or_wc() -> String {
1081 revision().unwrap_or_else(|| "@".into())
1082 }
1083
1084 pub fn from_or_wc() -> String {
1085 parse_flag(&["-f", "--from"], std::env::args())
1086 .next()
1087 .unwrap_or_else(|| "@".into())
1088 }
1089
1090 pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)>
1091 where
1092 T: Iterator<Item = String>,
1093 {
1094 let from = parse_flag(&["-f", "--from"], args()).next()?;
1095 let to = parse_flag(&["-t", "--to"], args())
1096 .next()
1097 .unwrap_or_else(|| "@".into());
1098
1099 Some((from, to))
1100 }
1101
1102 pub fn range() -> Option<(String, String)> {
1103 parse_range_impl(std::env::args)
1104 }
1105
1106 pub fn squash_revision() -> Option<String> {
1111 if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() {
1112 return Some(rev);
1113 }
1114 parse_flag(&["-f", "--from"], std::env::args()).next()
1115 }
1116
1117 pub fn log_revisions() -> Vec<String> {
1120 let candidates = &["-r", "--revisions"];
1121 parse_flag(candidates, std::env::args()).collect()
1122 }
1123
1124 fn strip_shell_quotes(s: &str) -> &str {
1125 if s.len() >= 2
1126 && (s.starts_with('"') && s.ends_with('"') || s.starts_with('\'') && s.ends_with('\''))
1127 {
1128 &s[1..s.len() - 1]
1129 } else {
1130 s
1131 }
1132 }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138
1139 #[test]
1140 fn test_split_revset_trailing_name() {
1141 assert_eq!(split_revset_trailing_name(""), Some(("", "")));
1142 assert_eq!(split_revset_trailing_name(" "), Some((" ", "")));
1143 assert_eq!(split_revset_trailing_name("foo"), Some(("", "foo")));
1144 assert_eq!(split_revset_trailing_name(" foo"), Some((" ", "foo")));
1145 assert_eq!(split_revset_trailing_name("foo "), None);
1146 assert_eq!(split_revset_trailing_name("foo_"), Some(("", "foo_")));
1147 assert_eq!(split_revset_trailing_name("foo/"), Some(("", "foo/")));
1148 assert_eq!(split_revset_trailing_name("foo/b"), Some(("", "foo/b")));
1149
1150 assert_eq!(split_revset_trailing_name("foo-"), Some(("", "foo-")));
1151 assert_eq!(split_revset_trailing_name("foo+"), Some(("", "foo+")));
1152 assert_eq!(
1153 split_revset_trailing_name("foo-bar-"),
1154 Some(("", "foo-bar-"))
1155 );
1156 assert_eq!(
1157 split_revset_trailing_name("foo-bar-b"),
1158 Some(("", "foo-bar-b"))
1159 );
1160
1161 assert_eq!(split_revset_trailing_name("foo."), Some(("", "foo.")));
1162 assert_eq!(split_revset_trailing_name("foo..b"), Some(("foo..", "b")));
1163 assert_eq!(split_revset_trailing_name("..foo"), Some(("..", "foo")));
1164
1165 assert_eq!(split_revset_trailing_name("foo(bar"), Some(("foo(", "bar")));
1166 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1167 assert_eq!(split_revset_trailing_name("(f"), Some(("(", "f")));
1168
1169 assert_eq!(split_revset_trailing_name("foo@"), Some(("", "foo@")));
1170 assert_eq!(split_revset_trailing_name("foo@b"), Some(("", "foo@b")));
1171 assert_eq!(split_revset_trailing_name("..foo@"), Some(("..", "foo@")));
1172 assert_eq!(
1173 split_revset_trailing_name("::F(foo@origin.1..bar@origin."),
1174 Some(("::F(foo@origin.1..", "bar@origin."))
1175 );
1176 }
1177
1178 #[test]
1179 fn test_split_revset_trailing_name_with_trailing_operator() {
1180 assert_eq!(split_revset_trailing_name("foo|"), Some(("foo|", "")));
1181 assert_eq!(split_revset_trailing_name("foo | "), Some(("foo | ", "")));
1182 assert_eq!(split_revset_trailing_name("foo&"), Some(("foo&", "")));
1183 assert_eq!(split_revset_trailing_name("foo~"), Some(("foo~", "")));
1184
1185 assert_eq!(split_revset_trailing_name(".."), Some(("..", "")));
1186 assert_eq!(split_revset_trailing_name("foo.."), Some(("foo..", "")));
1187 assert_eq!(split_revset_trailing_name("::"), Some(("::", "")));
1188 assert_eq!(split_revset_trailing_name("foo::"), Some(("foo::", "")));
1189
1190 assert_eq!(split_revset_trailing_name("("), Some(("(", "")));
1191 assert_eq!(split_revset_trailing_name("foo("), Some(("foo(", "")));
1192 assert_eq!(split_revset_trailing_name("foo()"), None);
1193 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1194 }
1195
1196 #[test]
1197 fn test_split_revset_trailing_name_with_modifier() {
1198 assert_eq!(split_revset_trailing_name("all:"), Some(("all:", "")));
1199 assert_eq!(split_revset_trailing_name("all: "), Some(("all: ", "")));
1200 assert_eq!(split_revset_trailing_name("all:f"), Some(("all:", "f")));
1201 assert_eq!(split_revset_trailing_name("all: f"), Some(("all: ", "f")));
1202 }
1203
1204 #[test]
1205 fn test_config_keys() {
1206 let _ = config_keys();
1208 }
1209
1210 #[test]
1211 fn test_parse_revision_impl() {
1212 let good_cases: &[&[&str]] = &[
1213 &["-r", "foo"],
1214 &["-r", "'foo'"],
1215 &["-r", "\"foo\""],
1216 &["-rfoo"],
1217 &["-r'foo'"],
1218 &["-r\"foo\""],
1219 &["--revision", "foo"],
1220 &["-r=foo"],
1221 &["-r='foo'"],
1222 &["-r=\"foo\""],
1223 &["--revision=foo"],
1224 &["--revision='foo'"],
1225 &["--revision=\"foo\""],
1226 &["preceding_arg", "-r", "foo"],
1227 &["-r", "foo", "following_arg"],
1228 ];
1229 for case in good_cases {
1230 let args = case.iter().map(|s| s.to_string());
1231 assert_eq!(
1232 parse::parse_revision_impl(args),
1233 Some("foo".into()),
1234 "case: {case:?}",
1235 );
1236 }
1237 let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1238 for case in bad_cases {
1239 let args = case.iter().map(|s| s.to_string());
1240 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1241 }
1242 }
1243
1244 #[test]
1245 fn test_parse_range_impl() {
1246 let wc_cases: &[&[&str]] = &[
1247 &["-f", "foo"],
1248 &["--from", "foo"],
1249 &["-f=foo"],
1250 &["preceding_arg", "-f", "foo"],
1251 &["-f", "foo", "following_arg"],
1252 ];
1253 for case in wc_cases {
1254 let args = case.iter().map(|s| s.to_string());
1255 assert_eq!(
1256 parse::parse_range_impl(|| args.clone()),
1257 Some(("foo".into(), "@".into())),
1258 "case: {case:?}",
1259 );
1260 }
1261 let to_cases: &[&[&str]] = &[
1262 &["-f", "foo", "-t", "bar"],
1263 &["-f", "foo", "--to", "bar"],
1264 &["-f=foo", "-t=bar"],
1265 &["-t=bar", "-f=foo"],
1266 ];
1267 for case in to_cases {
1268 let args = case.iter().map(|s| s.to_string());
1269 assert_eq!(
1270 parse::parse_range_impl(|| args.clone()),
1271 Some(("foo".into(), "bar".into())),
1272 "case: {case:?}",
1273 );
1274 }
1275 let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1276 for case in bad_cases {
1277 let args = case.iter().map(|s| s.to_string());
1278 assert_eq!(
1279 parse::parse_range_impl(|| args.clone()),
1280 None,
1281 "case: {case:?}"
1282 );
1283 }
1284 }
1285
1286 #[test]
1287 fn test_parse_multiple_flags() {
1288 let candidates = &["-r", "--revisions"];
1289 let args = &[
1290 "unrelated_arg_at_the_beginning",
1291 "-r",
1292 "1",
1293 "--revisions",
1294 "2",
1295 "-r=3",
1296 "--revisions=4",
1297 "unrelated_arg_in_the_middle",
1298 "-r5",
1299 "unrelated_arg_at_the_end",
1300 ];
1301 let flags: Vec<_> =
1302 parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect();
1303 let expected = ["1", "2", "3", "4", "5"];
1304 assert_eq!(flags, expected);
1305 }
1306}