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" " ++
50if(normal_target,
51 if(normal_target.description(),
52 normal_target.description().first_line(),
53 "(no description set)",
54 ),
55 "(conflicted bookmark)",
56)
57'''"#;
58const TAG_HELP_TEMPLATE: &str = r#"template-aliases.'tag_help()'='''
59" " ++
60if(normal_target,
61 if(normal_target.description(),
62 normal_target.description().first_line(),
63 "(no description set)",
64 ),
65 "(conflicted tag)",
66)
67'''"#;
68
69fn split_help_text(line: &str) -> (&str, Option<StyledStr>) {
72 match line.split_once(' ') {
73 Some((name, help)) => (name, Some(help.to_string().into())),
74 None => (line, None),
75 }
76}
77
78pub fn local_bookmarks() -> Vec<CompletionCandidate> {
79 with_jj(|jj, _| {
80 let output = jj
81 .build()
82 .arg("bookmark")
83 .arg("list")
84 .arg("--config")
85 .arg(BOOKMARK_HELP_TEMPLATE)
86 .arg("--template")
87 .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#)
88 .output()
89 .map_err(user_error)?;
90
91 Ok(String::from_utf8_lossy(&output.stdout)
92 .lines()
93 .map(split_help_text)
94 .map(|(name, help)| CompletionCandidate::new(name).help(help))
95 .collect())
96 })
97}
98
99pub fn tracked_bookmarks() -> Vec<CompletionCandidate> {
100 with_jj(|jj, _| {
101 let output = jj
102 .build()
103 .arg("bookmark")
104 .arg("list")
105 .arg("--tracked")
106 .arg("--config")
107 .arg(BOOKMARK_HELP_TEMPLATE)
108 .arg("--template")
109 .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#)
110 .output()
111 .map_err(user_error)?;
112
113 Ok(String::from_utf8_lossy(&output.stdout)
114 .lines()
115 .map(split_help_text)
116 .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))
117 .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)
120 .map(|((name, _remote), help)| CompletionCandidate::new(name).help(help))
121 .collect())
122 })
123}
124
125pub fn untracked_bookmarks() -> Vec<CompletionCandidate> {
126 with_jj(|jj, _settings| {
127 let remotes = jj
128 .build()
129 .arg("git")
130 .arg("remote")
131 .arg("list")
132 .output()
133 .map_err(user_error)?;
134 let remotes = String::from_utf8_lossy(&remotes.stdout);
135 let remotes = remotes
136 .lines()
137 .filter_map(|l| l.split_whitespace().next())
138 .collect_vec();
139
140 let bookmark_table = jj
141 .build()
142 .arg("bookmark")
143 .arg("list")
144 .arg("--all-remotes")
145 .arg("--config")
146 .arg(BOOKMARK_HELP_TEMPLATE)
147 .arg("--template")
148 .arg(
149 r#"
150 if(remote != "git",
151 if(!remote, name) ++ "\t" ++
152 if(remote, name ++ "@" ++ remote) ++ "\t" ++
153 if(tracked, "tracked") ++ "\t" ++
154 bookmark_help() ++ "\n"
155 )"#,
156 )
157 .output()
158 .map_err(user_error)?;
159 let bookmark_table = String::from_utf8_lossy(&bookmark_table.stdout);
160
161 let mut possible_bookmarks_to_track = Vec::new();
162 let mut already_tracked_bookmarks = HashSet::new();
163
164 for line in bookmark_table.lines() {
165 let [local, remote, tracked, help] =
166 line.split('\t').collect_array().unwrap_or_default();
167
168 if !local.is_empty() {
169 possible_bookmarks_to_track.extend(
170 remotes
171 .iter()
172 .map(|remote| (format!("{local}@{remote}"), help)),
173 );
174 } else if tracked.is_empty() {
175 possible_bookmarks_to_track.push((remote.to_owned(), help));
176 } else {
177 already_tracked_bookmarks.insert(remote);
178 }
179 }
180 possible_bookmarks_to_track
181 .retain(|(bookmark, _help)| !already_tracked_bookmarks.contains(&bookmark.as_str()));
182
183 Ok(possible_bookmarks_to_track
184 .iter()
185 .filter_map(|(symbol, help)| Some((symbol.split_once('@')?, help)))
186 .dedup_by(|((name1, _), _), ((name2, _), _)| name1 == name2)
189 .map(|((name, _remote), help)| {
190 CompletionCandidate::new(name).help(Some(help.to_string().into()))
191 })
192 .collect())
193 })
194}
195
196pub fn bookmarks() -> Vec<CompletionCandidate> {
197 with_jj(|jj, _settings| {
198 let output = jj
199 .build()
200 .arg("bookmark")
201 .arg("list")
202 .arg("--all-remotes")
203 .arg("--config")
204 .arg(BOOKMARK_HELP_TEMPLATE)
205 .arg("--template")
206 .arg(
207 r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#,
209 )
210 .output()
211 .map_err(user_error)?;
212 let stdout = String::from_utf8_lossy(&output.stdout);
213
214 Ok((&stdout
215 .lines()
216 .map(split_help_text)
217 .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name)))
218 .into_iter()
219 .map(|(bookmark, mut refs)| {
220 let help = refs.find_map(|(_, help)| help);
221 let local = help.is_some();
222 let display_order = match local {
223 true => 0,
224 false => 1,
225 };
226 CompletionCandidate::new(bookmark)
227 .help(help)
228 .display_order(Some(display_order))
229 })
230 .collect())
231 })
232}
233
234pub fn local_tags() -> Vec<CompletionCandidate> {
235 with_jj(|jj, _| {
236 let output = jj
237 .build()
238 .arg("tag")
239 .arg("list")
240 .arg("--config")
241 .arg(TAG_HELP_TEMPLATE)
242 .arg("--template")
243 .arg(r#"if(!remote, name ++ tag_help()) ++ "\n""#)
244 .output()
245 .map_err(user_error)?;
246
247 Ok(String::from_utf8_lossy(&output.stdout)
248 .lines()
249 .map(split_help_text)
250 .map(|(name, help)| CompletionCandidate::new(name).help(help))
251 .collect())
252 })
253}
254
255pub fn git_remotes() -> Vec<CompletionCandidate> {
256 with_jj(|jj, _| {
257 let output = jj
258 .build()
259 .arg("git")
260 .arg("remote")
261 .arg("list")
262 .output()
263 .map_err(user_error)?;
264
265 let stdout = String::from_utf8_lossy(&output.stdout);
266
267 Ok(stdout
268 .lines()
269 .filter_map(|line| line.split_once(' ').map(|(name, _url)| name))
270 .map(CompletionCandidate::new)
271 .collect())
272 })
273}
274
275pub fn template_aliases() -> Vec<CompletionCandidate> {
276 with_jj(|_, settings| {
277 let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else {
278 return Ok(Vec::new());
279 };
280 Ok(template_aliases
281 .symbol_names()
282 .map(CompletionCandidate::new)
283 .sorted()
284 .collect())
285 })
286}
287
288pub fn aliases() -> Vec<CompletionCandidate> {
289 with_jj(|_, settings| {
290 Ok(settings
291 .table_keys("aliases")
292 .filter(|alias| alias.len() > 2)
297 .map(CompletionCandidate::new)
298 .collect())
299 })
300}
301
302fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> {
303 with_jj(|jj, settings| {
304 const LOCAL_BOOKMARK: usize = 0;
306 const TAG: usize = 1;
307 const CHANGE_ID: usize = 2;
308 const REMOTE_BOOKMARK: usize = 3;
309 const REVSET_ALIAS: usize = 4;
310
311 let mut candidates = Vec::new();
312
313 let mut cmd = jj.build();
316 cmd.arg("bookmark")
317 .arg("list")
318 .arg("--all-remotes")
319 .arg("--config")
320 .arg(BOOKMARK_HELP_TEMPLATE)
321 .arg("--template")
322 .arg(
323 r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,
324 );
325 if let Some(revs) = revset_filter {
326 cmd.arg("--revisions").arg(revs);
327 }
328 let output = cmd.output().map_err(user_error)?;
329 let stdout = String::from_utf8_lossy(&output.stdout);
330
331 candidates.extend(
332 stdout
333 .lines()
334 .map(split_help_text)
335 .filter(|(bookmark, _)| bookmark.starts_with(match_prefix))
336 .map(|(bookmark, help)| {
337 let local = !bookmark.contains('@');
338 let display_order = match local {
339 true => LOCAL_BOOKMARK,
340 false => REMOTE_BOOKMARK,
341 };
342 CompletionCandidate::new(bookmark)
343 .help(help)
344 .display_order(Some(display_order))
345 }),
346 );
347
348 if revset_filter.is_none() {
355 let output = jj
356 .build()
357 .arg("tag")
358 .arg("list")
359 .arg("--config")
360 .arg(BOOKMARK_HELP_TEMPLATE)
361 .arg("--template")
362 .arg(r#"name ++ bookmark_help() ++ "\n""#)
363 .arg(format!("glob:{}*", globset::escape(match_prefix)))
364 .output()
365 .map_err(user_error)?;
366 let stdout = String::from_utf8_lossy(&output.stdout);
367
368 candidates.extend(stdout.lines().map(|line| {
369 let (name, desc) = split_help_text(line);
370 CompletionCandidate::new(name)
371 .help(desc)
372 .display_order(Some(TAG))
373 }));
374 }
375
376 let revisions = revset_filter
379 .map(String::from)
380 .or_else(|| settings.get_string("revsets.short-prefixes").ok())
381 .or_else(|| settings.get_string("revsets.log").ok())
382 .unwrap_or_default();
383
384 let output = jj
385 .build()
386 .arg("log")
387 .arg("--no-graph")
388 .arg("--limit")
389 .arg("100")
390 .arg("--revisions")
391 .arg(revisions)
392 .arg("--template")
393 .arg(
394 r#"
395 join(" ",
396 separate("/",
397 change_id.shortest(),
398 if(hidden || divergent, change_offset),
399 ),
400 if(description, description.first_line(), "(no description set)"),
401 ) ++ "\n""#,
402 )
403 .output()
404 .map_err(user_error)?;
405 let stdout = String::from_utf8_lossy(&output.stdout);
406
407 candidates.extend(
408 stdout
409 .lines()
410 .map(split_help_text)
411 .filter(|(id, _)| id.starts_with(match_prefix))
412 .map(|(id, desc)| {
413 CompletionCandidate::new(id)
414 .help(desc)
415 .display_order(Some(CHANGE_ID))
416 }),
417 );
418
419 let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;
422 let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect();
423 symbol_names.sort();
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 branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
759 let Some(current) = current.to_str() else {
760 return Vec::new();
761 };
762
763 let Some((branch_name, revision)) = current.split_once('=') else {
764 return Vec::new();
766 };
767 revset_expression(revision.as_ref(), None)
768 .into_iter()
769 .map(|rev| rev.add_prefix(format!("{branch_name}=")))
770 .collect()
771}
772
773fn path_completion_candidate_from(
774 current_prefix: &str,
775 normalized_prefix_path: &Path,
776 path: &Path,
777 mode: Option<clap::builder::StyledStr>,
778) -> Option<CompletionCandidate> {
779 let normalized_prefix = match normalized_prefix_path.to_str()? {
780 "." => "", normalized_prefix => normalized_prefix,
782 };
783
784 let path = slash_path(path);
785 let mut remainder = path.to_str()?.strip_prefix(normalized_prefix)?;
786
787 if current_prefix.ends_with(std::path::is_separator) {
791 remainder = remainder.strip_prefix('/').unwrap_or(remainder);
792 }
793
794 match remainder.split_inclusive('/').at_most_one() {
795 Ok(file_completion) => Some(
798 CompletionCandidate::new(format!(
799 "{current_prefix}{}",
800 file_completion.unwrap_or_default()
801 ))
802 .help(mode),
803 ),
804
805 Err(mut components) => Some(CompletionCandidate::new(format!(
807 "{current_prefix}{}",
808 components.next().unwrap()
809 ))),
810 }
811}
812
813fn current_prefix_to_fileset(current: &str) -> String {
814 let cur_esc = globset::escape(current);
815 let dir_pat = format!("{cur_esc}*/**");
816 let path_pat = format!("{cur_esc}*");
817 format!("glob:{dir_pat:?} | glob:{path_pat:?}")
818}
819
820fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
821 let Some(current) = current.to_str() else {
822 return Vec::new();
823 };
824
825 let normalized_prefix = normalize_path(Path::new(current));
826 let normalized_prefix = slash_path(&normalized_prefix);
827
828 with_jj(|jj, _| {
829 let mut child = jj
830 .build()
831 .arg("file")
832 .arg("list")
833 .arg("--revision")
834 .arg(rev)
835 .arg("--template")
836 .arg(r#"path.display() ++ "\n""#)
837 .arg(current_prefix_to_fileset(current))
838 .stdout(std::process::Stdio::piped())
839 .stderr(std::process::Stdio::null())
840 .spawn()
841 .map_err(user_error)?;
842 let stdout = child.stdout.take().unwrap();
843
844 Ok(std::io::BufReader::new(stdout)
845 .lines()
846 .take(1_000)
847 .map_while(Result::ok)
848 .filter_map(|path| {
849 path_completion_candidate_from(current, &normalized_prefix, Path::new(&path), None)
850 })
851 .dedup() .collect())
853 })
854}
855
856fn modified_files_from_rev_with_jj_cmd(
857 rev: (String, Option<String>),
858 mut cmd: std::process::Command,
859 current: &std::ffi::OsStr,
860) -> Result<Vec<CompletionCandidate>, CommandError> {
861 let Some(current) = current.to_str() else {
862 return Ok(Vec::new());
863 };
864
865 let normalized_prefix = normalize_path(Path::new(current));
866 let normalized_prefix = slash_path(&normalized_prefix);
867
868 let template = indoc! {r#"
870 concat(
871 status ++ ' ' ++ path.display() ++ "\n",
872 if(status == 'renamed', 'renamed.source ' ++ source.path().display() ++ "\n"),
873 )
874 "#};
875 cmd.arg("diff")
876 .args(["--template", template])
877 .arg(current_prefix_to_fileset(current));
878 match rev {
879 (rev, None) => cmd.arg("--revisions").arg(rev),
880 (from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to),
881 };
882 let output = cmd.output().map_err(user_error)?;
883 let stdout = String::from_utf8_lossy(&output.stdout);
884
885 let mut include_renames = false;
886 let mut candidates: Vec<_> = stdout
887 .lines()
888 .filter_map(|line| line.split_once(' '))
889 .filter_map(|(mode, path)| {
890 let mode = match mode {
891 "modified" => "Modified".into(),
892 "removed" => "Deleted".into(),
893 "added" => "Added".into(),
894 "renamed" => "Renamed".into(),
895 "renamed.source" => {
896 include_renames = true;
897 "Renamed".into()
898 }
899 "copied" => "Copied".into(),
900 _ => format!("unknown mode: '{mode}'").into(),
901 };
902 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), Some(mode))
903 })
904 .collect();
905
906 if include_renames {
907 candidates.sort_unstable_by(|a, b| Path::new(a.get_value()).cmp(Path::new(b.get_value())));
908 }
909 candidates.dedup();
910
911 Ok(candidates)
912}
913
914fn modified_files_from_rev(
915 rev: (String, Option<String>),
916 current: &std::ffi::OsStr,
917) -> Vec<CompletionCandidate> {
918 with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build(), current))
919}
920
921fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
922 let Some(current) = current.to_str() else {
923 return Vec::new();
924 };
925
926 let normalized_prefix = normalize_path(Path::new(current));
927 let normalized_prefix = slash_path(&normalized_prefix);
928
929 with_jj(|jj, _| {
930 let output = jj
931 .build()
932 .arg("resolve")
933 .arg("--list")
934 .arg("--revision")
935 .arg(rev)
936 .arg(current_prefix_to_fileset(current))
937 .output()
938 .map_err(user_error)?;
939 let stdout = String::from_utf8_lossy(&output.stdout);
940
941 Ok(stdout
942 .lines()
943 .filter_map(|line| {
944 let path = line
945 .split_whitespace()
946 .next()
947 .expect("resolve --list should contain whitespace after path");
948
949 path_completion_candidate_from(current, &normalized_prefix, Path::new(path), None)
950 })
951 .dedup() .collect())
953 })
954}
955
956pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
957 modified_files_from_rev(("@".into(), None), current)
958}
959
960pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
961 all_files_from_rev(parse::revision_or_wc(), current)
962}
963
964pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
965 modified_files_from_rev((parse::revision_or_wc(), None), current)
966}
967
968pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
969 match parse::range() {
970 Some((from, to)) => modified_files_from_rev((from, Some(to)), current),
971 None => modified_files_from_rev(("@".into(), None), current),
972 }
973}
974
975pub fn modified_from_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
978 modified_files_from_rev((parse::from_or_wc(), None), current)
979}
980
981pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
982 if let Some(rev) = parse::revision() {
983 return modified_files_from_rev((rev, None), current);
984 }
985 modified_range_files(current)
986}
987
988pub fn modified_changes_in_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
989 if let Some(rev) = parse::changes_in() {
990 return modified_files_from_rev((rev, None), current);
991 }
992 modified_range_files(current)
993}
994
995pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
996 conflicted_files_from_rev(&parse::revision_or_wc(), current)
997}
998
999pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1001 let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
1002 modified_files_from_rev((rev, None), current)
1003}
1004
1005pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1007 let Some((from, to)) = parse::range() else {
1008 return Vec::new();
1009 };
1010 with_jj(|jj, _| {
1014 let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build(), current)?;
1015 res.extend(modified_files_from_rev_with_jj_cmd(
1016 (to, None),
1017 jj.build(),
1018 current,
1019 )?);
1020 Ok(res)
1021 })
1022}
1023
1024pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
1026 let mut rev = parse::log_revisions().join(")|(");
1027 if rev.is_empty() {
1028 rev = "@".into();
1029 } else {
1030 rev = format!("latest(heads(({rev})))"); };
1032 all_files_from_rev(rev, current)
1033}
1034
1035fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate>
1039where
1040 F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>,
1041{
1042 get_jj_command()
1043 .and_then(|(jj, settings)| completion_fn(jj, &settings))
1044 .unwrap_or_else(|e| {
1045 eprintln!("{}", e.error);
1046 Vec::new()
1047 })
1048}
1049
1050fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
1060 let current_exe = std::env::current_exe().map_err(user_error)?;
1061 let mut cmd_args = Vec::<String>::new();
1062
1063 cmd_args.push("--ignore-working-copy".into());
1066 cmd_args.push("--color=never".into());
1067 cmd_args.push("--no-pager".into());
1068
1069 let app = crate::commands::default_app();
1073 let mut raw_config = config_from_environment(default_config_layers());
1074 let ui = Ui::null();
1075 let cwd = std::env::current_dir()
1076 .and_then(dunce::canonicalize)
1077 .map_err(user_error)?;
1078 let mut config_env = ConfigEnv::from_environment();
1080 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
1081 config_env.reload_user_config(&mut raw_config).ok();
1082 if let Ok(loader) = &maybe_cwd_workspace_loader {
1083 config_env.reset_repo_path(loader.repo_path());
1084 config_env.reload_repo_config(&mut raw_config).ok();
1085 config_env.reset_workspace_path(loader.workspace_root());
1086 config_env.reload_workspace_config(&mut raw_config).ok();
1087 }
1088 let mut config = config_env.resolve_config(&raw_config)?;
1089 let args = std::env::args_os().skip(2);
1091 let args = expand_args(&ui, &app, args, &config)?;
1092 let arg_matches = app
1093 .clone()
1094 .disable_version_flag(true)
1095 .disable_help_flag(true)
1096 .ignore_errors(true)
1097 .try_get_matches_from(args)?;
1098 let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?;
1099
1100 if let Some(repository) = args.repository {
1101 if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) {
1103 config_env.reset_repo_path(loader.repo_path());
1104 config_env.reload_repo_config(&mut raw_config).ok();
1105 config_env.reset_workspace_path(loader.workspace_root());
1106 config_env.reload_workspace_config(&mut raw_config).ok();
1107 if let Ok(new_config) = config_env.resolve_config(&raw_config) {
1108 config = new_config;
1109 }
1110 }
1111 cmd_args.push("--repository".into());
1112 cmd_args.push(repository);
1113 }
1114 if let Some(at_operation) = args.at_operation {
1115 let mut canary_cmd = std::process::Command::new(¤t_exe);
1126 canary_cmd.args(&cmd_args);
1127 canary_cmd.arg("--at-operation");
1128 canary_cmd.arg(&at_operation);
1129 canary_cmd.arg("debug");
1130 canary_cmd.arg("snapshot");
1131
1132 match canary_cmd.output() {
1133 Ok(output) if output.status.success() => {
1134 cmd_args.push("--at-operation".into());
1136 cmd_args.push(at_operation);
1137 }
1138 _ => {} }
1140 }
1141 for (kind, value) in args.early_args.merged_config_args(&arg_matches) {
1142 let arg = match kind {
1143 ConfigArgKind::Item => format!("--config={value}"),
1144 ConfigArgKind::File => format!("--config-file={value}"),
1145 };
1146 cmd_args.push(arg);
1147 }
1148
1149 let builder = JjBuilder {
1150 cmd: current_exe,
1151 args: cmd_args,
1152 };
1153 let settings = UserSettings::from_config(config)?;
1154
1155 Ok((builder, settings))
1156}
1157
1158struct JjBuilder {
1161 cmd: std::path::PathBuf,
1162 args: Vec<String>,
1163}
1164
1165impl JjBuilder {
1166 fn build(&self) -> std::process::Command {
1167 let mut cmd = std::process::Command::new(&self.cmd);
1168 cmd.args(&self.args);
1169 cmd
1170 }
1171}
1172
1173mod parse {
1182 pub(super) fn parse_flag(
1183 candidates: &[&str],
1184 mut args: impl Iterator<Item = String>,
1185 ) -> impl Iterator<Item = String> {
1186 std::iter::from_fn(move || {
1187 for arg in args.by_ref() {
1188 if candidates.contains(&arg.as_ref()) {
1190 match args.next() {
1191 Some(val) if !val.starts_with('-') => {
1192 return Some(strip_shell_quotes(&val).into());
1193 }
1194 _ => return None,
1195 }
1196 }
1197
1198 if let Some(value) = candidates.iter().find_map(|candidate| {
1200 let rest = arg.strip_prefix(candidate)?;
1201 match rest.strip_prefix('=') {
1202 Some(value) => Some(value),
1203
1204 None if candidate.len() == 2 => Some(rest),
1206
1207 None => None,
1208 }
1209 }) {
1210 return Some(strip_shell_quotes(value).into());
1211 };
1212 }
1213 None
1214 })
1215 }
1216
1217 pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> {
1218 parse_flag(&["-r", "--revision"], args).next()
1219 }
1220
1221 pub fn revision() -> Option<String> {
1222 parse_revision_impl(std::env::args())
1223 }
1224
1225 pub fn parse_changes_in_impl(args: impl Iterator<Item = String>) -> Option<String> {
1226 parse_flag(&["-c", "--changes-in"], args).next()
1227 }
1228
1229 pub fn changes_in() -> Option<String> {
1230 parse_changes_in_impl(std::env::args())
1231 }
1232
1233 pub fn revision_or_wc() -> String {
1234 revision().unwrap_or_else(|| "@".into())
1235 }
1236
1237 pub fn from_or_wc() -> String {
1238 parse_flag(&["-f", "--from"], std::env::args())
1239 .next()
1240 .unwrap_or_else(|| "@".into())
1241 }
1242
1243 pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)>
1244 where
1245 T: Iterator<Item = String>,
1246 {
1247 let from = parse_flag(&["-f", "--from"], args()).next()?;
1248 let to = parse_flag(&["-t", "--to"], args())
1249 .next()
1250 .unwrap_or_else(|| "@".into());
1251
1252 Some((from, to))
1253 }
1254
1255 pub fn range() -> Option<(String, String)> {
1256 parse_range_impl(std::env::args)
1257 }
1258
1259 pub fn squash_revision() -> Option<String> {
1264 if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() {
1265 return Some(rev);
1266 }
1267 parse_flag(&["-f", "--from"], std::env::args()).next()
1268 }
1269
1270 pub fn log_revisions() -> Vec<String> {
1273 let candidates = &["-r", "--revisions"];
1274 parse_flag(candidates, std::env::args()).collect()
1275 }
1276
1277 fn strip_shell_quotes(s: &str) -> &str {
1278 if s.len() >= 2
1279 && (s.starts_with('"') && s.ends_with('"') || s.starts_with('\'') && s.ends_with('\''))
1280 {
1281 &s[1..s.len() - 1]
1282 } else {
1283 s
1284 }
1285 }
1286}
1287
1288#[cfg(test)]
1289mod tests {
1290 use super::*;
1291
1292 #[test]
1293 fn test_split_revset_trailing_name() {
1294 assert_eq!(split_revset_trailing_name(""), Some(("", "")));
1295 assert_eq!(split_revset_trailing_name(" "), Some((" ", "")));
1296 assert_eq!(split_revset_trailing_name("foo"), Some(("", "foo")));
1297 assert_eq!(split_revset_trailing_name(" foo"), Some((" ", "foo")));
1298 assert_eq!(split_revset_trailing_name("foo "), None);
1299 assert_eq!(split_revset_trailing_name("foo_"), Some(("", "foo_")));
1300 assert_eq!(split_revset_trailing_name("foo/"), Some(("", "foo/")));
1301 assert_eq!(split_revset_trailing_name("foo/b"), Some(("", "foo/b")));
1302
1303 assert_eq!(split_revset_trailing_name("foo-"), Some(("", "foo-")));
1304 assert_eq!(split_revset_trailing_name("foo+"), Some(("", "foo+")));
1305 assert_eq!(
1306 split_revset_trailing_name("foo-bar-"),
1307 Some(("", "foo-bar-"))
1308 );
1309 assert_eq!(
1310 split_revset_trailing_name("foo-bar-b"),
1311 Some(("", "foo-bar-b"))
1312 );
1313
1314 assert_eq!(split_revset_trailing_name("foo."), Some(("", "foo.")));
1315 assert_eq!(split_revset_trailing_name("foo..b"), Some(("foo..", "b")));
1316 assert_eq!(split_revset_trailing_name("..foo"), Some(("..", "foo")));
1317
1318 assert_eq!(split_revset_trailing_name("foo(bar"), Some(("foo(", "bar")));
1319 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1320 assert_eq!(split_revset_trailing_name("(f"), Some(("(", "f")));
1321
1322 assert_eq!(split_revset_trailing_name("foo@"), Some(("", "foo@")));
1323 assert_eq!(split_revset_trailing_name("foo@b"), Some(("", "foo@b")));
1324 assert_eq!(split_revset_trailing_name("..foo@"), Some(("..", "foo@")));
1325 assert_eq!(
1326 split_revset_trailing_name("::F(foo@origin.1..bar@origin."),
1327 Some(("::F(foo@origin.1..", "bar@origin."))
1328 );
1329 }
1330
1331 #[test]
1332 fn test_split_revset_trailing_name_with_trailing_operator() {
1333 assert_eq!(split_revset_trailing_name("foo|"), Some(("foo|", "")));
1334 assert_eq!(split_revset_trailing_name("foo | "), Some(("foo | ", "")));
1335 assert_eq!(split_revset_trailing_name("foo&"), Some(("foo&", "")));
1336 assert_eq!(split_revset_trailing_name("foo~"), Some(("foo~", "")));
1337
1338 assert_eq!(split_revset_trailing_name(".."), Some(("..", "")));
1339 assert_eq!(split_revset_trailing_name("foo.."), Some(("foo..", "")));
1340 assert_eq!(split_revset_trailing_name("::"), Some(("::", "")));
1341 assert_eq!(split_revset_trailing_name("foo::"), Some(("foo::", "")));
1342
1343 assert_eq!(split_revset_trailing_name("("), Some(("(", "")));
1344 assert_eq!(split_revset_trailing_name("foo("), Some(("foo(", "")));
1345 assert_eq!(split_revset_trailing_name("foo()"), None);
1346 assert_eq!(split_revset_trailing_name("foo(bar)"), None);
1347 }
1348
1349 #[test]
1350 fn test_split_revset_trailing_name_with_modifier() {
1351 assert_eq!(split_revset_trailing_name("all:"), Some(("all:", "")));
1352 assert_eq!(split_revset_trailing_name("all: "), Some(("all: ", "")));
1353 assert_eq!(split_revset_trailing_name("all:f"), Some(("all:", "f")));
1354 assert_eq!(split_revset_trailing_name("all: f"), Some(("all: ", "f")));
1355 }
1356
1357 #[test]
1358 fn test_config_keys() {
1359 config_keys();
1361 }
1362
1363 #[test]
1364 fn test_parse_revision_impl() {
1365 let good_cases: &[&[&str]] = &[
1366 &["-r", "foo"],
1367 &["-r", "'foo'"],
1368 &["-r", "\"foo\""],
1369 &["-rfoo"],
1370 &["-r'foo'"],
1371 &["-r\"foo\""],
1372 &["--revision", "foo"],
1373 &["-r=foo"],
1374 &["-r='foo'"],
1375 &["-r=\"foo\""],
1376 &["--revision=foo"],
1377 &["--revision='foo'"],
1378 &["--revision=\"foo\""],
1379 &["preceding_arg", "-r", "foo"],
1380 &["-r", "foo", "following_arg"],
1381 ];
1382 for case in good_cases {
1383 let args = case.iter().map(|s| s.to_string());
1384 assert_eq!(
1385 parse::parse_revision_impl(args),
1386 Some("foo".into()),
1387 "case: {case:?}",
1388 );
1389 }
1390 let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1391 for case in bad_cases {
1392 let args = case.iter().map(|s| s.to_string());
1393 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1394 }
1395 }
1396
1397 #[test]
1398 fn test_parse_changes_in_impl() {
1399 let good_cases: &[&[&str]] = &[
1400 &["-c", "foo"],
1401 &["--changes-in", "foo"],
1402 &["-cfoo"],
1403 &["--changes-in=foo"],
1404 ];
1405 for case in good_cases {
1406 let args = case.iter().map(|s| s.to_string());
1407 assert_eq!(
1408 parse::parse_changes_in_impl(args),
1409 Some("foo".into()),
1410 "case: {case:?}",
1411 );
1412 }
1413 let bad_cases: &[&[&str]] = &[&[], &["-c"], &["-r"], &["foo"]];
1414 for case in bad_cases {
1415 let args = case.iter().map(|s| s.to_string());
1416 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}");
1417 }
1418 }
1419
1420 #[test]
1421 fn test_parse_range_impl() {
1422 let wc_cases: &[&[&str]] = &[
1423 &["-f", "foo"],
1424 &["--from", "foo"],
1425 &["-f=foo"],
1426 &["preceding_arg", "-f", "foo"],
1427 &["-f", "foo", "following_arg"],
1428 ];
1429 for case in wc_cases {
1430 let args = case.iter().map(|s| s.to_string());
1431 assert_eq!(
1432 parse::parse_range_impl(|| args.clone()),
1433 Some(("foo".into(), "@".into())),
1434 "case: {case:?}",
1435 );
1436 }
1437 let to_cases: &[&[&str]] = &[
1438 &["-f", "foo", "-t", "bar"],
1439 &["-f", "foo", "--to", "bar"],
1440 &["-f=foo", "-t=bar"],
1441 &["-t=bar", "-f=foo"],
1442 ];
1443 for case in to_cases {
1444 let args = case.iter().map(|s| s.to_string());
1445 assert_eq!(
1446 parse::parse_range_impl(|| args.clone()),
1447 Some(("foo".into(), "bar".into())),
1448 "case: {case:?}",
1449 );
1450 }
1451 let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]];
1452 for case in bad_cases {
1453 let args = case.iter().map(|s| s.to_string());
1454 assert_eq!(
1455 parse::parse_range_impl(|| args.clone()),
1456 None,
1457 "case: {case:?}"
1458 );
1459 }
1460 }
1461
1462 #[test]
1463 fn test_parse_multiple_flags() {
1464 let candidates = &["-r", "--revisions"];
1465 let args = &[
1466 "unrelated_arg_at_the_beginning",
1467 "-r",
1468 "1",
1469 "--revisions",
1470 "2",
1471 "-r=3",
1472 "--revisions=4",
1473 "unrelated_arg_in_the_middle",
1474 "-r5",
1475 "unrelated_arg_at_the_end",
1476 ];
1477 let flags: Vec<_> =
1478 parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect();
1479 let expected = ["1", "2", "3", "4", "5"];
1480 assert_eq!(flags, expected);
1481 }
1482}