diff --git a/src/internal/commands/base.rs b/src/internal/commands/base.rs index 8429f489..905f2ddb 100644 --- a/src/internal/commands/base.rs +++ b/src/internal/commands/base.rs @@ -1,14 +1,20 @@ use std::collections::BTreeMap; use std::process::exit; +use itertools::Itertools; + use crate::internal::commands::fromconfig::ConfigCommand; use crate::internal::commands::frommakefile::MakefileCommand; use crate::internal::commands::frompath::PathCommand; use crate::internal::commands::utils::abs_or_rel_path; +use crate::internal::commands::utils::path_auto_complete; use crate::internal::commands::void::VoidCommand; use crate::internal::config::parser::ParseArgsErrorKind; use crate::internal::config::parser::ParseArgsValue; use crate::internal::config::CommandSyntax; +use crate::internal::config::SyntaxGroup; +use crate::internal::config::SyntaxOptArg; +use crate::internal::config::SyntaxOptArgType; use crate::internal::dynenv::update_dynamic_env_for_command; use crate::internal::user_interface::colors::strip_colors; use crate::internal::user_interface::colors::strip_colors_if_needed; @@ -37,8 +43,13 @@ pub trait BuiltinCommand: std::fmt::Debug + Send + Sync { fn syntax(&self) -> Option; fn category(&self) -> Option>; fn exec(&self, argv: Vec); - fn autocompletion(&self) -> bool; - fn autocomplete(&self, comp_cword: usize, argv: Vec) -> Result<(), ()>; + fn autocompletion(&self) -> CommandAutocompletion; + fn autocomplete( + &self, + comp_cword: usize, + argv: Vec, + parameter: Option, + ) -> Result<(), ()>; } #[derive(Debug)] @@ -337,13 +348,7 @@ impl Command { argv: Vec, called_as: Vec, ) -> Option> { - let should_parse_args = match self { - Command::FromConfig(command) => command.argparser(), - Command::FromPath(command) => command.argparser(), - _ => false, - }; - - if !should_parse_args { + if !self.argparser() { return None; } @@ -382,6 +387,10 @@ impl Command { omni_print!(format!("{} {}", "error building parser:".red(), err)); exit(1); } + Err(ParseArgsErrorKind::InvalidValue(err)) => { + omni_print!(format!("{} {}", "error parsing arguments:".red(), err)); + exit(1); + } Err(ParseArgsErrorKind::ArgumentParsingError(err)) => { let clap_rich_error = err.render().ansi().to_string(); let clap_rich_error = strip_colors_if_needed(&clap_rich_error); @@ -478,48 +487,367 @@ impl Command { panic!("Command::exec() not implemented"); } - pub fn autocompletion(&self) -> bool { + pub fn argparser(&self) -> bool { match self { + Command::FromConfig(command) => command.argparser(), + Command::FromPath(command) => command.argparser(), + _ => false, + } + } + + pub fn autocompletion(&self) -> CommandAutocompletion { + let completion = match self { Command::Builtin(command) => command.autocompletion(), Command::FromPath(command) => command.autocompletion(), - Command::FromConfig(_command) => false, - Command::FromMakefile(_command) => false, - Command::Void(_) => false, + Command::FromConfig(_command) => CommandAutocompletion::Null, + Command::FromMakefile(_command) => CommandAutocompletion::Null, + Command::Void(_) => CommandAutocompletion::Null, + }; + + let trusted = match self { + Command::FromPath(_) | Command::FromConfig(_) | Command::FromMakefile(_) => { + // Check if the workdir where the command is located is trusted + is_trusted(self.source_dir()) + } + _ => true, + }; + + let argparser = self.argparser() || matches!(self, Command::Builtin(_)); + + match (completion, trusted, argparser) { + (CommandAutocompletion::Full, true, _) => CommandAutocompletion::Full, + (CommandAutocompletion::Partial, true, true) => CommandAutocompletion::Partial, + (CommandAutocompletion::Partial, true, false) => CommandAutocompletion::Null, + (CommandAutocompletion::Null, _, true) => CommandAutocompletion::Argparser, + (CommandAutocompletion::Null, _, false) => CommandAutocompletion::Null, + (_, false, true) => CommandAutocompletion::Argparser, + (_, false, false) => CommandAutocompletion::Null, + _ => CommandAutocompletion::Null, } } + /// Handle the autocompletion for the command + /// + /// This function is called when the command is being autocompleted. It handles + /// the autocompletion for the command and returns the result. If the command + /// has full autocompletion, this will be delegated directly to the command if + /// and only if that command is trusted. If that command has partial completion, + /// it will only be delegated to the command if and only if that command is + /// trusted _AND_ the argparser autocompletion did not provide any completion. + /// If the command has no autocompletion, then only the argparser autocompletion + /// will be used. If the command has neither autocompletion nor argparser, + /// then no autocompletion will be provided. pub fn autocomplete(&self, comp_cword: usize, argv: Vec) -> Result<(), ()> { - match self { - Command::FromPath(_) | Command::FromConfig(_) | Command::FromMakefile(_) => { - // Check if the workdir where the command is located is trusted - if !is_trusted(self.source_dir()) { - return Err(()); + let completion = self.autocompletion(); + + // If no autocompletion option is available, just return an error + if !completion.any() { + return Err(()); + } + + // Only try to autocomplete with the argparser if the completion is either + // disabled or partial, or if the command is not trusted + let mut parameter = None; + if completion.use_argparser() { + match self.autocomplete_with_argparser(comp_cword, &argv) { + Ok(None) => return Ok(()), + Ok(Some(param)) => { + // Continue with the autocompletion if partial + parameter = Some(param); } + Err(()) => return Err(()), } - _ => {} } - match self { - Command::Builtin(command) => return command.autocomplete(comp_cword, argv), - Command::FromPath(command) => { - // Load the dynamic environment for that command - update_dynamic_env_for_command(self.source_dir()); + // If we get here, try to do a completion with the command if + // it is trusted and has autocompletion + if completion.use_command() { + match self { + Command::Builtin(command) => { + return command.autocomplete(comp_cword, argv, parameter) + } + Command::FromPath(command) => { + // Load the dynamic environment for that command + update_dynamic_env_for_command(self.source_dir()); - let result = command.autocomplete(comp_cword, argv); + let result = command.autocomplete(comp_cword, argv, parameter); - // Reset the dynamic environment - update_dynamic_env_for_command("."); + // Reset the dynamic environment + update_dynamic_env_for_command("."); - return result; + return result; + } + Command::FromConfig(_command) => {} + Command::FromMakefile(_command) => {} + Command::Void(_) => {} } - Command::FromConfig(_command) => {} - Command::FromMakefile(_command) => {} - Command::Void(_) => {} } Err(()) } + /// Handle the autocompletion for the command using the argparser + /// + /// This will automatically suggest completions based on the argparser + /// of the command. This function is called when the command is being + /// autocompleted and the command has an argparser, unless the command + /// handles the full autocompletion by itself. + /// + /// The argparser autocompletion will suggest flags and options, but + /// also can suggest values for certain specific parameter types (e.g. + /// if the parameter value is supposed to be a path, it will suggest + /// paths available in the filesystem). + fn autocomplete_with_argparser( + &self, + comp_cword: usize, + argv: &[String], + ) -> Result, ()> { + let syntax = match self.syntax() { + Some(syntax) => syntax, + None => return Err(()), + }; + + fn allow_value_check_hyphen(param: &SyntaxOptArg, value: &str) -> bool { + if let Some(value_without_hyphen) = value.strip_prefix('-') { + if !param.allow_hyphen_values { + // All good if we allow for a parameter to start with `-` + return false; + } + + if !param.allow_negative_numbers || value_without_hyphen.parse::().is_err() { + // All good if we allow for negative numbers + return false; + } + } + + true + } + + let mut state = match ( + syntax.parameters.iter().find(|param| param.is_positional()), + argv.get(comp_cword), + ) { + (Some(param), Some(value)) if allow_value_check_hyphen(param, value) => { + ArgparserAutocompleteState::ValueAndParameters(param.clone()) + } + (Some(param), None) => ArgparserAutocompleteState::ValueAndParameters(param.clone()), + (_, _) => ArgparserAutocompleteState::Parameters, + }; + let mut parameters = syntax.parameters.clone(); + let last_parameter = syntax + .parameters + .iter() + .find(|param| param.is_last()) + .cloned(); + + // Go over the arguments we've seen until `comp_cword` and + // try to resolve the parameter in the syntax and the number + // of values that have been passed (according to the configuration + // of the parameter); if a given argument does not have a value, + // consider it is a positional (if any). + let args = argv.iter().take(comp_cword).cloned().collect::>(); + let mut current_arg = args.first().cloned(); + let mut current_idx = 0; + + 'loop_args: while let Some(arg) = current_arg { + current_arg = None; + + let (parameter, mut next_arg) = if arg == "--" { + // If we have `--`, find the parameter with the 'last' flag, if any + match last_parameter { + Some(ref parameter) => { + state = ArgparserAutocompleteState::Value(parameter.clone()); + } + None => { + // We do not need to autocomplete anything if we are + // after the `--` and there is no parameter with the + // 'last' flag + return Ok(None); + } + } + + // No need to keep reading parameters + break; + } else if arg == "-" { + // If we have `-` we just can't complete any parameter, let's just skip + (None, None) + } else if arg.starts_with("--") { + let parameter = syntax + .parameters + .iter() + .find(|param| param.all_names().iter().any(|name| name == arg.as_str())); + + (parameter, None) + } else if let Some(arg_name) = arg.strip_prefix('-') { + // Split the first char and the following ones, if any + // as they would become the next argument + let (arg, next_arg) = arg_name.split_at(1); + let arg = format!("-{}", arg); + + let next_arg = if next_arg.is_empty() { + None + } else { + Some(next_arg.to_string()) + }; + + let parameter = syntax + .parameters + .iter() + .find(|param| param.all_names().iter().any(|name| name == arg.as_str())); + + (parameter, next_arg) + } else { + // Get the parameters from the list of parameters left, since for positional + // we need to remove them from the list as we can't identify them by name alone + match syntax + .parameters + .iter() + .find(|param| param.is_positional() && parameters.contains(param)) + { + Some(parameter) => { + // We need to pass the parameter itself as "next_arg" since for a + // positional, the parameter itself is the value + (Some(parameter), Some(arg)) + } + None => { + // If we don't have any positional parameters left, then we can't + // autocomplete this, just skip it + (None, None) + } + } + }; + + // If the parameter is not found, skip to the next + let parameter = match parameter { + Some(parameter) => parameter, + None => { + current_idx += 1; + current_arg = args.get(current_idx).cloned(); + continue; + } + }; + + // If the parameter is not repeatable, remove it from the list + // TODO: how does that work for positionals? + if !parameter.is_repeatable() { + parameters.retain(|param| param != parameter); + } + + // Handle the conflicts between parameters + parameters.retain(|param| !check_parameter_conflicts(parameter, param, &syntax.groups)); + + // Consume values as needed + if parameter.takes_value() { + // How many values to consume at most? + let max_values = parameter.num_values.map_or(Some(1), |num| num.max()); + let min_values = parameter.num_values.map_or(1, |num| num.min().unwrap_or(0)); + + let mut value_idx = 0; + loop { + if let Some(max) = max_values { + if value_idx >= max { + // Stop here if we have the maximum number of values + break; + } + } + + value_idx += 1; + let value = if let Some(arg) = next_arg { + next_arg = None; + Some(arg) + } else { + current_idx += 1; + args.get(current_idx).cloned() + }; + + if let Some(ref value) = value { + if !allow_value_check_hyphen(parameter, value) { + // If the value is not allowed, then consider it + // is another argument, so exit this loop + current_arg = Some(value.to_string()); + break; + } + } + + if current_idx == comp_cword || value.is_none() { + state = if value_idx > min_values { + ArgparserAutocompleteState::ValueAndParameters(parameter.clone()) + } else { + ArgparserAutocompleteState::Value(parameter.clone()) + }; + break 'loop_args; + } + } + } else if let Some(next_arg) = next_arg { + current_arg = Some(format!("-{}", next_arg)); + } + + if current_arg.is_none() { + current_idx += 1; + current_arg = args.get(current_idx).cloned(); + } + } + + // Grab the value to be completed, or default to the empty string + let comp_value = argv.get(comp_cword).cloned().unwrap_or_default(); + + if state.complete_parameters() { + // If we get here, go over the parameters still in the list, filter + // them using the value to be completed, and return their names + parameters + .iter() + .filter(|param| !param.is_positional()) + .flat_map(|param| param.all_names()) + .filter(|name| name.starts_with(&comp_value)) + .sorted() + .for_each(|name| { + println!("{}", name); + }); + + // Autocomplete '--' if there is a last parameter + if last_parameter.is_some() && "--".starts_with(&comp_value) { + println!("--"); + } + } + + if let Some(param) = state.parameter() { + let arg_type = param.arg_type().terminal_type().clone(); + + if let Some(possible_values) = arg_type.possible_values() { + possible_values + .iter() + .filter(|val| val.starts_with(&comp_value)) + .for_each(|val| { + println!("{}", val); + }); + + // We've done the whole completion for that parameter, no + // need to delegate to the underlying command + return Ok(None); + } + + if matches!( + arg_type, + SyntaxOptArgType::DirPath | SyntaxOptArgType::FilePath | SyntaxOptArgType::RepoPath + ) { + let include_repositories = matches!(arg_type, SyntaxOptArgType::RepoPath); + let include_files = matches!(arg_type, SyntaxOptArgType::FilePath); + + path_auto_complete(&comp_value, include_repositories, include_files) + .iter() + .for_each(|s| println!("{}", s)); + + // We offered path autocompletions, no need to delegate + // to the underlying command + return Ok(None); + } + + Ok(Some(param.name())) + } else { + Ok(None) + } + } + fn command_type_sort_order(&self) -> usize { match self { Command::FromConfig(_) => 1, @@ -546,3 +874,193 @@ impl Command { } } } + +#[inline] +fn check_parameter_conflicts( + param1: &SyntaxOptArg, + param2: &SyntaxOptArg, + groups: &[SyntaxGroup], +) -> bool { + // Get the groups for param1 + let param1_groups = groups + .iter() + .filter(|group| { + group.parameters.iter().any(|name| { + param1 + .all_names() + .iter() + .any(|n| n.as_str() == name.as_str()) + }) + }) + .collect::>(); + let param1_all = param1 + .all_names() + .into_iter() + .chain(param1_groups.iter().map(|group| group.name.to_string())) + .collect::>(); + + // Get the groups for param2 + let param2_groups = groups + .iter() + .filter(|group| { + group.parameters.iter().any(|name| { + param2 + .all_names() + .iter() + .any(|n| n.as_str() == name.as_str()) + }) + }) + .collect::>(); + let param2_all = param2 + .all_names() + .into_iter() + .chain(param2_groups.iter().map(|group| group.name.to_string())) + .collect::>(); + + // If param1 defines conflicts with param2 or any of its groups, return true + if param1 + .conflicts_with + .iter() + .any(|name| param2_all.iter().any(|n| n == name)) + { + return true; + } + + // If param2 defines conflicts with param1 or any of its groups, return true + if param2 + .conflicts_with + .iter() + .any(|name| param1_all.iter().any(|n| n == name)) + { + return true; + } + + // If param1 and param2 are in the same group, and that group does + // not allow multiple, return true + let common_groups = param1_groups + .iter() + .filter(|group| param2_groups.contains(group)) + .collect::>(); + if common_groups.iter().any(|group| !group.multiple) { + return true; + } + + // Check if any of the param1 groups conflicts with param2 + if param1_groups.iter().any(|group| { + group + .conflicts_with + .iter() + .any(|name| param2_all.iter().any(|n| n == name)) + }) { + return true; + } + + // Check if any of the param2 groups conflicts with param1 + if param2_groups.iter().any(|group| { + group + .conflicts_with + .iter() + .any(|name| param1_all.iter().any(|n| n == name)) + }) { + return true; + } + + // If we get here, there is no conflict + false +} + +#[derive(Debug)] +enum ArgparserAutocompleteState { + Parameters, + ValueAndParameters(SyntaxOptArg), + Value(SyntaxOptArg), +} + +impl ArgparserAutocompleteState { + fn complete_parameters(&self) -> bool { + matches!(self, Self::Parameters | Self::ValueAndParameters(_)) + } + + fn parameter(&self) -> Option { + match self { + Self::Value(param) | Self::ValueAndParameters(param) => Some(param.clone()), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum CommandAutocompletion { + #[default] + Null, + Partial, + Full, + Argparser, +} + +impl CommandAutocompletion { + fn any(&self) -> bool { + !matches!(self, Self::Null) + } + + fn use_argparser(&self) -> bool { + matches!(self, Self::Argparser | Self::Partial) + } + + fn use_command(&self) -> bool { + matches!(self, Self::Full | Self::Partial) + } +} + +impl serde::Serialize for CommandAutocompletion { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Null => serializer.serialize_bool(false), + Self::Partial => serializer.serialize_str("partial"), + Self::Full => serializer.serialize_bool(true), + Self::Argparser => { + unreachable!("Argparser autocompletion is not serializable") + } + } + } +} + +impl From for bool { + fn from(value: CommandAutocompletion) -> Self { + match value { + CommandAutocompletion::Full + | CommandAutocompletion::Partial + | CommandAutocompletion::Argparser => true, + CommandAutocompletion::Null => false, + } + } +} + +impl From for CommandAutocompletion { + fn from(value: bool) -> Self { + if value { + Self::Full + } else { + Self::Null + } + } +} + +impl From for CommandAutocompletion { + fn from(value: String) -> Self { + Self::from(value.as_str()) + } +} + +impl From<&str> for CommandAutocompletion { + fn from(value: &str) -> Self { + match value.to_lowercase().as_str() { + "full" | "true" | "1" | "on" | "enable" | "enabled" => Self::Full, + "partial" => Self::Partial, + _ => Self::Null, + } + } +} diff --git a/src/internal/commands/builtin/cd.rs b/src/internal/commands/builtin/cd.rs index 5460c274..469c3df2 100644 --- a/src/internal/commands/builtin/cd.rs +++ b/src/internal/commands/builtin/cd.rs @@ -1,11 +1,12 @@ use std::collections::BTreeMap; -use std::path::PathBuf; use std::process::exit; use shell_escape::escape; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::utils::omni_cmd; +use crate::internal::commands::utils::path_auto_complete; use crate::internal::commands::Command; use crate::internal::config::config; use crate::internal::config::parser::ParseArgsValue; @@ -13,8 +14,6 @@ use crate::internal::config::CommandSyntax; use crate::internal::config::SyntaxOptArg; use crate::internal::config::SyntaxOptArgType; use crate::internal::env::omni_cmd_file; -use crate::internal::env::user_home; -use crate::internal::env::Shell; use crate::internal::git::ORG_LOADER; use crate::internal::user_interface::StringColor; use crate::internal::workdir; @@ -213,6 +212,7 @@ impl BuiltinCommand for CdCommand { .to_string() ), arg_type: SyntaxOptArgType::Flag, + conflicts_with: vec!["--no-include-packages".to_string()], ..Default::default() }, SyntaxOptArg { @@ -271,77 +271,23 @@ impl BuiltinCommand for CdCommand { exit(0); } - fn autocompletion(&self) -> bool { - true + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Partial } - fn autocomplete(&self, comp_cword: usize, argv: Vec) -> Result<(), ()> { - if comp_cword > 0 { - return Ok(()); - } - - let repo = if !argv.is_empty() { - argv[0].clone() - } else { - "".to_string() - }; - - // Figure out if this is a path, so we can avoid the expensive repository search - let path_only = repo.starts_with('/') - || repo.starts_with('.') - || repo.starts_with("~/") - || repo == "~" - || repo == "-"; - - // Print all the completion related to path completion - let (list_dir, strip_path_prefix, replace_home_prefix) = if repo == "~" { - (user_home(), false, true) - } else if let Some(repo) = repo.strip_prefix("~/") { - if let Some(slash) = repo.rfind('/') { - let abspath = format!("{}/{}", user_home(), &repo[..(slash + 1)]); - (abspath, false, true) - } else { - (user_home(), false, true) - } - } else if let Some(slash) = repo.rfind('/') { - (repo[..(slash + 1)].to_string(), false, false) - } else { - (".".to_string(), true, false) - }; - if let Ok(files) = std::fs::read_dir(&list_dir) { - for path in files.flatten() { - if path.path().is_dir() { - let path_buf; - let path_obj = path.path(); - let path = if strip_path_prefix { - path_obj.strip_prefix(&list_dir).unwrap() - } else if replace_home_prefix { - if let Ok(path_obj) = path_obj.strip_prefix(user_home()) { - path_buf = PathBuf::from("~").join(path_obj); - path_buf.as_path() - } else { - path_obj.as_path() - } - } else { - path_obj.as_path() - }; - let path_str = path.to_str().unwrap(); - - if !path_str.starts_with(repo.as_str()) { - continue; - } - - println!("{}/", path.display()); - } - } - } - - // Get all the repositories per org - if !path_only { - let add_space = if Shell::current().is_fish() { " " } else { "" }; - for match_repo in ORG_LOADER.complete(&repo) { - println!("{}{}", match_repo, add_space); - } + fn autocomplete( + &self, + comp_cword: usize, + argv: Vec, + parameter: Option, + ) -> Result<(), ()> { + // We only have the work directory to autocomplete + if parameter.unwrap_or_default() == "workdir" { + let repo = argv.get(comp_cword).map_or("", String::as_str); + + path_auto_complete(repo, true, false) + .iter() + .for_each(|s| println!("{}", s)); } Ok(()) diff --git a/src/internal/commands/builtin/clone.rs b/src/internal/commands/builtin/clone.rs index 16a74eca..bd428dbb 100644 --- a/src/internal/commands/builtin/clone.rs +++ b/src/internal/commands/builtin/clone.rs @@ -11,6 +11,7 @@ use shell_words::join as shell_join; use tokio::process::Command as TokioCommand; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::builtin::UpCommand; use crate::internal::commands::utils::omni_cmd; use crate::internal::commands::Command; @@ -488,11 +489,16 @@ impl BuiltinCommand for CloneCommand { exit(0); } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/config/bootstrap.rs b/src/internal/commands/builtin/config/bootstrap.rs index 7e887931..7c76a680 100644 --- a/src/internal/commands/builtin/config/bootstrap.rs +++ b/src/internal/commands/builtin/config/bootstrap.rs @@ -15,6 +15,7 @@ use serde::Deserialize; use serde::Serialize; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::builtin::TidyGitRepo; use crate::internal::commands::utils::abs_path; use crate::internal::commands::utils::file_auto_complete; @@ -137,16 +138,16 @@ impl BuiltinCommand for ConfigBootstrapCommand { exit(0); } - fn autocompletion(&self) -> bool { - true + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - println!("--organizations"); - println!("--repo-path-format"); - println!("--shell"); - println!("--worktree"); - + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { Ok(()) } } diff --git a/src/internal/commands/builtin/config/check.rs b/src/internal/commands/builtin/config/check.rs index b7d78633..27732677 100644 --- a/src/internal/commands/builtin/config/check.rs +++ b/src/internal/commands/builtin/config/check.rs @@ -6,6 +6,7 @@ use std::process::exit; use itertools::Itertools; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::frompath::PathCommand; use crate::internal::commands::Command; use crate::internal::config::config; @@ -305,12 +306,17 @@ impl BuiltinCommand for ConfigCheckCommand { self.filter_and_print_errors(&error_handler, &args); } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/config/path/switch.rs b/src/internal/commands/builtin/config/path/switch.rs index 2aef540b..d3b5d129 100644 --- a/src/internal/commands/builtin/config/path/switch.rs +++ b/src/internal/commands/builtin/config/path/switch.rs @@ -7,6 +7,7 @@ use indicatif::ProgressBar; use indicatif::ProgressStyle; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::builtin::CloneCommand; use crate::internal::commands::builtin::TidyGitRepo; use crate::internal::commands::builtin::UpCommand; @@ -471,11 +472,16 @@ impl BuiltinCommand for ConfigPathSwitchCommand { exit(0); } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/config/reshim.rs b/src/internal/commands/builtin/config/reshim.rs index 06ad5ab5..7d5c9632 100644 --- a/src/internal/commands/builtin/config/reshim.rs +++ b/src/internal/commands/builtin/config/reshim.rs @@ -1,6 +1,7 @@ use std::process::exit; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::Command; use crate::internal::config::up::utils::reshim; use crate::internal::config::up::utils::PrintProgressHandler; @@ -79,11 +80,16 @@ impl BuiltinCommand for ConfigReshimCommand { exit(0); } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/config/trust.rs b/src/internal/commands/builtin/config/trust.rs index d5e9e2b8..f1f3df0e 100644 --- a/src/internal/commands/builtin/config/trust.rs +++ b/src/internal/commands/builtin/config/trust.rs @@ -1,9 +1,9 @@ use std::collections::BTreeMap; -use std::path::PathBuf; use std::process::exit; use crate::internal::cache::WorkdirsCache; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::Command; use crate::internal::config::parser::ParseArgsValue; use crate::internal::config::CommandSyntax; @@ -14,7 +14,6 @@ use crate::internal::workdir; use crate::internal::workdir::add_trust; use crate::internal::workdir::is_trusted; use crate::internal::workdir::remove_trust; -use crate::internal::ORG_LOADER; use crate::omni_error; use crate::omni_info; @@ -116,6 +115,7 @@ impl BuiltinCommand for ConfigTrustCommand { ) .to_string(), ), + arg_type: SyntaxOptArgType::RepoPath, ..Default::default() }, ], @@ -135,20 +135,8 @@ impl BuiltinCommand for ConfigTrustCommand { .expect("should have args to parse"), ); - let path = if let Some(repo) = &args.workdir { - if let Some(repo_path) = ORG_LOADER.find_repo(repo, true, false, false) { - repo_path - } else { - omni_error!(format!("repository not found: {}", repo)); - exit(1); - } - } else { - PathBuf::from(".") - }; - - let path_str = path.display().to_string(); - - let wd = workdir(path_str.as_str()); + let path_str = args.workdir.as_deref().unwrap_or("."); + let wd = workdir(path_str); let wd_id = match wd.id() { Some(id) => id, None => { @@ -160,7 +148,7 @@ impl BuiltinCommand for ConfigTrustCommand { } }; - let is_trusted = is_trusted(path_str.as_str()); + let is_trusted = is_trusted(path_str); if args.check_status { if is_trusted { @@ -185,7 +173,7 @@ impl BuiltinCommand for ConfigTrustCommand { exit(0); } - if add_trust(path_str.as_str()) { + if add_trust(path_str) { omni_info!( format!("work directory is now {}", "trusted".light_green()), wd_id @@ -212,7 +200,7 @@ impl BuiltinCommand for ConfigTrustCommand { exit(1); } - if remove_trust(path_str.as_str()) { + if remove_trust(path_str) { omni_info!( format!("work directory is now {}", "untrusted".light_red()), wd_id @@ -224,12 +212,16 @@ impl BuiltinCommand for ConfigTrustCommand { } } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - // TODO: autocomplete repositories if first argument - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/help.rs b/src/internal/commands/builtin/help.rs index d44dab15..ec0b827a 100644 --- a/src/internal/commands/builtin/help.rs +++ b/src/internal/commands/builtin/help.rs @@ -6,6 +6,7 @@ use serde::Serialize; use crate::internal::cache::utils as cache_utils; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::command_loader; use crate::internal::commands::void::VoidCommand; use crate::internal::commands::Command; @@ -198,11 +199,17 @@ impl BuiltinCommand for HelpCommand { self.exec_with_exit_code(argv, 0); } - fn autocompletion(&self) -> bool { - true + fn autocompletion(&self) -> CommandAutocompletion { + // TODO: convert to partial so the autocompletion works for options too + CommandAutocompletion::Full } - fn autocomplete(&self, comp_cword: usize, argv: Vec) -> Result<(), ()> { + fn autocomplete( + &self, + comp_cword: usize, + argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { command_loader(".").complete(comp_cword, argv, false) } } diff --git a/src/internal/commands/builtin/hook/base.rs b/src/internal/commands/builtin/hook/base.rs index 18de8374..3a4bc2c5 100644 --- a/src/internal/commands/builtin/hook/base.rs +++ b/src/internal/commands/builtin/hook/base.rs @@ -1,4 +1,5 @@ use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::config::CommandSyntax; use crate::internal::config::SyntaxOptArg; @@ -59,17 +60,16 @@ impl BuiltinCommand for HookCommand { fn exec(&self, _argv: Vec) {} - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, comp_cword: usize, _argv: Vec) -> Result<(), ()> { - if comp_cword == 0 { - println!("env"); - println!("init"); - println!("uuid"); - } - - Ok(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Err(()) } } diff --git a/src/internal/commands/builtin/hook/env.rs b/src/internal/commands/builtin/hook/env.rs index 0bc4d3bb..4e21f9fb 100644 --- a/src/internal/commands/builtin/hook/env.rs +++ b/src/internal/commands/builtin/hook/env.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::process::exit; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::Command; use crate::internal::config::parser::ParseArgsValue; use crate::internal::config::CommandSyntax; @@ -165,11 +166,16 @@ impl BuiltinCommand for HookEnvCommand { } } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/hook/init.rs b/src/internal/commands/builtin/hook/init.rs index 57f07c4e..0679fc00 100644 --- a/src/internal/commands/builtin/hook/init.rs +++ b/src/internal/commands/builtin/hook/init.rs @@ -7,6 +7,7 @@ use tera::Context; use tera::Tera; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::Command; use crate::internal::config::global_config; use crate::internal::config::parser::ParseArgsValue; @@ -304,12 +305,17 @@ impl BuiltinCommand for HookInitCommand { exit(0); } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/hook/uuid.rs b/src/internal/commands/builtin/hook/uuid.rs index ae87ffb8..3252bdf1 100644 --- a/src/internal/commands/builtin/hook/uuid.rs +++ b/src/internal/commands/builtin/hook/uuid.rs @@ -3,6 +3,7 @@ use std::process::exit; use uuid::Uuid; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::config::CommandSyntax; #[derive(Debug, Clone)] @@ -54,11 +55,16 @@ impl BuiltinCommand for HookUuidCommand { exit(0); } - fn autocompletion(&self) -> bool { - false + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - Err(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Ok(()) } } diff --git a/src/internal/commands/builtin/scope.rs b/src/internal/commands/builtin/scope.rs index b3a70489..cbd18eb8 100644 --- a/src/internal/commands/builtin/scope.rs +++ b/src/internal/commands/builtin/scope.rs @@ -2,14 +2,15 @@ use std::collections::BTreeMap; use std::process::exit; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::command_loader; +use crate::internal::commands::utils::path_auto_complete; use crate::internal::commands::Command; use crate::internal::config::parser::ParseArgsValue; use crate::internal::config::CommandSyntax; use crate::internal::config::SyntaxOptArg; use crate::internal::config::SyntaxOptArgType; use crate::internal::env::current_dir; -use crate::internal::env::Shell; use crate::internal::git::ORG_LOADER; use crate::internal::user_interface::StringColor; use crate::omni_error; @@ -62,51 +63,6 @@ impl ScopeCommand { Self {} } - fn autocomplete_repo(&self, repo: String) -> Result<(), ()> { - // Figure out if this is a path, so we can avoid the expensive repository search - let path_only = repo.starts_with('/') - || repo.starts_with('.') - || repo.starts_with("~/") - || repo == "~" - || repo == "-"; - - // Print all the completion related to path completion - let (list_dir, strip_path_prefix) = if let Some(slash) = repo.rfind('/') { - (repo[..slash].to_string(), false) - } else { - (".".to_string(), true) - }; - if let Ok(files) = std::fs::read_dir(&list_dir) { - for path in files.flatten() { - if path.path().is_dir() { - let path_obj = path.path(); - let path = if strip_path_prefix { - path_obj.strip_prefix(&list_dir).unwrap() - } else { - path_obj.as_path() - }; - let path_str = path.to_str().unwrap(); - - if !path_str.starts_with(repo.as_str()) { - continue; - } - - println!("{}/", path.display()); - } - } - } - - // Get all the repositories per org - if !path_only { - let add_space = if Shell::current().is_fish() { " " } else { "" }; - for match_repo in ORG_LOADER.complete(&repo) { - println!("{}{}", match_repo, add_space); - } - } - - Ok(()) - } - fn switch_scope( &self, repo: &str, @@ -221,6 +177,7 @@ impl BuiltinCommand for ScopeCommand { .to_string(), ), required: true, + arg_type: SyntaxOptArgType::RepoPath, ..Default::default() }, SyntaxOptArg { @@ -280,19 +237,24 @@ impl BuiltinCommand for ScopeCommand { exit(1); } - fn autocompletion(&self) -> bool { - true + fn autocompletion(&self) -> CommandAutocompletion { + // TODO: convert to partial so the autocompletion work for options too + CommandAutocompletion::Full } - fn autocomplete(&self, comp_cword: usize, argv: Vec) -> Result<(), ()> { + fn autocomplete( + &self, + comp_cword: usize, + argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { match comp_cword.cmp(&0) { std::cmp::Ordering::Equal => { - let repo = if !argv.is_empty() { - argv[0].clone() - } else { - "".to_string() - }; - self.autocomplete_repo(repo) + let repo = argv.first().map_or("", String::as_str); + path_auto_complete(repo, true, false) + .iter() + .for_each(|s| println!("{}", s)); + Ok(()) } std::cmp::Ordering::Greater => { if argv.is_empty() { diff --git a/src/internal/commands/builtin/status.rs b/src/internal/commands/builtin/status.rs index 626408dd..83f30997 100644 --- a/src/internal/commands/builtin/status.rs +++ b/src/internal/commands/builtin/status.rs @@ -7,6 +7,7 @@ use regex::Regex; use crate::internal::cache::utils::Empty; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::path::omnipath_entries; use crate::internal::commands::Command; use crate::internal::config::config; @@ -390,24 +391,16 @@ impl BuiltinCommand for StatusCommand { exit(0); } - fn autocompletion(&self) -> bool { - true + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, argv: Vec) -> Result<(), ()> { - for arg in &[ - "--shell-integration", - "--config", - "--config-files", - "--worktree", - "--orgs", - "--path", - ] { - if !argv.contains(&arg.to_string()) { - println!("{}", arg); - } - } - + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { Ok(()) } } diff --git a/src/internal/commands/builtin/tidy.rs b/src/internal/commands/builtin/tidy.rs index 4b740403..b3c31d4b 100644 --- a/src/internal/commands/builtin/tidy.rs +++ b/src/internal/commands/builtin/tidy.rs @@ -11,6 +11,8 @@ use indicatif::ProgressStyle; use walkdir::WalkDir; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; +use crate::internal::commands::builtin::UpCommand; use crate::internal::commands::path::global_omnipath_entries; use crate::internal::commands::utils::abs_path; use crate::internal::commands::Command; @@ -248,7 +250,7 @@ impl BuiltinCommand for TidyCommand { ) .to_string(), ), - arg_type: SyntaxOptArgType::Array(Box::new(SyntaxOptArgType::String)), + arg_type: SyntaxOptArgType::Array(Box::new(SyntaxOptArgType::DirPath)), ..Default::default() }, SyntaxOptArg { @@ -503,19 +505,30 @@ impl BuiltinCommand for TidyCommand { exit(0); } - fn autocompletion(&self) -> bool { - true + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Partial } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - // TODO: if the last parameter before completion is `search-path`, - // TODO: we should autocomplete with the file system paths - println!("--search-path"); - println!("-y"); - println!("--yes"); - println!("--up-all"); - println!("-h"); - println!("--help"); + fn autocomplete( + &self, + comp_cword: usize, + argv: Vec, + parameter: Option, + ) -> Result<(), ()> { + if parameter.unwrap_or_default() == "up args" { + // Get the position of the `--` argument + let up_args_start = match argv.iter().position(|arg| arg == "--") { + Some(pos) => pos + 1, + None => return Ok(()), + }; + + // Compute the new comp_cword for the up command + let comp_cword = comp_cword - up_args_start; + + // Let's use the autocompletion of the up command + let up_command = UpCommand::new_command(); + return up_command.autocomplete(comp_cword, argv[up_args_start..].to_vec()); + } Ok(()) } diff --git a/src/internal/commands/builtin/up.rs b/src/internal/commands/builtin/up.rs index 09f8d503..d132f02e 100644 --- a/src/internal/commands/builtin/up.rs +++ b/src/internal/commands/builtin/up.rs @@ -21,6 +21,7 @@ use crate::internal::cache::utils::Empty; use crate::internal::cache::PromptsCache; use crate::internal::cache::WorkdirsCache; use crate::internal::commands::base::BuiltinCommand; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::Command; use crate::internal::config::config; use crate::internal::config::flush_config; @@ -1658,23 +1659,17 @@ impl BuiltinCommand for UpCommand { ); } - fn autocompletion(&self) -> bool { - true + fn autocompletion(&self) -> CommandAutocompletion { + CommandAutocompletion::Null } - fn autocomplete(&self, _comp_cword: usize, _argv: Vec) -> Result<(), ()> { - println!("--bootstrap"); - println!("--clone-suggested"); - println!("--fail-on-upgrade"); - println!("--no-cache"); - println!("--prompt"); - println!("--prompt-all"); - println!("--trust"); - println!("--update-repository"); - println!("--update-user-config"); - println!("--upgrade"); - - Ok(()) + fn autocomplete( + &self, + _comp_cword: usize, + _argv: Vec, + _parameter: Option, + ) -> Result<(), ()> { + Err(()) } } diff --git a/src/internal/commands/frompath.rs b/src/internal/commands/frompath.rs index 3b9742cb..245c533c 100644 --- a/src/internal/commands/frompath.rs +++ b/src/internal/commands/frompath.rs @@ -14,6 +14,7 @@ use serde::Serialize; use serde_yaml::Value as YamlValue; use walkdir::WalkDir; +use crate::internal::commands::base::CommandAutocompletion; use crate::internal::commands::path::omnipath; use crate::internal::commands::utils::str_to_bool; use crate::internal::commands::utils::SplitOnSeparators; @@ -285,17 +286,25 @@ impl PathCommand { panic!("Something went wrong: {:?}", err); } - pub fn autocompletion(&self) -> bool { + pub fn autocompletion(&self) -> CommandAutocompletion { self.file_details() .map(|details| details.autocompletion) - .unwrap_or(false) + .unwrap_or(CommandAutocompletion::Null) } - pub fn autocomplete(&self, comp_cword: usize, argv: Vec) -> Result<(), ()> { + pub fn autocomplete( + &self, + comp_cword: usize, + argv: Vec, + parameter: Option, + ) -> Result<(), ()> { let mut command = ProcessCommand::new(self.source.clone()); command.arg("--complete"); command.args(argv); command.env("COMP_CWORD", comp_cword.to_string()); + if let Some(parameter) = parameter { + command.env("OMNI_COMP_VALUE_OF", parameter); + } match command.output() { Ok(output) => { @@ -331,7 +340,7 @@ impl PathCommand { pub struct PathCommandFileDetails { category: Option>, help: Option, - autocompletion: bool, + autocompletion: CommandAutocompletion, syntax: Option, tags: BTreeMap, sync_update: bool, @@ -358,21 +367,27 @@ impl<'de> PathCommandFileDetails { let mut value = YamlValue::deserialize(deserializer)?; if let YamlValue::Mapping(ref mut map) = value { - // Deserialize the booleans - let autocompletion = map + // Deserialize the autocompletion field, which can be either a + // boolean or a string representing a boolean or 'partial' + // The result is stored as a CommandAutocompletion enum + // where 'true' is Full, 'partial' is Partial, and 'false' is Null + let autocompletion: CommandAutocompletion = map .remove(YamlValue::String("autocompletion".to_string())) - .is_some_and(|v| match bool::deserialize(v.clone()) { - Ok(b) => b, - Err(_err) => { + .map_or(CommandAutocompletion::Null, |v| match v { + YamlValue::Bool(b) => CommandAutocompletion::from(b), + YamlValue::String(s) => CommandAutocompletion::from(s), + _ => { error_handler .with_key("autocompletion") - .with_expected("boolean") + .with_expected(vec!["boolean", "string"]) .with_actual(v.to_owned()) .error(ConfigErrorKind::InvalidValueType); - false + CommandAutocompletion::Null } }); + + // Deserialize the booleans let sync_update = map .remove(YamlValue::String("sync_update".to_string())) .is_some_and(|v| match bool::deserialize(v.clone()) { @@ -867,7 +882,7 @@ impl PathCommandFileDetails { reader: &mut R, error_handler: &ConfigErrorHandler, ) -> Option { - let mut autocompletion = false; + let mut autocompletion = CommandAutocompletion::Null; let mut sync_update = false; let mut argparser = false; let mut category: Option> = None; @@ -962,7 +977,8 @@ impl PathCommandFileDetails { ("autocompletion", None, value) => { key_tracker.handle_seen_key(&key, lineno, false, error_handler); autocompletion = match str_to_bool(&value) { - Some(b) => b, + Some(b) => CommandAutocompletion::from(b), + None if value.to_lowercase() == "partial" => CommandAutocompletion::Partial, None => { error_handler .with_lineno(lineno) @@ -971,7 +987,7 @@ impl PathCommandFileDetails { .with_expected("boolean") .error(ConfigErrorKind::MetadataHeaderInvalidValueType); - false + CommandAutocompletion::Null } }; } @@ -1218,7 +1234,10 @@ mod tests { let details = details.unwrap(); assert_eq!(details.category, None); assert_eq!(details.help, None); - assert!(!details.autocompletion); + assert!(matches!( + details.autocompletion, + CommandAutocompletion::Null + )); assert_eq!(details.syntax, None); assert!(!details.sync_update); } @@ -1365,7 +1384,27 @@ mod tests { assert!(details.is_some()); let details = details.unwrap(); - assert!(details.autocompletion); + assert!(matches!( + details.autocompletion, + CommandAutocompletion::Full + )); + } + + #[test] + fn autocompletion_partial() { + let mut reader = BufReader::new("# autocompletion: partial\n".as_bytes()); + let details = PathCommandFileDetails::from_source_file_header( + &mut reader, + &ConfigErrorHandler::noop(), + ); + + assert!(details.is_some()); + let details = details.unwrap(); + + assert!(matches!( + details.autocompletion, + CommandAutocompletion::Partial + )); } #[test] @@ -1379,7 +1418,10 @@ mod tests { assert!(details.is_some()); let details = details.unwrap(); - assert!(!details.autocompletion); + assert!(matches!( + details.autocompletion, + CommandAutocompletion::Null + )); } #[test] @@ -2678,7 +2720,10 @@ mod tests { Some(vec!["test cat".to_string(), "more cat".to_string()]) ); assert_eq!(details.help, Some("test help\nmore help".to_string())); - assert!(details.autocompletion); + assert!(matches!( + details.autocompletion, + CommandAutocompletion::Full + )); assert!(details.argparser); assert!(!details.sync_update); assert!(details.syntax.is_some(), "Syntax is not present"); diff --git a/src/internal/commands/loader.rs b/src/internal/commands/loader.rs index 758b8729..ffc6a08d 100644 --- a/src/internal/commands/loader.rs +++ b/src/internal/commands/loader.rs @@ -252,7 +252,7 @@ impl CommandLoader { .into_iter() .find(|x| x.match_level == match_pos as f32 && x.match_name.len() == match_pos) { - if parent_command.command.autocompletion() { + if parent_command.command.autocompletion().into() { // Set the environment variables that we need to pass to the // subcommand let new_comp_cword = comp_cword - parent_command.match_level as usize; @@ -299,7 +299,7 @@ impl CommandLoader { } } - if matched_command.command.autocompletion() { + if matched_command.command.autocompletion().into() { // Set the environment variables that we need to pass to the // subcommand let new_comp_cword = comp_cword - matched_command.match_level as usize; diff --git a/src/internal/commands/utils.rs b/src/internal/commands/utils.rs index 49ef2e48..fc71870e 100644 --- a/src/internal/commands/utils.rs +++ b/src/internal/commands/utils.rs @@ -1,16 +1,18 @@ +use std::collections::BTreeSet; use std::fs::OpenOptions; use std::io; use std::io::Write; use std::path::Path; use std::path::PathBuf; -use normalize_path::NormalizePath; use path_clean::PathClean; use requestty::question::completions; use requestty::question::Completions; use crate::internal::env::omni_cmd_file; use crate::internal::env::user_home; +use crate::internal::env::Shell; +use crate::internal::ORG_LOADER; pub fn split_name(string: &str, split_on: &str) -> Vec { string.split(split_on).map(|s| s.to_string()).collect() @@ -18,7 +20,7 @@ pub fn split_name(string: &str, split_on: &str) -> Vec { pub fn abs_or_rel_path(path: &str) -> String { let current_dir = std::env::current_dir().unwrap(); - let path = std::path::PathBuf::from(&path).normalize(); + let path = std::path::PathBuf::from(&path).clean(); let path = if path.is_absolute() { path } else { @@ -53,7 +55,7 @@ pub fn abs_path_from_path(path: T, frompath: Option) -> PathBuf where T: AsRef, { - let path = path.as_ref().normalize(); + let path = path.as_ref(); let absolute_path = if path.is_absolute() { path.to_path_buf() @@ -167,6 +169,84 @@ pub fn file_auto_complete(p: String) -> Completions { files } +pub fn path_auto_complete( + value: &str, + include_repositories: bool, + include_files: bool, +) -> BTreeSet { + // Figure out if this is a path, so we can avoid the + // expensive repository search + let path_only = value.starts_with('/') + || value.starts_with('.') + || value.starts_with("~/") + || value == "~" + || value == "-"; + + // To store the completions we find + let mut completions = BTreeSet::new(); + + // Print all the completion related to path completion + let (list_dir, strip_path_prefix, replace_home_prefix) = if value == "~" { + (user_home(), false, true) + } else if let Some(value) = value.strip_prefix("~/") { + if let Some(slash) = value.rfind('/') { + let abspath = format!("{}/{}", user_home(), &value[..(slash + 1)]); + (abspath, false, true) + } else { + (user_home(), false, true) + } + } else if let Some(slash) = value.rfind('/') { + (value[..(slash + 1)].to_string(), false, false) + } else { + (".".to_string(), true, false) + }; + + if let Ok(files) = std::fs::read_dir(&list_dir) { + for path in files.flatten() { + let is_dir = path.path().is_dir(); + if !is_dir && !include_files { + continue; + } + + let path_buf; + let path_obj = path.path(); + let path = if strip_path_prefix { + path_obj.strip_prefix(&list_dir).unwrap() + } else if replace_home_prefix { + if let Ok(path_obj) = path_obj.strip_prefix(user_home()) { + path_buf = PathBuf::from("~").join(path_obj); + path_buf.as_path() + } else { + path_obj.as_path() + } + } else { + path_obj.as_path() + }; + + let path_str = path.to_string_lossy().to_string(); + if !path_str.starts_with(value) { + continue; + } + + completions.insert(if is_dir { + format!("{}/", path_str) + } else { + path_str + }); + } + } + + // Get all the repositories per org that match the value + if include_repositories && !path_only { + let add_space = if Shell::current().is_fish() { " " } else { "" }; + for match_value in ORG_LOADER.complete(value) { + completions.insert(format!("{}{}", match_value, add_space)); + } + } + + completions +} + pub fn str_to_bool(value: &str) -> Option { match value.to_lowercase().as_str() { "true" | "1" | "on" | "enable" | "enabled" | "yes" | "y" => Some(true), diff --git a/src/internal/config/parser/command_definition.rs b/src/internal/config/parser/command_definition.rs index 459efc64..6010762c 100644 --- a/src/internal/config/parser/command_definition.rs +++ b/src/internal/config/parser/command_definition.rs @@ -8,6 +8,7 @@ use serde::Deserialize; use serde::Serialize; use crate::internal::cache::utils as cache_utils; +use crate::internal::commands::utils::abs_path; use crate::internal::commands::utils::str_to_bool; use crate::internal::commands::HelpCommand; use crate::internal::config::parser::ConfigErrorHandler; @@ -18,6 +19,7 @@ use crate::internal::config::ConfigScope; use crate::internal::config::ConfigSource; use crate::internal::config::ConfigValue; use crate::internal::user_interface::colors::StringColor; +use crate::internal::ORG_LOADER; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CommandDefinition { @@ -725,11 +727,11 @@ impl CommandSyntax { let mut args = BTreeMap::new(); for param in &self.parameters { - param.add_to_args(&mut args, &matches, None); + param.add_to_args(&mut args, &matches, None)?; } for group in &self.groups { - group.add_to_args(&mut args, &matches, &self.parameters); + group.add_to_args(&mut args, &matches, &self.parameters)?; } Ok(args) @@ -1170,6 +1172,10 @@ impl SyntaxOptArg { self.last_arg_double_hyphen } + pub fn is_repeatable(&self) -> bool { + self.arg_type().is_array() || matches!(self.arg_type(), SyntaxOptArgType::Counter) + } + pub fn takes_value(&self) -> bool { if matches!( self.arg_type(), @@ -1611,6 +1617,9 @@ impl SyntaxOptArg { // Set the action, i.e. how the values are stored when the selfeter is used match &self.arg_type() { SyntaxOptArgType::String + | SyntaxOptArgType::DirPath + | SyntaxOptArgType::FilePath + | SyntaxOptArgType::RepoPath | SyntaxOptArgType::Integer | SyntaxOptArgType::Float | SyntaxOptArgType::Boolean @@ -1657,7 +1666,7 @@ impl SyntaxOptArg { args: &mut BTreeMap, matches: &clap::ArgMatches, override_dest: Option, - ) { + ) -> Result<(), ParseArgsErrorKind> { let dest = self.dest(); // has_occurrences is when an argument can take multiple values @@ -1667,10 +1676,16 @@ impl SyntaxOptArg { .is_some_and(|num_values| num_values.is_many()); // has_multi is when an argument can be called multiple times - let has_multi = self.arg_type().is_array(); + let arg_type = self.arg_type(); + let has_multi = arg_type.is_array(); - match &self.arg_type().terminal_type() { - SyntaxOptArgType::String | SyntaxOptArgType::Enum(_) => { + let terminal_type = &arg_type.terminal_type(); + match terminal_type { + SyntaxOptArgType::String + | SyntaxOptArgType::DirPath + | SyntaxOptArgType::FilePath + | SyntaxOptArgType::RepoPath + | SyntaxOptArgType::Enum(_) => { extract_value_to_typed::( matches, &dest, @@ -1680,7 +1695,14 @@ impl SyntaxOptArg { has_occurrences, has_multi, self.group_occurrences, - ); + match terminal_type { + SyntaxOptArgType::DirPath | SyntaxOptArgType::FilePath => { + Some(transform_path) + } + SyntaxOptArgType::RepoPath => Some(transform_repo_path), + _ => None, + }, + )?; } SyntaxOptArgType::Integer => { extract_value_to_typed::( @@ -1692,7 +1714,8 @@ impl SyntaxOptArg { has_occurrences, has_multi, self.group_occurrences, - ); + None, + )?; } SyntaxOptArgType::Counter => { extract_value_to_typed::( @@ -1704,7 +1727,8 @@ impl SyntaxOptArg { has_occurrences, has_multi, self.group_occurrences, - ); + None, + )?; } SyntaxOptArgType::Float => { extract_value_to_typed::( @@ -1716,7 +1740,8 @@ impl SyntaxOptArg { has_occurrences, has_multi, self.group_occurrences, - ); + None, + )?; } SyntaxOptArgType::Boolean | SyntaxOptArgType::Flag => { let default = Some( @@ -1733,13 +1758,51 @@ impl SyntaxOptArg { has_occurrences, has_multi, self.group_occurrences, - ); + None, + )?; } SyntaxOptArgType::Array(_) => unreachable!("array type should be handled differently"), - }; + } + + Ok(()) } } +/// If the provided value is a path, we want to return the +/// absolute path no matter what was passed (relative, absolute, ~, etc.) +fn transform_path(value: Option) -> Result, ParseArgsErrorKind> { + let value = match value { + Some(value) => value, + None => return Ok(None), + }; + + let path = abs_path(&value); + Ok(Some(path.to_string_lossy().to_string())) +} + +/// If the provided value is a path to a repository, we want to return the +/// absolute path no matter what was passed (relative, absolute, ~, etc.) +fn transform_repo_path(value: Option) -> Result, ParseArgsErrorKind> { + let value = match value { + Some(value) => value, + None => return Ok(None), + }; + + if let Ok(path) = std::fs::canonicalize(&value) { + return Ok(Some(path.to_string_lossy().to_string())); + } + + let only_worktree = false; + if let Some(path) = ORG_LOADER.find_repo(&value, only_worktree, false, true) { + return Ok(Some(path.to_string_lossy().to_string())); + } + + Err(ParseArgsErrorKind::InvalidValue(format!( + "invalid repository path: {}", + value + ))) +} + trait ParserExtractType { type BaseType; type Output; @@ -1814,6 +1877,13 @@ impl + Clone + FromStr + Send + Sync + 'static> ParserEx } } +/// A function that can transform a value into another value of the same type +type TransformFn = fn(Option) -> Result, ParseArgsErrorKind>; + +/// Extracts a value from the matches and inserts it into the args map +/// The value is extracted based on the type of the argument and the number of values +/// The value is then transformed if a transform function is provided +/// The value is then inserted into the args map with the correct destination #[allow(clippy::too_many_arguments)] #[inline] fn extract_value_to_typed( @@ -1825,8 +1895,9 @@ fn extract_value_to_typed( has_occurrences: bool, has_multi: bool, group_occurrences: bool, -) where - // W: ParserExtractType, + transform_fn: Option>, +) -> Result<(), ParseArgsErrorKind> +where T: Into + Clone + Send + Sync + FromStr + 'static, ParseArgsValue: From>, ParseArgsValue: From>>, @@ -1836,16 +1907,44 @@ fn extract_value_to_typed( let value = if has_occurrences && has_multi && group_occurrences { let value = >> as ParserExtractType>::extract(matches, dest, default); + let value = if let Some(transform_fn) = transform_fn { + value + .into_iter() + .map(|values| { + values + .into_iter() + .map(transform_fn) + .collect::>() + }) + .collect::>()? + } else { + value + }; ParseArgsValue::from(value) } else if has_multi || has_occurrences { let value = > as ParserExtractType>::extract(matches, dest, default); + let value = if let Some(transform_fn) = transform_fn { + value + .into_iter() + .map(transform_fn) + .collect::>()? + } else { + value + }; ParseArgsValue::from(value) } else { let value = as ParserExtractType>::extract(matches, dest, default); + let value = if let Some(transform_fn) = transform_fn { + transform_fn(value)? + } else { + value + }; ParseArgsValue::from(value) }; args.insert(arg_dest, value); + + Ok(()) } pub fn parse_arg_name(arg_name: &str) -> (Vec, SyntaxOptArgType, Vec, bool) { @@ -2125,7 +2224,7 @@ impl SyntaxOptArgNumValues { } } - fn max(&self) -> Option { + pub fn max(&self) -> Option { match self { Self::Any => None, Self::Exactly(value) => Some(*value), @@ -2134,6 +2233,16 @@ impl SyntaxOptArgNumValues { Self::Between(_min, max) => Some(*max), } } + + pub fn min(&self) -> Option { + match self { + Self::Any => None, + Self::Exactly(value) => Some(*value), + Self::AtLeast(min) => Some(*min), + Self::AtMost(_max) => None, + Self::Between(min, _max) => Some(*min), + } + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] @@ -2141,6 +2250,12 @@ pub enum SyntaxOptArgType { #[default] #[serde(rename = "str", alias = "string")] String, + #[serde(rename = "dir", alias = "dirpath", alias = "path")] + DirPath, + #[serde(rename = "file", alias = "filepath")] + FilePath, + #[serde(rename = "repopath")] + RepoPath, #[serde(rename = "int", alias = "integer")] Integer, #[serde(rename = "float")] @@ -2171,6 +2286,9 @@ impl SyntaxOptArgType { pub fn to_str(&self) -> &'static str { match self { Self::String => "string", + Self::DirPath => "dir", + Self::FilePath => "file", + Self::RepoPath => "repopath", Self::Integer => "int", Self::Float => "float", Self::Boolean => "bool", @@ -2179,6 +2297,9 @@ impl SyntaxOptArgType { Self::Enum(_) => "enum", Self::Array(inner) => match **inner { Self::String => "array/str", + Self::DirPath => "array/dir", + Self::FilePath => "array/file", + Self::RepoPath => "array/repopath", Self::Integer => "array/int", Self::Float => "array/float", Self::Boolean => "array/bool", @@ -2259,6 +2380,9 @@ impl SyntaxOptArgType { "flag" => Self::Flag, "count" | "counter" => Self::Counter, "str" | "string" => Self::String, + "dir" | "path" | "dirpath" => Self::DirPath, + "file" | "filepath" => Self::FilePath, + "repopath" => Self::RepoPath, "enum" => Self::Enum(vec![]), _ => { // If the string is in format array/enum(xx, yy, zz) or enum(xx, yy, zz) or (xx, yy, zz) @@ -2281,7 +2405,17 @@ impl SyntaxOptArgType { Self::Enum(values) } else { error_handler - .with_expected("int, float, bool, flag, count, str, enum or array/") + .with_expected(vec![ + "int", + "float", + "bool", + "flag", + "count", + "str", + "path", + "enum", + "array/", + ]) .with_actual(value) .error(ConfigErrorKind::InvalidValue); @@ -2565,20 +2699,20 @@ impl SyntaxGroup { args: &mut BTreeMap, matches: &clap::ArgMatches, parameters: &[SyntaxOptArg], - ) { + ) -> Result<(), ParseArgsErrorKind> { let dest = self.dest(); let param_id = match matches.get_one::(&dest) { Some(param_id) => param_id.to_string(), - None => return, + None => return Ok(()), }; let param = match parameters.iter().find(|param| *param.dest() == param_id) { Some(param) => param, - None => return, + None => return Ok(()), }; - param.add_to_args(args, matches, Some(dest.clone())); + param.add_to_args(args, matches, Some(dest.clone())) } } diff --git a/src/internal/config/parser/errors.rs b/src/internal/config/parser/errors.rs index 3c568a42..fdd262ea 100644 --- a/src/internal/config/parser/errors.rs +++ b/src/internal/config/parser/errors.rs @@ -823,14 +823,15 @@ impl ConfigErrorKind { pub enum ParseArgsErrorKind { ParserBuildError(String), ArgumentParsingError(clap::Error), + InvalidValue(String), } impl ParseArgsErrorKind { #[cfg(test)] pub fn simple(&self) -> String { match self { - ParseArgsErrorKind::ParserBuildError(e) => e.clone(), - ParseArgsErrorKind::ArgumentParsingError(e) => { + Self::ParserBuildError(e) => e.clone(), + Self::ArgumentParsingError(e) => { // Return the first block until the first empty line let err_str = e .to_string() @@ -842,6 +843,7 @@ impl ParseArgsErrorKind { let err_str = err_str.trim_start_matches("error: "); err_str.to_string() } + Self::InvalidValue(e) => e.clone(), } } } @@ -849,13 +851,11 @@ impl ParseArgsErrorKind { impl PartialEq for ParseArgsErrorKind { fn eq(&self, other: &Self) -> bool { match (self, other) { - (ParseArgsErrorKind::ParserBuildError(a), ParseArgsErrorKind::ParserBuildError(b)) => { - a == b + (Self::ParserBuildError(a), Self::ParserBuildError(b)) => a == b, + (Self::ArgumentParsingError(a), Self::ArgumentParsingError(b)) => { + a.to_string() == b.to_string() } - ( - ParseArgsErrorKind::ArgumentParsingError(a), - ParseArgsErrorKind::ArgumentParsingError(b), - ) => a.to_string() == b.to_string(), + (Self::InvalidValue(a), Self::InvalidValue(b)) => a == b, _ => false, } } @@ -864,8 +864,9 @@ impl PartialEq for ParseArgsErrorKind { impl fmt::Display for ParseArgsErrorKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - ParseArgsErrorKind::ParserBuildError(e) => write!(f, "{}", e), - ParseArgsErrorKind::ArgumentParsingError(e) => write!(f, "{}", e), + Self::ParserBuildError(e) => write!(f, "{}", e), + Self::ArgumentParsingError(e) => write!(f, "{}", e), + Self::InvalidValue(e) => write!(f, "{}", e), } } }