From 59321fe0316c67ecff4ba5cbc38de5a30b3cbdb6 Mon Sep 17 00:00:00 2001 From: battye Date: Fri, 3 May 2019 15:53:51 +0000 Subject: [PATCH] [Console] Fix auto-complete for ChoiceQuestion (multi-select answers) --- .../Console/Helper/QuestionHelper.php | 41 ++++++++++++++++--- .../Tests/Helper/QuestionHelperTest.php | 31 ++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 677aa764d4a0..757fed4746de 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -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; @@ -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"); } @@ -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) { @@ -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; } } @@ -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(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $i)).''); + // Write highlighted text, complete the partially entered response + $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))); + $output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).''); // Restore cursor position $output->write("\0338"); } @@ -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; } /** diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index 56ba1c689132..3c030e04983d 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -1018,6 +1018,37 @@ public function testTraversableAutocomplete() $this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); } + public function testTraversableMultiselectAutocomplete() + { + // + // F + // A<3x UP ARROW>,F + // F00o,A,SecurityBundle + // Acme,As<29x BACKSPACE>S + // Ac,As<3x BACKSPACE>d + $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);