Skip to content

Commit

Permalink
bug #31377 [Console] Fix auto-complete for ChoiceQuestion (multi-sele…
Browse files Browse the repository at this point in the history
…ct answers) (battye)

This PR was squashed before being merged into the 3.4 branch (closes #31377).

Discussion
----------

[Console] Fix auto-complete for ChoiceQuestion (multi-select answers)

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #24072
| License       | MIT
| Doc PR        | -

Previously, a bug existed whereby for multi-select questions, the auto-complete would only work for the first answer supplied by the user. On all subsequent answers, the auto-complete would not appear.

Now it works as expected:

![screenshot](https://user-images.githubusercontent.com/2110222/57158657-8c147e80-6e16-11e9-94f7-a9bc95506545.png)

Commits
-------

59321fe [Console] Fix auto-complete for ChoiceQuestion (multi-select answers)
  • Loading branch information
fabpot committed May 6, 2019
2 parents 5453f3e + 59321fe commit 60b505e
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 6 deletions.
41 changes: 35 additions & 6 deletions src/Symfony/Component/Console/Helper/QuestionHelper.php
Expand Up @@ -239,6 +239,7 @@ protected function writeError(OutputInterface $output, \Exception $error)
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete)
{
$fullChoice = '';
$ret = '';

$i = 0;
Expand All @@ -265,6 +266,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
--$i;
$fullChoice = substr($fullChoice, 0, -1);
// Move cursor backwards
$output->write("\033[1D");
}
Expand Down Expand Up @@ -301,8 +303,10 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
if ($numMatches > 0 && -1 !== $ofs) {
$ret = $matches[$ofs];
// Echo out remaining chars for current match
$output->write(substr($ret, $i));
$i = \strlen($ret);
$remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
$output->write($remainingCharacters);
$fullChoice .= $remainingCharacters;
$i = \strlen($fullChoice);
}

if ("\n" === $c) {
Expand All @@ -321,14 +325,21 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu

$output->write($c);
$ret .= $c;
$fullChoice .= $c;
++$i;

$tempRet = $ret;

if ($question instanceof ChoiceQuestion && $question->isMultiselect()) {
$tempRet = $this->mostRecentlyEnteredValue($fullChoice);
}

$numMatches = 0;
$ofs = 0;

foreach ($autocomplete as $value) {
// If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
if (0 === strpos($value, $ret)) {
if (0 === strpos($value, $tempRet)) {
$matches[$numMatches++] = $value;
}
}
Expand All @@ -340,8 +351,9 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
if ($numMatches > 0 && -1 !== $ofs) {
// Save cursor position
$output->write("\0337");
// Write highlighted text
$output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $i)).'</hl>');
// Write highlighted text, complete the partially entered response
$charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
$output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
// Restore cursor position
$output->write("\0338");
}
Expand All @@ -350,7 +362,24 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
// Reset stty so it behaves normally again
shell_exec(sprintf('stty %s', $sttyMode));

return $ret;
return $fullChoice;
}

private function mostRecentlyEnteredValue($entered)
{
$tempEntered = $entered;

// Determine the most recent value that the user entered
if (false !== strpos($entered, ',')) {
$choices = explode(',', $entered);
$lastChoice = trim($choices[\count($choices) - 1]);

if (\strlen($lastChoice) > 0) {
$tempEntered = $lastChoice;
}
}

return $tempEntered;
}

/**
Expand Down
31 changes: 31 additions & 0 deletions src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php
Expand Up @@ -1018,6 +1018,37 @@ public function testTraversableAutocomplete()
$this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}

public function testTraversableMultiselectAutocomplete()
{
// <NEWLINE>
// F<TAB><NEWLINE>
// A<3x UP ARROW><TAB>,F<TAB><NEWLINE>
// F00<BACKSPACE><BACKSPACE>o<TAB>,A<DOWN ARROW>,<SPACE>SecurityBundle<NEWLINE>
// Acme<TAB>,<SPACE>As<TAB><29x BACKSPACE>S<TAB><NEWLINE>
// Ac<TAB>,As<TAB><3x BACKSPACE>d<TAB><NEWLINE>
$inputStream = $this->getInputStream("\nF\t\nA\033[A\033[A\033[A\t,F\t\nF00\177\177o\t,A\033[B\t, SecurityBundle\nAcme\t, As\t\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177S\t\nAc\t,As\t\177\177\177d\t\n");

$dialog = new QuestionHelper();
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);

$question = new ChoiceQuestion(
'Please select a bundle (defaults to AcmeDemoBundle and AsseticBundle)',
['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle'],
'0,1'
);

// This tests that autocomplete works for all multiselect choices entered by the user
$question->setMultiselect(true);

$this->assertEquals(['AcmeDemoBundle', 'AsseticBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals(['FooBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals(['AsseticBundle', 'FooBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals(['FooBundle', 'AsseticBundle', 'SecurityBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals(['SecurityBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals(['AcmeDemoBundle', 'AsseticBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}

protected function getInputStream($input)
{
$stream = fopen('php://memory', 'r+', false);
Expand Down

0 comments on commit 60b505e

Please sign in to comment.