Skip to content

Commit a37c6c2

Browse files
committed
Show list of available word completions if multiple match
1 parent 89da20c commit a37c6c2

File tree

4 files changed

+91
-11
lines changed

4 files changed

+91
-11
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ return an array of possible word matches from a callable like this:
393393
$readline->setAutocomplete(function () {
394394
return array(
395395
'exit',
396+
'echo',
396397
'help',
397398
);
398399
});
@@ -402,9 +403,19 @@ If the user types `he [TAB]`, the first match will be skipped as it does not
402403
match the current word prefix and the second one will be picked automatically,
403404
so that the resulting input buffer is `hello `.
404405

406+
If the user types `e [TAB]`, then this will match multiple entries and the user
407+
will be presented with a list of up to 8 available word completions to choose
408+
from like this:
409+
410+
```php
411+
> e [TAB]
412+
exit echo
413+
> e
414+
```
415+
405416
Unless otherwise specified, the matches will be performed against the current
406417
word boundaries in the input buffer.
407-
This means that if the user types `hello [SPACE] e [TAB]`, then the resulting
418+
This means that if the user types `hello [SPACE] ex [TAB]`, then the resulting
408419
input buffer is `hello exit `, which may or may not be what you need depending
409420
on your particular use case.
410421

@@ -414,8 +425,8 @@ actually receives two arguments (similar to `ext-readline`'s
414425
The first argument will be the current incomplete word according to current
415426
cursor position and word boundaries, while the second argument will be the
416427
offset of this word within the complete input buffer.
417-
The first example will thus be invoked as `$fn('he', 0)`, while the second one
418-
will be invoked as `$fn('e', 6)`.
428+
The above examples will be invoked as `$fn('he', 0)`, `$fn('e', 0)` and
429+
`$fn('ex', 6)` respectively.
419430
You may want to use the `$offset` argument to check if the current word is an
420431
argument or a root command and the `$word` argument to autocomplete partial
421432
filename matches like this:

examples/periodic.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
// autocomplete the following commands (at offset=0 only)
3535
$readline->setAutocomplete(function ($_, $offset) {
36-
return $offset ? array() : array('exit', 'quit', 'help');
36+
return $offset ? array() : array('exit', 'quit', 'help', 'echo', 'print', 'printf');
3737
});
3838

3939
$stdio->writeln('Will print periodic messages until you type "quit" or "exit"');

src/Readline.php

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class Readline extends EventEmitter implements ReadableStreamInterface
1515
private $linebuffer = '';
1616
private $linepos = 0;
1717
private $echo = true;
18-
private $autocomplete = null;
1918
private $move = true;
2019
private $encoding = 'utf-8';
2120

@@ -29,6 +28,9 @@ class Readline extends EventEmitter implements ReadableStreamInterface
2928
private $historyUnsaved = null;
3029
private $historyLimit = 500;
3130

31+
private $autocomplete = null;
32+
private $autocompleteSuggestions = 8;
33+
3234
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
3335
{
3436
$this->input = $input;
@@ -546,30 +548,39 @@ public function onKeyTab()
546548

547549
// search longest common prefix among all possible matches
548550
$found = reset($words);
549-
array_shift($words);
550-
$others = count($words);
551+
$all = count($words);
551552
while ($found !== $word) {
552553
// count all words that start with $found
553554
$matches = count(array_filter($words, function ($word) use ($found) {
554555
return strpos($word, $found) === 0;
555556
}));
556557

557558
// ALL words match $found => common substring found
558-
if ($others === $matches) {
559+
if ($all === $matches) {
559560
break;
560561
}
561562

562563
// remove last letter from $found and try again
563564
$found = (string)substr($found, 0, -1);
564565
}
565566

566-
// current prefix has multiple possible completions => abort
567-
if ($found === $word && $others) {
567+
// found more than once possible match with this prefix => print options
568+
if ($found === $word && $all > 1) {
569+
// limit number of possible matches
570+
if (count($words) > $this->autocompleteSuggestions) {
571+
$more = count($words) - ($this->autocompleteSuggestions - 1);
572+
$words = array_slice($words, 0, $this->autocompleteSuggestions - 1);
573+
$words []= '(+' . $more . ' others)';
574+
}
575+
576+
$this->output->write("\n" . implode(' ', $words) . "\n");
577+
$this->redraw();
578+
568579
return;
569580
}
570581

571582
// append single space after match unless there's a postfix or there are multiple completions
572-
if ($postfix === '' && $others === 0) {
583+
if ($postfix === '' && $all === 1) {
573584
$found .= ' ';
574585
}
575586

tests/ReadlineTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,64 @@ public function testAutocompleteUsesCommonPrefixWhenMultipleMatchAndEnd()
651651
$this->assertEquals('count', $this->readline->getInput());
652652
}
653653

654+
public function testAutocompleteShowsAvailableOptionsWhenMultipleMatch()
655+
{
656+
$buffer = '';
657+
$this->output->expects($this->atLeastOnce())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
658+
$buffer .= $data;
659+
}));
660+
661+
$this->readline->setAutocomplete(function () { return array('a', 'b'); });
662+
663+
$this->readline->onKeyTab();
664+
665+
$this->assertContains("\na b\n", $buffer);
666+
}
667+
668+
public function testAutocompleteShowsAvailableOptionsWhenMultipleMatchIncompleteWord()
669+
{
670+
$buffer = '';
671+
$this->output->expects($this->atLeastOnce())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
672+
$buffer .= $data;
673+
}));
674+
675+
$this->readline->setAutocomplete(function () { return array('hello', 'hellö'); });
676+
677+
$this->readline->setInput('hell');
678+
679+
$this->readline->onKeyTab();
680+
681+
$this->assertContains("\nhello hellö\n", $buffer);
682+
}
683+
684+
public function testAutocompleteShowsAvailableOptionsWithoutDuplicatesWhenMultipleMatch()
685+
{
686+
$buffer = '';
687+
$this->output->expects($this->atLeastOnce())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
688+
$buffer .= $data;
689+
}));
690+
691+
$this->readline->setAutocomplete(function () { return array('a', 'b', 'b', 'a'); });
692+
693+
$this->readline->onKeyTab();
694+
695+
$this->assertContains("\na b\n", $buffer);
696+
}
697+
698+
public function testAutocompleteShowsLimitedNumberOfAvailableOptionsWhenMultipleMatch()
699+
{
700+
$buffer = '';
701+
$this->output->expects($this->atLeastOnce())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
702+
$buffer .= $data;
703+
}));
704+
705+
$this->readline->setAutocomplete(function () { return range('a', 'z'); });
706+
707+
$this->readline->onKeyTab();
708+
709+
$this->assertContains("\na b c d e f g (+19 others)\n", $buffer);
710+
}
711+
654712
public function testEmitEmptyInputOnEnter()
655713
{
656714
$this->readline->on('data', $this->expectCallableOnceWith(''));

0 commit comments

Comments
 (0)