[Console] Enable arrow keys to browse matched autocomplete options #6564

Merged
merged 4 commits into from Jan 6, 2013
@@ -86,10 +86,13 @@ public function ask(OutputInterface $output, $question, $default = null, array $
}
$ret = trim($ret);
} else {
- $i = 0;
- $currentMatched = false;
$ret = '';
+ $i = 0;
+ $matches = array();
+ $numMatches = 0;
+ $ofs = -1;
+
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
shell_exec('stty -icanon -echo');
@@ -98,91 +101,92 @@ public function ask(OutputInterface $output, $question, $default = null, array $
// 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) {
+ if (0 === $numMatches && 0 !== $i) {
$i--;
// Move cursor backwards
$output->write("\033[1D");
}
+ if ($i === 0) {
+ $ofs = -1;
+ }
+
// Erase characters from cursor to end of line
$output->write("\033[K");
$ret = substr($ret, 0, $i);
- $currentMatched = false;
+ $numMatches = 0;
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;
+ // Did we read an escape sequence?
+ if ("\033" === $c) {
+ $c .= fread($inputStream, 2);
- continue;
- }
+ if ('A' === $c[2] || 'B' === $c[2]) {
+ if (0 === $i) {
+ $matches = $autocomplete;
+ $numMatches = count($matches);
- if (ord($c) < 32) {
- continue;
- }
-
- $output->write($c);
- $ret .= $c;
- $i++;
+ if ('A' === $c[2] && -1 === $ofs) {
+ $ofs = 0;
+ }
+ }
- // Erase characters from cursor to end of line
- $output->write("\033[K");
+ if (0 === $numMatches) {
+ continue;
+ }
- 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);
+ $ofs += ('A' === $c[2]) ? -1 : 1;
+ $ofs = ($numMatches + $ofs) % $numMatches;
+ }
+ } else if (ord($c) < 32) {
+ if ("\t" === $c || "\n" === $c) {
+ if ($numMatches > 0) {
+ $ret = $matches[$ofs];
+ // Echo out completed match
+ $output->write(substr($ret, $i));
+ $i = strlen($ret);
+ }
- if ($ret === $matchTest) {
- if ($i === strlen($value)) {
- $currentMatched = false;
+ if ("\n" === $c) {
+ $output->write($c);
break;
}
-
- // Save cursor position
- $output->write("\0337");
- $output->write('<hl>' . substr($value, $i) . '</hl>');
-
- // Restore cursor position
- $output->write("\0338");
+ $numMatches = 0;
+ }
- $currentMatched = $j;
- break;
+ continue;
+ } else {
+ $output->write($c);
+ $ret .= $c;
+ $i++;
+
+ $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) && $i !== strlen($value)) {
+ $matches[$numMatches++] = $value;
+ }
}
+ }
+
+ // Erase characters from cursor to end of line
+ $output->write("\033[K");
- $currentMatched = false;
+ if ($numMatches > 0) {
+ // Save cursor position
+ $output->write("\0337");
+ // Write highlighted text
+ $output->write('<hl>' . substr($matches[$ofs], $i) . '</hl>');
+ // Restore cursor position
+ $output->write("\0338");
}
}
@@ -55,18 +55,24 @@ public function testAsk()
rewind($output->getStream());
$this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
- $bundles = array('AcmeDemoBundle', 'AsseticBundle');
+ $bundles = array('AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle');
// Acm<NEWLINE>
// Ac<BACKSPACE><BACKSPACE>s<TAB>Test<NEWLINE>
// <NEWLINE>
- $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n");
+ // <UP ARROW><UP ARROW><NEWLINE>
+ // <UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><TAB><NEWLINE>
+ // <DOWN ARROW><NEWLINE>
+ $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\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));
+ $this->assertEquals('SecurityBundle', $dialog->ask($this->getOutputStream(), 'Please select a bundle', 'FrameworkBundle', $bundles));
+ $this->assertEquals('FooBundleTest', $dialog->ask($this->getOutputStream(), 'Please select a bundle', 'FrameworkBundle', $bundles));
+ $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->getOutputStream(), 'Please select a bundle', 'FrameworkBundle', $bundles));
} else {
$this->markTestSkipped();
}