Skip to content

Commit 0018ca2

Browse files
committed
Preserve quotes around autocomplete suggestions
1 parent a8158e9 commit 0018ca2

File tree

4 files changed

+172
-15
lines changed

4 files changed

+172
-15
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,8 +433,8 @@ filename matches like this:
433433

434434
```php
435435
$readline->setAutocomplete(function ($word, $offset) {
436-
if ($offset === 0) {
437-
// autocomplete root commands at offset=0 only
436+
if ($offset <= 1) {
437+
// autocomplete root commands at offset=0/1 only
438438
return array('cat', 'rm', 'stat');
439439
} else {
440440
// autocomplete all command arguments as glob pattern
@@ -443,6 +443,12 @@ $readline->setAutocomplete(function ($word, $offset) {
443443
});
444444
```
445445

446+
> Note that the user may also use quotes and/or leading whitespace around the
447+
root command, for example `"hell [TAB]`, in which case the offset will be
448+
advanced such as this will be invoked as `$fn('hell', 1)`.
449+
Unless you use a more sophisticated argument parser, a decent approximation may
450+
be using `$offset <= 1` to check this is a root command.
451+
446452
If you need even more control over autocompletion, you may also want to access
447453
and/or manipulate the [input buffer](#input-buffer) and [cursor](#cursor)
448454
directly like this:

examples/periodic.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
}
3232
});
3333

34-
// autocomplete the following commands (at offset=0 only)
34+
// autocomplete the following commands (at offset=0/1 only)
3535
$readline->setAutocomplete(function ($_, $offset) {
36-
return $offset ? array() : array('exit', 'quit', 'help', 'echo', 'print', 'printf');
36+
return $offset > 1 ? 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: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,17 @@ public function onKeyTab()
520520
$pos = strrpos($word, ' ');
521521
if ($pos !== false) {
522522
$offset = $pos + 1;
523-
$prefix = (string)substr($word, 0, $pos + 1);
524-
$word = (string)substr($word, $pos + 1);
523+
$prefix = (string)substr($word, 0, $offset);
524+
$word = (string)substr($word, $offset);
525+
}
526+
527+
// skip double quote (") or single quote (') from argument
528+
$quote = null;
529+
if (isset($word[0]) && ($word[0] === '"' || $word[0] === '\'')) {
530+
$quote = $word[0];
531+
++$offset;
532+
$prefix .= $word[0];
533+
$word = (string)substr($word, 1);
525534
}
526535

527536
// invoke autocomplete callback
@@ -533,9 +542,12 @@ public function onKeyTab()
533542
}
534543

535544
// 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-
});
545+
$words = array_unique($words);
546+
if ($word !== '' && $words) {
547+
$words = array_filter($words, function ($w) use ($word) {
548+
return strpos($w, $word) === 0;
549+
});
550+
}
539551

540552
// return if neither of the possible words match
541553
if (!$words) {
@@ -577,8 +589,16 @@ public function onKeyTab()
577589
}
578590
}
579591

580-
// append single space after match unless there's a postfix or there are multiple completions
592+
if ($quote !== null && $all === 1 && (strpos($postfix, $quote) === false || strpos($postfix, $quote) > strpos($postfix, ' '))) {
593+
// add closing quote if word started in quotes and postfix does not already contain closing quote before next space
594+
$found .= $quote;
595+
} elseif ($found === '') {
596+
// add single quotes around empty match
597+
$found = '\'\'';
598+
}
599+
581600
if ($postfix === '' && $all === 1) {
601+
// append single space after match unless there's a postfix or there are multiple completions
582602
$found .= ' ';
583603
}
584604

tests/ReadlineTest.php

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,109 @@ public function testAutocompleteAddsSpaceAfterComplete()
584584
$this->assertEquals('exit ', $this->readline->getInput());
585585
}
586586

