aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/completion
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-12 01:01:35 +0900
committernsfisis <nsfisis@gmail.com>2026-06-12 01:01:43 +0900
commit1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch)
tree9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/completion
parentddf0a624145b618c05c2ee47414545b99b685f37 (diff)
downloadphp-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/completion')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs288
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/completion_suggestions.rs66
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/mod.rs2
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/output/bash_completion_output.rs23
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/output/completion_output_interface.rs7
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/output/mod.rs5
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/suggestion.rs17
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(&current_argument_name)
+ {
+ break;
+ }
+ argument_name = Some(current_argument_name.clone());
+
+ let argument_value = self.inner.inner.arguments[&current_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())
}
}