8000 Zsh shell autocompletions by adhocore · Pull Request #43970 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

Zsh shell autocompletions #43970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Symfony/Component/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() : '');
Copy link
Member
@GromNaN GromNaN Nov 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing if shell is zsh seems very specific for this place.

We could use a DTO. The ZshCompletionOutput would format the value with its specificities. By implementing the __toString method, the BashCompletionOutput would not require any change.

namespace Symfony\Component\Console\Completion;

final class SuggestedValue {
    private $value;
    private $description;

    public function __construct(string $value, string $description = null) {
        $this->value = $value;
        $this->description = $description;
    }

    public function __toString(): string {
        return $this->value;
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe some abstraction to tell/handle if the shell supports description for autocompleted suggestion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would add complexity in all commands that implements completion. I'm not sure this is something we want.
Each command should set suggestions values, optionally with a description using this DTO.

}, $this->all())));

return;
Expand Down
8 changes: 7 additions & 1 deletion src/Symfony/Component/Console/Command/CompleteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +26,7 @@
* Responsible for providing the values to the shell completion.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Jitendra A <adhocore@gmail.com>
*/
final class CompleteCommand extends Command
{
Expand All @@ -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();
}
Expand Down Expand Up @@ -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());
Expand Down
12 changes: 12 additions & 0 deletions src/Symfony/Component/Console/Completion/CompletionInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* completion is expected.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Jitendra A <adhocore@gmail.com>
*/
final class CompletionInput extends ArgvInput
{
Expand All @@ -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;
Expand Down Expand Up @@ -179,6 +181,16 @@ public function mustSuggestArgumentValuesFor(string $argumentName): bool
return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
}

public function setShell(string $shell): void
{
$this->shell = $shell;
}

public function isShell(string $shell): bool
{
return $this->shell === $shell;
}

protected function parseToken(string $token, bool $parseOptions): bool
{
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <adhocore@gmail.com>
*/
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));
}
}
80 changes: 80 additions & 0 deletions src/Symfony/Component/Console/Resources/completion.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# This file is part of the Symfony package.
#
# (c) Fabien Potencier <fabien@symfony.com>
#
# 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=<TAB>)
# 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\" \""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes the CompletionInput::getCurrentValue() equals to a space? it would be better to skip the value entirely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it still requires -i option equal or 1 more than -c option that's why

Copy link
Member
@GromNaN GromNaN Nov 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, does it work with an empty string?

Suggested change
requestComp="${requestComp} -i\" \""
requestComp="${requestComp} -i\"\"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no it can't be empty

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=${c 10000 omp//:/\\:}
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 }}
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}

Expand Down Expand Up @@ -122,6 +122,54 @@ public function provideCompleteCommandInputDefinitionInputs()
yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']];
}

/**
* @dataProvider provideZshCompleteCommandNameInputs
*/
public function testZshCompleteCommandName(array $input, array $suggestions)
{
$this->execute(['--current' => '1', '--input' => $input, '--shell' => 'zsh']);
$this->assertEquals(implode("\n", $suggestions).\PHP_EOL, $this->tester->getDisplay());
}

public function provideZshCompleteCommandNameInputs()
{
yield 'empty' => [['bin/console'], [
'help'."\t".'Display help for a command',
'list'."\t".'List commands',
'completion'."\t".'Dump the shell completion script',
'hello'."\t".'Hello test command',
]];
yield 'partial' => [['bin/console', 'he'], [
'help'."\t".'Display help for a command',
'list'."\t".'List commands',
'completion'."\t".'Dump the shell completion script',
'hello'."\t".'Hello test command',
]];
yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello']];
}

/**
* @dataProvider provideZshCompleteCommandInputDefinitionInputs
*/
public function testZshCompleteCommandInputDefinition(array $input, array $suggestions)
{
$this->execute(['--current' => '2', '--input' => $input, '--shell' => 'zsh']);
$this->assertEquals(implode("\n", $suggestions).\PHP_EOL, $this->tester->getDisplay());
}

public function provideZshCompleteCommandInputDefinitionInputs()
{
yield 'definition' => [['bin/console', 'hello', '-'], [
'--help'."\t".'Display help for the given command. When no command is given display help for the list command',
'--quiet'."\t".'Do not output any message',
'--verbose'."\t".'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug',
'--version'."\t".'Display this application version',
'--ansi'."\t".'Force (or disable --no-ansi) ANSI output',
'--no-interaction'."\t".'Do not ask any interactive question',
]];
yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']];
}

private function execute(array $input)
{
// run in verbose mode to assert exceptions
Expand All @@ -134,6 +182,7 @@ class CompleteCommandTest_HelloCommand extends Command
public function configure(): void
{
$this->setName('hello')
->setDescription('Hello test command')
->addArgument('name', InputArgument::REQUIRED)
;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function provideCompletionSuggestions()
{
yield 'shell' => [
[''],
['bash'],
['bash', 'zsh'],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,12 @@ public function provideFromStringData()
yield ['bin/console cache:clear "multi word string"', ['bin/console', 'cache:clear', '"multi word string"']];
yield ['bin/console cache:clear \'multi word string\'', ['bin/console', 'cache:clear', '\'multi word string\'']];
}

public function testShell()
{
$input = CompletionInput::fromString('bin/console cache:clear \'multi word string\'', 1);
$input->setShell('zsh');

$this->assertTrue($input->isShell('zsh'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<arguments/>
<options>
<option name="--shell" shortcut="-s" accept_value="1" is_value_required="1" is_multiple="0">
<description>The shell type ("bash")</description>
<description>The shell type ("bash", "zsh")</description>
<defaults/>
</option>
<option name="--input" shortcut="-i" accept_value="1" is_value_required="1" is_multiple="1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,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": {
Expand Down
80D2 2 changes: 1 addition & 1 deletion src/Symfony/Component/Console/Tests/Fixtures/application_2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<arguments/>
<options>
<option name="--shell" shortcut="-s" accept_value="1" is_value_required="1" is_multiple="0">
<description>The shell type ("bash")</description>
<description>The shell type ("bash", "zsh")</description>
<defaults/>
</option>
<option name="--input" shortcut="-i" accept_value="1" is_value_required="1" is_multiple="1">
Expand Down
0