1118 lines · 37.6 KB · Rust
RawDownload
// Copyright 2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::io::BufRead as _; use clap::builder::StyledStr;use clap::FromArgMatches as _;use clap_complete::CompletionCandidate;use itertools::Itertools as _;use jj_lib::config::ConfigNamePathBuf;use jj_lib::settings::UserSettings;use jj_lib::workspace::DefaultWorkspaceLoaderFactory;use jj_lib::workspace::WorkspaceLoaderFactory as _; use crate::cli_util::expand_args;use crate::cli_util::find_workspace_dir;use crate::cli_util::load_template_aliases;use crate::cli_util::GlobalArgs;use crate::command_error::user_error;use crate::command_error::CommandError;use crate::config::config_from_environment;use crate::config::default_config_layers;use crate::config::ConfigArgKind;use crate::config::ConfigEnv;use crate::config::CONFIG_SCHEMA;use crate::revset_util::load_revset_aliases;use crate::ui::Ui; const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'='''" " ++if(normal_target,    if(normal_target.description(),        normal_target.description().first_line(),        "(no description set)",    ),    "(conflicted bookmark)",)'''"#; /// A helper function for various completer functions. It returns/// (candidate, help) assuming they are separated by a space.fn split_help_text(line: &str) -> (&str, Option<StyledStr>) {    match line.split_once(' ') {        Some((name, help)) => (name, Some(help.to_string().into())),        None => (line, None),    }} pub fn local_bookmarks() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#)            .output()            .map_err(user_error)?;         Ok(String::from_utf8_lossy(&output.stdout)            .lines()            .map(split_help_text)            .map(|(name, help)| CompletionCandidate::new(name).help(help))            .collect())    })} pub fn tracked_bookmarks() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--tracked")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#)            .output()            .map_err(user_error)?;         Ok(String::from_utf8_lossy(&output.stdout)            .lines()            .map(split_help_text)            .map(|(name, help)| CompletionCandidate::new(name).help(help))            .collect())    })} pub fn untracked_bookmarks() -> Vec<CompletionCandidate> {    with_jj(|jj, settings| {        let output = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(                r#"if(remote && !tracked && remote != "git",                    name ++ '@' ++ remote ++ bookmark_help() ++ "\n"                )"#,            )            .output()            .map_err(user_error)?;         let prefix = settings.get_string("git.push-bookmark-prefix").ok();         Ok(String::from_utf8_lossy(&output.stdout)            .lines()            .map(|line| {                let (name, help) = split_help_text(line);                 let display_order = match prefix.as_ref() {                    // own bookmarks are more interesting                    Some(prefix) if name.starts_with(prefix) => 0,                    _ => 1,                };                CompletionCandidate::new(name)                    .help(help)                    .display_order(Some(display_order))            })            .collect())    })} pub fn bookmarks() -> Vec<CompletionCandidate> {    with_jj(|jj, settings| {        let output = jj            .build()            .arg("bookmark")            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(                // only provide help for local refs, remote could be ambiguous                r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#,            )            .output()            .map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         let prefix = settings.get_string("git.push-bookmark-prefix").ok();         Ok((&stdout            .lines()            .map(split_help_text)            .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name)))            .into_iter()            .map(|(bookmark, mut refs)| {                let help = refs.find_map(|(_, help)| help);                 let local = help.is_some();                let mine = prefix.as_ref().is_some_and(|p| bookmark.starts_with(p));                 let display_order = match (local, mine) {                    (true, true) => 0,                    (true, false) => 1,                    (false, true) => 2,                    (false, false) => 3,                };                CompletionCandidate::new(bookmark)                    .help(help)                    .display_order(Some(display_order))            })            .collect())    })} pub fn git_remotes() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("git")            .arg("remote")            .arg("list")            .output()            .map_err(user_error)?;         let stdout = String::from_utf8_lossy(&output.stdout);         Ok(stdout            .lines()            .filter_map(|line| line.split_once(' ').map(|(name, _url)| name))            .map(CompletionCandidate::new)            .collect())    })} pub fn template_aliases() -> Vec<CompletionCandidate> {    with_jj(|_, settings| {        let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else {            return Ok(Vec::new());        };        Ok(template_aliases            .symbol_names()            .map(CompletionCandidate::new)            .sorted()            .collect())    })} pub fn aliases() -> Vec<CompletionCandidate> {    with_jj(|_, settings| {        Ok(settings            .table_keys("aliases")            // This is opinionated, but many people probably have several            // single- or two-letter aliases they use all the time. These            // aliases don't need to be completed and they would only clutter            // the output of `jj <TAB>`.            .filter(|alias| alias.len() > 2)            .map(CompletionCandidate::new)            .collect())    })} fn revisions(revisions: Option<&str>) -> Vec<CompletionCandidate> {    with_jj(|jj, settings| {        // display order        const LOCAL_BOOKMARK_MINE: usize = 0;        const LOCAL_BOOKMARK: usize = 1;        const TAG: usize = 2;        const CHANGE_ID: usize = 3;        const REMOTE_BOOKMARK_MINE: usize = 4;        const REMOTE_BOOKMARK: usize = 5;        const REVSET_ALIAS: usize = 6;         let mut candidates = Vec::new();         // bookmarks         let prefix = settings.get_string("git.push-bookmark-prefix").ok();         let mut cmd = jj.build();        cmd.arg("bookmark")            .arg("list")            .arg("--all-remotes")            .arg("--config")            .arg(BOOKMARK_HELP_TEMPLATE)            .arg("--template")            .arg(                r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#,            );        if let Some(revs) = revisions {            cmd.arg("--revisions").arg(revs);        }        let output = cmd.output().map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         candidates.extend(stdout.lines().map(|line| {            let (bookmark, help) = split_help_text(line);             let local = !bookmark.contains('@');            let mine = prefix.as_ref().is_some_and(|p| bookmark.starts_with(p));             let display_order = match (local, mine) {                (true, true) => LOCAL_BOOKMARK_MINE,                (true, false) => LOCAL_BOOKMARK,                (false, true) => REMOTE_BOOKMARK_MINE,                (false, false) => REMOTE_BOOKMARK,            };            CompletionCandidate::new(bookmark)                .help(help)                .display_order(Some(display_order))        }));         // tags         // Tags cannot be filtered by revisions. In order to avoid suggesting        // immutable tags for mutable revision args, we skip tags entirely if        // revisions is set. This is not a big loss, since tags usually point        // to immutable revisions anyway.        if revisions.is_none() {            let output = jj                .build()                .arg("tag")                .arg("list")                .arg("--config")                .arg(BOOKMARK_HELP_TEMPLATE)                .arg("--template")                .arg(r#"name ++ bookmark_help() ++ "\n""#)                .output()                .map_err(user_error)?;            let stdout = String::from_utf8_lossy(&output.stdout);             candidates.extend(stdout.lines().map(|line| {                let (name, desc) = split_help_text(line);                CompletionCandidate::new(name)                    .help(desc)                    .display_order(Some(TAG))            }));        }         // change IDs         let revisions = revisions            .map(String::from)            .or_else(|| settings.get_string("revsets.short-prefixes").ok())            .or_else(|| settings.get_string("revsets.log").ok())            .unwrap_or_default();         let output = jj            .build()            .arg("log")            .arg("--no-graph")            .arg("--limit")            .arg("100")            .arg("--revisions")            .arg(revisions)            .arg("--template")            .arg(r#"change_id.shortest() ++ " " ++ if(description, description.first_line(), "(no description set)") ++ "\n""#)            .output()            .map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         candidates.extend(stdout.lines().map(|line| {            let (id, desc) = split_help_text(line);            CompletionCandidate::new(id)                .help(desc)                .display_order(Some(CHANGE_ID))        }));         // revset aliases         let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?;        let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect();        symbol_names.sort();        candidates.extend(symbol_names.into_iter().map(|symbol| {            let (_, defn) = revset_aliases.get_symbol(symbol).unwrap();            CompletionCandidate::new(symbol)                .help(Some(defn.into()))                .display_order(Some(REVSET_ALIAS))        }));         Ok(candidates)    })} pub fn mutable_revisions() -> Vec<CompletionCandidate> {    revisions(Some("mutable()"))} pub fn all_revisions() -> Vec<CompletionCandidate> {    revisions(None)} pub fn operations() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("operation")            .arg("log")            .arg("--no-graph")            .arg("--limit")            .arg("100")            .arg("--template")            .arg(                r#"                separate(" ",                    id.short(),                    "(" ++ format_timestamp(time.end()) ++ ")",                    description.first_line(),                ) ++ "\n""#,            )            .output()            .map_err(user_error)?;         Ok(String::from_utf8_lossy(&output.stdout)            .lines()            .map(|line| {                let (id, help) = split_help_text(line);                CompletionCandidate::new(id).help(help)            })            .collect())    })} pub fn workspaces() -> Vec<CompletionCandidate> {    with_jj(|jj, _| {        let output = jj            .build()            .arg("--config")            .arg(r#"templates.commit_summary='if(description, description.first_line(), "(no description set)")'"#)            .arg("workspace")            .arg("list")            .output()            .map_err(user_error)?;        let stdout = String::from_utf8_lossy(&output.stdout);         Ok(stdout            .lines()            .map(|line| {                let (name, desc) = line.split_once(": ").unwrap_or((line, ""));                CompletionCandidate::new(name).help(Some(desc.to_string().into()))            })            .collect())    })} fn config_keys_rec(    prefix: ConfigNamePathBuf,    properties: &serde_json::Map<String, serde_json::Value>,    acc: &mut Vec<CompletionCandidate>,    only_leaves: bool,    suffix: &str,) {    for (key, value) in properties {        let mut prefix = prefix.clone();        prefix.push(key);         let value = value.as_object().unwrap();        match value.get("type").and_then(|v| v.as_str()) {            Some("object") => {                if !only_leaves {                    let help = value                        .get("description")                        .map(|desc| desc.as_str().unwrap().to_string().into());                    let escaped_key = prefix.to_string();                    acc.push(CompletionCandidate::new(escaped_key).help(help));                }                let Some(properties) = value.get("properties") else {                    continue;                };                let properties = properties.as_object().unwrap();                config_keys_rec(prefix, properties, acc, only_leaves, suffix);            }            _ => {                let help = value                    .get("description")                    .map(|desc| desc.as_str().unwrap().to_string().into());                let escaped_key = format!("{prefix}{suffix}");                acc.push(CompletionCandidate::new(escaped_key).help(help));            }        }    }} fn json_keypath<'a>(    schema: &'a serde_json::Value,    keypath: &str,    separator: &str,) -> Option<&'a serde_json::Value> {    keypath        .split(separator)        .try_fold(schema, |value, step| value.get(step))}fn jsonschema_keypath<'a>(    schema: &'a serde_json::Value,    keypath: &ConfigNamePathBuf,) -> Option<&'a serde_json::Value> {    keypath.components().try_fold(schema, |value, step| {        let value = value.as_object()?;        if value.get("type")?.as_str()? != "object" {            return None;        }        let properties = value.get("properties")?.as_object()?;        properties.get(step.get())    })} fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> {    let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap();     let mut config_entry = jsonschema_keypath(&schema, path)?;    if let Some(reference) = config_entry.get("$ref") {        let reference = reference.as_str()?.strip_prefix("#/")?;        config_entry = json_keypath(&schema, reference, "/")?;    };     if let Some(possibile_values) = config_entry.get("enum") {        return Some(            possibile_values                .as_array()?                .iter()                .filter_map(|val| val.as_str())                .map(ToOwned::to_owned)                .collect(),        );    }     Some(match config_entry.get("type")?.as_str()? {        "boolean" => vec!["false".into(), "true".into()],        _ => vec![],    })

Lines 1–500 of 1118.