Skip to content

Commit 265ee15

Browse files
committed
[Console] #26321 - rewrite autocomplete interactive mode from console
1 parent 6c32bcd commit 265ee15

File tree

1 file changed

+76
-93
lines changed

1 file changed

+76
-93
lines changed

src/Symfony/Component/Console/Helper/QuestionHelper.php

Lines changed: 76 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
namespace Symfony\Component\Console\Helper;
1313

1414
use Symfony\Component\Console\Exception\RuntimeException;
15-
use Symfony\Component\Console\Formatter\OutputFormatter;
16-
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
1715
use Symfony\Component\Console\Input\InputInterface;
1816
use Symfony\Component\Console\Input\StreamableInputInterface;
1917
use Symfony\Component\Console\Output\ConsoleOutputInterface;
@@ -176,114 +174,99 @@ protected function writeError(OutputInterface $output, \Exception $error)
176174
* @param OutputInterface $output
177175
* @param Question $question
178176
* @param resource $inputStream
177+
* @param array $autocomplete
178+
*
179+
* @return string the response
179180
*/
180181
private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete): string
181182
{
182-
$ret = '';
183-
184-
$i = 0;
185-
$ofs = -1;
186-
$matches = $autocomplete;
187-
$numMatches = count($matches);
188-
189-
$sttyMode = shell_exec('stty -g');
190-
191-
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
192-
shell_exec('stty -icanon -echo');
193-
194-
// Add highlighted text style
195-
$output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
196-
197-
// Read a keypress
198-
while (!feof($inputStream)) {
199-
$c = fread($inputStream, 1);
200-
201-
// Backspace Character
202-
if ("\177" === $c) {
203-
if (0 === $numMatches && 0 !== $i) {
204-
--$i;
205-
// Move cursor backwards
206-
$output->write("\033[1D");
207-
}
208-
209-
if (0 === $i) {
210-
$ofs = -1;
211-
$matches = $autocomplete;
212-
$numMatches = count($matches);
213-
} else {
214-
$numMatches = 0;
215-
}
216-
217-
// Pop the last character off the end of our string
218-
$ret = substr($ret, 0, $i);
219-
} elseif ("\033" === $c) {
220-
// Did we read an escape sequence?
221-
$c .= fread($inputStream, 2);
183+
$word = $this->readLineFromStream($inputStream);
222184

223-
// A = Up Arrow. B = Down Arrow
224-
if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
225-
if ('A' === $c[2] && -1 === $ofs) {
226-
$ofs = 0;
227-
}
185+
/* if the typed word exits on autocomplete list will return it */
186+
if (in_array($word, $autocomplete)) {
187+
return $word;
188+
}
228189

229-
if (0 === $numMatches) {
230-
continue;
231-
}
190+
/* calculate the word matches from autocomplete */
191+
$matches = $this->getWordMatchesSuggestion($word, $autocomplete);
232192

233-
$ofs += ('A' === $c[2]) ? -1 : 1;
234-
$ofs = ($numMatches + $ofs) % $numMatches;
235-
}
236-
} elseif (ord($c) < 32) {
237-
if ("\t" === $c || "\n" === $c) {
238-
if ($numMatches > 0 && -1 !== $ofs) {
239-
$ret = $matches[$ofs];
240-
// Echo out remaining chars for current match
241-
$output->write(substr($ret, $i));
242-
$i = strlen($ret);
243-
}
193+
if (count($matches) > 0) {
194+
do {
195+
$response = $this->suggestWordsList($output, $matches);
196+
} while ($response && !in_array($response, $autocomplete));
244197

245-
if ("\n" === $c) {
246-
$output->write($c);
247-
break;
248-
}
198+
/* replace the old word only if response is different than false and the old one */
199+
if ($response && $word != $response) {
200+
$word = $response;
201+
}
202+
}
249203

250-
$numMatches = 0;
251-
}
204+
return $word;
205+
}
252206

253-
continue;
254-
} else {
255-
$output->write($c);
256-
$ret .= $c;
257-
++$i;
207+
/**
208+
* Ask question with keywords list and return response.
209+
*
210+
* @param OutputInterface $output
211+
* @param array $suggestionList
212+
* @param int $returnedRows
213+
*
214+
* @return string|bool The typed word
215+
*/
216+
private function suggestWordsList(OutputInterface $output, $suggestionList = array())
217+
{
218+
/* Suggest new words based on matches result */
219+
$output->writeln(sprintf('Did you mean : %s ? type No if you want to keep your choice.', implode(', ', $suggestionList)));
220+
$response = $this->readLineFromStream();
258221

259-
$numMatches = 0;
260-
$ofs = 0;
222+
return ('no' == strtolower($response)) ? false : $response;
223+
}
261224

262-
foreach ($autocomplete as $value) {
263-
// If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
264-
if (0 === strpos($value, $ret) && $i !== strlen($value)) {
265-
$matches[$numMatches++] = $value;
266-
}
225+
/**
226+
* Return similar word list from suggestion list.
227+
*
228+
* @param OutputInterface $output
229+
* @param array $suggestionList
230+
* @param int $returnedRows
231+
*
232+
* @return string|array the matche(s) word(s) from suggestions
233+
*/
234+
private function getWordMatchesSuggestion($word, array $suggestionList, int $returnedRows = 1)
235+
{
236+
$wordLength = strlen($word);
237+
$minCharsToMatch = $wordLength / 2;
238+
239+
foreach ($suggestionList as $suggestion) {
240+
/* calculate matches keys */
241+
$matchesKeys = similar_text($word, $suggestion);
242+
if ($matchesKeys > $minCharsToMatch) {
243+
if (isset($matches[$matchesKeys])) {
244+
$matches[$matchesKeys] = array_merge((array) $matches[$matchesKeys], (array) $suggestion);
245+
} else {
246+
$matches[$matchesKeys] = $suggestion;
267247
}
268248
}
249+
}
269250

270-
// Erase characters from cursor to end of line
271-
$output->write("\033[K");
251+
/* sort the matches keywords */
252+
krsort($matches);
272253

273-
if ($numMatches > 0 && -1 !== $ofs) {
274-
// Save cursor position
275-
$output->write("\0337");
276-
// Write highlighted text
277-
$output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $i)).'</hl>');
278-
// Restore cursor position
279-
$output->write("\0338");
280-
}
281-
}
254+
return ($returnedRows > 1) ? array_slice($matches, 0, $returnedRows) : current($matches);
255+
}
282256

283-
// Reset stty so it behaves normally again
284-
shell_exec(sprintf('stty %s', $sttyMode));
257+
/**
258+
* read line from console.
259+
*
260+
* @return string The typed word
261+
*/
262+
private function readLineFromStream($inputStream): string
263+
{
264+
/* read until getting an empty word */
265+
do {
266+
$word = trim(fgets($inputStream));
267+
} while (empty($word));
285268

286-
return $ret;
269+
return $word;
287270
}
288271

289272
/**

0 commit comments

Comments
 (0)