diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php index f49c484717d5..e7773035ddf1 100644 --- a/src/Composer/Command/CompletionTrait.php +++ b/src/Composer/Command/CompletionTrait.php @@ -13,7 +13,9 @@ namespace Composer\Command; use Composer\Composer; +use Composer\Package\BasePackage; use Composer\Package\PackageInterface; +use Composer\Pcre\Preg; use Composer\Repository\CompositeRepository; use Composer\Repository\InstalledRepository; use Composer\Repository\PlatformRepository; @@ -46,9 +48,9 @@ private function suggestPreferInstall(): array /** * Suggest package names from installed. */ - private function suggestInstalledPackage(): \Closure + private function suggestInstalledPackage(bool $includePlatformPackages = false): \Closure { - return function (): array { + return function (CompletionInput $input) use ($includePlatformPackages): array { $composer = $this->requireComposer(); $installedRepos = [new RootPackageRepository(clone $composer->getPackage())]; @@ -59,46 +61,113 @@ private function suggestInstalledPackage(): \Closure $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); } + $platformHint = []; + if ($includePlatformPackages) { + if ($locker->isLocked()) { + $platformRepo = new PlatformRepository(array(), $locker->getPlatformOverrides()); + } else { + $platformRepo = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); + } + if ($input->getCompletionValue() === '') { + // to reduce noise, when no text is yet entered we list only two entries for ext- and lib- prefixes + $hintsToFind = ['ext-' => 0, 'lib-' => 0, 'php' => 99, 'composer' => 99]; + foreach ($platformRepo->getPackages() as $pkg) { + foreach ($hintsToFind as $hintPrefix => $hintCount) { + if (str_starts_with($pkg->getName(), $hintPrefix)) { + if ($hintCount === 0 || $hintCount >= 99) { + $platformHint[] = $pkg->getName(); + $hintsToFind[$hintPrefix]++; + } elseif ($hintCount === 1) { + unset($hintsToFind[$hintPrefix]); + $platformHint[] = substr($pkg->getName(), 0, max(strlen($pkg->getName()) - 3, strlen($hintPrefix) + 1)).'...'; + } + continue 2; + } + } + } + } else { + $installedRepos[] = $platformRepo; + } + } + $installedRepo = new InstalledRepository($installedRepos); - return array_map(function (PackageInterface $package) { - return $package->getName(); - }, $installedRepo->getPackages()); + return array_merge( + array_map(function (PackageInterface $package) { + return $package->getName(); + }, $installedRepo->getPackages()), + $platformHint + ); }; } /** * Suggest package names available on all configured repositories. - * @todo rework to list packages from cache */ - private function suggestAvailablePackage(): \Closure + private function suggestAvailablePackage(int $max = 99): \Closure { - return function (CompletionInput $input) { + return function (CompletionInput $input) use ($max): array { + if ($max < 1) { + return []; + } + $composer = $this->requireComposer(); $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); - $packages = $repos->search('^' . preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); + $results = []; + if (!str_contains($input->getCompletionValue(), '/')) { + $results = $repos->search('^' . preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_VENDOR); + $vendors = true; + } + + // if we get a single vendor, we expand it into its contents already + if (\count($results) <= 1) { + $results = $repos->search('^'.preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); + $vendors = false; + } - return array_column(array_slice($packages, 0, 150), 'name'); + $results = array_column(array_slice($results, 0, $max), 'name'); + if ($vendors) { + $results = array_map(function (string $name): string { + return $name.'/'; + }, $results); + } + + return $results; }; } /** * Suggest package names available on all configured repositories or - * ext- packages from the ones available on the currently-running PHP + * platform packages from the ones available on the currently-running PHP */ - private function suggestAvailablePackageOrExtension(): \Closure + private function suggestAvailablePackageInclPlatform(): \Closure { - return function (CompletionInput $input) { - if (!str_starts_with($input->getCompletionValue(), 'ext-')) { - return $this->suggestAvailablePackage()($input); + return function (CompletionInput $input): array { + if (Preg::isMatch('{^(ext|lib|php)(-|$)|^com}', $input->getCompletionValue())) { + $matches = $this->suggestPlatformPackage()($input); + } else { + $matches = []; } + return array_merge($matches, $this->suggestAvailablePackage(99 - \count($matches))($input)); + }; + } + + /** + * Suggest platform packages from the ones available on the currently-running PHP + */ + private function suggestPlatformPackage(): \Closure + { + return function (CompletionInput $input): array { $repos = new PlatformRepository([], $this->requireComposer()->getConfig()->get('platform') ?? []); - return array_map(function (PackageInterface $package) { + $pattern = BasePackage::packageNameToRegexp($input->getCompletionValue().'*'); + return array_filter(array_map(function (PackageInterface $package) { return $package->getName(); - }, $repos->getPackages()); + }, $repos->getPackages()), function (string $name) use ($pattern): bool { + return Preg::isMatch($pattern, $name); + }); }; } } diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index 53ba7115f804..1b353807aad4 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -36,7 +36,7 @@ protected function configure(): void ->setAliases(array('why')) ->setDescription('Shows which packages cause the given package to be installed.') ->setDefinition(array( - new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestInstalledPackage()), + new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestInstalledPackage(true)), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), )) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 6677da1019ef..29382606fbf3 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -59,8 +59,8 @@ protected function configure() new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), - new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackage()), - new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackage()), + new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), + new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'), diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 86a11193f246..6db97660242d 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -43,7 +43,7 @@ protected function configure() ->setName('remove') ->setDescription('Removes a package from the require or require-dev.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.', null, $this->suggestInstalledPackage()), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.', null, $this->suggestInstalledPackage(true)), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index d8fead4f91cc..3feb98e6c77d 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -68,7 +68,7 @@ protected function configure() ->setName('require') ->setDescription('Adds required packages to your composer.json and installs them.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageOrExtension()), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index dcce4f16a68a..d96a409fe1e7 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -40,6 +40,7 @@ use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; use Composer\Util\PackageInfo; +use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Console\Input\InputArgument; @@ -75,7 +76,7 @@ protected function configure() ->setAliases(array('info')) ->setDescription('Shows information about packages.') ->setDefinition(array( - new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestInstalledPackage()), + new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestPackageBasedOnMode()), new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'), new InputOption('all', null, InputOption::VALUE_NONE, 'List all packages'), new InputOption('locked', null, InputOption::VALUE_NONE, 'List all locked packages'), @@ -109,6 +110,21 @@ protected function configure() ; } + protected function suggestPackageBasedOnMode(): \Closure + { + return function (CompletionInput $input) { + if ($input->getOption('available') || $input->getOption('all')) { + return $this->suggestAvailablePackageInclPlatform()($input); + } + + if ($input->getOption('platform')) { + return $this->suggestPlatformPackage()($input); + } + + return $this->suggestInstalledPackage()($input); + }; + } + protected function execute(InputInterface $input, OutputInterface $output) { $this->versionParser = new VersionParser;