Skip to content

Commit 340b8f0

Browse files
committed
Initial skeleton for autocomplete support
1 parent 7a0fa02 commit 340b8f0

File tree

8 files changed

+196
-39
lines changed

8 files changed

+196
-39
lines changed

examples/periodic.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Clue\React\Stdio\Stdio;
4+
use Clue\React\Stdio\Readline\WordAutocomplete;
45

56
require __DIR__ . '/../vendor/autoload.php';
67

@@ -31,6 +32,9 @@
3132
}
3233
});
3334

35+
$autocomplete = new WordAutocomplete(array('exit', 'quit', 'hello', 'test', 'help', 'here'));
36+
$stdio->getReadline()->setAutocomplete($autocomplete);
37+
3438
$stdio->writeln('Will print periodic messages until you type "quit" or "exit"');
3539

3640
$stdio->on('line', function ($line) use ($stdio, $loop, &$timer) {

src/Autocomplete.php

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Readline.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use React\Stream\Util;
99
use Clue\React\Utf8\Sequencer as Utf8Sequencer;
1010
use Clue\React\Term\ControlCodeParser;
11+
use Clue\React\Stdio\Readline\Autocomplete;
12+
use Clue\React\Stdio\Readline\NullAutocomplete;
1113

1214
class Readline extends EventEmitter implements ReadableStreamInterface
1315
{
@@ -29,15 +31,19 @@ class Readline extends EventEmitter implements ReadableStreamInterface
2931
private $historyUnsaved = null;
3032
private $historyLimit = 500;
3133

32-
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
34+
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output, Autocomplete $autocomplete = null)
3335
{
36+
if ($autocomplete === null) {
37+
$autocomplete = new NullAutocomplete();
38+
}
39+
3440
$this->input = $input;
3541
$this->output = $output;
42+
$this->autocomplete = $autocomplete;
3643

3744
if (!$this->input->isReadable()) {
3845
return $this->close();
3946
}
40-
4147
// push input through control code parser
4248
$parser = new ControlCodeParser($input);
4349

@@ -387,15 +393,18 @@ public function limitHistory($limit)
387393
}
388394

389395
/**
390-
* set autocompletion handler to use (or none)
396+
* set autocompletion handler to use
391397
*
392398
* The autocomplete handler will be called whenever the user hits the TAB
393399
* key.
394400
*
395-
* @param AutocompleteInterface|null $autocomplete
401+
* If you do not want to use autocomplet support, simply pass a `NullAutocomplete` object.
402+
*
403+
* @param Autocomplete $autocomplete
396404
* @return self
397405
*/
398-
public function setAutocomplete(AutocompleteInterface $autocomplete = null)
406+
407+
public function setAutocomplete(Autocomplete $autocomplete)
399408
{
400409
$this->autocomplete = $autocomplete;
401410

@@ -495,9 +504,7 @@ public function onKeyEnd()
495504
/** @internal */
496505
public function onKeyTab()
497506
{
498-
if ($this->autocomplete !== null) {
499-
$this->autocomplete->run();
500-
}
507+
$this->autocomplete->go($this);
501508
}
502509

503510
/** @internal */

src/Readline/Autocomplete.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Clue\React\Stdio\Readline;
4+
5+
use Clue\React\Stdio\Readline;
6+
7+
interface Autocomplete
8+
{
9+
public function go(Readline $readline);
10+
}

src/Readline/NullAutocomplete.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Clue\React\Stdio\Readline;
4+
5+
use Clue\React\Stdio\Readline;
6+
7+
class NullAutocomplete implements Autocomplete
8+
{
9+
public function go(Readline $readline)
10+
{
11+
// NOOP
12+
}
13+
}

src/Readline/WordAutocomplete.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Clue\React\Stdio\Readline;
4+
5+
use Clue\React\Stdio\Readline\Autocomplete;
6+
use Clue\React\Stdio\Readline;
7+
8+
class WordAutocomplete implements Autocomplete
9+
{
10+
private $charset = 'UTF-8';
11+
12+
public function __construct(array $words)
13+
{
14+
$this->words = $words;
15+
}
16+
17+
public function go(Readline $readline)
18+
{
19+
$input = $readline->getInput();
20+
$cursor = $readline->getCursorPosition();
21+
22+
$search = mb_substr($input, 0, $cursor, $this->charset);
23+
$prefix = '';
24+
$postfix = (string)mb_substr($input, $cursor, null, $this->charset);
25+
26+
// skip everything before last space
27+
$pos = strrpos($search, ' ');
28+
if ($pos !== false) {
29+
$prefix = substr($search, 0, $pos + 1);
30+
$search = (string)substr($search, $pos + 1);
31+
}
32+
33+
$len = strlen($search);
34+
35+
if ($len === 0) {
36+
// cursor at the beginning => do not match against anything
37+
return;
38+
}
39+
40+
$found = array();
41+
42+
foreach ($this->words as $word) {
43+
// TODO: only check for leading substring
44+
if ($search === substr($word, 0, $len)) {
45+
$found []= $word;
46+
}
47+
}
48+
49+
if ($found) {
50+
// TODO: always picks first match for now
51+
52+
$found = $found[0];
53+
54+
if ($postfix === '') {
55+
$found .= ' ';
56+
}
57+
58+
$readline->setInput($prefix . $found . $postfix);
59+
$readline->moveCursorTo($cursor + $this->strlen($found) - $this->strlen($search));
60+
}
61+
}
62+
63+
private function strlen($str)
64+
{
65+
return mb_strlen($str, $this->charset);
66+
}
67+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
use Clue\React\Stdio\Readline\NullAutocomplete;
4+
5+
class NullAutocompleteTest extends TestCase
6+
{
7+
public function testDoesNothing()
8+
{
9+
$autocomplete = new NullAutocomplete();
10+
11+
$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
12+
13+
$autocomplete->go($readline);
14+
}
15+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
use Clue\React\Stdio\Readline\WordAutocomplete;
4+
5+
class WordAutocompleteTest extends TestCase
6+
{
7+
private $readline;
8+
9+
public function setUp()
10+
{
11+
$this->readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
12+
}
13+
14+
public function testWordEndDoesCompleteCorrectWordAndAppendsTrailingSpaceAndMovesCursor()
15+
{
16+
$autocomplete = new WordAutocomplete(array('first', 'second'));
17+
18+
$this->readline->expects($this->once())->method('getInput')->will($this->returnValue('fir'));
19+
$this->readline->expects($this->once())->method('getCursorPosition')->will($this->returnValue(3));
20+
21+
$this->readline->expects($this->once())->method('setInput')->with($this->equalTo('first '));
22+
$this->readline->expects($this->once())->method('moveCursorTo')->with($this->equalTo(6));
23+
24+
$autocomplete->go($this->readline);
25+
}
26+
27+
public function testCursorInMiddleOfWordDoesCompleteCorrectWordAndMovesCursorBehindCompleted()
28+
{
29+
$autocomplete = new WordAutocomplete(array('first', 'second'));
30+
31+
$this->readline->expects($this->once())->method('getInput')->will($this->returnValue('fir'));
32+
$this->readline->expects($this->once())->method('getCursorPosition')->will($this->returnValue(2));
33+
34+
$this->readline->expects($this->once())->method('setInput')->with($this->equalTo('firstr'));
35+
$this->readline->expects($this->once())->method('moveCursorTo')->with($this->equalTo(5));
36+
37+
$autocomplete->go($this->readline);
38+
}
39+
40+
public function testUnknownInputWillNotBeCompleted()
41+
{
42+
$autocomplete = new WordAutocomplete(array('first', 'second'));
43+
44+
$this->readline->expects($this->once())->method('getInput')->will($this->returnValue('test'));
45+
$this->readline->expects($this->never())->method('setInput');
46+
47+
$autocomplete->go($this->readline);
48+
}
49+
50+
public function testEmptyInputWillNotBeCompleted()
51+
{
52+
$autocomplete = new WordAutocomplete(array('first', 'second'));
53+
54+
$this->readline->expects($this->once())->method('getInput')->will($this->returnValue(''));
55+
$this->readline->expects($this->never())->method('setInput');
56+
57+
$autocomplete->go($this->readline);
58+
}
59+
60+
public function testWordInSentenceDoesCompleteSentence()
61+
{
62+
$autocomplete = new WordAutocomplete(array('first', 'second'));
63+
64+
$this->readline->expects($this->once())->method('getInput')->will($this->returnValue('hello fir'));
65+
$this->readline->expects($this->once())->method('getCursorPosition')->will($this->returnValue(9));
66+
67+
$this->readline->expects($this->once())->method('setInput')->with($this->equalTo('hello first '));
68+
$this->readline->expects($this->once())->method('moveCursorTo')->with($this->equalTo(12));
69+
70+
$autocomplete->go($this->readline);
71+
}
72+
}

0 commit comments

Comments
 (0)