// 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. Load more
// 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![], })