587+
public function testAutocompleteAddsSpaceAfterSecondWordIsComplete()
588+
{
589+
$this->readline->setAutocomplete(function () { return array('exit'); });
590+
591+
$this->readline->setInput('exit ex');
592+
593+
$this->readline->onKeyTab();
594+
595+
$this->assertEquals('exit exit ', $this->readline->getInput());
596+
}
597+
598+
public function testAutocompleteAddsSpaceAfterCompleteWithClosingDoubleQuote()
599+
{
600+
$this->readline->setAutocomplete(function () { return array('exit'); });
601+
602+
$this->readline->setInput('"exit');
603+
604+
$this->readline->onKeyTab();
605+
606+
$this->assertEquals('"exit" ', $this->readline->getInput());
607+
}
608+
609+
public function testAutocompleteAddsSpaceAfterCompleteWithClosingSingleQuote()
610+
{
611+
$this->readline->setAutocomplete(function () { return array('exit'); });
612+
613+
$this->readline->setInput('\'exit');
614+
615+
$this->readline->onKeyTab();
616+
617+
$this->assertEquals('\'exit\' ', $this->readline->getInput());
618+
}
619+
620+
public function testAutocompleteAddsSpaceAfterSecondWordIsCompleteWithClosingDoubleQuote()
621+
{
622+
$this->readline->setAutocomplete(function () { return array('exit'); });
623+
624+
$this->readline->setInput('exit "exit');
625+
626+
$this->readline->onKeyTab();
627+
628+
$this->assertEquals('exit "exit" ', $this->readline->getInput());
629+
}
630+
631+
public function testAutocompleteStaysInQuotedStringAtEnd()
632+
{
633+
$this->readline->setAutocomplete(function () { return array('exit'); });
634+
635+
// move cursor before closing quote
636+
$this->readline->setInput('exit "ex"');
637+
$this->readline->moveCursorBy(-1);
638+
639+
$this->readline->onKeyTab();
640+
641+
$this->assertEquals('exit "exit"', $this->readline->getInput());
642+
$this->assertEquals(10, $this->readline->getCursorPosition());
643+
}
644+
645+
public function testAutocompleteStaysInQuotedStringInMiddle()
646+
{
647+
$this->readline->setAutocomplete(function () { return array('exit'); });
648+
649+
// move cursor before closing quote
650+
$this->readline->setInput('exit "ex" exit');
651+
$this->readline->moveCursorTo(8);
652+
653+
$this->readline->onKeyTab();
654+
655+
$this->assertEquals('exit "exit" exit', $this->readline->getInput());
656+
$this->assertEquals(10, $this->readline->getCursorPosition());
657+
}
658+
659+
public function testAutocompleteAddsClosingSingleQuoteAndSpaceWhenMatchingEmptyString()
660+
{
661+
$this->readline->setAutocomplete(function () { return array(''); });
662+
663+
$this->readline->setInput('\'');
664+
665+
$this->readline->onKeyTab();
666+
667+
$this->assertEquals('\'\' ', $this->readline->getInput());
668+
}
669+
670+
public function testAutocompleteAddsClosingDoubleQuoteAndSpaceWhenMatchingEmptyString()
671+
{
672+
$this->readline->setAutocomplete(function () { return array(''); });
673+
674+
$this->readline->setInput('"');
675+
676+
$this->readline->onKeyTab();
677+
678+
$this->assertEquals('"" ', $this->readline->getInput());
679+
}
680+
681+
public function testAutocompleteAddsSingleQuotesAndSpaceWhenMatchingEmptyString()
682+
{
683+
$this->readline->setAutocomplete(function () { return array(''); });
684+
685+
$this->readline->onKeyTab();
686+
687+
$this->assertEquals('\'\' ', $this->readline->getInput());
688+
}
689+
587690
public function testAutocompletePicksFirstComplete()
588691
{
589692
$this->readline->setAutocomplete(function () { return array('exit'); });
@@ -633,18 +736,32 @@ public function testAutocompleteUsesCommonPrefixWhenMultipleMatch()
633736
$this->assertEquals('fir', $this->readline->getInput());
634737
}
635738

636-
public function testAutocompleteUsesExactMatchWhenDuplicateMatch()
739+
public function testAutocompleteUsesCommonPrefixWithoutClosingQUotesWhenMultipleMatchAfterQuotes()
637740
{
638-
$this->readline->setAutocomplete(function () { return array('first', 'first'); });
741+
$this->readline->setAutocomplete(function () { return array('first', 'firm'); });
742+
743+
$this->readline->setInput('"');
639744

640745
$this->readline->onKeyTab();
641746

642-
$this->assertEquals('first ', $this->readline->getInput());
747+
$this->assertEquals('"fir', $this->readline->getInput());
748+
}
749+
750+
public function testAutocompleteUsesCommonPrefixBetweenQuotesWhenMultipleMatchBetweenQuotes()
751+
{
752+
$this->readline->setAutocomplete(function () { return array('first', 'firm'); });
753+
754+
$this->readline->setInput('""');
755+
$this->readline->moveCursorBy(-1);
756+
757+
$this->readline->onKeyTab();
758+
759+
$this->assertEquals('"fir"', $this->readline->getInput());
643760
}
644761

645-
public function testAutocompleteUsesExactMatchWhenDuplicateOrEmptyMatch()
762+
public function testAutocompleteUsesExactMatchWhenDuplicateMatch()
646763
{
647-
$this->readline->setAutocomplete(function () { return array('', 'first', '', 'first'); });
764+
$this->readline->setAutocomplete(function () { return array('first', 'first'); });
648765

649766
$this->readline->onKeyTab();
650767

@@ -674,6 +791,20 @@ public function testAutocompleteShowsAvailableOptionsWhenMultipleMatch()
674791
$this->assertContains("\na b\n", $buffer);
675792
}
676793

794+
public function testAutocompleteShowsAvailableOptionsWhenMultipleMatchWithEmptyWord()
795+
{
796+
$buffer = '';
797+
$this->output->expects($this->atLeastOnce())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
798+
$buffer .= $data;
799+
}));
800+
801+
$this->readline->setAutocomplete(function () { return array('', 'a'); });
802+
803+
$this->readline->onKeyTab();
804+
805+
$this->assertContains("\n a\n", $buffer);
806+
}
807+
677808
public function testAutocompleteShowsAvailableOptionsWhenMultipleMatchIncompleteWord()
678809
{
679810
$buffer = '';

0 commit comments

Comments
 (0)