From 9b33ce3341800dc555ba83068a721d277eecb290 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Sun, 9 Aug 2020 15:54:15 +1200 Subject: [PATCH 01/45] Document the word-break limitations of the Readline completion --- src/TabCompletion/AutoCompleter.php | 86 ++++++++++++++++++- .../Matcher/ClassAttributesMatcher.php | 6 ++ .../Matcher/ClassMethodsMatcher.php | 6 ++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/TabCompletion/AutoCompleter.php b/src/TabCompletion/AutoCompleter.php index 13fcd39ac..59edb6550 100644 --- a/src/TabCompletion/AutoCompleter.php +++ b/src/TabCompletion/AutoCompleter.php @@ -23,6 +23,84 @@ class AutoCompleter /** @var Matcher\AbstractMatcher[] */ protected $matchers; + /** + * The set of characters which separate completeable 'words', and + * therefore determines the precise $input word for which completion + * candidates should be generated (noting that each candidate must + * begin with the original $input text). + * + * PHP's readline support does not provide any control over the + * characters which constitute a word break for completion purposes, + * which means that we are restricted to the default -- being the + * value of GNU Readline's rl_basic_word_break_characters variable: + * + * The basic list of characters that signal a break between words + * for the completer routine. The default value of this variable + * is the characters which break words for completion in Bash: + * " \t\n\"\\’‘@$><=;|&{(". + * + * This limitation has several ramifications for PHP completion: + * + * 1. The namespace separator '\' introduces a word break, and so + * class name completion is on a per-namespace-component basis. + * When completing a namespaced class, the (incomplete) $input + * parameter (and hence the completion candidates we return) will + * not include the separator or preceding namespace components. + * + * 2. The double-colon (nekudotayim) operator '::' does NOT introduce + * a word break (as ':' is not a word break character), and so the + * $input parameter will include the preceding 'ClassName::' text + * (typically back to, but not including, a space character or + * namespace-separator '\'). Completion candidates for class + * attributes and methods must therefore include this same prefix. + * + * 3. The object operator '->' introduces a word break (as '>' is a + * word break character), so when completing an object attribute + * or method, $input will contain only the text following the + * operator, and therefore (unlike '::') the completion candidates + * we return must NOT include the preceding object and operator. + * + * 4. '$' is a word break character, and so completion for variable + * names does not include the leading '$'. The $input parameter + * contains only the text following the '$' and therefore the + * candidates we return must do likewise... + * + * 5. ...Except when we are returning ALL variables (amongst other + * things) as candidates for completing the empty string '', in + * which case we DO need to include the '$' character in each of + * our candidates, because it was not already present in the text. + * (Note that $input will be '' when we are completing either '' + * or '$', so we need to distinguish between those two cases.) + * + * 6. Only a sub-set of other PHP operators constitute (or end with) + * word breaks, and so inconsistent behaviour can be expected if + * operators are used without surrounding whitespace to ensure a + * word break has occurred. + * + * Operators which DO break words include: '>' '<' '<<' '>>' '<>' + * '=' '==' '===' '!=' '!==' '>=' '<=' '<=>' '->' '|' '||' '&' '&&' + * '+=' '-=' '*=' '/=' '.=' '%=' '&=' '|=' '^=' '>>=' '<<=' '??=' + * + * Operators which do NOT break words include: '!' '+' '-' '*' '/' + * '++' '--' '**' '%' '.' '~' '^' '??' '? :' '::' + * + * E.g.: although "foo()+bar()" is valid PHP, we would be unable + * to use completion to obtain the function name "bar" in that + * situation, as the $input string would actually begin with ")+" + * and the Matcher in question would not be returning candidates + * with that prefix. + * + * @see self::processCallback() + * @see \Psy\Shell::getTabCompletions() + */ + public const WORD_BREAK_CHARS = " \t\n\"\\’‘@$><=;|&{("; + + /** + * A regular expression based on WORD_BREAK_CHARS which will match the + * completable word at the end of the string. + */ + public const WORD_REGEXP = "/[^ \t\n\"\\\\’‘@$><=;|&{(]*$/"; + /** * Register a tab completion Matcher. * @@ -42,7 +120,13 @@ public function activate() } /** - * Handle readline completion. + * Handle readline completion for the $input parameter (word). + * + * @see WORD_BREAK_CHARS + * + * @TODO: Post-process the completion candidates returned by each + * Matcher to ensure that they use the common prefix established by + * the $input parameter. * * @param string $input Readline current word * @param int $index Current word index diff --git a/src/TabCompletion/Matcher/ClassAttributesMatcher.php b/src/TabCompletion/Matcher/ClassAttributesMatcher.php index 5ecd4cf8b..e886ef929 100644 --- a/src/TabCompletion/Matcher/ClassAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ClassAttributesMatcher.php @@ -52,6 +52,12 @@ function ($var) { \array_keys($reflection->getConstants()) ); + // We have no control over the word-break characters used by + // Readline's completion, and ':' isn't included in that set, + // which means the $input which AutoCompleter::processCallback() + // is completing includes the preceding "ClassName::" text, and + // therefore the candidate strings we are returning must do + // likewise. return \array_map( function ($name) use ($class) { $chunks = \explode('\\', $class); diff --git a/src/TabCompletion/Matcher/ClassMethodsMatcher.php b/src/TabCompletion/Matcher/ClassMethodsMatcher.php index d980766dd..4cf6ea6b9 100644 --- a/src/TabCompletion/Matcher/ClassMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodsMatcher.php @@ -52,6 +52,12 @@ public function getMatches(array $tokens, array $info = []) return $method->getName(); }, $methods); + // We have no control over the word-break characters used by + // Readline's completion, and ':' isn't included in that set, + // which means the $input which AutoCompleter::processCallback() + // is completing includes the preceding "ClassName::" text, and + // therefore the candidate strings we are returning must do + // likewise. return \array_map( function ($name) use ($class) { $chunks = \explode('\\', $class); From a9f59169e99cf4784c6d4fea32d6d1ac3a285947 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 23 Jul 2020 17:04:50 +1200 Subject: [PATCH 02/45] Add 'completions' command We establish the readline-equivalent $input word for consistency, so that the $input value that we pass to processCallback() will be equivalent to the value passed by GNU Readline in the same situation. This is not strictly necessary for the present code, but may prove to be useful for subsequent enhancements. --- src/Command/CompletionsCommand.php | 63 ++++++++++++++++++++++++++++++ src/Shell.php | 17 ++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/Command/CompletionsCommand.php diff --git a/src/Command/CompletionsCommand.php b/src/Command/CompletionsCommand.php new file mode 100644 index 000000000..25c202680 --- /dev/null +++ b/src/Command/CompletionsCommand.php @@ -0,0 +1,63 @@ +setName('completions') + ->setDefinition([ + new CodeArgument('target', CodeArgument::OPTIONAL, 'PHP code to complete.'), + ]) + ->setDescription('List possible code completions for the input.') + ->setHelp( + <<<'HELP' +This command enables PsySH wrappers to obtain completions for the current +input, for the purpose of implementing their own completion UI. +HELP + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $target = $input->getArgument('target'); + if (!isset($target)) { + $target = ''; + } + + // n.b. All of the relevant parts of \Psy\Shell are protected + // or private, so getTabCompletions() itself is a Shell method. + $completions = $this->getApplication()->getTabCompletions($target); + + // Ouput the completion candidates as newline-separated text. + $str = implode("\n", array_filter($completions))."\n"; + $output->write($str, false, OutputInterface::OUTPUT_RAW); + + return 0; + } +} diff --git a/src/Shell.php b/src/Shell.php index de678bbba..966645796 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -237,6 +237,23 @@ protected function getTabCompletionMatchers() @\trigger_error('getTabCompletionMatchers is no longer used', \E_USER_DEPRECATED); } + /** + * Get completion matches. + * + * @return array An array of completion matches for $input + */ + public function getTabCompletions(string $input) + { + $ac = $this->autoCompleter; + $word = ''; + $regexp = $ac::WORD_REGEXP; + $matches = []; + if (preg_match($regexp, $input, $matches) === 1) { + $word = $matches[0]; + } + return $ac->processCallback($word, null, ['line_buffer' => $input]); + } + /** * Gets the default command loop listeners. * From 05afb93770d43a67f9d1bd6397da14c383692e61 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Sun, 9 Aug 2020 17:01:59 +1200 Subject: [PATCH 03/45] Add 'completions' to the needCompleteClass() list --- src/TabCompletion/Matcher/AbstractMatcher.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index fe66f5fca..211254fea 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -171,9 +171,17 @@ public static function isOperator($token) return \strpos(self::MISC_OPERATORS, $token) !== false; } + /** + * Used both to test $tokens[1] (i.e. following T_OPEN_TAG) to + * see whether it's a PsySH introspection command, and also by + * self::getNamespaceAndClass() to prevent these commands from + * being considered part of the namespace (which could happen + * on account of all the whitespace tokens having been removed + * from the tokens array by AutoCompleter::processCallback(). + */ public static function needCompleteClass($token) { - return \in_array($token[1], ['doc', 'ls', 'show']); + return \in_array($token[1], ['doc', 'ls', 'show', 'completions']); } /** From d177094dd7ae8c699b4cfd0e2b91a33663e57c40 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 23 Jul 2020 16:58:22 +1200 Subject: [PATCH 04/45] Make AbstractMatcher::startsWith() treat $prefix as a verbatim string ClassNamesMatcher was the only matcher that was using preg_quote(); all of the others mis-handled regexp special characters. --- src/TabCompletion/Matcher/AbstractMatcher.php | 2 +- src/TabCompletion/Matcher/ClassNamesMatcher.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 211254fea..c8b90635e 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -116,7 +116,7 @@ abstract public function getMatches(array $tokens, array $info = []); */ public static function startsWith($prefix, $word) { - return \preg_match(\sprintf('#^%s#', $prefix), $word); + return \preg_match(\sprintf('#^%s#', \preg_quote($prefix)), $word); } /** diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index 7b17830dd..19be00274 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -29,7 +29,6 @@ public function getMatches(array $tokens, array $info = []) if (\strlen($class) > 0 && $class[0] === '\\') { $class = \substr($class, 1, \strlen($class)); } - $quotedClass = \preg_quote($class); return \array_map( function ($className) use ($class) { @@ -41,8 +40,8 @@ function ($className) use ($class) { }, \array_filter( \array_merge(\get_declared_classes(), \get_declared_interfaces()), - function ($className) use ($quotedClass) { - return AbstractMatcher::startsWith($quotedClass, $className); + function ($className) use ($class) { + return AbstractMatcher::startsWith($class, $className); } ) ); From 025784a41f6f1e9bc48812989504d7e2c20859d7 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 18:30:17 +1200 Subject: [PATCH 05/45] Add tokenIsValidIdentifier() for generic tab-completion tests This will be used in place of tests for the T_STRING token, which is inherently unreliable for completion purposes, because an incomplete identifier can be any of ~70 different tokens. --- src/TabCompletion/Matcher/AbstractMatcher.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index c8b90635e..a475498a8 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -171,6 +171,23 @@ public static function isOperator($token) return \strpos(self::MISC_OPERATORS, $token) !== false; } + /** + * Check whether $token is a valid prefix for a PHP identifier. + * + * @param mixed $token A PHP token (see token_get_all) + * @param bool $allowEmpty Whether an empty string is valid. + * + * @return bool + */ + public static function tokenIsValidIdentifier($token, bool $allowEmpty = false) + { + // See AutoCompleter::processCallback() regarding the '' token. + if ($token === '') { + return $allowEmpty; + } + return self::hasSyntax($token, self::CONSTANT_SYNTAX); + } + /** * Used both to test $tokens[1] (i.e. following T_OPEN_TAG) to * see whether it's a PsySH introspection command, and also by From 6b8a9d13c2177948b894bcbfc2b9efd34fc158ad Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 23 Jul 2020 17:02:47 +1200 Subject: [PATCH 06/45] Use tokenIsValidIdentifier() in getInput() instead of matching T_STRING --- src/TabCompletion/Matcher/AbstractMatcher.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index a475498a8..621c5d9b0 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -58,15 +58,24 @@ public function hasMatched(array $tokens) * Get current readline input word. * * @param array $tokens Tokenized readline input (see token_get_all) + * @param array|null $t_valid Acceptable tokens. If null then strings + * which are valid identifiers (or empty) are considered valid. * - * @return string + * @return string|bool */ - protected function getInput(array $tokens) + protected function getInput(array $tokens, array $t_valid = null) { $var = ''; - $firstToken = \array_pop($tokens); - if (self::tokenIs($firstToken, self::T_STRING)) { - $var = $firstToken[1]; + $token = \array_pop($tokens); + $input = \is_array($token) ? $token[1] : $token; + + if (isset($t_valid)) { + if (self::hasToken($t_valid, $token)) { + $var = $input; + } + } + elseif (self::tokenIsValidIdentifier($token, true)) { + $var = $input; } return $var; From 69739d10d5e42ab5ec017808c61d0b5e3f0a3f20 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 18:40:17 +1200 Subject: [PATCH 07/45] Renaming variables for consistency Unless there are multiple token variables being acted upon, name the completion candidate $input, for consistency with the other Matchers. --- src/TabCompletion/Matcher/ConstantsMatcher.php | 6 +++--- src/TabCompletion/Matcher/FunctionsMatcher.php | 6 +++--- src/TabCompletion/Matcher/VariablesMatcher.php | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/TabCompletion/Matcher/ConstantsMatcher.php b/src/TabCompletion/Matcher/ConstantsMatcher.php index 178adf8c2..877d5c331 100644 --- a/src/TabCompletion/Matcher/ConstantsMatcher.php +++ b/src/TabCompletion/Matcher/ConstantsMatcher.php @@ -25,10 +25,10 @@ class ConstantsMatcher extends AbstractMatcher */ public function getMatches(array $tokens, array $info = []) { - $const = $this->getInput($tokens); + $input = $this->getInput($tokens); - return \array_filter(\array_keys(\get_defined_constants()), function ($constant) use ($const) { - return AbstractMatcher::startsWith($const, $constant); + return \array_filter(\array_keys(\get_defined_constants()), function ($constant) use ($input) { + return AbstractMatcher::startsWith($input, $constant); }); } diff --git a/src/TabCompletion/Matcher/FunctionsMatcher.php b/src/TabCompletion/Matcher/FunctionsMatcher.php index 1f6e6dbb5..8f2187385 100644 --- a/src/TabCompletion/Matcher/FunctionsMatcher.php +++ b/src/TabCompletion/Matcher/FunctionsMatcher.php @@ -25,13 +25,13 @@ class FunctionsMatcher extends AbstractMatcher */ public function getMatches(array $tokens, array $info = []) { - $func = $this->getInput($tokens); + $input = $this->getInput($tokens); $functions = \get_defined_functions(); $allFunctions = \array_merge($functions['user'], $functions['internal']); - return \array_filter($allFunctions, function ($function) use ($func) { - return AbstractMatcher::startsWith($func, $function); + return \array_filter($allFunctions, function ($function) use ($input) { + return AbstractMatcher::startsWith($input, $function); }); } diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index f2438d3d1..3a20dc88b 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -25,10 +25,10 @@ class VariablesMatcher extends AbstractContextAwareMatcher */ public function getMatches(array $tokens, array $info = []) { - $var = \str_replace('$', '', $this->getInput($tokens)); + $input = \str_replace('$', '', $this->getInput($tokens)); - return \array_filter(\array_keys($this->getVariables()), function ($variable) use ($var) { - return AbstractMatcher::startsWith($var, $variable); + return \array_filter(\array_keys($this->getVariables()), function ($variable) use ($input) { + return AbstractMatcher::startsWith($input, $variable); }); } From 4bb289842c1d4bfade47415291126ca6485a007d Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 20:21:38 +1200 Subject: [PATCH 08/45] Do not return completions if valid input was not obtained --- src/TabCompletion/Matcher/AbstractMatcher.php | 7 +++---- src/TabCompletion/Matcher/ClassAttributesMatcher.php | 3 +++ src/TabCompletion/Matcher/ClassMethodsMatcher.php | 3 +++ src/TabCompletion/Matcher/CommandsMatcher.php | 3 +++ src/TabCompletion/Matcher/ConstantsMatcher.php | 3 +++ src/TabCompletion/Matcher/FunctionsMatcher.php | 3 +++ src/TabCompletion/Matcher/KeywordsMatcher.php | 3 +++ src/TabCompletion/Matcher/MongoClientMatcher.php | 3 +++ src/TabCompletion/Matcher/MongoDatabaseMatcher.php | 3 +++ src/TabCompletion/Matcher/ObjectAttributesMatcher.php | 3 +++ src/TabCompletion/Matcher/ObjectMethodsMatcher.php | 3 +++ src/TabCompletion/Matcher/VariablesMatcher.php | 3 +++ 12 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 621c5d9b0..bac5d8ea3 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -65,20 +65,19 @@ public function hasMatched(array $tokens) */ protected function getInput(array $tokens, array $t_valid = null) { - $var = ''; $token = \array_pop($tokens); $input = \is_array($token) ? $token[1] : $token; if (isset($t_valid)) { if (self::hasToken($t_valid, $token)) { - $var = $input; + return $input; } } elseif (self::tokenIsValidIdentifier($token, true)) { - $var = $input; + return $input; } - return $var; + return false; } /** diff --git a/src/TabCompletion/Matcher/ClassAttributesMatcher.php b/src/TabCompletion/Matcher/ClassAttributesMatcher.php index e886ef929..387c7a2e3 100644 --- a/src/TabCompletion/Matcher/ClassAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ClassAttributesMatcher.php @@ -27,6 +27,9 @@ class ClassAttributesMatcher extends AbstractMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { diff --git a/src/TabCompletion/Matcher/ClassMethodsMatcher.php b/src/TabCompletion/Matcher/ClassMethodsMatcher.php index 4cf6ea6b9..6070cc7ea 100644 --- a/src/TabCompletion/Matcher/ClassMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodsMatcher.php @@ -27,6 +27,9 @@ class ClassMethodsMatcher extends AbstractMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { diff --git a/src/TabCompletion/Matcher/CommandsMatcher.php b/src/TabCompletion/Matcher/CommandsMatcher.php index bdeb45d4c..20a10d71c 100644 --- a/src/TabCompletion/Matcher/CommandsMatcher.php +++ b/src/TabCompletion/Matcher/CommandsMatcher.php @@ -87,6 +87,9 @@ protected function matchCommand($name) public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } return \array_filter($this->commands, function ($command) use ($input) { return AbstractMatcher::startsWith($input, $command); diff --git a/src/TabCompletion/Matcher/ConstantsMatcher.php b/src/TabCompletion/Matcher/ConstantsMatcher.php index 877d5c331..908ffc1c4 100644 --- a/src/TabCompletion/Matcher/ConstantsMatcher.php +++ b/src/TabCompletion/Matcher/ConstantsMatcher.php @@ -26,6 +26,9 @@ class ConstantsMatcher extends AbstractMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } return \array_filter(\array_keys(\get_defined_constants()), function ($constant) use ($input) { return AbstractMatcher::startsWith($input, $constant); diff --git a/src/TabCompletion/Matcher/FunctionsMatcher.php b/src/TabCompletion/Matcher/FunctionsMatcher.php index 8f2187385..4c7d6b197 100644 --- a/src/TabCompletion/Matcher/FunctionsMatcher.php +++ b/src/TabCompletion/Matcher/FunctionsMatcher.php @@ -26,6 +26,9 @@ class FunctionsMatcher extends AbstractMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } $functions = \get_defined_functions(); $allFunctions = \array_merge($functions['user'], $functions['internal']); diff --git a/src/TabCompletion/Matcher/KeywordsMatcher.php b/src/TabCompletion/Matcher/KeywordsMatcher.php index 393674c62..c441c4ddc 100644 --- a/src/TabCompletion/Matcher/KeywordsMatcher.php +++ b/src/TabCompletion/Matcher/KeywordsMatcher.php @@ -57,6 +57,9 @@ public function isKeyword($keyword) public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } return \array_filter($this->keywords, function ($keyword) use ($input) { return AbstractMatcher::startsWith($input, $keyword); diff --git a/src/TabCompletion/Matcher/MongoClientMatcher.php b/src/TabCompletion/Matcher/MongoClientMatcher.php index f38836d02..c5815c294 100644 --- a/src/TabCompletion/Matcher/MongoClientMatcher.php +++ b/src/TabCompletion/Matcher/MongoClientMatcher.php @@ -26,6 +26,9 @@ class MongoClientMatcher extends AbstractContextAwareMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { diff --git a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php index a0d4d49c7..7df5d801c 100644 --- a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php +++ b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php @@ -26,6 +26,9 @@ class MongoDatabaseMatcher extends AbstractContextAwareMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { diff --git a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php index 8b3d29057..473feff5a 100644 --- a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php @@ -29,6 +29,9 @@ class ObjectAttributesMatcher extends AbstractContextAwareMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { diff --git a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php index 2a1d22407..f7f0bc6ac 100644 --- a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php @@ -29,6 +29,9 @@ class ObjectMethodsMatcher extends AbstractContextAwareMatcher public function getMatches(array $tokens, array $info = []) { $input = $this->getInput($tokens); + if ($input === false) { + return []; + } $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index 3a20dc88b..a623d5471 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -26,6 +26,9 @@ class VariablesMatcher extends AbstractContextAwareMatcher public function getMatches(array $tokens, array $info = []) { $input = \str_replace('$', '', $this->getInput($tokens)); + if ($input === false) { + return []; + } return \array_filter(\array_keys($this->getVariables()), function ($variable) use ($input) { return AbstractMatcher::startsWith($input, $variable); From f40b749f49fdf322af30f47de9ab9af4c56f9b53 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 20:43:25 +1200 Subject: [PATCH 09/45] Add missing documentation --- .../Matcher/ClassMethodDefaultParametersMatcher.php | 11 +++++++++++ .../Matcher/FunctionDefaultParametersMatcher.php | 11 +++++++++++ .../Matcher/ObjectMethodDefaultParametersMatcher.php | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php b/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php index d88cb69b6..ac6789a52 100644 --- a/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php @@ -11,8 +11,16 @@ namespace Psy\TabCompletion\Matcher; +/** + * A class method tab completion Matcher. + * + * This provides completions for all parameters of the specifed method. + */ class ClassMethodDefaultParametersMatcher extends AbstractDefaultParametersMatcher { + /** + * {@inheritdoc} + */ public function getMatches(array $tokens, array $info = []) { $openBracket = \array_pop($tokens); @@ -39,6 +47,9 @@ public function getMatches(array $tokens, array $info = []) return []; } + /** + * {@inheritdoc} + */ public function hasMatched(array $tokens) { $openBracket = \array_pop($tokens); diff --git a/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php b/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php index e1277c2ee..226f5a934 100644 --- a/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php @@ -11,8 +11,16 @@ namespace Psy\TabCompletion\Matcher; +/** + * A function parameter tab completion Matcher. + * + * This provides completions for all parameters of the specifed function. + */ class FunctionDefaultParametersMatcher extends AbstractDefaultParametersMatcher { + /** + * {@inheritdoc} + */ public function getMatches(array $tokens, array $info = []) { \array_pop($tokens); // open bracket @@ -30,6 +38,9 @@ public function getMatches(array $tokens, array $info = []) return $this->getDefaultParameterCompletion($parameters); } + /** + * {@inheritdoc} + */ public function hasMatched(array $tokens) { $openBracket = \array_pop($tokens); diff --git a/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php b/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php index 08bab8bc9..e431fa4e1 100644 --- a/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php @@ -11,8 +11,16 @@ namespace Psy\TabCompletion\Matcher; +/** + * An object method parameter tab completion Matcher. + * + * This provides completions for all parameters of the specifed method. + */ class ObjectMethodDefaultParametersMatcher extends AbstractDefaultParametersMatcher { + /** + * {@inheritdoc} + */ public function getMatches(array $tokens, array $info = []) { $openBracket = \array_pop($tokens); @@ -46,6 +54,9 @@ public function getMatches(array $tokens, array $info = []) return []; } + /** + * {@inheritdoc} + */ public function hasMatched(array $tokens) { $openBracket = \array_pop($tokens); From 20a5c0a0a25c03f3fb6d1e14c235f32c1b55fd6d Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 18:51:34 +1200 Subject: [PATCH 10/45] Bug fix for ClassNamesMatcher::hasMatched() We rejected plain '$' as $token, so don't allow self::T_VARIABLE. T_OPEN_TAG is fine, but was already handled. --- src/TabCompletion/Matcher/ClassNamesMatcher.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index 19be00274..48b00f981 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -66,7 +66,6 @@ public function hasMatched(array $tokens) return false; case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR, self::T_STRING], $prevToken): case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $token): - case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): case self::isOperator($token): return true; } From 3c92986c56eb62646cf12265faf3e205e127baed Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 19:52:50 +1200 Subject: [PATCH 11/45] Bug fix for KeywordsMatcher::hasMatched() A T_VARIABLE cannot be completed to a Keyword. --- src/TabCompletion/Matcher/KeywordsMatcher.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/TabCompletion/Matcher/KeywordsMatcher.php b/src/TabCompletion/Matcher/KeywordsMatcher.php index c441c4ddc..fb32f8387 100644 --- a/src/TabCompletion/Matcher/KeywordsMatcher.php +++ b/src/TabCompletion/Matcher/KeywordsMatcher.php @@ -75,10 +75,8 @@ public function hasMatched(array $tokens) $prevToken = \array_pop($tokens); switch (true) { - case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): -// case is_string($token) && $token === '$': - case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $prevToken) && - self::tokenIs($token, self::T_STRING): + case self::tokenIs($token, self::T_OPEN_TAG): + case self::tokenIs($prevToken, self::T_OPEN_TAG) && self::tokenIs($token, self::T_STRING): case self::isOperator($token): return true; } From 17d1c4b8217cd2201d7512c48fd654e9dab34b57 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 21:26:01 +1200 Subject: [PATCH 12/45] Bug fix for CommandsMatcher::hasMatched() Don't assume that one command isn't the prefix of another. Do the cheap empty() test first. --- src/TabCompletion/Matcher/CommandsMatcher.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/TabCompletion/Matcher/CommandsMatcher.php b/src/TabCompletion/Matcher/CommandsMatcher.php index 20a10d71c..c6c4d0ab0 100644 --- a/src/TabCompletion/Matcher/CommandsMatcher.php +++ b/src/TabCompletion/Matcher/CommandsMatcher.php @@ -105,10 +105,9 @@ public function hasMatched(array $tokens) $command = \array_shift($tokens); switch (true) { - case self::tokenIs($command, self::T_STRING) && - !$this->isCommand($command[1]) && - $this->matchCommand($command[1]) && - empty($tokens): + case empty($tokens) && + self::tokenIs($command, self::T_STRING) && + $this->matchCommand($command[1]): return true; } From e0c0b2a82c96fc4a6335062668e6f6d306f01277 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 21:26:30 +1200 Subject: [PATCH 13/45] Use tokenIsValidIdentifier() instead of T_STRING matching in hasMatched() methods --- src/TabCompletion/Matcher/ClassAttributesMatcher.php | 3 ++- .../Matcher/ClassMethodDefaultParametersMatcher.php | 2 +- src/TabCompletion/Matcher/ClassMethodsMatcher.php | 3 ++- src/TabCompletion/Matcher/ClassNamesMatcher.php | 3 ++- src/TabCompletion/Matcher/CommandsMatcher.php | 2 +- src/TabCompletion/Matcher/ConstantsMatcher.php | 3 ++- src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php | 2 +- src/TabCompletion/Matcher/FunctionsMatcher.php | 3 ++- src/TabCompletion/Matcher/KeywordsMatcher.php | 3 ++- .../Matcher/ObjectMethodDefaultParametersMatcher.php | 2 +- 10 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/TabCompletion/Matcher/ClassAttributesMatcher.php b/src/TabCompletion/Matcher/ClassAttributesMatcher.php index 387c7a2e3..8b87eae02 100644 --- a/src/TabCompletion/Matcher/ClassAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ClassAttributesMatcher.php @@ -86,7 +86,8 @@ public function hasMatched(array $tokens) $prevToken = \array_pop($tokens); switch (true) { - case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): + case self::tokenIs($prevToken, self::T_DOUBLE_COLON): + return self::tokenIsValidIdentifier($token, true); case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } diff --git a/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php b/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php index ac6789a52..8d0ca2c33 100644 --- a/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php @@ -60,7 +60,7 @@ public function hasMatched(array $tokens) $functionName = \array_pop($tokens); - if (!self::tokenIs($functionName, self::T_STRING)) { + if (!self::tokenIsValidIdentifier($functionName)) { return false; } diff --git a/src/TabCompletion/Matcher/ClassMethodsMatcher.php b/src/TabCompletion/Matcher/ClassMethodsMatcher.php index 6070cc7ea..3f621d9fa 100644 --- a/src/TabCompletion/Matcher/ClassMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodsMatcher.php @@ -83,7 +83,8 @@ public function hasMatched(array $tokens) $prevToken = \array_pop($tokens); switch (true) { - case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): + case self::tokenIs($prevToken, self::T_DOUBLE_COLON): + return self::tokenIsValidIdentifier($token, true); case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index 48b00f981..f5cf0d6df 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -64,7 +64,8 @@ public function hasMatched(array $tokens) case self::hasToken([$blacklistedTokens], $prevToken): case \is_string($token) && $token === '$': return false; - case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR, self::T_STRING], $prevToken): + case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $prevToken): + return self::tokenIsValidIdentifier($token, true); case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $token): case self::isOperator($token): return true; diff --git a/src/TabCompletion/Matcher/CommandsMatcher.php b/src/TabCompletion/Matcher/CommandsMatcher.php index c6c4d0ab0..969a18e1e 100644 --- a/src/TabCompletion/Matcher/CommandsMatcher.php +++ b/src/TabCompletion/Matcher/CommandsMatcher.php @@ -106,7 +106,7 @@ public function hasMatched(array $tokens) switch (true) { case empty($tokens) && - self::tokenIs($command, self::T_STRING) && + self::tokenIsValidIdentifier($command, true) && $this->matchCommand($command[1]): return true; } diff --git a/src/TabCompletion/Matcher/ConstantsMatcher.php b/src/TabCompletion/Matcher/ConstantsMatcher.php index 908ffc1c4..b612e0bf5 100644 --- a/src/TabCompletion/Matcher/ConstantsMatcher.php +++ b/src/TabCompletion/Matcher/ConstantsMatcher.php @@ -47,8 +47,9 @@ public function hasMatched(array $tokens) case self::tokenIs($prevToken, self::T_NEW): case self::tokenIs($prevToken, self::T_NS_SEPARATOR): return false; - case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): + case self::tokenIs($token, self::T_OPEN_TAG): case self::isOperator($token): + case self::tokenIsValidIdentifier($token, true); return true; } diff --git a/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php b/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php index 226f5a934..abdb7153b 100644 --- a/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php @@ -51,7 +51,7 @@ public function hasMatched(array $tokens) $functionName = \array_pop($tokens); - if (!self::tokenIs($functionName, self::T_STRING)) { + if (!self::tokenIsValidIdentifier($functionName)) { return false; } diff --git a/src/TabCompletion/Matcher/FunctionsMatcher.php b/src/TabCompletion/Matcher/FunctionsMatcher.php index 4c7d6b197..c38cd4051 100644 --- a/src/TabCompletion/Matcher/FunctionsMatcher.php +++ b/src/TabCompletion/Matcher/FunctionsMatcher.php @@ -49,8 +49,9 @@ public function hasMatched(array $tokens) switch (true) { case self::tokenIs($prevToken, self::T_NEW): return false; - case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): + case self::hasToken([self::T_OPEN_TAG], $token): case self::isOperator($token): + case self::tokenIsValidIdentifier($token, true): return true; } diff --git a/src/TabCompletion/Matcher/KeywordsMatcher.php b/src/TabCompletion/Matcher/KeywordsMatcher.php index fb32f8387..cdeca928b 100644 --- a/src/TabCompletion/Matcher/KeywordsMatcher.php +++ b/src/TabCompletion/Matcher/KeywordsMatcher.php @@ -75,8 +75,9 @@ public function hasMatched(array $tokens) $prevToken = \array_pop($tokens); switch (true) { + case self::tokenIs($prevToken, self::T_OPEN_TAG): + return self::tokenIsValidIdentifier($token, true); case self::tokenIs($token, self::T_OPEN_TAG): - case self::tokenIs($prevToken, self::T_OPEN_TAG) && self::tokenIs($token, self::T_STRING): case self::isOperator($token): return true; } diff --git a/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php b/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php index e431fa4e1..e2fed51b5 100644 --- a/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php @@ -67,7 +67,7 @@ public function hasMatched(array $tokens) $functionName = \array_pop($tokens); - if (!self::tokenIs($functionName, self::T_STRING)) { + if (!self::tokenIsValidIdentifier($functionName)) { return false; } From f206480240a4f1d3d1b39f09919ae4501f18f9ab Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 19:37:01 +1200 Subject: [PATCH 14/45] Use tokenIsValidIdentifier() with T_OBJECT_OPERATOR matching in hasMatched() methods tokenIsValidIdentifier($token, true) allows the empty string to match. --- src/TabCompletion/Matcher/MongoClientMatcher.php | 3 ++- src/TabCompletion/Matcher/MongoDatabaseMatcher.php | 3 ++- src/TabCompletion/Matcher/ObjectAttributesMatcher.php | 3 ++- src/TabCompletion/Matcher/ObjectMethodsMatcher.php | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/TabCompletion/Matcher/MongoClientMatcher.php b/src/TabCompletion/Matcher/MongoClientMatcher.php index c5815c294..7b1732d42 100644 --- a/src/TabCompletion/Matcher/MongoClientMatcher.php +++ b/src/TabCompletion/Matcher/MongoClientMatcher.php @@ -65,8 +65,9 @@ public function hasMatched(array $tokens) switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): - case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; + case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): + return self::tokenIsValidIdentifier($token, true); } return false; diff --git a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php index 7df5d801c..89d4989d2 100644 --- a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php +++ b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php @@ -61,8 +61,9 @@ public function hasMatched(array $tokens) switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): - case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; + case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): + return self::tokenIsValidIdentifier($token, true); } return false; diff --git a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php index 473feff5a..09a7fa08f 100644 --- a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php @@ -72,8 +72,9 @@ public function hasMatched(array $tokens) switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): - case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; + case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): + return self::tokenIsValidIdentifier($token, true); } return false; diff --git a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php index f7f0bc6ac..a6b32e9e8 100644 --- a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php @@ -74,8 +74,9 @@ public function hasMatched(array $tokens) switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): - case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; + case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): + return self::tokenIsValidIdentifier($token, true); } return false; From 3cd36d0048f1fd6657c24bcdf8faf72378b17cc1 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 19:46:55 +1200 Subject: [PATCH 15/45] Comments --- src/TabCompletion/Matcher/ClassAttributesMatcher.php | 1 + .../Matcher/ClassMethodDefaultParametersMatcher.php | 1 + src/TabCompletion/Matcher/ClassMethodsMatcher.php | 1 + src/TabCompletion/Matcher/ClassNamesMatcher.php | 3 +++ src/TabCompletion/Matcher/CommandsMatcher.php | 1 + src/TabCompletion/Matcher/ConstantsMatcher.php | 2 ++ src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php | 1 + src/TabCompletion/Matcher/FunctionsMatcher.php | 2 ++ src/TabCompletion/Matcher/KeywordsMatcher.php | 2 ++ src/TabCompletion/Matcher/MongoClientMatcher.php | 1 + src/TabCompletion/Matcher/MongoDatabaseMatcher.php | 1 + src/TabCompletion/Matcher/ObjectAttributesMatcher.php | 1 + .../Matcher/ObjectMethodDefaultParametersMatcher.php | 1 + src/TabCompletion/Matcher/ObjectMethodsMatcher.php | 1 + 14 files changed, 19 insertions(+) diff --git a/src/TabCompletion/Matcher/ClassAttributesMatcher.php b/src/TabCompletion/Matcher/ClassAttributesMatcher.php index 8b87eae02..1c4c92f5f 100644 --- a/src/TabCompletion/Matcher/ClassAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ClassAttributesMatcher.php @@ -85,6 +85,7 @@ public function hasMatched(array $tokens) $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + // Valid following '::'. switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON): return self::tokenIsValidIdentifier($token, true); diff --git a/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php b/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php index 8d0ca2c33..d5861c62d 100644 --- a/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php @@ -52,6 +52,7 @@ public function getMatches(array $tokens, array $info = []) */ public function hasMatched(array $tokens) { + // Valid following '::METHOD('. $openBracket = \array_pop($tokens); if ($openBracket !== '(') { diff --git a/src/TabCompletion/Matcher/ClassMethodsMatcher.php b/src/TabCompletion/Matcher/ClassMethodsMatcher.php index 3f621d9fa..13d4f055d 100644 --- a/src/TabCompletion/Matcher/ClassMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodsMatcher.php @@ -82,6 +82,7 @@ public function hasMatched(array $tokens) $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + // Valid following '::'. switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON): return self::tokenIsValidIdentifier($token, true); diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index f5cf0d6df..d6c4fc39c 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -60,12 +60,15 @@ public function hasMatched(array $tokens) ]; switch (true) { + // Blacklisted. case self::hasToken([$blacklistedTokens], $token): case self::hasToken([$blacklistedTokens], $prevToken): case \is_string($token) && $token === '$': return false; + // Previous token. case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $prevToken): return self::tokenIsValidIdentifier($token, true); + // Current token (whitelist). case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $token): case self::isOperator($token): return true; diff --git a/src/TabCompletion/Matcher/CommandsMatcher.php b/src/TabCompletion/Matcher/CommandsMatcher.php index 969a18e1e..e03d76a3a 100644 --- a/src/TabCompletion/Matcher/CommandsMatcher.php +++ b/src/TabCompletion/Matcher/CommandsMatcher.php @@ -104,6 +104,7 @@ public function hasMatched(array $tokens) /* $openTag */ \array_shift($tokens); $command = \array_shift($tokens); + // Valid for completion only if this was the only token. switch (true) { case empty($tokens) && self::tokenIsValidIdentifier($command, true) && diff --git a/src/TabCompletion/Matcher/ConstantsMatcher.php b/src/TabCompletion/Matcher/ConstantsMatcher.php index b612e0bf5..bcbfd48cd 100644 --- a/src/TabCompletion/Matcher/ConstantsMatcher.php +++ b/src/TabCompletion/Matcher/ConstantsMatcher.php @@ -44,9 +44,11 @@ public function hasMatched(array $tokens) $prevToken = \array_pop($tokens); switch (true) { + // Previous token (blacklist). case self::tokenIs($prevToken, self::T_NEW): case self::tokenIs($prevToken, self::T_NS_SEPARATOR): return false; + // Current token (whitelist). case self::tokenIs($token, self::T_OPEN_TAG): case self::isOperator($token): case self::tokenIsValidIdentifier($token, true); diff --git a/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php b/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php index abdb7153b..2f5f820b3 100644 --- a/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php @@ -43,6 +43,7 @@ public function getMatches(array $tokens, array $info = []) */ public function hasMatched(array $tokens) { + // Valid following 'FUNCTION('. $openBracket = \array_pop($tokens); if ($openBracket !== '(') { diff --git a/src/TabCompletion/Matcher/FunctionsMatcher.php b/src/TabCompletion/Matcher/FunctionsMatcher.php index c38cd4051..c2231e6eb 100644 --- a/src/TabCompletion/Matcher/FunctionsMatcher.php +++ b/src/TabCompletion/Matcher/FunctionsMatcher.php @@ -47,8 +47,10 @@ public function hasMatched(array $tokens) $prevToken = \array_pop($tokens); switch (true) { + // Previous token (blacklist). case self::tokenIs($prevToken, self::T_NEW): return false; + // Current token (whitelist). case self::hasToken([self::T_OPEN_TAG], $token): case self::isOperator($token): case self::tokenIsValidIdentifier($token, true): diff --git a/src/TabCompletion/Matcher/KeywordsMatcher.php b/src/TabCompletion/Matcher/KeywordsMatcher.php index cdeca928b..e34ed57e6 100644 --- a/src/TabCompletion/Matcher/KeywordsMatcher.php +++ b/src/TabCompletion/Matcher/KeywordsMatcher.php @@ -75,8 +75,10 @@ public function hasMatched(array $tokens) $prevToken = \array_pop($tokens); switch (true) { + // Previous token. case self::tokenIs($prevToken, self::T_OPEN_TAG): return self::tokenIsValidIdentifier($token, true); + // Current token (whitelist). case self::tokenIs($token, self::T_OPEN_TAG): case self::isOperator($token): return true; diff --git a/src/TabCompletion/Matcher/MongoClientMatcher.php b/src/TabCompletion/Matcher/MongoClientMatcher.php index 7b1732d42..3d905002b 100644 --- a/src/TabCompletion/Matcher/MongoClientMatcher.php +++ b/src/TabCompletion/Matcher/MongoClientMatcher.php @@ -63,6 +63,7 @@ public function hasMatched(array $tokens) $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + // Valid following '->'. switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): return true; diff --git a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php index 89d4989d2..341e63215 100644 --- a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php +++ b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php @@ -59,6 +59,7 @@ public function hasMatched(array $tokens) $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + // Valid following '->'. switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): return true; diff --git a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php index 09a7fa08f..5ccb88edc 100644 --- a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php @@ -70,6 +70,7 @@ public function hasMatched(array $tokens) $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + // Valid following '->'. switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): return true; diff --git a/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php b/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php index e2fed51b5..d75a84260 100644 --- a/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php +++ b/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php @@ -59,6 +59,7 @@ public function getMatches(array $tokens, array $info = []) */ public function hasMatched(array $tokens) { + // Valid following '->METHOD('. $openBracket = \array_pop($tokens); if ($openBracket !== '(') { diff --git a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php index a6b32e9e8..c6ded5113 100644 --- a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php @@ -72,6 +72,7 @@ public function hasMatched(array $tokens) $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + // Valid following '->'. switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): return true; From 63ae75d3ee8b4bf1ce8504069b0d4201b1037f92 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Sun, 9 Aug 2020 16:50:35 +1200 Subject: [PATCH 16/45] Minor refactoring - Optimisation for ClassAttributesMatcher and ClassMethodsMatcher. - Also remove additional redundant type-checking, already catered for by '===' equality test. --- .../Matcher/ClassAttributesMatcher.php | 16 ++++++---------- .../Matcher/ClassMethodsMatcher.php | 7 +++---- src/TabCompletion/Matcher/ClassNamesMatcher.php | 2 +- src/TabCompletion/Matcher/VariablesMatcher.php | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/TabCompletion/Matcher/ClassAttributesMatcher.php b/src/TabCompletion/Matcher/ClassAttributesMatcher.php index 1c4c92f5f..5634f97ec 100644 --- a/src/TabCompletion/Matcher/ClassAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ClassAttributesMatcher.php @@ -38,6 +38,8 @@ public function getMatches(array $tokens, array $info = []) } $class = $this->getNamespaceAndClass($tokens); + $chunks = \explode('\\', $class); + $className = \array_pop($chunks); try { $reflection = new \ReflectionClass($class); @@ -62,18 +64,12 @@ function ($var) { // therefore the candidate strings we are returning must do // likewise. return \array_map( - function ($name) use ($class) { - $chunks = \explode('\\', $class); - $className = \array_pop($chunks); - + function ($name) use ($className) { return $className.'::'.$name; }, - \array_filter( - $vars, - function ($var) use ($input) { - return AbstractMatcher::startsWith($input, $var); - } - ) + \array_filter($vars, function ($var) use ($input) { + return AbstractMatcher::startsWith($input, $var); + }) ); } diff --git a/src/TabCompletion/Matcher/ClassMethodsMatcher.php b/src/TabCompletion/Matcher/ClassMethodsMatcher.php index 13d4f055d..b95a8f53f 100644 --- a/src/TabCompletion/Matcher/ClassMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodsMatcher.php @@ -38,6 +38,8 @@ public function getMatches(array $tokens, array $info = []) } $class = $this->getNamespaceAndClass($tokens); + $chunks = \explode('\\', $class); + $className = \array_pop($chunks); try { $reflection = new \ReflectionClass($class); @@ -62,10 +64,7 @@ public function getMatches(array $tokens, array $info = []) // therefore the candidate strings we are returning must do // likewise. return \array_map( - function ($name) use ($class) { - $chunks = \explode('\\', $class); - $className = \array_pop($chunks); - + function ($name) use ($className) { return $className.'::'.$name; }, \array_filter($methods, function ($method) use ($input) { diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index d6c4fc39c..2f5b0a814 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -63,7 +63,7 @@ public function hasMatched(array $tokens) // Blacklisted. case self::hasToken([$blacklistedTokens], $token): case self::hasToken([$blacklistedTokens], $prevToken): - case \is_string($token) && $token === '$': + case $token === '$': return false; // Previous token. case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $prevToken): diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index a623d5471..ac048c7b7 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -44,7 +44,7 @@ public function hasMatched(array $tokens) switch (true) { case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): - case \is_string($token) && $token === '$': + case $token === '$': case self::isOperator($token): return true; } From f12e1e8a211a1eb6eac5b7962f7930cbd5256326 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 20:45:30 +1200 Subject: [PATCH 17/45] If not completing a valid prefix, consider it an empty string This ensures that a $token which is valid neither as an identifier nor as a variable will only appear as a 'previous' token, and never as the token being actively completed. This reduces the variety of cases which the matchers need to care about. This also ensures that completing against whitespace does not attempt to complete the preceding token (which was already complete, and not what the user asked for). --- src/TabCompletion/AutoCompleter.php | 32 ++++++++++++++++++- src/TabCompletion/Matcher/AbstractMatcher.php | 8 +++-- .../Matcher/ClassAttributesMatcher.php | 9 ++---- .../Matcher/ClassMethodsMatcher.php | 9 ++---- .../Matcher/ClassNamesMatcher.php | 6 ++-- src/TabCompletion/Matcher/CommandsMatcher.php | 1 + .../Matcher/ConstantsMatcher.php | 2 -- .../Matcher/FunctionsMatcher.php | 2 -- src/TabCompletion/Matcher/KeywordsMatcher.php | 3 +- .../Matcher/MongoClientMatcher.php | 2 -- .../Matcher/MongoDatabaseMatcher.php | 2 -- .../Matcher/ObjectAttributesMatcher.php | 10 +++--- .../Matcher/ObjectMethodsMatcher.php | 10 +++--- .../Matcher/VariablesMatcher.php | 5 ++- 14 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/TabCompletion/AutoCompleter.php b/src/TabCompletion/AutoCompleter.php index 59edb6550..1e397570f 100644 --- a/src/TabCompletion/AutoCompleter.php +++ b/src/TabCompletion/AutoCompleter.php @@ -148,11 +148,41 @@ public function processCallback($input, $index, $info = []) $tokens = \token_get_all('matchers as $matcher) { if ($matcher->hasMatched($tokens)) { diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index bac5d8ea3..107033421 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -45,7 +45,11 @@ abstract class AbstractMatcher /** * Check whether this matcher can provide completions for $tokens. * - * @param array $tokens Tokenized readline input + * @param array $tokens Tokenized readline input, with whitespace + * tokens removed. The final token is the identifier prefix to + * be completed (if the input did not end in a valid identifier + * prefix then the final token will be an empty string). Refer + * to AutoCompleter::processCallback() for details. * * @return bool */ @@ -172,7 +176,7 @@ public static function tokenIs($token, $which) */ public static function isOperator($token) { - if (!\is_string($token)) { + if (!\is_string($token) || $token === '') { return false; } diff --git a/src/TabCompletion/Matcher/ClassAttributesMatcher.php b/src/TabCompletion/Matcher/ClassAttributesMatcher.php index 5634f97ec..cee37f6a9 100644 --- a/src/TabCompletion/Matcher/ClassAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ClassAttributesMatcher.php @@ -32,10 +32,9 @@ public function getMatches(array $tokens, array $info = []) } $firstToken = \array_pop($tokens); - if (self::tokenIs($firstToken, self::T_STRING)) { - // second token is the nekudotayim operator - \array_pop($tokens); - } + + // Second token is the nekudotayim operator '::'. + \array_pop($tokens); $class = $this->getNamespaceAndClass($tokens); $chunks = \explode('\\', $class); @@ -85,8 +84,6 @@ public function hasMatched(array $tokens) switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON): return self::tokenIsValidIdentifier($token, true); - case self::tokenIs($token, self::T_DOUBLE_COLON): - return true; } return false; diff --git a/src/TabCompletion/Matcher/ClassMethodsMatcher.php b/src/TabCompletion/Matcher/ClassMethodsMatcher.php index b95a8f53f..14ed0774b 100644 --- a/src/TabCompletion/Matcher/ClassMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ClassMethodsMatcher.php @@ -32,10 +32,9 @@ public function getMatches(array $tokens, array $info = []) } $firstToken = \array_pop($tokens); - if (self::tokenIs($firstToken, self::T_STRING)) { - // second token is the nekudotayim operator - \array_pop($tokens); - } + + // Second token is the nekudotayim operator '::'. + \array_pop($tokens); $class = $this->getNamespaceAndClass($tokens); $chunks = \explode('\\', $class); @@ -85,8 +84,6 @@ public function hasMatched(array $tokens) switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON): return self::tokenIsValidIdentifier($token, true); - case self::tokenIs($token, self::T_DOUBLE_COLON): - return true; } return false; diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index 2f5b0a814..bc9a69274 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -60,8 +60,7 @@ public function hasMatched(array $tokens) ]; switch (true) { - // Blacklisted. - case self::hasToken([$blacklistedTokens], $token): + // Previous token (blacklist). case self::hasToken([$blacklistedTokens], $prevToken): case $token === '$': return false; @@ -69,8 +68,7 @@ public function hasMatched(array $tokens) case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $prevToken): return self::tokenIsValidIdentifier($token, true); // Current token (whitelist). - case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $token): - case self::isOperator($token): + case self::tokenIsValidIdentifier($token, true): return true; } diff --git a/src/TabCompletion/Matcher/CommandsMatcher.php b/src/TabCompletion/Matcher/CommandsMatcher.php index e03d76a3a..a7d1d79e0 100644 --- a/src/TabCompletion/Matcher/CommandsMatcher.php +++ b/src/TabCompletion/Matcher/CommandsMatcher.php @@ -106,6 +106,7 @@ public function hasMatched(array $tokens) // Valid for completion only if this was the only token. switch (true) { + case empty($command): case empty($tokens) && self::tokenIsValidIdentifier($command, true) && $this->matchCommand($command[1]): diff --git a/src/TabCompletion/Matcher/ConstantsMatcher.php b/src/TabCompletion/Matcher/ConstantsMatcher.php index bcbfd48cd..0c6b11ba7 100644 --- a/src/TabCompletion/Matcher/ConstantsMatcher.php +++ b/src/TabCompletion/Matcher/ConstantsMatcher.php @@ -49,8 +49,6 @@ public function hasMatched(array $tokens) case self::tokenIs($prevToken, self::T_NS_SEPARATOR): return false; // Current token (whitelist). - case self::tokenIs($token, self::T_OPEN_TAG): - case self::isOperator($token): case self::tokenIsValidIdentifier($token, true); return true; } diff --git a/src/TabCompletion/Matcher/FunctionsMatcher.php b/src/TabCompletion/Matcher/FunctionsMatcher.php index c2231e6eb..ea18eaf7f 100644 --- a/src/TabCompletion/Matcher/FunctionsMatcher.php +++ b/src/TabCompletion/Matcher/FunctionsMatcher.php @@ -51,8 +51,6 @@ public function hasMatched(array $tokens) case self::tokenIs($prevToken, self::T_NEW): return false; // Current token (whitelist). - case self::hasToken([self::T_OPEN_TAG], $token): - case self::isOperator($token): case self::tokenIsValidIdentifier($token, true): return true; } diff --git a/src/TabCompletion/Matcher/KeywordsMatcher.php b/src/TabCompletion/Matcher/KeywordsMatcher.php index e34ed57e6..145697305 100644 --- a/src/TabCompletion/Matcher/KeywordsMatcher.php +++ b/src/TabCompletion/Matcher/KeywordsMatcher.php @@ -79,8 +79,7 @@ public function hasMatched(array $tokens) case self::tokenIs($prevToken, self::T_OPEN_TAG): return self::tokenIsValidIdentifier($token, true); // Current token (whitelist). - case self::tokenIs($token, self::T_OPEN_TAG): - case self::isOperator($token): + case self::tokenIsValidIdentifier($token, true): return true; } diff --git a/src/TabCompletion/Matcher/MongoClientMatcher.php b/src/TabCompletion/Matcher/MongoClientMatcher.php index 3d905002b..e65877c08 100644 --- a/src/TabCompletion/Matcher/MongoClientMatcher.php +++ b/src/TabCompletion/Matcher/MongoClientMatcher.php @@ -65,8 +65,6 @@ public function hasMatched(array $tokens) // Valid following '->'. switch (true) { - case self::tokenIs($token, self::T_OBJECT_OPERATOR): - return true; case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return self::tokenIsValidIdentifier($token, true); } diff --git a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php index 341e63215..458174df8 100644 --- a/src/TabCompletion/Matcher/MongoDatabaseMatcher.php +++ b/src/TabCompletion/Matcher/MongoDatabaseMatcher.php @@ -61,8 +61,6 @@ public function hasMatched(array $tokens) // Valid following '->'. switch (true) { - case self::tokenIs($token, self::T_OBJECT_OPERATOR): - return true; case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return self::tokenIsValidIdentifier($token, true); } diff --git a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php index 5ccb88edc..6adae7035 100644 --- a/src/TabCompletion/Matcher/ObjectAttributesMatcher.php +++ b/src/TabCompletion/Matcher/ObjectAttributesMatcher.php @@ -34,10 +34,10 @@ public function getMatches(array $tokens, array $info = []) } $firstToken = \array_pop($tokens); - if (self::tokenIs($firstToken, self::T_STRING)) { - // second token is the object operator - \array_pop($tokens); - } + + // Second token is the object operator '->'. + \array_pop($tokens); + $objectToken = \array_pop($tokens); if (!\is_array($objectToken)) { return []; @@ -72,8 +72,6 @@ public function hasMatched(array $tokens) // Valid following '->'. switch (true) { - case self::tokenIs($token, self::T_OBJECT_OPERATOR): - return true; case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return self::tokenIsValidIdentifier($token, true); } diff --git a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php index c6ded5113..ee26a36f7 100644 --- a/src/TabCompletion/Matcher/ObjectMethodsMatcher.php +++ b/src/TabCompletion/Matcher/ObjectMethodsMatcher.php @@ -34,10 +34,10 @@ public function getMatches(array $tokens, array $info = []) } $firstToken = \array_pop($tokens); - if (self::tokenIs($firstToken, self::T_STRING)) { - // second token is the object operator - \array_pop($tokens); - } + + // Second token is the object operator '->'. + \array_pop($tokens); + $objectToken = \array_pop($tokens); if (!\is_array($objectToken)) { return []; @@ -74,8 +74,6 @@ public function hasMatched(array $tokens) // Valid following '->'. switch (true) { - case self::tokenIs($token, self::T_OBJECT_OPERATOR): - return true; case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return self::tokenIsValidIdentifier($token, true); } diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index ac048c7b7..5afdbf1eb 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -43,9 +43,8 @@ public function hasMatched(array $tokens) $token = \array_pop($tokens); switch (true) { - case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): - case $token === '$': - case self::isOperator($token): + case self::tokenIs($token, self::T_VARIABLE): + case in_array($token, ['', '$'], true): return true; } From 21b1f38c6eddfe94a0dc96ff1d5217c4b79bb507 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Thu, 30 Jul 2020 11:25:20 +1200 Subject: [PATCH 18/45] Don't trim trailing spaces and tabs from command input Doing so prevents a CodeArgument from seeing that trailing whitespace, which in turn meant that the 'completions' command was unable to tell whether the user wanted to complete the previous token, or (after a space) an empty string. --- src/Input/CodeArgument.php | 5 +++++ src/Shell.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Input/CodeArgument.php b/src/Input/CodeArgument.php index a2189af7f..a8cdea93e 100644 --- a/src/Input/CodeArgument.php +++ b/src/Input/CodeArgument.php @@ -26,6 +26,11 @@ * parse function() { return "wheee\n"; } * * ... without having to put the code in a quoted string and escape everything. + * + * Certain trailing whitespace characters are exceptions. Trailing Spaces and + * tabs will be included in the argument value, but trailing newlines, carriage + * returns, vertical tabs, and nulls are trimmed from the command before the + * arguments are established. */ class CodeArgument extends InputArgument { diff --git a/src/Shell.php b/src/Shell.php index 966645796..c1ac04c2a 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -926,7 +926,7 @@ protected function runCommand($input) throw new \InvalidArgumentException('Command not found: '.$input); } - $input = new ShellInput(\str_replace('\\', '\\\\', \rtrim($input, " \t\n\r\0\x0B;"))); + $input = new ShellInput(\str_replace('\\', '\\\\', \rtrim($input, "\n\r\0\x0B;"))); if ($input->hasParameterOption(['--help', '-h'])) { $helpCommand = $this->get('help'); From 16555841ed73bf6659ec0d5317ee72506af58107 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Fri, 14 Aug 2020 17:51:00 +1200 Subject: [PATCH 19/45] Treat T_OPEN_TAG and ';' the same way for the purposes of completion Both indicate that whatever comes next is a new expression. --- src/TabCompletion/Matcher/AbstractMatcher.php | 15 +++++++++++++++ src/TabCompletion/Matcher/ClassNamesMatcher.php | 3 ++- src/TabCompletion/Matcher/KeywordsMatcher.php | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 107033421..e9f3d03c2 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -200,6 +200,21 @@ public static function tokenIsValidIdentifier($token, bool $allowEmpty = false) return self::hasSyntax($token, self::CONSTANT_SYNTAX); } + /** + * Check whether $token 'separates' PHP expressions, meaning that + * whatever follows is a new expression. + * + * Separators include the initial T_OPEN_TAG token, and ";". + * + * @param mixed $token A PHP token (see token_get_all) + * + * @return bool + */ + public static function tokenIsExpressionDelimiter($token) + { + return $token === ';' || self::tokenIs($token, self::T_OPEN_TAG); + } + /** * Used both to test $tokens[1] (i.e. following T_OPEN_TAG) to * see whether it's a PsySH introspection command, and also by diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index bc9a69274..bc9cfd14e 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -65,7 +65,8 @@ public function hasMatched(array $tokens) case $token === '$': return false; // Previous token. - case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $prevToken): + case self::tokenIsExpressionDelimiter($prevToken): + case self::hasToken([self::T_NEW, self::T_NS_SEPARATOR], $prevToken): return self::tokenIsValidIdentifier($token, true); // Current token (whitelist). case self::tokenIsValidIdentifier($token, true): diff --git a/src/TabCompletion/Matcher/KeywordsMatcher.php b/src/TabCompletion/Matcher/KeywordsMatcher.php index 145697305..58710c237 100644 --- a/src/TabCompletion/Matcher/KeywordsMatcher.php +++ b/src/TabCompletion/Matcher/KeywordsMatcher.php @@ -76,7 +76,7 @@ public function hasMatched(array $tokens) switch (true) { // Previous token. - case self::tokenIs($prevToken, self::T_OPEN_TAG): + case self::tokenIsExpressionDelimiter($prevToken): return self::tokenIsValidIdentifier($token, true); // Current token (whitelist). case self::tokenIsValidIdentifier($token, true): From 0611ead3f02195ea0cc49106a597b30d55a159d1 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Fri, 7 Aug 2020 12:33:30 +1200 Subject: [PATCH 20/45] Support string tokens in AbstractMatcher::hasToken() and tokenIs() --- src/TabCompletion/Matcher/AbstractMatcher.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index e9f3d03c2..737978a02 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -160,11 +160,12 @@ public static function hasSyntax($token, $syntax = self::VAR_SYNTAX) */ public static function tokenIs($token, $which) { - if (!\is_array($token)) { - return false; + if (\is_array($token)) { + return \token_name($token[0]) === $which; + } + else { + return $token === $which; } - - return \token_name($token[0]) === $which; } /** @@ -238,10 +239,11 @@ public static function needCompleteClass($token) */ public static function hasToken(array $coll, $token) { - if (!\is_array($token)) { - return false; + if (\is_array($token)) { + return \in_array(\token_name($token[0]), $coll); + } + else { + return \in_array($token, $coll, true); } - - return \in_array(\token_name($token[0]), $coll); } } From 3a829c78ab4d0b81b30436a237dc0a2d5027c562 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Fri, 14 Aug 2020 16:55:46 +1200 Subject: [PATCH 21/45] Fix inconsistencies with completion of variables --- src/TabCompletion/AutoCompleter.php | 3 ++ .../Matcher/AbstractContextAwareMatcher.php | 18 +++++++++-- .../Matcher/VariablesMatcher.php | 32 ++++++++++++++++--- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/TabCompletion/AutoCompleter.php b/src/TabCompletion/AutoCompleter.php index 1e397570f..9d7379227 100644 --- a/src/TabCompletion/AutoCompleter.php +++ b/src/TabCompletion/AutoCompleter.php @@ -162,6 +162,9 @@ public function processCallback($input, $index, $info = []) $tokens[] = ''; break; case AbstractMatcher::tokenIs($token, AbstractMatcher::T_VARIABLE): + case $token === '$': + // We allow a special case for '$', which for completion + // purposes we will treat the same way as T_VARIABLE. $tokens[] = $token; break; case !AbstractMatcher::tokenIsValidIdentifier($token): diff --git a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php index f7da443b9..ce28a647f 100644 --- a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php +++ b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php @@ -56,10 +56,24 @@ protected function getVariable($var) /** * Get all variables in the current Context. * + * @param bool $dollarPrefix + * Whether to prefix '$' to each variable name. + * * @return array */ - protected function getVariables() + protected function getVariables($dollarPrefix = false) { - return $this->context->getAll(); + $variables = $this->context->getAll(); + if (!$dollarPrefix) { + return $variables; + } + else { + // Add '$' prefix to each name. + $newvars = []; + foreach ($variables as $name => $value) { + $newvars['$'.$name] = $value; + } + return $newvars; + } } } diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index 5afdbf1eb..a242f2d8d 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -20,19 +20,43 @@ */ class VariablesMatcher extends AbstractContextAwareMatcher { + /** + * {@inheritdoc} + */ + protected function getInput(array $tokens, array $t_valid = null) + { + return parent::getInput($tokens, [self::T_VARIABLE, '$', '']); + } + /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []) { - $input = \str_replace('$', '', $this->getInput($tokens)); + $input = $this->getInput($tokens); if ($input === false) { return []; } - return \array_filter(\array_keys($this->getVariables()), function ($variable) use ($input) { - return AbstractMatcher::startsWith($input, $variable); - }); + // '$' is a readline completion word-break character (refer to + // AutoCompleter::WORD_BREAK_CHARS), and so the completion + // candidates we generate must not include the leading '$' -- + // *unless* we are completing an empty string, in which case + // the '$' is required. + if ($input === '') { + $dollarPrefix = true; + } + else { + $dollarPrefix = false; + $input = \str_replace('$', '', $input); + } + + return \array_filter( + \array_keys($this->getVariables($dollarPrefix)), + function ($variable) use ($input) { + return AbstractMatcher::startsWith($input, $variable); + } + ); } /** From 2b566fe21d6f7563a7fa67160f00dd5f98bc3841 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Fri, 7 Aug 2020 20:10:05 +1200 Subject: [PATCH 22/45] Improve the 'previous token' blacklisting in hasMatched() methods This includes blacklisting T_NEW and T_NS_SEPARATOR when not completing classes; and blacklisting T_OBJECT_OPERATOR and T_DOUBLE_COLON when not completing attributes/methods for objects and classes (respectively). --- src/TabCompletion/Matcher/ClassNamesMatcher.php | 14 ++++++++++---- src/TabCompletion/Matcher/ConstantsMatcher.php | 9 +++++++-- src/TabCompletion/Matcher/FunctionsMatcher.php | 8 +++++++- src/TabCompletion/Matcher/KeywordsMatcher.php | 9 +++++++++ src/TabCompletion/Matcher/VariablesMatcher.php | 11 +++++++++++ 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/TabCompletion/Matcher/ClassNamesMatcher.php b/src/TabCompletion/Matcher/ClassNamesMatcher.php index bc9cfd14e..94eed1a0f 100644 --- a/src/TabCompletion/Matcher/ClassNamesMatcher.php +++ b/src/TabCompletion/Matcher/ClassNamesMatcher.php @@ -54,14 +54,20 @@ public function hasMatched(array $tokens) { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); - - $blacklistedTokens = [ - self::T_INCLUDE, self::T_INCLUDE_ONCE, self::T_REQUIRE, self::T_REQUIRE_ONCE, + $prevTokenBlacklist = [ + self::T_INCLUDE, + self::T_INCLUDE_ONCE, + self::T_REQUIRE, + self::T_REQUIRE_ONCE, + self::T_OBJECT_OPERATOR, + self::T_DOUBLE_COLON, ]; switch (true) { // Previous token (blacklist). - case self::hasToken([$blacklistedTokens], $prevToken): + case self::hasToken($prevTokenBlacklist, $prevToken): + return false; + // Current token (blacklist). case $token === '$': return false; // Previous token. diff --git a/src/TabCompletion/Matcher/ConstantsMatcher.php b/src/TabCompletion/Matcher/ConstantsMatcher.php index 0c6b11ba7..299190973 100644 --- a/src/TabCompletion/Matcher/ConstantsMatcher.php +++ b/src/TabCompletion/Matcher/ConstantsMatcher.php @@ -42,11 +42,16 @@ public function hasMatched(array $tokens) { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + $prevTokenBlacklist = [ + self::T_NEW, + self::T_NS_SEPARATOR, + self::T_OBJECT_OPERATOR, + self::T_DOUBLE_COLON, + ]; switch (true) { // Previous token (blacklist). - case self::tokenIs($prevToken, self::T_NEW): - case self::tokenIs($prevToken, self::T_NS_SEPARATOR): + case self::hasToken($prevTokenBlacklist, $prevToken): return false; // Current token (whitelist). case self::tokenIsValidIdentifier($token, true); diff --git a/src/TabCompletion/Matcher/FunctionsMatcher.php b/src/TabCompletion/Matcher/FunctionsMatcher.php index ea18eaf7f..a4b0d9f8e 100644 --- a/src/TabCompletion/Matcher/FunctionsMatcher.php +++ b/src/TabCompletion/Matcher/FunctionsMatcher.php @@ -45,10 +45,16 @@ public function hasMatched(array $tokens) { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + $prevTokenBlacklist = [ + self::T_NEW, + self::T_NS_SEPARATOR, + self::T_OBJECT_OPERATOR, + self::T_DOUBLE_COLON, + ]; switch (true) { // Previous token (blacklist). - case self::tokenIs($prevToken, self::T_NEW): + case self::hasToken($prevTokenBlacklist, $prevToken): return false; // Current token (whitelist). case self::tokenIsValidIdentifier($token, true): diff --git a/src/TabCompletion/Matcher/KeywordsMatcher.php b/src/TabCompletion/Matcher/KeywordsMatcher.php index 58710c237..bf8657d5e 100644 --- a/src/TabCompletion/Matcher/KeywordsMatcher.php +++ b/src/TabCompletion/Matcher/KeywordsMatcher.php @@ -73,8 +73,17 @@ public function hasMatched(array $tokens) { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); + $prevTokenBlacklist = [ + self::T_NEW, + self::T_NS_SEPARATOR, + self::T_OBJECT_OPERATOR, + self::T_DOUBLE_COLON, + ]; switch (true) { + // Previous token (blacklist). + case self::hasToken($prevTokenBlacklist, $prevToken): + return false; // Previous token. case self::tokenIsExpressionDelimiter($prevToken): return self::tokenIsValidIdentifier($token, true); diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index a242f2d8d..b0b5864b3 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -65,8 +65,19 @@ function ($variable) use ($input) { public function hasMatched(array $tokens) { $token = \array_pop($tokens); + $prevToken = \array_pop($tokens); + $prevTokenBlacklist = [ + self::T_NEW, + self::T_NS_SEPARATOR, + self::T_OBJECT_OPERATOR, + self::T_DOUBLE_COLON, + ]; switch (true) { + // Previous token (blacklist). + case self::hasToken($prevTokenBlacklist, $prevToken): + return false; + // Current token (whitelist). case self::tokenIs($token, self::T_VARIABLE): case in_array($token, ['', '$'], true): return true; From c66e4aa9babc6f2db8e6fedbc138346ec7a64af8 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Sun, 9 Aug 2020 16:55:49 +1200 Subject: [PATCH 23/45] Bug fix for getNamespaceAndClass() If we encounter a needCompleteClass() token, don't continue to prepend the tokens which preceded that. --- src/TabCompletion/Matcher/AbstractMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 737978a02..535091c16 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -99,7 +99,7 @@ protected function getNamespaceAndClass($tokens) $token = \array_pop($tokens) )) { if (self::needCompleteClass($token)) { - continue; + break; } $class = $token[1].$class; From 4c7a285b6cd620991a88146c5e8cdadc36b63a9a Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Sun, 9 Aug 2020 16:59:28 +1200 Subject: [PATCH 24/45] Allow an incomplete class name in getNamespaceAndClass() ClassNamesMatcher::getMatches() passes the incomplete class, so we need to support an incomplete (potentially empty) final component. --- src/TabCompletion/Matcher/AbstractMatcher.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 535091c16..7b8cbb9b6 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -93,11 +93,20 @@ protected function getInput(array $tokens, array $t_valid = null) */ protected function getNamespaceAndClass($tokens) { - $class = ''; - while (self::hasToken( - [self::T_NS_SEPARATOR, self::T_STRING], - $token = \array_pop($tokens) - )) { + $validTokens = [ + self::T_NS_SEPARATOR, + self::T_STRING, + ]; + + $token = \array_pop($tokens); + if (!self::hasToken($validTokens, $token) + && !self::tokenIsValidIdentifier($token, true)) + { + return ''; + } + $class = \is_array($token) ? $token[1] : $token; + + while (self::hasToken($validTokens, $token = \array_pop($tokens))) { if (self::needCompleteClass($token)) { break; } From 80717d0a1ae167782c7281b58a50a3e40dca5438 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 11:21:58 +1200 Subject: [PATCH 25/45] fixup! Add 'completions' command --- src/Command/CompletionsCommand.php | 3 +-- src/Shell.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Command/CompletionsCommand.php b/src/Command/CompletionsCommand.php index 25c202680..7b2b4525d 100644 --- a/src/Command/CompletionsCommand.php +++ b/src/Command/CompletionsCommand.php @@ -13,7 +13,6 @@ use Psy\Input\CodeArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -55,7 +54,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $completions = $this->getApplication()->getTabCompletions($target); // Ouput the completion candidates as newline-separated text. - $str = implode("\n", array_filter($completions))."\n"; + $str = \implode("\n", \array_filter($completions))."\n"; $output->write($str, false, OutputInterface::OUTPUT_RAW); return 0; diff --git a/src/Shell.php b/src/Shell.php index c1ac04c2a..0bb6be893 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -248,7 +248,7 @@ public function getTabCompletions(string $input) $word = ''; $regexp = $ac::WORD_REGEXP; $matches = []; - if (preg_match($regexp, $input, $matches) === 1) { + if (\preg_match($regexp, $input, $matches) === 1) { $word = $matches[0]; } return $ac->processCallback($word, null, ['line_buffer' => $input]); From 72a0924a94f92baad0f4e7d35d48b4cd386679b0 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 12:04:50 +1200 Subject: [PATCH 26/45] fixup! Add tokenIsValidIdentifier() for generic tab-completion tests --- src/TabCompletion/Matcher/AbstractMatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 7b8cbb9b6..e9fe29e36 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -196,8 +196,8 @@ public static function isOperator($token) /** * Check whether $token is a valid prefix for a PHP identifier. * - * @param mixed $token A PHP token (see token_get_all) - * @param bool $allowEmpty Whether an empty string is valid. + * @param mixed $token A PHP token (see token_get_all) + * @param bool $allowEmpty Whether an empty string is valid. * * @return bool */ From 4ae9f12e547a8aef1cff8c3d2d66de4d9a187f32 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 11:57:51 +1200 Subject: [PATCH 27/45] fixup! Use tokenIsValidIdentifier() in getInput() instead of matching T_STRING --- src/TabCompletion/Matcher/AbstractMatcher.php | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index e9fe29e36..72242ba21 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -59,25 +59,31 @@ public function hasMatched(array $tokens) } /** - * Get current readline input word. + * Get the input word to be completed, based on the tokenised input. * - * @param array $tokens Tokenized readline input (see token_get_all) - * @param array|null $t_valid Acceptable tokens. If null then strings - * which are valid identifiers (or empty) are considered valid. + * Note that this may not be identical to the word which readline needs to + * complete (see AutoCompleter::WORD_BREAK_CHARS), and so Matchers must + * take care to return candidate values that match what readline wants. + * + * We return the string value of the final token if it is valid, and false + * if that token is invalid. By default, the token is valid if it is + * valid prefix (including '') for a PHP identifier. + * + * @param array $tokens Tokenized readline input (see token_get_all). + * @param array|null $validTokens Acceptable tokens. * * @return string|bool */ - protected function getInput(array $tokens, array $t_valid = null) + protected function getInput(array $tokens, array $validTokens = null) { $token = \array_pop($tokens); $input = \is_array($token) ? $token[1] : $token; - if (isset($t_valid)) { - if (self::hasToken($t_valid, $token)) { + if (isset($validTokens)) { + if (self::hasToken($validTokens, $token)) { return $input; } - } - elseif (self::tokenIsValidIdentifier($token, true)) { + } elseif (self::tokenIsValidIdentifier($token, true)) { return $input; } From bfaeb466d5263ed5469b978ad84b8ac08a64e3b7 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 12:07:20 +1200 Subject: [PATCH 28/45] fixup! Use tokenIsValidIdentifier() instead of T_STRING matching in hasMatched() methods --- src/TabCompletion/Matcher/ConstantsMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/ConstantsMatcher.php b/src/TabCompletion/Matcher/ConstantsMatcher.php index 299190973..4703d48f0 100644 --- a/src/TabCompletion/Matcher/ConstantsMatcher.php +++ b/src/TabCompletion/Matcher/ConstantsMatcher.php @@ -54,7 +54,7 @@ public function hasMatched(array $tokens) case self::hasToken($prevTokenBlacklist, $prevToken): return false; // Current token (whitelist). - case self::tokenIsValidIdentifier($token, true); + case self::tokenIsValidIdentifier($token, true): return true; } From 61fd4d2539d0b4866baad3c34264e89cf3860fd1 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 11:35:32 +1200 Subject: [PATCH 29/45] fixup! If not completing a valid prefix, consider it an empty string --- src/TabCompletion/Matcher/AbstractMatcher.php | 14 +++++++++----- src/TabCompletion/Matcher/VariablesMatcher.php | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 72242ba21..49ca34f1d 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -45,11 +45,15 @@ abstract class AbstractMatcher /** * Check whether this matcher can provide completions for $tokens. * - * @param array $tokens Tokenized readline input, with whitespace - * tokens removed. The final token is the identifier prefix to - * be completed (if the input did not end in a valid identifier - * prefix then the final token will be an empty string). Refer - * to AutoCompleter::processCallback() for details. + * The final token is the 'word' to be completed. If the input + * did not end in a valid identifier prefix then the final token + * will be an empty string. + * + * All whitespace tokens have been removed from the $tokens array. + * + * @see AutoCompleter::processCallback(). + * + * @param array $tokens Tokenized readline input. * * @return bool */ diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index b0b5864b3..b4c1e431f 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -79,7 +79,7 @@ public function hasMatched(array $tokens) return false; // Current token (whitelist). case self::tokenIs($token, self::T_VARIABLE): - case in_array($token, ['', '$'], true): + case \in_array($token, ['', '$'], true): return true; } From 09e74223ea200657e6acde0039d64d6a4219fd33 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 11:59:45 +1200 Subject: [PATCH 30/45] fixup! Support string tokens in AbstractMatcher::hasToken() and tokenIs() --- src/TabCompletion/Matcher/AbstractMatcher.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 49ca34f1d..e7bbca7ca 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -181,8 +181,7 @@ public static function tokenIs($token, $which) { if (\is_array($token)) { return \token_name($token[0]) === $which; - } - else { + } else { return $token === $which; } } @@ -260,8 +259,7 @@ public static function hasToken(array $coll, $token) { if (\is_array($token)) { return \in_array(\token_name($token[0]), $coll); - } - else { + } else { return \in_array($token, $coll, true); } } From 91c485fa9a8f845c4b5813e12e50690a6b554c70 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 11:27:00 +1200 Subject: [PATCH 31/45] fixup! Fix inconsistencies with completion of variables --- src/TabCompletion/Matcher/AbstractContextAwareMatcher.php | 8 ++++---- src/TabCompletion/Matcher/VariablesMatcher.php | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php index ce28a647f..c49f9a9bc 100644 --- a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php +++ b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php @@ -56,8 +56,9 @@ protected function getVariable($var) /** * Get all variables in the current Context. * - * @param bool $dollarPrefix - * Whether to prefix '$' to each variable name. + * The '$' prefix for each variable name is not included by default. + * + * @param bool $dollarPrefix Whether to prefix '$' to each name. * * @return array */ @@ -66,8 +67,7 @@ protected function getVariables($dollarPrefix = false) $variables = $this->context->getAll(); if (!$dollarPrefix) { return $variables; - } - else { + } else { // Add '$' prefix to each name. $newvars = []; foreach ($variables as $name => $value) { diff --git a/src/TabCompletion/Matcher/VariablesMatcher.php b/src/TabCompletion/Matcher/VariablesMatcher.php index b4c1e431f..9f0949ed1 100644 --- a/src/TabCompletion/Matcher/VariablesMatcher.php +++ b/src/TabCompletion/Matcher/VariablesMatcher.php @@ -45,8 +45,7 @@ public function getMatches(array $tokens, array $info = []) // the '$' is required. if ($input === '') { $dollarPrefix = true; - } - else { + } else { $dollarPrefix = false; $input = \str_replace('$', '', $input); } From e5856631a10460c244a8bf2b4bce24c05a07c6cf Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 12:01:16 +1200 Subject: [PATCH 32/45] fixup! Allow an incomplete class name in getNamespaceAndClass() --- src/TabCompletion/Matcher/AbstractMatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index e7bbca7ca..e6abbf09f 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -110,8 +110,8 @@ protected function getNamespaceAndClass($tokens) $token = \array_pop($tokens); if (!self::hasToken($validTokens, $token) - && !self::tokenIsValidIdentifier($token, true)) - { + && !self::tokenIsValidIdentifier($token, true) + ) { return ''; } $class = \is_array($token) ? $token[1] : $token; From 4d0041bffe0a4ef1f5d6250c430ac02178590a9a Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:17:45 +1200 Subject: [PATCH 33/45] fixup! Fix inconsistencies with completion of variables --- src/TabCompletion/Matcher/AbstractContextAwareMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php index c49f9a9bc..3bc028794 100644 --- a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php +++ b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php @@ -58,7 +58,7 @@ protected function getVariable($var) * * The '$' prefix for each variable name is not included by default. * - * @param bool $dollarPrefix Whether to prefix '$' to each name. + * @param bool $dollarPrefix Whether to prefix '$' to each name * * @return array */ From a16f51c461a64add43ff3f5aab89e89a1415ebcf Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:18:00 +1200 Subject: [PATCH 34/45] fixup! Add 'completions' command --- src/Shell.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Shell.php b/src/Shell.php index 0bb6be893..b8256a298 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -251,6 +251,7 @@ public function getTabCompletions(string $input) if (\preg_match($regexp, $input, $matches) === 1) { $word = $matches[0]; } + return $ac->processCallback($word, null, ['line_buffer' => $input]); } From 0e0b3a14ff020f38a3ebb19cc6945b9a90b7dda4 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:19:24 +1200 Subject: [PATCH 35/45] fixup! If not completing a valid prefix, consider it an empty string --- src/TabCompletion/Matcher/AbstractMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index e6abbf09f..b87aeb356 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -53,7 +53,7 @@ abstract class AbstractMatcher * * @see AutoCompleter::processCallback(). * - * @param array $tokens Tokenized readline input. + * @param array $tokens Tokenized readline input * * @return bool */ From 454e3dbfa8d3701442e12c12823b796f94fdec0b Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:21:04 +1200 Subject: [PATCH 36/45] fixup! Use tokenIsValidIdentifier() in getInput() instead of matching T_STRING --- src/TabCompletion/Matcher/AbstractMatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index b87aeb356..1e5fb428c 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -73,8 +73,8 @@ public function hasMatched(array $tokens) * if that token is invalid. By default, the token is valid if it is * valid prefix (including '') for a PHP identifier. * - * @param array $tokens Tokenized readline input (see token_get_all). - * @param array|null $validTokens Acceptable tokens. + * @param array $tokens Tokenized readline input (see token_get_all) + * @param array|null $validTokens Acceptable tokens * * @return string|bool */ From f00e8c2b8425b393101af5abc4c636ab9a9e2422 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:21:47 +1200 Subject: [PATCH 37/45] fixup! Add tokenIsValidIdentifier() for generic tab-completion tests --- src/TabCompletion/Matcher/AbstractMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 1e5fb428c..490bc1fd7 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -206,7 +206,7 @@ public static function isOperator($token) * Check whether $token is a valid prefix for a PHP identifier. * * @param mixed $token A PHP token (see token_get_all) - * @param bool $allowEmpty Whether an empty string is valid. + * @param bool $allowEmpty Whether an empty string is valid * * @return bool */ From cdd69c2c6f487626b6dc39fac0f0bdeaef204e2e Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:22:41 +1200 Subject: [PATCH 38/45] fixup! Add tokenIsValidIdentifier() for generic tab-completion tests --- src/TabCompletion/Matcher/AbstractMatcher.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 490bc1fd7..cff6f1a71 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -216,6 +216,7 @@ public static function tokenIsValidIdentifier($token, bool $allowEmpty = false) if ($token === '') { return $allowEmpty; } + return self::hasSyntax($token, self::CONSTANT_SYNTAX); } From 5a22d5260d8767c9d2ed462b1c0f1dc5e9ae19d9 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:24:56 +1200 Subject: [PATCH 39/45] fixup! Fix inconsistencies with completion of variables --- src/TabCompletion/Matcher/AbstractContextAwareMatcher.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php index 3bc028794..0c748590c 100644 --- a/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php +++ b/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php @@ -73,6 +73,7 @@ protected function getVariables($dollarPrefix = false) foreach ($variables as $name => $value) { $newvars['$'.$name] = $value; } + return $newvars; } } From 51f66e11cf4df474179aef6a17a939fd5a2c9d84 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:29:40 +1200 Subject: [PATCH 40/45] fixup! Document the word-break limitations of the Readline completion --- src/TabCompletion/AutoCompleter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TabCompletion/AutoCompleter.php b/src/TabCompletion/AutoCompleter.php index 9d7379227..edcdc40be 100644 --- a/src/TabCompletion/AutoCompleter.php +++ b/src/TabCompletion/AutoCompleter.php @@ -93,13 +93,13 @@ class AutoCompleter * @see self::processCallback() * @see \Psy\Shell::getTabCompletions() */ - public const WORD_BREAK_CHARS = " \t\n\"\\’‘@$><=;|&{("; + const WORD_BREAK_CHARS = " \t\n\"\\’‘@$><=;|&{("; /** * A regular expression based on WORD_BREAK_CHARS which will match the * completable word at the end of the string. */ - public const WORD_REGEXP = "/[^ \t\n\"\\\\’‘@$><=;|&{(]*$/"; + const WORD_REGEXP = "/[^ \t\n\"\\\\’‘@$><=;|&{(]*$/"; /** * Register a tab completion Matcher. From cf8a487e4df0a69bc7004522e999b8c1bcbeba37 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:45:03 +1200 Subject: [PATCH 41/45] fixup! Add 'completions' command --- src/Shell.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Shell.php b/src/Shell.php index b8256a298..e41f6c242 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -22,6 +22,7 @@ use Psy\Formatter\TraceFormatter; use Psy\Input\ShellInput; use Psy\Input\SilentInput; +use Psy\TabCompletion\AutoCompleter; use Psy\TabCompletion\Matcher; use Psy\VarDumper\PresenterAware; use Symfony\Component\Console\Application; @@ -246,7 +247,7 @@ public function getTabCompletions(string $input) { $ac = $this->autoCompleter; $word = ''; - $regexp = $ac::WORD_REGEXP; + $regexp = AutoCompleter::WORD_REGEXP; $matches = []; if (\preg_match($regexp, $input, $matches) === 1) { $word = $matches[0]; From 57be706b09d9dd3b708617d2cbc14d0fb9f6ac34 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 13:52:29 +1200 Subject: [PATCH 42/45] fixup! Support string tokens in AbstractMatcher::hasToken() and tokenIs() --- src/TabCompletion/Matcher/AbstractMatcher.php | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index cff6f1a71..6484e4421 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -170,7 +170,10 @@ public static function hasSyntax($token, $syntax = self::VAR_SYNTAX) } /** - * Check whether $token type is $which. + * Check whether $token is of type $which. + * + * $which may either be a token type name (e.g. self::T_VARIABLE), + * or a literal string token (e.g. '+'). * * @param mixed $token A PHP token (see token_get_all) * @param string $which A PHP token type @@ -180,10 +183,10 @@ public static function hasSyntax($token, $syntax = self::VAR_SYNTAX) public static function tokenIs($token, $which) { if (\is_array($token)) { - return \token_name($token[0]) === $which; - } else { - return $token === $which; + $token = \token_name($token[0]); } + + return $token === $which; } /** @@ -249,7 +252,10 @@ public static function needCompleteClass($token) } /** - * Check whether $token type is present in $coll. + * Check whether $token has a type which is present in $coll. + * + * $coll may include a mixture of token type names (e.g. self::T_VARIABLE), + * and literal string tokens (e.g. '+'). * * @param array $coll A list of token types * @param mixed $token A PHP token (see token_get_all) @@ -259,9 +265,9 @@ public static function needCompleteClass($token) public static function hasToken(array $coll, $token) { if (\is_array($token)) { - return \in_array(\token_name($token[0]), $coll); - } else { - return \in_array($token, $coll, true); + $token = \token_name($token[0]); } + + return \in_array($token, $coll, true); } } From bb27322480d33af4565a83a6e6e9163db02554b0 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 14:44:51 +1200 Subject: [PATCH 43/45] fixup! Add tokenIsValidIdentifier() for generic tab-completion tests Travis says: Fatal error: Default value for parameters with a class type hint can only be NULL in /home/travis/build/bobthecow/psysh/src/TabCompletion/Matcher/AbstractMatcher.php on line 216 --- src/TabCompletion/Matcher/AbstractMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 6484e4421..b35e327fb 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -213,7 +213,7 @@ public static function isOperator($token) * * @return bool */ - public static function tokenIsValidIdentifier($token, bool $allowEmpty = false) + public static function tokenIsValidIdentifier($token, $allowEmpty = false) { // See AutoCompleter::processCallback() regarding the '' token. if ($token === '') { From b89af21377769dacd6cc84e482140418a6e1d64c Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 14:54:51 +1200 Subject: [PATCH 44/45] Update the list of commands in AbstractMatcher::needCompleteClass() --- src/TabCompletion/Matcher/AbstractMatcher.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index b35e327fb..3eb2cfb8f 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -248,7 +248,22 @@ public static function tokenIsExpressionDelimiter($token) */ public static function needCompleteClass($token) { - return \in_array($token[1], ['doc', 'ls', 'show', 'completions']); + // PsySH introspection commands. + $commands = [ + 'completions', + 'dir', + 'doc', + 'dump', + 'ls', + 'man', + 'parse', + 'rtfm', + 'show', + 'sudo', + 'throw-up', + 'timeit', + ]; + return \in_array($token[1], $commands); } /** From 695f0d5c8ca06e87657aab8c5f70b8914204ff42 Mon Sep 17 00:00:00 2001 From: Phil Sainty Date: Mon, 17 Aug 2020 15:01:05 +1200 Subject: [PATCH 45/45] fixup! Update the list of commands in AbstractMatcher::needCompleteClass() --- src/TabCompletion/Matcher/AbstractMatcher.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TabCompletion/Matcher/AbstractMatcher.php b/src/TabCompletion/Matcher/AbstractMatcher.php index 3eb2cfb8f..fc3ec1c6e 100644 --- a/src/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/TabCompletion/Matcher/AbstractMatcher.php @@ -263,6 +263,7 @@ public static function needCompleteClass($token) 'throw-up', 'timeit', ]; + return \in_array($token[1], $commands); }