Skip to content

Commit f767d71

Browse files
committed
Simplify autocomplete API, now inspired by the readline API
1 parent 67b4273 commit f767d71

File tree

9 files changed

+246
-213
lines changed

9 files changed

+246
-213
lines changed

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Async, event-driven and UTF-8 aware standard console input & output (STDIN, STDO
1515
* [Input buffer](#input-buffer)
1616
* [Cursor](#cursor)
1717
* [History](#history)
18+
* [Autocomplete](#autocomplete)
1819
* [Advanced](#advanced)
1920
* [Stdout](#stdout)
2021
* [Stdin](#stdin)
@@ -377,6 +378,83 @@ or a single `listHistory()` call respectively should be fairly straight
377378
forward and is left up as an exercise for the reader of this documentation
378379
(i.e. *you*).
379380

381+
#### Autocomplete
382+
383+
By default, users can use autocompletion by using their TAB keys on the keyboard.
384+
The autocomplete function is not registered by default, thus this feature is
385+
effectively disabled, as the TAB key has no function then.
386+
387+
The `setAutocomplete(?callable $autocomplete): Readline` method can be used to
388+
register a new autocomplete handler.
389+
In its most simple form, you won't need to assign any arguments and can simply
390+
return an array of possible word matches from a callable like this:
391+
392+
```php
393+
$readline->setAutocomplete(function () {
394+
return array(
395+
'exit',
396+
'help',
397+
);
398+
});
399+
```
400+
401+
If the user types `he [TAB]`, the first match will be skipped as it does not
402+
match the current word prefix and the second one will be picked automatically,
403+
so that the resulting input buffer is `hello `.
404+
405+
Unless otherwise specified, the matches will be performed against the current
406+
word boundaries in the input buffer.
407+
This means that if the user types `hello [SPACE] e [TAB]`, then the resulting
408+
input buffer is `hello exit `, which may or may not be what you need depending
409+
on your particular use case.
410+
411+
In order to give your more control over this behavior, the autocomplete function
412+
actually receives two arguments (similar to `ext-readline`'s
413+
[`readline_completion_function()`](http://php.net/manual/en/function.readline-completion-function.php)):
414+
The first argument will be the current incomplete word according to current
415+
cursor position and word boundaries, while the second argument will be the
416+
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)`.
419+
You may want to use the `$offset` argument to check if the current word is an
420+
argument or a root command and the `$word` argument to autocomplete partial
421+
filename matches like this:
422+
423+
```php
424+
$readline->setAutocomplete(function ($word, $offset) {
425+
if ($offset === 0) {
426+
// autocomplete root commands at offset=0 only
427+
return array('cat', 'rm', 'stat');
428+
} else {
429+
// autocomplete all command arguments as glob pattern
430+
return glob($word . '*', GLOB_MARK);
431+
}
432+
});
433+
```
434+
435+
If you need even more control over autocompletion, you may also want to access
436+
and/or manipulate the [input buffer](#input-buffer) and [cursor](#cursor)
437+
directly like this:
438+
439+
```php
440+
$readline->setAutocomplete(function () use ($readline) {
441+
if ($readline->getInput() === 'run') {
442+
$readline->setInput('run --test --value=42');
443+
$readline->moveCursorBy(-2);
444+
}
445+
446+
// return empty array so normal autocompletion doesn't kick in
447+
return array();
448+
});
449+
```
450+
451+
You can use a `null` value to remove the autocomplete function again and thus
452+
disable the autocomplete function:
453+
454+
```php
455+
$readline->setAutocomplete(null);
456+
```
457+
380458
### Advanced
381459

382460
#### Stdout

examples/periodic.php

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

33
use Clue\React\Stdio\Stdio;
4-
use Clue\React\Stdio\Readline\WordAutocomplete;
54

65
require __DIR__ . '/../vendor/autoload.php';
76

@@ -32,15 +31,17 @@
3231
}
3332
});
3433

35-
$autocomplete = new WordAutocomplete(array('exit', 'quit', 'hello', 'test', 'help', 'here'));
36-
$stdio->getReadline()->setAutocomplete($autocomplete);
34+
// autocomplete the following commands (at offset=0 only)
35+
$readline->setAutocomplete(function ($_, $offset) {
36+
return $offset ? array() : array('exit', 'quit', 'help');
37+
});
3738

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

4041
$stdio->on('line', function ($line) use ($stdio, $loop, &$timer) {
4142
$stdio->writeln('you just said: ' . $line . ' (' . strlen($line) . ')');
4243

43-
if ($line === 'quit' || $line === 'exit') {
44+
if (in_array(trim($line), array('quit', 'exit'))) {
4445
$timer->cancel();
4546
$stdio->end();
4647
}

src/Readline.php

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
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;
1311

1412
class Readline extends EventEmitter implements ReadableStreamInterface
1513
{
@@ -31,15 +29,10 @@ class Readline extends EventEmitter implements ReadableStreamInterface
3129
private $historyUnsaved = null;
3230
private $historyLimit = 500;
3331

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

4437
if (!$this->input->isReadable()) {
4538
return $this->close();
@@ -398,30 +391,22 @@ public function limitHistory($limit)
398391
* The autocomplete handler will be called whenever the user hits the TAB
399392
* key.
400393
*
401-
* If you do not want to use autocomplet support, simply pass a `NullAutocomplete` object.
402-
*
403-
* @param Autocomplete $autocomplete
394+
* @param callable|null $autocomplete
404395
* @return self
396+
* @throws InvalidArgumentException if the given callable is invalid
405397
*/
406398

407-
public function setAutocomplete(Autocomplete $autocomplete)
399+
public function setAutocomplete($autocomplete)
408400
{
401+
if ($autocomplete !== null && !is_callable($autocomplete)) {
402+
throw new \InvalidArgumentException('Invalid autocomplete function given');
403+
}
404+
409405
$this->autocomplete = $autocomplete;
410406

411407
return $this;
412408
}
413409

414-
/**
415-
* Gets the current autocomplete handler in use
416-
*
417-
* @return Autocomplete
418-
* @see self::setAutocomplete()
419-
*/
420-
public function getAutocomplete()
421-
{
422-
return $this->autocomplete;
423-
}
424-
425410
/**
426411
* redraw the current input prompt
427412
*
@@ -515,7 +500,61 @@ public function onKeyEnd()
515500
/** @internal */
516501
public function onKeyTab()
517502
{
518-
$this->autocomplete->go($this);
503+
if ($this->autocomplete === null) {
504+
return;
505+
}
506+
507+
// current word prefix and offset for start of word in input buffer
508+
// "echo foo|bar world" will return just "foo" with word offset 5
509+
$word = $this->substr($this->linebuffer, 0, $this->linepos);
510+
$offset = 0;
511+
512+
// buffer prefix and postfix for everything that will *not* be matched
513+
// above example will return "echo " and "bar world"
514+
$prefix = '';
515+
$postfix = (string)$this->substr($this->linebuffer, $this->linepos);
516+
517+
// skip everything before last space
518+
$pos = strrpos($word, ' ');
519+
if ($pos !== false) {
520+
$offset = $pos + 1;
521+
$prefix = (string)substr($word, 0, $pos + 1);
522+
$word = (string)substr($word, $pos + 1);
523+
}
524+
525+
// invoke automcomplete callback
526+
$words = call_user_func($this->autocomplete, $word, $offset);
527+
528+
// return early if autocomplete does not return anything
529+
if ($words === null) {
530+
return;
531+
}
532+
533+
// remove all from list of possible words that do not start with $word
534+
$len = strlen($word);
535+
foreach ($words as $i => $w) {
536+
if ($word !== substr($w, 0, $len)) {
537+
unset($words[$i]);
538+
}
539+
}
540+
541+
// return if neither of the possible words match
542+
if (!$words) {
543+
return;
544+
}
545+
546+
// TODO: find the BEST match
547+
// TODO: always picks first match for now
548+
$found = reset($words);
549+
550+
// append single space after this argument unless there's a postfix
551+
if ($postfix === '') {
552+
$found .= ' ';
553+
}
554+
555+
// replace word in input with best match and adjust cursor
556+
$this->linebuffer = $prefix . $found . $postfix;
557+
$this->moveCursorBy($this->strlen($found) - $this->strlen($word));
519558
}
520559

521560
/** @internal */

src/Readline/Autocomplete.php

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

src/Readline/NullAutocomplete.php

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

src/Readline/WordAutocomplete.php

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

tests/Readline/NullAutocompleteTest.php

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

0 commit comments

Comments
 (0)