@@ -15,7 +15,6 @@ class Readline extends EventEmitter implements ReadableStreamInterface
15
15
private $ linebuffer = '' ;
16
16
private $ linepos = 0 ;
17
17
private $ echo = true ;
18
- private $ autocomplete = null ;
19
18
private $ move = true ;
20
19
private $ encoding = 'utf-8 ' ;
21
20
@@ -29,6 +28,9 @@ class Readline extends EventEmitter implements ReadableStreamInterface
29
28
private $ historyUnsaved = null ;
30
29
private $ historyLimit = 500 ;
31
30
31
+ private $ autocomplete = null ;
32
+ private $ autocompleteSuggestions = 8 ;
33
+
32
34
public function __construct (ReadableStreamInterface $ input , WritableStreamInterface $ output )
33
35
{
34
36
$ this ->input = $ input ;
@@ -37,7 +39,6 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf
37
39
if (!$ this ->input ->isReadable ()) {
38
40
return $ this ->close ();
39
41
}
40
-
41
42
// push input through control code parser
42
43
$ parser = new ControlCodeParser ($ input );
43
44
@@ -387,16 +388,22 @@ public function limitHistory($limit)
387
388
}
388
389
389
390
/**
390
- * set autocompletion handler to use (or none)
391
+ * set autocompletion handler to use
391
392
*
392
393
* The autocomplete handler will be called whenever the user hits the TAB
393
394
* key.
394
395
*
395
- * @param AutocompleteInterface |null $autocomplete
396
+ * @param callable |null $autocomplete
396
397
* @return self
398
+ * @throws InvalidArgumentException if the given callable is invalid
397
399
*/
398
- public function setAutocomplete (AutocompleteInterface $ autocomplete = null )
400
+
401
+ public function setAutocomplete ($ autocomplete )
399
402
{
403
+ if ($ autocomplete !== null && !is_callable ($ autocomplete )) {
404
+ throw new \InvalidArgumentException ('Invalid autocomplete function given ' );
405
+ }
406
+
400
407
$ this ->autocomplete = $ autocomplete ;
401
408
402
409
return $ this ;
@@ -495,9 +502,110 @@ public function onKeyEnd()
495
502
/** @internal */
496
503
public function onKeyTab ()
497
504
{
498
- if ($ this ->autocomplete !== null ) {
499
- $ this ->autocomplete ->run ();
505
+ if ($ this ->autocomplete === null ) {
506
+ return ;
507
+ }
508
+
509
+ // current word prefix and offset for start of word in input buffer
510
+ // "echo foo|bar world" will return just "foo" with word offset 5
511
+ $ word = $ this ->substr ($ this ->linebuffer , 0 , $ this ->linepos );
512
+ $ start = 0 ;
513
+ $ end = $ this ->linepos ;
514
+
515
+ // buffer prefix and postfix for everything that will *not* be matched
516
+ // above example will return "echo " and "bar world"
517
+ $ prefix = '' ;
518
+ $ postfix = $ this ->substr ($ this ->linebuffer , $ this ->linepos );
519
+
520
+ // skip everything before last space
521
+ $ pos = strrpos ($ word , ' ' );
522
+ if ($ pos !== false ) {
523
+ $ prefix = (string )substr ($ word , 0 , $ pos + 1 );
524
+ $ word = (string )substr ($ word , $ pos + 1 );
525
+ $ start = $ this ->strlen ($ prefix );
500
526
}
527
+
528
+ // skip double quote (") or single quote (') from argument
529
+ $ quote = null ;
530
+ if (isset ($ word [0 ]) && ($ word [0 ] === '" ' || $ word [0 ] === '\'' )) {
531
+ $ quote = $ word [0 ];
532
+ ++$ start ;
533
+ $ prefix .= $ word [0 ];
534
+ $ word = (string )substr ($ word , 1 );
535
+ }
536
+
537
+ // invoke autocomplete callback
538
+ $ words = call_user_func ($ this ->autocomplete , $ word , $ start , $ end );
539
+
540
+ // return early if autocomplete does not return anything
541
+ if ($ words === null ) {
542
+ return ;
543
+ }
544
+
545
+ // remove from list of possible words that do not start with $word or are duplicates
546
+ $ words = array_unique ($ words );
547
+ if ($ word !== '' && $ words ) {
548
+ $ words = array_filter ($ words , function ($ w ) use ($ word ) {
549
+ return strpos ($ w , $ word ) === 0 ;
550
+ });
551
+ }
552
+
553
+ // return if neither of the possible words match
554
+ if (!$ words ) {
555
+ return ;
556
+ }
557
+
558
+ // search longest common prefix among all possible matches
559
+ $ found = reset ($ words );
560
+ $ all = count ($ words );
561
+ if ($ all > 1 ) {
562
+ while ($ found !== '' ) {
563
+ // count all words that start with $found
564
+ $ matches = count (array_filter ($ words , function ($ w ) use ($ found ) {
565
+ return strpos ($ w , $ found ) === 0 ;
566
+ }));
567
+
568
+ // ALL words match $found => common substring found
569
+ if ($ all === $ matches ) {
570
+ break ;
571
+ }
572
+
573
+ // remove last letter from $found and try again
574
+ $ found = $ this ->substr ($ found , 0 , -1 );
575
+ }
576
+
577
+ // found more than one possible match with this prefix => print options
578
+ if ($ found === $ word || $ found === '' ) {
579
+ // limit number of possible matches
580
+ if (count ($ words ) > $ this ->autocompleteSuggestions ) {
581
+ $ more = count ($ words ) - ($ this ->autocompleteSuggestions - 1 );
582
+ $ words = array_slice ($ words , 0 , $ this ->autocompleteSuggestions - 1 );
583
+ $ words []= '(+ ' . $ more . ' others) ' ;
584
+ }
585
+
586
+ $ this ->output ->write ("\n" . implode (' ' , $ words ) . "\n" );
587
+ $ this ->redraw ();
588
+
589
+ return ;
590
+ }
591
+ }
592
+
593
+ if ($ quote !== null && $ all === 1 && (strpos ($ postfix , $ quote ) === false || strpos ($ postfix , $ quote ) > strpos ($ postfix , ' ' ))) {
594
+ // add closing quote if word started in quotes and postfix does not already contain closing quote before next space
595
+ $ found .= $ quote ;
596
+ } elseif ($ found === '' ) {
597
+ // add single quotes around empty match
598
+ $ found = '\'\'' ;
599
+ }
600
+
601
+ if ($ postfix === '' && $ all === 1 ) {
602
+ // append single space after match unless there's a postfix or there are multiple completions
603
+ $ found .= ' ' ;
604
+ }
605
+
606
+ // replace word in input with best match and adjust cursor
607
+ $ this ->linebuffer = $ prefix . $ found . $ postfix ;
608
+ $ this ->moveCursorBy ($ this ->strlen ($ found ) - $ this ->strlen ($ word ));
501
609
}
502
610
503
611
/** @internal */
0 commit comments