From f8462565e2f843095c13a4736b489a42a6cc2494 Mon Sep 17 00:00:00 2001 From: Jitendra Adhikari Date: Tue, 9 Nov 2021 16:26:48 +0700 Subject: [PATCH 01/10] Add zsh completion template resource --- .../Console/Resources/completion.zsh | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/Symfony/Component/Console/Resources/completion.zsh diff --git a/src/Symfony/Component/Console/Resources/completion.zsh b/src/Symfony/Component/Console/Resources/completion.zsh new file mode 100644 index 0000000000000..fed761c162c08 --- /dev/null +++ b/src/Symfony/Component/Console/Resources/completion.zsh @@ -0,0 +1,80 @@ +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +# +# zsh completions for {{ COMMAND_NAME }} +# +# References: +# - https://github.com/spf13/cobra/blob/master/zsh_completions.go +# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash +# +_sf_{{ COMMAND_NAME }}() { + local lastParam flagPrefix requestComp out comp + local -a completions + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") lastParam=${words[-1]} + + # For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[0]} ${words[1]} _complete -szsh -S{{ VERSION }} -c$((CURRENT-1))" i="" + for w in ${words[@]}; do + w=$(printf -- '%b' "$w") + # remove quotes from typed values + quote="${w:0:1}" + if [ "$quote" = \' ]; then + w="${w%\'}" + w="${w#\'}" + elif [ "$quote" = \" ]; then + w="${w%\"}" + w="${w#\"}" + fi + # empty values are ignored + if [ ! -z "$w" ]; then + i="${i}-i${w} " + fi + done + + # Ensure atleast 1 input + if [ "${i}" = "" ]; then + requestComp="${requestComp} -i\" \"" + else + requestComp="${requestComp} ${i}" + fi + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + + while IFS='\n' read -r comp; do + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + local tab=$(printf '\t') + comp=${comp//$tab/:} + completions+=${comp} + fi + done < <(printf "%s\n" "${out[@]}") + + # Let inbuilt _describe handle completions + eval _describe "completions" completions $flagPrefix + return $? +} + +compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} From 2458f633bff61716b79ecd2b324f5a993dd00fa5 Mon Sep 17 00:00:00 2001 From: Jitendra Adhikari Date: Tue, 9 Nov 2021 16:27:34 +0700 Subject: [PATCH 02/10] Add zsh completion writer (options with description) --- .../Completion/Output/ZshCompletionOutput.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/Symfony/Component/Console/Completion/Output/ZshCompletionOutput.php diff --git a/src/Symfony/Component/Console/Completion/Output/ZshCompletionOutput.php b/src/Symfony/Component/Console/Completion/Output/ZshCompletionOutput.php new file mode 100644 index 0000000000000..5828e4d2b2627 --- /dev/null +++ b/src/Symfony/Component/Console/Completion/Output/ZshCompletionOutput.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jitendra A + */ +class ZshCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $options = $suggestions->getValueSuggestions(); + foreach ($suggestions->getOptionSuggestions() as $option) { + $options[] = '--'.$option->getName()."\t".$option->getDescription(); + } + $output->writeln(implode("\n", $options)); + } +} From 3d38c3954a12de53b8eae42f74df57751a543c26 Mon Sep 17 00:00:00 2001 From: Jitendra Adhikari Date: Tue, 9 Nov 2021 16:31:43 +0700 Subject: [PATCH 03/10] Add a way to make completion input aware of shell --- .../Console/Completion/CompletionInput.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Symfony/Component/Console/Completion/CompletionInput.php b/src/Symfony/Component/Console/Completion/CompletionInput.php index eda95bef55468..229af9dfe15cc 100644 --- a/src/Symfony/Component/Console/Completion/CompletionInput.php +++ b/src/Symfony/Component/Console/Completion/CompletionInput.php @@ -23,6 +23,7 @@ * completion is expected. * * @author Wouter de Jong + * @author Jitendra A */ final class CompletionInput extends ArgvInput { @@ -32,6 +33,7 @@ final class CompletionInput extends ArgvInput public const TYPE_NONE = 'none'; private $tokens; + private $shell = ''; private $currentIndex; private $completionType; private $completionName = null; @@ -179,6 +181,18 @@ public function mustSuggestArgumentValuesFor(string $argumentName): bool return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName(); } + public function setShell(string $shell): string + { + [$this->shell, $old] = [$shell, $this->shell]; + + return $old; + } + + public function isShell(string $shell): bool + { + return $this->shell === $shell; + } + protected function parseToken(string $token, bool $parseOptions): bool { try { From 166851a69b97a4d08a44ae12d51d41601acc5826 Mon Sep 17 00:00:00 2001 From: Jitendra Adhikari Date: Tue, 9 Nov 2021 16:34:04 +0700 Subject: [PATCH 04/10] Register zsh completion output writer, set shell on completion input --- src/Symfony/Component/Console/Command/CompleteCommand.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Console/Command/CompleteCommand.php b/src/Symfony/Component/Console/Command/CompleteCommand.php index 97357d6737ed3..50c634b0adc84 100644 --- a/src/Symfony/Component/Console/Command/CompleteCommand.php +++ b/src/Symfony/Component/Console/Command/CompleteCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Output\BashCompletionOutput; use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Completion\Output\ZshCompletionOutput; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\InputInterface; @@ -25,6 +26,7 @@ * Responsible for providing the values to the shell completion. * * @author Wouter de Jong + * @author Jitendra A */ final class CompleteCommand extends Command { @@ -41,7 +43,10 @@ final class CompleteCommand extends Command public function __construct(array $completionOutputs = []) { // must be set before the parent constructor, as the property value is used in configure() - $this->completionOutputs = $completionOutputs + ['bash' => BashCompletionOutput::class]; + $this->completionOutputs = $completionOutputs + [ + 'bash' => BashCompletionOutput::class, + 'zsh' => ZshCompletionOutput::class, + ]; parent::__construct(); } @@ -168,6 +173,7 @@ private function createCompletionInput(InputInterface $input): CompletionInput } $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex); + $completionInput->setShell($input->getOption('shell')); try { $completionInput->bind($this->getApplication()->getDefinition()); From fc7f870a46c2d40da2579025383be3dfa1c47b06 Mon Sep 17 00:00:00 2001 From: Jitendra Adhikari Date: Tue, 9 Nov 2021 16:34:48 +0700 Subject: [PATCH 05/10] For zsh shell, add command description as well --- src/Symfony/Component/Console/Application.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 91403c275c9b0..8e7f9e36b577a 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -363,8 +363,8 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && 'command' === $input->getCompletionName() ) { - $suggestions->suggestValues(array_filter(array_map(function (Command $command) { - return $command->isHidden() ? null : $command->getName(); + $suggestions->suggestValues(array_filter(array_map(function (Command $command) use ($input) { + return $command->isHidden() ? null : $command->getName() . ($input->isShell('zsh') ? "\t".$command->getDescription() : ''); }, $this->all()))); return; From 310a7c8aa71db622d49240b24cb673c0770ef7b1 Mon Sep 17 00:00:00 2001 From: Jitendra Adhikari Date: Tue, 9 Nov 2021 16:35:49 +0700 Subject: [PATCH 06/10] Fix existing tests for zsh --- .../Component/Console/Tests/Command/CompleteCommandTest.php | 2 +- .../Console/Tests/Command/DumpCompletionCommandTest.php | 2 +- src/Symfony/Component/Console/Tests/Fixtures/application_1.json | 2 +- src/Symfony/Component/Console/Tests/Fixtures/application_1.xml | 2 +- src/Symfony/Component/Console/Tests/Fixtures/application_2.json | 2 +- src/Symfony/Component/Console/Tests/Fixtures/application_2.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php index 189928897cc7c..681b7043a774e 100644 --- a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php @@ -47,7 +47,7 @@ public function testRequiredShellOption() public function testUnsupportedShellOption() { - $this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash").'); + $this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash", "zsh").'); $this->execute(['--shell' => 'unsupported']); } diff --git a/src/Symfony/Component/Console/Tests/Command/DumpCompletionCommandTest.php b/src/Symfony/Component/Console/Tests/Command/DumpCompletionCommandTest.php index de8a3d4a60a3a..cd2c56c5fced4 100644 --- a/src/Symfony/Component/Console/Tests/Command/DumpCompletionCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/DumpCompletionCommandTest.php @@ -23,7 +23,7 @@ public function provideCompletionSuggestions() { yield 'shell' => [ [''], - ['bash'], + ['bash', 'zsh'], ]; } } diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json index 8c8ba2285f59a..e14f72297781c 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json @@ -89,7 +89,7 @@ "accept_value": true, "is_value_required": true, "is_multiple": false, - "description": "The shell type (\"bash\")", + "description": "The shell type (\"bash\", \"zsh\")", "default": null }, "current": { diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml index 5a17229343fcf..5c9c51f83e197 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml @@ -10,7 +10,7 @@