jj_cli/
complete.rs

1// Copyright 2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
56/// A helper function for various completer functions. It returns
57/// (candidate, help) assuming they are separated by a space.
58fn 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                // only provide help for local refs, remote could be ambiguous
148                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            // This is opinionated, but many people probably have several
212            // single- or two-letter aliases they use all the time. These
213            // aliases don't need to be completed and they would only clutter
214            // the output of `jj <TAB>`.
215            .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        // display order
224        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        // bookmarks
233
234        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        // tags
268
269        // Tags cannot be filtered by revisions. In order to avoid suggesting
270        // immutable tags for mutable revision args, we skip tags entirely if
271        // revset_filter is set. This is not a big loss, since tags usually point
272        // to immutable revisions anyway.
273        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        // change IDs
296
297        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        // revset aliases
330
331        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
381/// Identifies if an incomplete expression ends with a name, or may be continued
382/// with a name.
383///
384/// If the expression ends with an name or a partial name, returns a tuple that
385/// splits the string at the point the name starts.
386/// If the expression is empty or ends with a prefix or infix operator that
387/// could plausibly be followed by a name, returns a tuple where the first
388/// item is the entire input string, and the second item is empty.
389/// Otherwise, returns `None`.
390///
391/// The input expression may be incomplete (e.g. missing closing parentheses),
392/// and the ability to reject invalid expressions is limited.
393fn 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
486/// Approximate list of known diff editors
487pub fn diff_editors() -> Vec<CompletionCandidate> {
488    with_jj(|_, settings| {
489        Ok(std::iter::once(":builtin")
490            .chain(merge_tools_filtered_by(
491                settings,
492                // The args are empty only if `edit-args` are explicitly set to
493                // `[]` in TOML. If they are not specified, the default
494                // `["$left", "$right"]` value would be used.
495                |tool| !tool.edit_args.is_empty(),
496            ))
497            .map(CompletionCandidate::new)
498            .collect())
499    })
500}
501
502/// Approximate list of known diff tools
503pub 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                // The args are empty only if `diff-args` are explicitly set to
512                // `[]` in TOML. If they are not specified, the default
513                // `["$left", "$right"]` value would be used.
514                |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        // Don't complete branch names since we want to create a new branch
663        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() // directories may occur multiple times
713            .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    // In case of a rename, one entry of `diff` results in two suggestions.
727    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() // directories may occur multiple times
818            .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
841/// Completes files in `@` *or* the `--from` revision (not the diff between
842/// `--from` and `@`)
843pub 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
858/// Specific function for completing file paths for `jj squash`
859pub 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
864/// Specific function for completing file paths for `jj interdiff`
865pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
866    let Some((from, to)) = parse::range() else {
867        return Vec::new();
868    };
869    // Complete all modified files in "from" and "to". This will also suggest
870    // files that are the same in both, which is a false positive. This approach
871    // is more lightweight than actually doing a temporary rebase here.
872    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
883/// Specific function for completing file paths for `jj log`
884pub 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})))"); // limit to one
890    };
891    all_files_from_rev(rev, current)
892}
893
894/// Shell out to jj during dynamic completion generation
895///
896/// In case of errors, print them and early return an empty vector.
897fn 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
909/// Shell out to jj during dynamic completion generation
910///
911/// This is necessary because dynamic completion code needs to be aware of
912/// global configuration like custom storage backends. Dynamic completion
913/// code via clap_complete doesn't accept arguments, so they cannot be passed
914/// that way. Another solution would've been to use global mutable state, to
915/// give completion code access to custom backends. Shelling out was chosen as
916/// the preferred method, because it's more maintainable and the performance
917/// requirements of completions aren't very high.
918fn 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    // Snapshotting could make completions much slower in some situations
923    // and be undesired by the user.
924    cmd_args.push("--ignore-working-copy".into());
925    cmd_args.push("--color=never".into());
926    cmd_args.push("--no-pager".into());
927
928    // Parse some of the global args we care about for passing along to the
929    // child process. This shouldn't fail, since none of the global args are
930    // required.
931    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    // No config migration for completion. Simply ignore deprecated variables.
938    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    // skip 2 because of the clap_complete prelude: jj -- jj <actual args...>
947    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        // Try to update repo-specific config on a best-effort basis.
959        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        // We cannot assume that the value of at_operation is valid, because
971        // the user may be requesting completions precisely for this invalid
972        // operation ID. Additionally, the user may have mistyped the ID,
973        // in which case adding the argument blindly would break all other
974        // completions, even unrelated ones.
975        //
976        // To avoid this, we shell out to ourselves once with the argument
977        // and check the exit code. There is some performance overhead to this,
978        // but this code path is probably only executed in exceptional
979        // situations.
980        let mut canary_cmd = std::process::Command::new(&current_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                // Operation ID is valid, add it to the completion command.
990                cmd_args.push("--at-operation".into());
991                cmd_args.push(at_operation);
992            }
993            _ => {} // Invalid operation ID, ignore.
994        }
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
1013/// A helper struct to allow completion functions to call jj multiple times with
1014/// different arguments.
1015struct 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
1028/// Functions for parsing revisions and revision ranges from the command line.
1029/// Parsing is done on a best-effort basis and relies on the heuristic that
1030/// most command line flags are consistent across different subcommands.
1031///
1032/// In some cases, this parsing will be incorrect, but it's not worth the effort
1033/// to fix that. For example, if the user specifies any of the relevant flags
1034/// multiple times, the parsing will pick any of the available ones, while the
1035/// actual execution of the command would fail.
1036mod 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                // -r REV syntax
1044                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                // -r=REV syntax
1054                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                        // -rREV syntax
1060                        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    // Special parse function only for `jj squash`. While squash has --from and
1107    // --to arguments, only files within --from should be completed, because
1108    // the files changed only in some other revision in the range between
1109    // --from and --to cannot be squashed into --to like that.
1110    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    // Special parse function only for `jj log`. It has a --revisions flag,
1118    // instead of the usual --revision, and it can be supplied multiple times.
1119    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        // Just make sure the schema is parsed without failure.
1207        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}