diff --git a/src/Configuration.php b/src/Configuration.php index d7039d81..dcbe892b 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -87,7 +87,8 @@ class Configuration private $historySize; private $eraseDuplicates; private $manualDbFile; - private $hasReadline; + /** @bool Native readline implementation provided by {@see readline()} function */ + private $hasNativeReadline; private $useReadline; private $useBracketedPaste; private $hasPcntl; @@ -370,7 +371,7 @@ public static function getInputOptions(): array public function init() { // feature detection - $this->hasReadline = \function_exists('readline'); + $this->hasNativeReadline = \function_exists('readline'); $this->hasPcntl = ProcessForker::isSupported(); if ($configFile = $this->getConfigFile()) { @@ -735,13 +736,25 @@ public function getPipe(string $type, int $pid): string } /** - * Check whether this PHP instance has Readline available. + * Check whether this PHP instance has native readline available. + * + * @deprecated Call {@see Configuration::hasNativeReadline()} instead * * @return bool True if Readline is available */ public function hasReadline(): bool { - return $this->hasReadline; + return $this->hasNativeReadline; + } + + /** + * Check whether this PHP instance has native readline available. + * + * @return bool True if Readline is available + */ + public function hasNativeReadline(): bool + { + return $this->hasNativeReadline; } /** @@ -764,7 +777,7 @@ public function setUseReadline(bool $useReadline) */ public function useReadline(): bool { - return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline; + return isset($this->useReadline) ? ($this->hasNativeReadline && $this->useReadline) : $this->hasNativeReadline; } /** @@ -1081,7 +1094,9 @@ public function setTabCompletion(bool $useTabCompletion) */ public function useTabCompletion(): bool { - return isset($this->useTabCompletion) ? ($this->hasReadline && $this->useTabCompletion) : $this->hasReadline; + // TODO: In the future, if stability other than GNU Readline improves, + // it will no longer depend on $this->hasNativeReadline property. + return $this->useTabCompletion ?? $this->hasNativeReadline; } /** diff --git a/src/Readline/GNUReadline.php b/src/Readline/GNUReadline.php index d4dba0cc..3b0c30fa 100644 --- a/src/Readline/GNUReadline.php +++ b/src/Readline/GNUReadline.php @@ -11,6 +11,8 @@ namespace Psy\Readline; +use Psy\TabCompletion\AutoCompleter; + /** * A Readline interface implementation for GNU Readline. * @@ -176,4 +178,18 @@ public function writeHistory(): bool return true; } + + public function activateAutoCompleter(AutoCompleter $autoCompleter): void + { + \readline_completion_function([$autoCompleter, 'callback']); + } + + public function deactivateAutoCompleter(): void + { + // PHP didn't implement the whole readline API when they first switched + // to libedit. And they still haven't. + if (\function_exists('readline_callback_handler_remove')) { + \readline_callback_handler_remove(); + } + } } diff --git a/src/Readline/Hoa/Autocompleter.php b/src/Readline/Hoa/Autocompleter.php index 6955de62..8c34f307 100644 --- a/src/Readline/Hoa/Autocompleter.php +++ b/src/Readline/Hoa/Autocompleter.php @@ -46,8 +46,11 @@ interface Autocompleter /** * Complete a word. * Returns null for no word, a full-word or an array of full-words. + * + * @param array{line_buffer: string} $info A subset of {@see readline_info()}'s return value. + * @see https://www.php.net/readline_info */ - public function complete(&$prefix); + public function complete(string $prefix, int $index, array $info); /** * Get definition of a word. diff --git a/src/Readline/Hoa/AutocompleterAggregate.php b/src/Readline/Hoa/AutocompleterAggregate.php index b7a2223d..c7de10dc 100644 --- a/src/Readline/Hoa/AutocompleterAggregate.php +++ b/src/Readline/Hoa/AutocompleterAggregate.php @@ -48,9 +48,6 @@ class AutocompleterAggregate implements Autocompleter */ protected $_autocompleters = null; - /** - * Constructor. - */ public function __construct(array $autocompleters) { $this->setAutocompleters($autocompleters); @@ -58,11 +55,7 @@ public function __construct(array $autocompleters) return; } - /** - * Complete a word. - * Returns null for no word, a full-word or an array of full-words. - */ - public function complete(&$prefix) + public function complete(string $prefix, int $index, array $info) { foreach ($this->getAutocompleters() as $autocompleter) { $preg = \preg_match( @@ -108,9 +101,6 @@ public function getAutocompleters() return $this->_autocompleters; } - /** - * Get definition of a word. - */ public function getWordDefinition(): string { return '.*'; diff --git a/src/Readline/Hoa/AutocompleterPath.php b/src/Readline/Hoa/AutocompleterPath.php index a922b789..54711983 100644 --- a/src/Readline/Hoa/AutocompleterPath.php +++ b/src/Readline/Hoa/AutocompleterPath.php @@ -58,9 +58,6 @@ class AutocompleterPath implements Autocompleter */ protected $_iteratorFactory = null; - /** - * Constructor. - */ public function __construct( string $root = null, \Closure $iteratorFactory = null @@ -78,11 +75,7 @@ public function __construct( } } - /** - * Complete a word. - * Returns null for no word, a full-word or an array of full-words. - */ - public function complete(&$prefix) + public function complete(string $prefix, int $index, array $info) { $root = $this->getRoot(); @@ -136,9 +129,6 @@ public function complete(&$prefix) return $out; } - /** - * Get definition of a word. - */ public function getWordDefinition(): string { return '/?[\w\d\\_\-\.]+(/[\w\d\\_\-\.]*)*'; diff --git a/src/Readline/Hoa/AutocompleterWord.php b/src/Readline/Hoa/AutocompleterWord.php index c60823ea..2d02175d 100644 --- a/src/Readline/Hoa/AutocompleterWord.php +++ b/src/Readline/Hoa/AutocompleterWord.php @@ -56,15 +56,7 @@ public function __construct(array $words) $this->setWords($words); } - /** - * Complete a word. - * Returns null for no word, a full-word or an array of full-words. - * - * @param string &$prefix Prefix to autocomplete - * - * @return mixed - */ - public function complete(&$prefix) + public function complete(string $prefix, int $index, array $info) { $out = []; $length = \mb_strlen($prefix); @@ -86,9 +78,6 @@ public function complete(&$prefix) return $out; } - /** - * Get definition of a word. - */ public function getWordDefinition(): string { return '\b\w+'; diff --git a/src/Readline/Hoa/ConsoleWindow.php b/src/Readline/Hoa/ConsoleWindow.php index 4ebd5cbe..d4d4e8d6 100644 --- a/src/Readline/Hoa/ConsoleWindow.php +++ b/src/Readline/Hoa/ConsoleWindow.php @@ -120,7 +120,7 @@ public static function getSize(): array } $command = $term.'tput cols && '.$term.'tput lines'; - $tput = Processus::execute($command, false); + $tput = ConsoleProcessus::execute($command, false); if (!empty($tput)) { list($x, $y) = \explode("\n", $tput); diff --git a/src/Readline/Hoa/Readline.php b/src/Readline/Hoa/Readline.php index c2a6064b..f77e86e6 100644 --- a/src/Readline/Hoa/Readline.php +++ b/src/Readline/Hoa/Readline.php @@ -763,7 +763,10 @@ public function _bindTab(self $self): int return $state; } - $solution = $autocompleter->complete($word); + $solution = $autocompleter->complete($word, $current, [ + 'line_buffer' => $line, + ]); + $length = \mb_strlen($word); if (null === $solution) { diff --git a/src/Readline/HoaAutocompleterAdapter.php b/src/Readline/HoaAutocompleterAdapter.php new file mode 100644 index 00000000..c819f4eb --- /dev/null +++ b/src/Readline/HoaAutocompleterAdapter.php @@ -0,0 +1,27 @@ +autoCompleter = $autoCompleter; + } + + public function complete(string $prefix, int $index, array $info) + { + return $this->autoCompleter->complete($prefix, $index, $info); + } + + public function getWordDefinition(): string + { + return '.'; + } +} diff --git a/src/Readline/Readline.php b/src/Readline/Readline.php index 8a8f79bb..29e0e2e0 100644 --- a/src/Readline/Readline.php +++ b/src/Readline/Readline.php @@ -11,6 +11,8 @@ namespace Psy\Readline; +use Psy\TabCompletion\AutoCompleter; + /** * An interface abstracting the various readline_* functions. */ @@ -80,4 +82,14 @@ public function redisplay(); * @return bool Success */ public function writeHistory(): bool; + + /** + * Activete auto completer for tab completion. + */ + public function activateAutoCompleter(AutoCompleter $autoCompleter): void; + + /** + * Deactivete auto completer for tab completion. + */ + public function deactivateAutoCompleter(): void; } diff --git a/src/Readline/Transient.php b/src/Readline/Transient.php index 128ef867..e474089f 100644 --- a/src/Readline/Transient.php +++ b/src/Readline/Transient.php @@ -12,6 +12,7 @@ namespace Psy\Readline; use Psy\Exception\BreakException; +use Psy\TabCompletion\AutoCompleter; /** * An array-based Readline emulation implementation. @@ -152,4 +153,14 @@ private function getStdin() return $this->stdin; } + + public function activateAutoCompleter(AutoCompleter $autoCompleter): void + { + // noop + } + + public function deactivateAutoCompleter(): void + { + // noop + } } diff --git a/src/Readline/Userland.php b/src/Readline/Userland.php index 2faf47a5..9f798631 100644 --- a/src/Readline/Userland.php +++ b/src/Readline/Userland.php @@ -19,6 +19,7 @@ use Psy\Readline\Hoa\ConsoleTput as HoaConsoleTput; use Psy\Readline\Hoa\Readline as HoaReadline; use Psy\Readline\Hoa\Ustring as HoaUstring; +use Psy\TabCompletion\AutoCompleter; /** * Userland Readline implementation. @@ -158,4 +159,14 @@ public function writeHistory(): bool { return true; } + + public function activateAutoCompleter(AutoCompleter $autoCompleter): void + { + $this->hoaReadline->setAutocompleter(new HoaAutocompleterAdapter($autoCompleter)); + } + + public function deactivateAutoCompleter(): void + { + // noop + } } diff --git a/src/Shell.php b/src/Shell.php index 2fe643a3..b4300371 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -1501,7 +1501,7 @@ protected function initializeTabCompletion() $this->addMatchersToAutoCompleter($this->getDefaultMatchers()); $this->addMatchersToAutoCompleter($this->matchers); - $this->autoCompleter->activate(); + $this->readline->activateAutoCompleter($this->autoCompleter); } /** diff --git a/src/TabCompletion/AutoCompleter.php b/src/TabCompletion/AutoCompleter.php index 17d0ec56..b1760844 100644 --- a/src/TabCompletion/AutoCompleter.php +++ b/src/TabCompletion/AutoCompleter.php @@ -11,6 +11,7 @@ namespace Psy\TabCompletion; +use Psy\Readline\Readline; use Psy\TabCompletion\Matcher\AbstractMatcher; /** @@ -23,6 +24,9 @@ class AutoCompleter /** @var Matcher\AbstractMatcher[] */ protected $matchers; + /** @var ?Readline */ + protected $readline; + /** * Register a tab completion Matcher. * @@ -33,24 +37,16 @@ public function addMatcher(AbstractMatcher $matcher) $this->matchers[] = $matcher; } - /** - * Activate readline tab completion. - */ - public function activate() - { - \readline_completion_function([&$this, 'callback']); - } - /** * Handle readline completion. * * @param string $input Readline current word * @param int $index Current word index - * @param array $info readline_info() data + * @phpstan-param array{line_buffer: string, end: int} $info {@see readline_info()} data * - * @return array + * @return list */ - public function processCallback(string $input, int $index, array $info = []): array + public function complete(string $input, int $index, array $info = []): array { // Some (Windows?) systems provide incomplete `readline_info`, so let's // try to work around it. @@ -78,15 +74,13 @@ public function processCallback(string $input, int $index, array $info = []): ar } } - $matches = \array_unique($matches); - - return !empty($matches) ? $matches : ['']; + return \array_values(\array_unique($matches)) ?: ['']; } /** * The readline_completion_function callback handler. * - * @see processCallback + * @see AutoCompleter::complete() * * @param string $input * @param int $index @@ -95,7 +89,7 @@ public function processCallback(string $input, int $index, array $info = []): ar */ public function callback(string $input, int $index): array { - return $this->processCallback($input, $index, \readline_info()); + return $this->complete($input, $index, \readline_info()); } /** @@ -103,10 +97,9 @@ public function callback(string $input, int $index): array */ public function __destruct() { - // PHP didn't implement the whole readline API when they first switched - // to libedit. And they still haven't. - if (\function_exists('readline_callback_handler_remove')) { - \readline_callback_handler_remove(); + if (isset($this->readline)) { + $this->readline->deactivateCompletion(); + $this->readline = null; } } } diff --git a/test/ConfigurationTest.php b/test/ConfigurationTest.php index e8d90c60..55340412 100644 --- a/test/ConfigurationTest.php +++ b/test/ConfigurationTest.php @@ -35,7 +35,7 @@ public function testDefaults() { $config = $this->getConfig(); - $this->assertSame(\function_exists('readline'), $config->hasReadline()); + $this->assertSame(\function_exists('readline'), $config->hasNativeReadline()); $this->assertSame(\function_exists('readline'), $config->useReadline()); $this->assertSame(ProcessForker::isSupported(), $config->hasPcntl()); $this->assertSame($config->hasPcntl(), $config->usePcntl()); diff --git a/test/TabCompletion/AutoCompleterTest.php b/test/TabCompletion/AutoCompleterTest.php index 73d59c94..66eec3b4 100644 --- a/test/TabCompletion/AutoCompleterTest.php +++ b/test/TabCompletion/AutoCompleterTest.php @@ -59,7 +59,7 @@ public function testClassesCompletion($line, $mustContain, $mustNotContain) $context->setAll(['foo' => 12, 'bar' => new \DOMDocument()]); - $code = $tabCompletion->processCallback('', 0, [ + $code = $tabCompletion->complete('', 0, [ 'line_buffer' => $line, 'point' => 0, 'end' => \strlen($line),