From aa9228710e497f3947d4f7309128d08e0bf9d257 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 16 May 2026 00:35:24 +0900 Subject: feat(port): port CompletionTrait.php --- crates/shirabe/src/command/completion_trait.rs | 220 +++++++++++++++++++++++++ 1 file changed, 220 insertions(+) (limited to 'crates/shirabe/src/command/completion_trait.rs') diff --git a/crates/shirabe/src/command/completion_trait.rs b/crates/shirabe/src/command/completion_trait.rs index d58acd1..2594407 100644 --- a/crates/shirabe/src/command/completion_trait.rs +++ b/crates/shirabe/src/command/completion_trait.rs @@ -1 +1,221 @@ //! ref: composer/src/Composer/Command/CompletionTrait.php + +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_external_packages::symfony::component::console::completion::completion_input::CompletionInput; +use shirabe_php_shim::preg_quote; +use crate::composer::Composer; +use crate::package::base_package::BasePackage; +use crate::package::package_interface::PackageInterface; +use crate::repository::composite_repository::CompositeRepository; +use crate::repository::installed_repository::InstalledRepository; +use crate::repository::platform_repository::PlatformRepository; +use crate::repository::repository_interface::RepositoryInterface; +use crate::repository::root_package_repository::RootPackageRepository; + +pub trait CompletionTrait { + fn require_composer(&self, disable_plugins: Option, disable_scripts: Option) -> Composer; + + fn suggest_prefer_install(&self) -> Vec { + vec!["dist".to_string(), "source".to_string(), "auto".to_string()] + } + + fn suggest_root_requirement(&self) -> Box Vec + '_> { + Box::new(move |_input: &CompletionInput| -> Vec { + let composer = self.require_composer(None, None); + + let requires: Vec = composer.get_package().get_requires().keys().cloned().collect(); + let dev_requires: Vec = composer.get_package().get_dev_requires().keys().cloned().collect(); + [requires, dev_requires].concat() + }) + } + + fn suggest_installed_package(&self, include_root_package: bool, include_platform_packages: bool) -> Box Vec + '_> { + Box::new(move |input: &CompletionInput| -> Vec { + let composer = self.require_composer(None, None); + let mut installed_repos: Vec> = Vec::new(); + + if include_root_package { + installed_repos.push(Box::new(RootPackageRepository::new(composer.get_package().clone()))); + } + + let locker = composer.get_locker(); + if locker.is_locked() { + installed_repos.push(Box::new(locker.get_locked_repository(true))); + } else { + installed_repos.push(Box::new(composer.get_repository_manager().get_local_repository())); + } + + let mut platform_hint: Vec = Vec::new(); + if include_platform_packages { + let platform_repo = if locker.is_locked() { + PlatformRepository::new(vec![], locker.get_platform_overrides()) + } else { + PlatformRepository::new(vec![], composer.get_config().get("platform")) + }; + if input.get_completion_value() == "" { + // to reduce noise, when no text is yet entered we list only two entries for ext- and lib- prefixes + let mut hints_to_find: indexmap::IndexMap = indexmap::IndexMap::new(); + hints_to_find.insert("ext-".to_string(), 0); + hints_to_find.insert("lib-".to_string(), 0); + hints_to_find.insert("php".to_string(), 99); + hints_to_find.insert("composer".to_string(), 99); + + 'pkg_loop: for pkg in platform_repo.get_packages() { + for (hint_prefix, hint_count) in hints_to_find.iter_mut() { + if pkg.get_name().starts_with(hint_prefix.as_str()) { + if *hint_count == 0 || *hint_count >= 99 { + platform_hint.push(pkg.get_name().to_string()); + *hint_count += 1; + } else if *hint_count == 1 { + hints_to_find.remove(hint_prefix); + platform_hint.push(format!( + "{}...", + &pkg.get_name()[..pkg.get_name().len().saturating_sub(3).max(hint_prefix.len() + 1)] + )); + } + continue 'pkg_loop; + } + } + } + } else { + installed_repos.push(Box::new(platform_repo)); + } + } + + let installed_repo = InstalledRepository::new(installed_repos); + + let mut result: Vec = installed_repo.get_packages().iter() + .map(|package| package.get_name().to_string()) + .collect(); + result.extend(platform_hint); + result + }) + } + + fn suggest_installed_package_types(&self, include_root_package: bool) -> Box Vec + '_> { + Box::new(move |_input: &CompletionInput| -> Vec { + let composer = self.require_composer(None, None); + let mut installed_repos: Vec> = Vec::new(); + + if include_root_package { + installed_repos.push(Box::new(RootPackageRepository::new(composer.get_package().clone()))); + } + + let locker = composer.get_locker(); + if locker.is_locked() { + installed_repos.push(Box::new(locker.get_locked_repository(true))); + } else { + installed_repos.push(Box::new(composer.get_repository_manager().get_local_repository())); + } + + let installed_repo = InstalledRepository::new(installed_repos); + + let mut types: Vec = installed_repo.get_packages().iter() + .map(|package| package.get_type().to_string()) + .collect(); + types.sort(); + types.dedup(); + types + }) + } + + fn suggest_available_package(&self, max: i64) -> Box Vec + '_> { + Box::new(move |input: &CompletionInput| -> Vec { + if max < 1 { + return Vec::new(); + } + + let composer = self.require_composer(None, None); + let repos = CompositeRepository::new(composer.get_repository_manager().get_repositories()); + + let mut results: Vec; + let mut show_vendors = false; + if !input.get_completion_value().contains('/') { + let search_results = repos.search( + format!("^{}", preg_quote(input.get_completion_value(), None)), + RepositoryInterface::SEARCH_VENDOR, + None, + ); + results = search_results.iter().map(|r| r.name.clone()).collect(); + show_vendors = true; + } else { + results = Vec::new(); + } + + // if we get a single vendor, we expand it into its contents already + if results.len() <= 1 { + let search_results = repos.search( + format!("^{}", preg_quote(input.get_completion_value(), None)), + RepositoryInterface::SEARCH_NAME, + None, + ); + results = search_results.iter().map(|r| r.name.clone()).collect(); + show_vendors = false; + } + + if show_vendors { + let mut results: Vec = results.into_iter() + .map(|name| format!("{}/", name)) + .collect(); + + // sort shorter results first to avoid auto-expanding the completion to a longer string than needed + results.sort_by(|a, b| { + let len_a = a.len(); + let len_b = b.len(); + if len_a == len_b { + a.cmp(b) + } else { + len_a.cmp(&len_b) + } + }); + + let mut pinned: Vec = Vec::new(); + + // ensure if the input is an exact match that it is always in the result set + let completion_input = format!("{}/", input.get_completion_value()); + if let Some(exact_index) = results.iter().position(|x| x == &completion_input) { + pinned.push(completion_input); + results.remove(exact_index); + } + + let take_count = (max as usize).saturating_sub(pinned.len()); + let mut final_results = pinned; + final_results.extend(results.into_iter().take(take_count)); + return final_results; + } + + results.into_iter().take(max as usize).collect() + }) + } + + fn suggest_available_package_incl_platform(&self) -> Box Vec + '_> { + Box::new(move |input: &CompletionInput| -> Vec { + let matches = if Preg::is_match(r"{^(ext|lib|php)(-|$)|^com}", input.get_completion_value()) { + self.suggest_platform_package()(input) + } else { + Vec::new() + }; + + let max = 99i64 - matches.len() as i64; + let mut result = matches; + result.extend(self.suggest_available_package(max)(input)); + result + }) + } + + fn suggest_platform_package(&self) -> Box Vec + '_> { + Box::new(move |input: &CompletionInput| -> Vec { + let repos = PlatformRepository::new( + vec![], + self.require_composer(None, None).get_config().get("platform"), + ); + + let pattern = BasePackage::package_name_to_regexp(&format!("{}*", input.get_completion_value())); + + repos.get_packages().iter() + .map(|package| package.get_name().to_string()) + .filter(|name| Preg::is_match(&pattern, name)) + .collect() + }) + } +} -- cgit v1.3.1