Skip to content

Commit a8158e9

Browse files
committed
Do not match empty suggestions or partial UTF-8 byte sequences
1 parent a37c6c2 commit a8158e9

File tree

2 files changed

+59
-36
lines changed

2 files changed

+59
-36
lines changed

src/Readline.php

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ public function onKeyTab()
514514
// buffer prefix and postfix for everything that will *not* be matched
515515
// above example will return "echo " and "bar world"
516516
$prefix = '';
517-
$postfix = (string)$this->substr($this->linebuffer, $this->linepos);
517+
$postfix = $this->substr($this->linebuffer, $this->linepos);
518518

519519
// skip everything before last space
520520
$pos = strrpos($word, ' ');
@@ -524,22 +524,18 @@ public function onKeyTab()
524524
$word = (string)substr($word, $pos + 1);
525525
}
526526

527-
// invoke automcomplete callback
527+
// invoke autocomplete callback
528528
$words = call_user_func($this->autocomplete, $word, $offset);
529529

530530
// return early if autocomplete does not return anything
531531
if ($words === null) {
532532
return;
533533
}
534534

535-
// remove all from list of possible words that do not start with $word or are duplicates
536-
$len = strlen($word);
537-
$words = array_unique($words);
538-
foreach ($words as $i => $w) {
539-
if ($word !== substr($w, 0, $len)) {
540-
unset($words[$i]);
541-
}
542-
}
535+
// remove from list of possible words that do not start with $word or are duplicates
536+
$words = array_filter(array_unique($words), function ($w) use ($word) {
537+
return isset($w[0]) && (!isset($word[0]) || strpos($w, $word) === 0);
538+
});
543539

544540
// return if neither of the possible words match
545541
if (!$words) {
@@ -549,34 +545,36 @@ public function onKeyTab()
549545
// search longest common prefix among all possible matches
550546
$found = reset($words);
551547
$all = count($words);
552-
while ($found !== $word) {
553-
// count all words that start with $found
554-
$matches = count(array_filter($words, function ($word) use ($found) {
555-
return strpos($word, $found) === 0;
556-
}));
557-
558-
// ALL words match $found => common substring found
559-
if ($all === $matches) {
560-
break;
561-
}
562-
563-
// remove last letter from $found and try again
564-
$found = (string)substr($found, 0, -1);
565-
}
548+
if ($all > 1) {
549+
while ($found !== '') {
550+
// count all words that start with $found
551+
$matches = count(array_filter($words, function ($w) use ($found) {
552+
return strpos($w, $found) === 0;
553+
}));
554+
555+
// ALL words match $found => common substring found
556+
if ($all === $matches) {
557+
break;
558+
}
566559

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)';
560+
// remove last letter from $found and try again
561+
$found = $this->substr($found, 0, -1);
574562
}
575563

576-
$this->output->write("\n" . implode(' ', $words) . "\n");
577-
$this->redraw();
564+
// found more than one possible match with this prefix => print options
565+
if ($found === $word || $found === '') {
566+
// limit number of possible matches
567+
if (count($words) > $this->autocompleteSuggestions) {
568+
$more = count($words) - ($this->autocompleteSuggestions - 1);
569+
$words = array_slice($words, 0, $this->autocompleteSuggestions - 1);
570+
$words []= '(+' . $more . ' others)';
571+
}
578572

579-
return;
573+
$this->output->write("\n" . implode(' ', $words) . "\n");
574+
$this->redraw();
575+
576+
return;
577+
}
580578
}
581579

582580
// append single space after match unless there's a postfix or there are multiple completions

tests/ReadlineTest.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,15 @@ public function testAutocompleteUsesExactMatchWhenDuplicateMatch()
642642
$this->assertEquals('first ', $this->readline->getInput());
643643
}
644644

645+
public function testAutocompleteUsesExactMatchWhenDuplicateOrEmptyMatch()
646+
{
647+
$this->readline->setAutocomplete(function () { return array('', 'first', '', 'first'); });
648+
649+
$this->readline->onKeyTab();
650+
651+
$this->assertEquals('first ', $this->readline->getInput());
652+
}
653+
645654
public function testAutocompleteUsesCommonPrefixWhenMultipleMatchAndEnd()
646655
{
647656
$this->readline->setAutocomplete(function () { return array('counter', 'count'); });
@@ -672,13 +681,29 @@ public function testAutocompleteShowsAvailableOptionsWhenMultipleMatchIncomplete
672681
$buffer .= $data;
673682
}));
674683

675-
$this->readline->setAutocomplete(function () { return array('hello', 'hellö'); });
684+
$this->readline->setAutocomplete(function () { return array('hello', 'hellu'); });
676685

677686
$this->readline->setInput('hell');
678687

679688
$this->readline->onKeyTab();
680689

681-
$this->assertContains("\nhello hellö\n", $buffer);
690+
$this->assertContains("\nhello hellu\n", $buffer);
691+
}
692+
693+
public function testAutocompleteShowsAvailableOptionsWhenMultipleMatchIncompleteWordWithUmlauts()
694+
{
695+
$buffer = '';
696+
$this->output->expects($this->atLeastOnce())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
697+
$buffer .= $data;
698+
}));
699+
700+
$this->readline->setAutocomplete(function () { return array('hällö', 'hällü'); });
701+
702+
$this->readline->setInput('häll');
703+
704+
$this->readline->onKeyTab();
705+
706+
$this->assertContains("\nhällö hällü\n", $buffer);
682707
}
683708

684709
public function testAutocompleteShowsAvailableOptionsWithoutDuplicatesWhenMultipleMatch()

0 commit comments

Comments
 (0)