diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md
index ee0459e13d..739b0e08a6 100644
--- a/src/Symfony/Component/Console/CHANGELOG.md
+++ b/src/Symfony/Component/Console/CHANGELOG.md
@@ -7,6 +7,7 @@ CHANGELOG
* added support for colorization on Windows via ConEmu
* add a method to Dialog Helper to ask for a question and hide the response
* added support for interactive selections in console (DialogHelper::select())
+ * added support for autocompletion as you type in Dialog Helper
2.1.0
-----
diff --git a/src/Symfony/Component/Console/Helper/DialogHelper.php b/src/Symfony/Component/Console/Helper/DialogHelper.php
index 5dbfe419c3..7e6a51ed26 100644
--- a/src/Symfony/Component/Console/Helper/DialogHelper.php
+++ b/src/Symfony/Component/Console/Helper/DialogHelper.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Formatter\OutputFormatterStyle;
/**
* The Dialog class provides helpers to interact with the user.
@@ -63,23 +64,131 @@ class DialogHelper extends Helper
/**
* Asks a question to the user.
*
- * @param OutputInterface $output An Output instance
- * @param string|array $question The question to ask
- * @param string $default The default answer if none is given by the user
+ * @param OutputInterface $output An Output instance
+ * @param string|array $question The question to ask
+ * @param string $default The default answer if none is given by the user
+ * @param array $autocomplete List of values to autocomplete
*
* @return string The user answer
*
* @throws \RuntimeException If there is no data to read in the input stream
*/
- public function ask(OutputInterface $output, $question, $default = null)
+ public function ask(OutputInterface $output, $question, $default = null, array $autocomplete = null)
{
$output->write($question);
- $ret = fgets($this->inputStream ?: STDIN, 4096);
- if (false === $ret) {
- throw new \RuntimeException('Aborted');
+ $inputStream = $this->inputStream ?: STDIN;
+
+ if (null === $autocomplete || !$this->hasSttyAvailable()) {
+ $ret = fgets($inputStream, 4096);
+ if (false === $ret) {
+ throw new \RuntimeException('Aborted');
+ }
+ $ret = trim($ret);
+ } else {
+ $i = 0;
+ $currentMatched = false;
+ $ret = '';
+
+ // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
+ shell_exec('stty -icanon -echo');
+
+ // Add highlighted text style
+ $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
+
+ // Read a keypress
+ while ($c = fread($inputStream, 1)) {
+ // Did we read an escape sequence?
+ if ("\033" === $c) {
+ $c .= fread($inputStream, 2);
+
+ // Escape sequences for arrow keys
+ if ('A' === $c[2] || 'B' === $c[2] || 'C' === $c[2] || 'D' === $c[2]) {
+ // todo
+ }
+
+ continue;
+ }
+
+ // Backspace Character
+ if ("\177" === $c) {
+ if ($i === 0) {
+ continue;
+ }
+
+ if (false === $currentMatched) {
+ $i--;
+ // Move cursor backwards
+ $output->write("\033[1D");
+ }
+
+ // Erase characters from cursor to end of line
+ $output->write("\033[K");
+ $ret = substr($ret, 0, $i);
+
+ $currentMatched = false;
+
+ continue;
+ }
+
+ if ("\t" === $c || "\n" === $c) {
+ if (false !== $currentMatched) {
+ // Echo out completed match
+ $output->write(substr($autocomplete[$currentMatched], strlen($ret)));
+ $ret = $autocomplete[$currentMatched];
+ $i = strlen($ret);
+ }
+
+ if ("\n" === $c) {
+ $output->write($c);
+ break;
+ }
+
+ $currentMatched = false;
+
+ continue;
+ }
+
+ if (ord($c) < 32) {
+ continue;
+ }
+
+ $output->write($c);
+ $ret .= $c;
+ $i++;
+
+ // Erase characters from cursor to end of line
+ $output->write("\033[K");
+
+ foreach ($autocomplete as $j => $value) {
+ // Get a substring of the current autocomplete item based on number of chars typed (e.g. AcmeDemoBundle = Acme)
+ $matchTest = substr($value, 0, $i);
+
+ if ($ret === $matchTest) {
+ if ($i === strlen($value)) {
+ $currentMatched = false;
+ break;
+ }
+
+ // Save cursor position
+ $output->write("\0337");
+
+ $output->write('' . substr($value, $i) . '');
+
+ // Restore cursor position
+ $output->write("\0338");
+
+ $currentMatched = $j;
+ break;
+ }
+
+ $currentMatched = false;
+ }
+ }
+
+ // Reset stty so it behaves normally again
+ shell_exec('stty icanon echo');
}
- $ret = trim($ret);
return strlen($ret) > 0 ? $ret : $default;
}
@@ -186,22 +295,23 @@ class DialogHelper extends Helper
* validated data when the data is valid and throw an exception
* otherwise.
*
- * @param OutputInterface $output An Output instance
- * @param string|array $question The question to ask
- * @param callable $validator A PHP callback
- * @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
- * @param string $default The default answer if none is given by the user
+ * @param OutputInterface $output An Output instance
+ * @param string|array $question The question to ask
+ * @param callable $validator A PHP callback
+ * @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
+ * @param string $default The default answer if none is given by the user
+ * @param array $autocomplete List of values to autocomplete
*
* @return mixed
*
* @throws \Exception When any of the validators return an error
*/
- public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null)
+ public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null, array $autocomplete = null)
{
$that = $this;
- $interviewer = function() use ($output, $question, $default, $that) {
- return $that->ask($output, $question, $default);
+ $interviewer = function() use ($output, $question, $default, $autocomplete, $that) {
+ return $that->ask($output, $question, $default, $autocomplete);
};
return $this->validateAttempts($interviewer, $output, $validator, $attempts);
@@ -300,7 +410,7 @@ class DialogHelper extends Helper
return self::$stty;
}
- exec('/usr/bin/env stty', $output, $exitcode);
+ exec('stty 2>&1', $output, $exitcode);
return self::$stty = $exitcode === 0;
}
diff --git a/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php
index d0da968a15..87b47cdfe1 100644
--- a/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php
+++ b/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php
@@ -54,6 +54,22 @@ class DialogHelperTest extends \PHPUnit_Framework_TestCase
rewind($output->getStream());
$this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
+
+ $bundles = array('AcmeDemoBundle', 'AsseticBundle');
+
+ // Acm
+ // AcsTest
+ //
+ $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n");
+ $dialog->setInputStream($inputStream);
+
+ if ($this->hasSttyAvailable()) {
+ $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->getOutputStream(), 'Please select a bundle', 'FrameworkBundle', $bundles));
+ $this->assertEquals('AsseticBundleTest', $dialog->ask($this->getOutputStream(), 'Please select a bundle', 'FrameworkBundle', $bundles));
+ $this->assertEquals('FrameworkBundle', $dialog->ask($this->getOutputStream(), 'Please select a bundle', 'FrameworkBundle', $bundles));
+ } else {
+ $this->markTestSkipped();
+ }
}
public function testAskHiddenResponse()
@@ -128,4 +144,11 @@ class DialogHelperTest extends \PHPUnit_Framework_TestCase
{
return new StreamOutput(fopen('php://memory', 'r+', false));
}
+
+ private function hasSttyAvailable()
+ {
+ exec('stty 2>&1', $output, $exitcode);
+
+ return $exitcode === 0;
+ }
}