diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-02 11:08:57 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-02 11:08:57 +0900 |
| commit | 145b4c0a7fadf6dfd8c03b22c735695037930b0a (patch) | |
| tree | cac7c4e265b2f6aeaefdf0431e2c22e54b26b8ef /crates/shirabe/src/command/base_command.rs | |
| parent | 2592062434eeb5fe814dbca953d5fd23458bc135 (diff) | |
| download | php-shirabe-145b4c0a7fadf6dfd8c03b22c735695037930b0a.tar.gz php-shirabe-145b4c0a7fadf6dfd8c03b22c735695037930b0a.tar.zst php-shirabe-145b4c0a7fadf6dfd8c03b22c735695037930b0a.zip | |
feat(console-input): port the suggested-values backport onto InputArgument/InputOption
Composer backports symfony/console 6.1's $suggestedValues parameter in
Composer\Console\Input\{InputArgument,InputOption}; the Rust newtypes had
dropped it. PHP closures are bound to the command ($this), but a command
cannot capture a handle to itself while configure() runs inside new(), so
the closure receives the bound command as an explicit `this` argument at
call time instead.
- add SuggestedValues (list | this-taking closure) and wire it through
InputArgument::new5 / InputOption::new6 and their complete() methods
- track Composer-typed definition entries by name in BaseCommandData side
maps, standing in for PHP's instanceof checks (set_definition converts
entries to the Symfony types for storage)
- add base_command_complete, the BaseCommand::complete dispatch shared by
every Composer command
- introduce BaseCommand::base_command_data and make command_data a default
method on top of it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/command/base_command.rs')
| -rw-r--r-- | crates/shirabe/src/command/base_command.rs | 95 |
1 files changed, 88 insertions, 7 deletions
diff --git a/crates/shirabe/src/command/base_command.rs b/crates/shirabe/src/command/base_command.rs index e05478f4..7e8a3f5a 100644 --- a/crates/shirabe/src/command/base_command.rs +++ b/crates/shirabe/src/command/base_command.rs @@ -42,6 +42,15 @@ pub struct BaseCommandData { inner: CommandData, pub(crate) composer: std::cell::RefCell<Option<PartialComposerHandle>>, pub(crate) io: std::cell::RefCell<Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>>, + /// The definition entries that were registered through the Composer-typed + /// InputArgument/InputOption (which carry the suggested-values backport). PHP checks + /// `$definition->getArgument($name) instanceof Composer\Console\Input\InputArgument`; in + /// this port `set_definition` converts the entries to the Symfony types for storage, so + /// the Composer-typedness is tracked by name in these side maps instead. + pub(crate) composer_arguments: + std::cell::RefCell<IndexMap<String, std::rc::Rc<crate::console::input::InputArgument>>>, + pub(crate) composer_options: + std::cell::RefCell<IndexMap<String, std::rc::Rc<crate::console::input::InputOption>>>, } impl BaseCommandData { @@ -50,6 +59,8 @@ impl BaseCommandData { inner: CommandData::new(name), composer: std::cell::RefCell::new(None), io: std::cell::RefCell::new(None), + composer_arguments: std::cell::RefCell::new(IndexMap::new()), + composer_options: std::cell::RefCell::new(IndexMap::new()), } } @@ -65,17 +76,40 @@ impl BaseCommandData { /// `Command` trait. The Symfony state methods are inherited from the [`Command`] supertrait; /// only the Composer-specific behavior and the Composer-typed definition builders live here. pub trait BaseCommand: Command { - /// Access to the embedded Symfony command state. Each command returns - /// `self.base_command_data.command_data()`; this lets the Composer-typed definition - /// builders below forward to `CommandData`'s Symfony-typed entry points. `CommandData` is - /// interior-mutable, so a shared reference is enough. - fn command_data(&self) -> &CommandData; + /// Access to the embedded base-command state. Each command returns + /// `&self.base_command_data`; the definition builders and the completion dispatch below + /// go through it. The state is interior-mutable, so a shared reference is enough. + fn base_command_data(&self) -> &BaseCommandData; + + /// Access to the embedded Symfony command state, forwarding to `CommandData`'s + /// Symfony-typed entry points. + fn command_data(&self) -> &CommandData { + self.base_command_data().command_data() + } /// Sets the definition from Composer-typed argument/option instances. fn set_definition(&self, definition: &[InputDefinitionItem]) -> &Self where Self: Sized, { + let data = self.base_command_data(); + // PHP replaces the whole definition, so the Composer-typed side maps follow suit. + data.composer_arguments.borrow_mut().clear(); + data.composer_options.borrow_mut().clear(); + for item in definition { + match item { + InputDefinitionItem::Argument(argument) => { + data.composer_arguments + .borrow_mut() + .insert(argument.get_name(), argument.clone()); + } + InputDefinitionItem::Option(option) => { + data.composer_options + .borrow_mut() + .insert(option.get_name(), option.clone()); + } + } + } let items = definition .iter() .map(|item| item.to_definition_item()) @@ -255,8 +289,8 @@ impl Command for BaseCommandData { } impl BaseCommand for BaseCommandData { - fn command_data(&self) -> &CommandData { - &self.inner + fn base_command_data(&self) -> &BaseCommandData { + self } fn require_composer( @@ -736,6 +770,53 @@ fn apply_application_defaults( (disable_plugins, disable_scripts) } +/// \Composer\Command\BaseCommand::complete — the suggested-values backport dispatch +/// (BaseCommand.php:198-215). Shared via a free function because Rust has no inheritance; +/// each command's `Command::complete` forwards here. +pub fn base_command_complete( + cmd: &dyn BaseCommand, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, +) -> anyhow::Result<()> { + use shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput; + + // PHP: (string) $input->getCompletionName() + let name = input.get_completion_name().unwrap_or_default(); + // The definition Ref and the side-map Refs must be dropped before the suggestion closure + // runs: the closure may re-enter the command (require_composer etc.). + if CompletionInput::TYPE_OPTION_VALUE == input.get_completion_type() + && cmd.get_definition().has_option(&name) + { + // PHP: ($option = $definition->getOption($name)) instanceof InputOption — the + // Composer-typedness is tracked in the side map (see BaseCommandData). + let option = cmd + .base_command_data() + .composer_options + .borrow() + .get(&name) + .cloned(); + if let Some(option) = option { + return option.complete(cmd, input, suggestions); + } + } else if CompletionInput::TYPE_ARGUMENT_VALUE == input.get_completion_type() + && cmd + .get_definition() + .has_argument(&PhpMixed::String(name.clone())) + { + let argument = cmd + .base_command_data() + .composer_arguments + .borrow() + .get(&name) + .cloned(); + if let Some(argument) = argument { + return argument.complete(cmd, input, suggestions); + } + } + // PHP: parent::complete($input, $suggestions) — Symfony 5.4's Command::complete is a no-op. + Ok(()) +} + /// \Composer\Command\BaseCommand::initialize — runs for every Composer command after the /// input is bound. Shared via a free function because Rust has no inheritance; each command's /// `Command::initialize` forwards here so the leaf's `is_self_update_command()` override (and |
