|
12 | 12 | namespace Symfony\Component\Console\Helper;
|
13 | 13 |
|
14 | 14 | use Symfony\Component\Console\Exception\RuntimeException;
|
15 |
| -use Symfony\Component\Console\Formatter\OutputFormatter; |
16 |
| -use Symfony\Component\Console\Formatter\OutputFormatterStyle; |
17 | 15 | use Symfony\Component\Console\Input\InputInterface;
|
18 | 16 | use Symfony\Component\Console\Input\StreamableInputInterface;
|
19 | 17 | use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
@@ -176,114 +174,99 @@ protected function writeError(OutputInterface $output, \Exception $error)
|
176 | 174 | * @param OutputInterface $output
|
177 | 175 | * @param Question $question
|
178 | 176 | * @param resource $inputStream
|
| 177 | + * @param array $autocomplete |
| 178 | + * |
| 179 | + * @return string the response |
179 | 180 | */
|
180 | 181 | private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete): string
|
181 | 182 | {
|
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); |
222 | 184 |
|
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 | + } |
228 | 189 |
|
229 |
| - if (0 === $numMatches) { |
230 |
| - continue; |
231 |
| - } |
| 190 | + /* calculate the word matches from autocomplete */ |
| 191 | + $matches = $this->getWordMatchesSuggestion($word, $autocomplete); |
232 | 192 |
|
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)); |
244 | 197 |
|
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 | + } |
249 | 203 |
|
250 |
| - $numMatches = 0; |
251 |
| - } |
| 204 | + return $word; |
| 205 | + } |
252 | 206 |
|
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(); |
258 | 221 |
|
259 |
| - $numMatches = 0; |
260 |
| - $ofs = 0; |
| 222 | + return ('no' == strtolower($response)) ? false : $response; |
| 223 | + } |
261 | 224 |
|
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; |
267 | 247 | }
|
268 | 248 | }
|
| 249 | + } |
269 | 250 |
|
270 |
| - // Erase characters from cursor to end of line |
271 |
| - $output->write("\033[K"); |
| 251 | + /* sort the matches keywords */ |
| 252 | + krsort($matches); |
272 | 253 |
|
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 | + } |
282 | 256 |
|
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)); |
285 | 268 |
|
286 |
| - return $ret; |
| 269 | + return $word; |
287 | 270 | }
|
288 | 271 |
|
289 | 272 | /**
|
|
0 commit comments