diff options
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/completion')
7 files changed, 388 insertions, 20 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs b/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs index 1203ec8..32d83ce 100644 --- a/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs +++ b/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs @@ -1,24 +1,296 @@ +use indexmap::IndexMap; +use shirabe_php_shim::PhpMixed; + +use crate::symfony::console::input::argv_input::ArgvInput; +use crate::symfony::console::input::input_definition::InputDefinition; +use crate::symfony::console::input::input_option::InputOption; + +/// An input specialized for shell completion. +/// +/// This input allows unfinished option names or values and exposes what kind of +/// completion is expected. #[derive(Debug)] -pub struct CompletionInput; +pub struct CompletionInput { + inner: ArgvInput, + tokens: Vec<String>, + current_index: i64, + completion_type: String, + completion_name: Option<String>, + completion_value: String, +} impl CompletionInput { + pub const TYPE_ARGUMENT_VALUE: &'static str = "argument_value"; + pub const TYPE_OPTION_VALUE: &'static str = "option_value"; + pub const TYPE_OPTION_NAME: &'static str = "option_name"; + pub const TYPE_NONE: &'static str = "none"; + + /// Converts a terminal string into tokens. + /// + /// This is required for shell completions without COMP_WORDS support. + pub fn from_string(input_str: &str, current_index: i64) -> anyhow::Result<Self> { + let tokens = shirabe_php_shim::preg_match_all( + "/(?<=^|\\s)(['\"]?)(.+?)(?<!\\\\)\\1(?=$|\\s)/", + input_str, + ); + + Self::from_tokens(tokens[0].clone(), current_index) + } + + /// Create an input based on an COMP_WORDS token list. + /// + /// `tokens` is the set of split tokens (e.g. COMP_WORDS or argv), + /// `current_index` the index of the cursor (e.g. COMP_CWORD). + pub fn from_tokens(tokens: Vec<String>, current_index: i64) -> anyhow::Result<Self> { + let mut input = Self { + inner: ArgvInput::new(Some(tokens.clone()), None)?, + tokens: vec![], + current_index: 0, + completion_type: String::new(), + completion_name: None, + completion_value: String::new(), + }; + input.tokens = tokens; + input.current_index = current_index; + + Ok(input) + } + + pub fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> { + self.inner.bind(definition)?; + + let relevant_token = self.get_relevant_token(); + if "-" == &relevant_token[0..1] { + // the current token is an input option: complete either option name or option value + let parts = shirabe_php_shim::explode_with_limit("=", &relevant_token, 2); + let option_token = parts.first().cloned().unwrap_or_default(); + let option_value = parts.get(1).cloned().unwrap_or_default(); + + let option = self.get_option_from_token(&option_token); + if option.is_none() && !self.is_cursor_free() { + self.completion_type = Self::TYPE_OPTION_NAME.to_string(); + self.completion_value = relevant_token; + + return Ok(()); + } + + if let Some(option) = &option { + if option.accept_value() { + self.completion_type = Self::TYPE_OPTION_VALUE.to_string(); + self.completion_name = Some(option.get_name().to_string()); + self.completion_value = if !option_value.is_empty() { + option_value + } else if !shirabe_php_shim::str_starts_with(&option_token, "--") { + shirabe_php_shim::substr(&option_token, 2, None) + } else { + String::new() + }; + + return Ok(()); + } + } + } + + let previous_token = self.tokens[(self.current_index - 1) as usize].clone(); + if "-" == &previous_token[0..1] && "" != shirabe_php_shim::trim(&previous_token, Some("-")) + { + // check if previous option accepted a value + let previous_option = self.get_option_from_token(&previous_token); + if let Some(previous_option) = &previous_option { + if previous_option.accept_value() { + self.completion_type = Self::TYPE_OPTION_VALUE.to_string(); + self.completion_name = Some(previous_option.get_name().to_string()); + self.completion_value = relevant_token; + + return Ok(()); + } + } + } + + // complete argument value + self.completion_type = Self::TYPE_ARGUMENT_VALUE.to_string(); + + let mut argument_name: Option<String> = None; + let argument_names: Vec<String> = self + .inner + .inner + .definition + .get_arguments() + .keys() + .cloned() + .collect(); + for current_argument_name in argument_names { + if !self + .inner + .inner + .arguments + .contains_key(¤t_argument_name) + { + break; + } + argument_name = Some(current_argument_name.clone()); + + let argument_value = self.inner.inner.arguments[¤t_argument_name].clone(); + self.completion_name = Some(current_argument_name.clone()); + if let PhpMixed::Array(argument_value) = &argument_value { + self.completion_value = if !argument_value.is_empty() { + argument_value[shirabe_php_shim::array_key_last(argument_value)].to_string() + } else { + // null + String::new() + }; + } else { + self.completion_value = argument_value.to_string(); + } + } + + if self.current_index >= self.tokens.len() as i64 { + let argument_name = argument_name.unwrap_or_default(); + if !self.inner.inner.arguments.contains_key(&argument_name) + || self + .inner + .inner + .definition + .get_argument(&PhpMixed::String(argument_name.clone())) + .unwrap() + .is_array() + { + self.completion_name = Some(argument_name); + self.completion_value = String::new(); + } else { + // we've reached the end + self.completion_type = Self::TYPE_NONE.to_string(); + self.completion_name = None; + self.completion_value = String::new(); + } + } + + Ok(()) + } + + /// Returns the type of completion required. + /// + /// TYPE_ARGUMENT_VALUE when completing the value of an input argument + /// TYPE_OPTION_VALUE when completing the value of an input option + /// TYPE_OPTION_NAME when completing the name of an input option + /// TYPE_NONE when nothing should be completed pub fn get_completion_type(&self) -> String { - todo!() + self.completion_type.clone() } + /// The name of the input option or argument when completing a value. + /// + /// Returns null when completing an option name. pub fn get_completion_name(&self) -> Option<String> { - todo!() + self.completion_name.clone() } + /// The value already typed by the user (or empty string). pub fn get_completion_value(&self) -> String { - todo!() + self.completion_value.clone() + } + + pub fn must_suggest_option_values_for(&self, option_name: &str) -> bool { + Self::TYPE_OPTION_VALUE == self.get_completion_type() + && Some(option_name.to_string()) == self.get_completion_name() + } + + pub fn must_suggest_argument_values_for(&self, argument_name: &str) -> bool { + Self::TYPE_ARGUMENT_VALUE == self.get_completion_type() + && Some(argument_name.to_string()) == self.get_completion_name() + } + + pub fn get_first_argument(&self) -> Option<String> { + self.inner.get_first_argument() + } + + pub(crate) fn parse_token(&mut self, token: &str, parse_options: bool) -> bool { + match self.inner.parse_token(token, parse_options) { + Ok(value) => return value, + Err(_e) => { + // suppress errors, completed input is almost never valid + } + } + + parse_options + } + + fn get_option_from_token(&self, option_token: &str) -> Option<std::rc::Rc<InputOption>> { + let option_name = shirabe_php_shim::ltrim(option_token, Some("-")); + if option_name.is_empty() { + return None; + } + + if "-" + == option_token + .chars() + .nth(1) + .map(|c| c.to_string()) + .unwrap_or_else(|| " ".to_string()) + { + // long option name + return if self.inner.inner.definition.has_option(&option_name) { + self.inner.inner.definition.get_option(&option_name).ok() + } else { + None + }; + } + + // short option name + let first = &option_name[0..1]; + if self.inner.inner.definition.has_shortcut(first) { + self.inner + .inner + .definition + .get_option_for_shortcut(first) + .ok() + } else { + None + } } - pub fn must_suggest_option_values_for(&self, _name: &str) -> bool { - todo!() + /// The token of the cursor, or the last token if the cursor is at the end of the input. + fn get_relevant_token(&self) -> String { + let index = if self.is_cursor_free() { + self.current_index - 1 + } else { + self.current_index + }; + self.tokens[index as usize].clone() + } + + /// Whether the cursor is "free" (i.e. at the end of the input preceded by a space). + fn is_cursor_free(&self) -> bool { + let nr_of_tokens = self.tokens.len() as i64; + if self.current_index > nr_of_tokens { + // LogicException: recoverable usage as a "convenient fatal error"; panic. + panic!("Current index is invalid, it must be the number of input tokens or one more."); + } + + self.current_index >= nr_of_tokens } +} + +impl std::fmt::Display for CompletionInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut str = String::new(); + let mut last_i: i64 = 0; + for (i, token) in self.tokens.iter().enumerate() { + let i = i as i64; + last_i = i; + str += token; + + if self.current_index == i { + str += "|"; + } + + str += " "; + } + + if self.current_index > last_i { + str += "|"; + } - pub fn must_suggest_argument_values_for(&self, _name: &str) -> bool { - todo!() + write!(f, "{}", shirabe_php_shim::rtrim(&str, None)) } } diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/completion_suggestions.rs b/crates/shirabe-external-packages/src/symfony/console/completion/completion_suggestions.rs index 7dae1fc..fd1a317 100644 --- a/crates/shirabe-external-packages/src/symfony/console/completion/completion_suggestions.rs +++ b/crates/shirabe-external-packages/src/symfony/console/completion/completion_suggestions.rs @@ -1,14 +1,68 @@ -use shirabe_php_shim::PhpMixed; +use crate::symfony::console::completion::suggestion::Suggestion; +use crate::symfony::console::input::input_option::InputOption; +/// PHP union type `string|Suggestion` used by `suggestValue`/`suggestValues`. #[derive(Debug)] -pub struct CompletionSuggestions; +pub enum StringOrSuggestion { + String(String), + Suggestion(Suggestion), +} + +/// Stores all completion suggestions for the current input. +#[derive(Debug)] +pub struct CompletionSuggestions { + value_suggestions: Vec<Suggestion>, + option_suggestions: Vec<InputOption>, +} impl CompletionSuggestions { - pub fn suggest_values(&mut self, _values: Vec<PhpMixed>) { - todo!() + pub fn new() -> Self { + Self { + value_suggestions: vec![], + option_suggestions: vec![], + } + } + + /// Add a suggested value for an input option or argument. + pub fn suggest_value(&mut self, value: StringOrSuggestion) -> &mut Self { + self.value_suggestions.push(match value { + StringOrSuggestion::Suggestion(value) => value, + StringOrSuggestion::String(value) => Suggestion::new(value), + }); + + self + } + + /// Add multiple suggested values at once for an input option or argument. + pub fn suggest_values(&mut self, values: Vec<StringOrSuggestion>) -> &mut Self { + for value in values { + self.suggest_value(value); + } + + self + } + + /// Add a suggestion for an input option name. + pub fn suggest_option(&mut self, option: InputOption) -> &mut Self { + self.option_suggestions.push(option); + + self + } + + /// Add multiple suggestions for input option names at once. + pub fn suggest_options(&mut self, options: Vec<InputOption>) -> &mut Self { + for option in options { + self.suggest_option(option); + } + + self + } + + pub fn get_option_suggestions(&self) -> &Vec<InputOption> { + &self.option_suggestions } - pub fn suggest_value(&mut self, _value: PhpMixed) { - todo!() + pub fn get_value_suggestions(&self) -> &Vec<Suggestion> { + &self.value_suggestions } } diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/mod.rs b/crates/shirabe-external-packages/src/symfony/console/completion/mod.rs index 0d1c115..cb7f1d1 100644 --- a/crates/shirabe-external-packages/src/symfony/console/completion/mod.rs +++ b/crates/shirabe-external-packages/src/symfony/console/completion/mod.rs @@ -1,7 +1,9 @@ pub mod completion_input; pub mod completion_suggestions; +pub mod output; pub mod suggestion; pub use completion_input::*; pub use completion_suggestions::*; +pub use output::*; pub use suggestion::*; diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/output/bash_completion_output.rs b/crates/shirabe-external-packages/src/symfony/console/completion/output/bash_completion_output.rs new file mode 100644 index 0000000..b781f51 --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/completion/output/bash_completion_output.rs @@ -0,0 +1,23 @@ +use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions; +use crate::symfony::console::completion::output::completion_output_interface::CompletionOutputInterface; +use crate::symfony::console::output::output_interface::OutputInterface; + +#[derive(Debug)] +pub struct BashCompletionOutput; + +impl CompletionOutputInterface for BashCompletionOutput { + fn write(&self, suggestions: &CompletionSuggestions, output: &dyn OutputInterface) { + let mut values: Vec<String> = suggestions + .get_value_suggestions() + .iter() + .map(|suggestion| suggestion.get_value()) + .collect(); + for option in suggestions.get_option_suggestions() { + values.push(format!("--{}", option.get_name())); + if option.is_negatable() { + values.push(format!("--no-{}", option.get_name())); + } + } + output.writeln(&[values.join("\n")], 0); + } +} diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/output/completion_output_interface.rs b/crates/shirabe-external-packages/src/symfony/console/completion/output/completion_output_interface.rs new file mode 100644 index 0000000..e92adf3 --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/completion/output/completion_output_interface.rs @@ -0,0 +1,7 @@ +use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions; +use crate::symfony::console::output::output_interface::OutputInterface; + +/// Transforms the `CompletionSuggestions` object into output readable by the shell completion. +pub trait CompletionOutputInterface: std::fmt::Debug { + fn write(&self, suggestions: &CompletionSuggestions, output: &dyn OutputInterface); +} diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/output/mod.rs b/crates/shirabe-external-packages/src/symfony/console/completion/output/mod.rs new file mode 100644 index 0000000..6bb0a2e --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/completion/output/mod.rs @@ -0,0 +1,5 @@ +pub mod bash_completion_output; +pub mod completion_output_interface; + +pub use bash_completion_output::*; +pub use completion_output_interface::*; diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/suggestion.rs b/crates/shirabe-external-packages/src/symfony/console/completion/suggestion.rs index 666e474..5c45b92 100644 --- a/crates/shirabe-external-packages/src/symfony/console/completion/suggestion.rs +++ b/crates/shirabe-external-packages/src/symfony/console/completion/suggestion.rs @@ -1,16 +1,21 @@ +/// Represents a single suggested value. #[derive(Debug)] -pub struct Suggestion; +pub struct Suggestion { + value: String, +} impl Suggestion { - pub fn new(_value: String, _description: Option<String>) -> Self { - todo!() + pub fn new(value: String) -> Self { + Self { value } } pub fn get_value(&self) -> String { - todo!() + self.value.clone() } +} - pub fn get_description(&self) -> String { - todo!() +impl std::fmt::Display for Suggestion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.get_value()) } } |
