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