From 1e44f5723e4c0e0903d00a61f254901e612fe5e1 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 12 Jun 2026 01:01:35 +0900 Subject: feat(symfony-console): port Symfony Console and make the workspace compile Co-Authored-By: Claude Opus 4.8 --- .../console/command/dump_completion_command.rs | 337 +++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs (limited to 'crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs') diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs new file mode 100644 index 0000000..51287c7 --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs @@ -0,0 +1,337 @@ +use crate::symfony::console::command::command::{BaseCommand, Command}; +use crate::symfony::console::completion::completion_input::CompletionInput; +use crate::symfony::console::completion::completion_suggestions::{ + CompletionSuggestions, StringOrSuggestion, +}; +use crate::symfony::console::input::input_argument::InputArgument; +use crate::symfony::console::input::input_interface::InputInterface; +use crate::symfony::console::input::input_option::InputOption; +use crate::symfony::console::output::output_interface::{self, OutputInterface}; +use shirabe_php_shim::PhpMixed; +use std::cell::RefCell; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; + +/// Dumps the completion script for the current shell. +#[derive(Debug)] +pub struct DumpCompletionCommand { + inner: BaseCommand, +} + +impl Deref for DumpCompletionCommand { + type Target = BaseCommand; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for DumpCompletionCommand { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl DumpCompletionCommand { + pub const DEFAULT_NAME: &'static str = "completion"; + pub const DEFAULT_DESCRIPTION: &'static str = "Dump the shell completion script"; + + pub fn complete_impl(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { + if input.must_suggest_argument_values_for("shell") { + suggestions.suggest_values( + self.get_supported_shells() + .into_iter() + .map(StringOrSuggestion::String) + .collect(), + ); + } + } + + fn configure(&mut self) -> anyhow::Result<()> { + let full_command = shirabe_php_shim::server_php_self(); + let command_name = shirabe_php_shim::basename(&full_command); + // @realpath($fullCommand) ?: $fullCommand + let full_command = match shirabe_php_shim::realpath(&full_command) { + Some(p) if !p.is_empty() => p, + _ => full_command, + }; + + self.inner.set_help(&format!( + "The %command.name% command dumps the shell completion script required\n\ + to use shell autocompletion (currently only bash completion is supported).\n\ + \n\ + Static installation\n\ + -------------------\n\ + \n\ + Dump the script to a global completion file and restart your shell:\n\ + \n\ + \x20\x20\x20\x20%command.full_name% bash | sudo tee /etc/bash_completion.d/{command_name}\n\ + \n\ + Or dump the script to a local file and source it:\n\ + \n\ + \x20\x20\x20\x20%command.full_name% bash > completion.sh\n\ + \n\ + \x20\x20\x20\x20# source the file whenever you use the project\n\ + \x20\x20\x20\x20source completion.sh\n\ + \n\ + \x20\x20\x20\x20# or add this line at the end of your \"~/.bashrc\" file:\n\ + \x20\x20\x20\x20source /path/to/completion.sh\n\ + \n\ + Dynamic installation\n\ + --------------------\n\ + \n\ + Add this to the end of your shell configuration file (e.g. \"~/.bashrc\"):\n\ + \n\ + \x20\x20\x20\x20eval \"$({full_command} completion bash)\"", + )) + .add_argument( + "shell", + Some(InputArgument::OPTIONAL), + "The shell type (e.g. \"bash\"), the value of the \"$SHELL\" env var will be used if this is not given", + PhpMixed::Null, + )? + .add_option( + "debug", + PhpMixed::Null, + Some(InputOption::VALUE_NONE), + "Tail the completion debug log", + PhpMixed::Null, + )?; + + Ok(()) + } + + fn execute( + &mut self, + input: &mut dyn InputInterface, + output: &mut dyn OutputInterface, + ) -> anyhow::Result { + let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]); + + if input.get_option("debug")?.to_bool() { + self.tail_debug_log(&command_name, output); + + return Ok(0); + } + + let shell = match input.get_argument("shell")?.as_string() { + Some(s) => s.to_string(), + None => Self::guess_shell(), + }; + let completion_file = format!( + "{}/../Resources/completion.{}", + shirabe_php_shim::dir(), + shell + ); + if !shirabe_php_shim::file_exists(&completion_file) { + let supported_shells = self.get_supported_shells(); + + // TODO: PHP does `$output instanceof ConsoleOutputInterface ? $output->getErrorOutput() + // : $output`. There is no way to test trait membership through `&dyn OutputInterface` + // here; OutputInterface would need a downcast hook (Phase C). Writing to `output`. + if !shell.is_empty() { + output.writeln( + &[format!( + "Detected shell \"{}\", which is not supported by Symfony shell completion (supported shells: \"{}\").", + shell, + supported_shells.join("\", \"") + )], + output_interface::OUTPUT_NORMAL, + ); + } else { + output.writeln( + &[format!( + "Shell not detected, Symfony shell completion only supports \"{}\").", + supported_shells.join("\", \"") + )], + output_interface::OUTPUT_NORMAL, + ); + } + + return Ok(2); + } + + let application = self.get_application().unwrap(); + let version = application.borrow().get_version(); + output.write( + &[shirabe_php_shim::str_replace_arrays( + &[ + "{{ COMMAND_NAME }}".to_string(), + "{{ VERSION }}".to_string(), + ], + &[command_name, version], + &shirabe_php_shim::file_get_contents(&completion_file).unwrap_or_default(), + )], + false, + output_interface::OUTPUT_NORMAL, + ); + + Ok(0) + } + + fn guess_shell() -> String { + shirabe_php_shim::basename(&shirabe_php_shim::server_shell().unwrap_or_default()) + } + + fn tail_debug_log(&self, command_name: &str, _output: &dyn OutputInterface) { + let debug_file = format!( + "{}/sf_{}.log", + shirabe_php_shim::sys_get_temp_dir(), + command_name + ); + if !shirabe_php_shim::file_exists(&debug_file) { + shirabe_php_shim::touch(&debug_file); + } + // TODO: Process::run() expects a `'static` callback, but the PHP closure captures + // `$output` by reference and writes each line to it. Bridging the borrowed `output` + // into a `'static` callback requires shared ownership of the output (Phase C). + todo!() + } + + fn get_supported_shells(&self) -> Vec { + let mut shells = vec![]; + + // foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) + for file in shirabe_php_shim::directory_iterator(&format!( + "{}/../Resources/", + shirabe_php_shim::dir() + )) { + if shirabe_php_shim::str_starts_with(&file.get_basename(), "completion.") + && file.is_file() + { + shells.push(file.get_extension()); + } + } + + shells + } +} + +impl Command for DumpCompletionCommand { + fn configure(&mut self) { + let _ = DumpCompletionCommand::configure(self); + } + + fn run( + &mut self, + input: &mut dyn InputInterface, + output: &mut dyn OutputInterface, + ) -> anyhow::Result { + self.inner.run(input, output) + } + + fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { + self.complete_impl(input, suggestions); + } + + fn is_enabled(&self) -> bool { + self.inner.is_enabled() + } + + fn set_application( + &mut self, + application: Option>>, + ) { + self.inner.set_application(application); + } + + fn get_application( + &self, + ) -> Option>> { + self.inner.get_application() + } + + fn set_helper_set( + &mut self, + helper_set: Rc>, + ) { + self.inner.set_helper_set(helper_set); + } + + fn get_helper_set( + &self, + ) -> Option>> { + self.inner.get_helper_set() + } + + fn merge_application_definition(&mut self, merge_args: bool) { + self.inner.merge_application_definition(merge_args); + } + + fn get_definition(&self) -> &crate::symfony::console::input::input_definition::InputDefinition { + self.inner.get_definition() + } + + fn get_native_definition( + &self, + ) -> &crate::symfony::console::input::input_definition::InputDefinition { + self.inner.get_native_definition() + } + + fn set_name(&mut self, name: &str) -> anyhow::Result<()> { + self.inner.set_name(name)?; + Ok(()) + } + + fn get_name(&self) -> Option { + self.inner.get_name() + } + + fn set_hidden(&mut self, hidden: bool) { + self.inner.set_hidden(hidden); + } + + fn is_hidden(&self) -> bool { + self.inner.is_hidden() + } + + fn set_description(&mut self, description: &str) { + self.inner.set_description(description); + } + + fn get_description(&self) -> String { + self.inner.get_description() + } + + fn set_help(&mut self, help: &str) { + self.inner.set_help(help); + } + + fn get_help(&self) -> String { + self.inner.get_help() + } + + fn get_processed_help(&self) -> String { + self.inner.get_processed_help() + } + + fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<()> { + self.inner.set_aliases(aliases)?; + Ok(()) + } + + fn get_aliases(&self) -> Vec { + self.inner.get_aliases() + } + + fn get_synopsis(&mut self, short: bool) -> String { + self.inner.get_synopsis(short) + } + + fn get_usages(&self) -> Vec { + self.inner.get_usages() + } + + fn get_helper( + &self, + name: &str, + ) -> anyhow::Result< + Result, + > { + self.inner.get_helper(name) + } + + fn ignore_validation_errors(&mut self) { + self.inner.ignore_validation_errors(); + } +} -- cgit v1.3.1