From 04471e120cb51225815b383527370e3c8beaa6e5 Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Fri, 27 Feb 2015 12:59:24 +0100 Subject: [PATCH] commands have completion for arguments, early stage file matcher and operator matcher added --- .../Matcher/AbstractContextAwareMatcher.php | 12 +++ .../TabCompletion/Matcher/AbstractMatcher.php | 1 + .../TabCompletion/Matcher/CommandsMatcher.php | 78 ++++++++++++++++--- .../TabCompletion/Matcher/FilesMatcher.php | 71 +++++++++++++++++ .../Matcher/OperatorsMatcher.php | 44 +++++++++++ 5 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 src/Psy/TabCompletion/Matcher/FilesMatcher.php create mode 100644 src/Psy/TabCompletion/Matcher/OperatorsMatcher.php diff --git a/src/Psy/TabCompletion/Matcher/AbstractContextAwareMatcher.php b/src/Psy/TabCompletion/Matcher/AbstractContextAwareMatcher.php index 9d8cb94b9..60103f457 100644 --- a/src/Psy/TabCompletion/Matcher/AbstractContextAwareMatcher.php +++ b/src/Psy/TabCompletion/Matcher/AbstractContextAwareMatcher.php @@ -53,6 +53,18 @@ protected function getVariable($var) return $this->context->get($var); } + /** + * Returns true if the variable exists. + * + * @param $var Variable name + * + * @return bool + */ + protected function hasVariable($var) + { + return (bool) $this->getVariable($var); + } + /** * Get all variables in the current Context. * diff --git a/src/Psy/TabCompletion/Matcher/AbstractMatcher.php b/src/Psy/TabCompletion/Matcher/AbstractMatcher.php index 0ab32a298..759f275fe 100644 --- a/src/Psy/TabCompletion/Matcher/AbstractMatcher.php +++ b/src/Psy/TabCompletion/Matcher/AbstractMatcher.php @@ -31,6 +31,7 @@ abstract class AbstractMatcher const T_CLONE = 'T_CLONE'; const T_NS_SEPARATOR = 'T_NS_SEPARATOR'; const T_STRING = 'T_STRING'; + const T_LNUMBER = 'T_LNUMBER'; const T_WHITESPACE = 'T_WHITESPACE'; const T_AND_EQUAL = 'T_AND_EQUAL'; const T_BOOLEAN_AND = 'T_BOOLEAN_AND'; diff --git a/src/Psy/TabCompletion/Matcher/CommandsMatcher.php b/src/Psy/TabCompletion/Matcher/CommandsMatcher.php index ceb6645f9..9e8502b37 100644 --- a/src/Psy/TabCompletion/Matcher/CommandsMatcher.php +++ b/src/Psy/TabCompletion/Matcher/CommandsMatcher.php @@ -12,6 +12,7 @@ namespace Psy\TabCompletion\Matcher; use Psy\Command\Command; +use Symfony\Component\Console\Input\InputOption; /** * A Psy Command tab completion Matcher. @@ -24,6 +25,9 @@ class CommandsMatcher extends AbstractMatcher { /** @var string[] */ + protected $commandNames = array(); + + /** @var Command[] */ protected $commands = array(); /** @@ -43,22 +47,34 @@ public function __construct(array $commands) */ public function setCommands(array $commands) { - $names = array(); - foreach ($commands as $command) { - $names = array_merge(array($command->getName()), $names); - $names = array_merge($command->getAliases(), $names); + // use the same object so is leaks the lowest memory possible + foreach ($commands as &$command) { + $this->commands[$command->getName()] = &$command; + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = &$command; + } + $this->commandNames = array_keys($this->commands); } - $this->commands = $names; } + /** + * @param $name + * + * @return bool + */ protected function isCommand($name) { - return in_array($name, $this->commands); + return in_array($name, $this->commandNames); } + /** + * @param $name + * + * @return bool + */ protected function matchCommand($name) { - foreach ($this->commands as $cmd) { + foreach ($this->commandNames as $cmd) { if ($this->startsWith($name, $cmd)) { return true; } @@ -73,8 +89,45 @@ protected function matchCommand($name) public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); + $prevToken = array_pop($tokens); + if (self::tokenIs($prevToken, self::T_STRING) && $prevToken[1] === $input) { + $prevToken = array_pop($tokens); + } + + if (is_string($prevToken) && $prevToken === '-') { + // user is asking for the command parameters + // php open tag + array_shift($tokens); + // the command + $commandToken = array_shift($tokens); + $commandName = $commandToken[1]; + $command = &$this->commands[$commandName]; - return array_filter($this->commands, function ($command) use ($input) { + $options = $command->getDefinition()->getOptions(); + $shorts = array_filter(array_map(function (InputOption $option) { + if ($shortcut = $option->getShortcut()) { + return $shortcut; + } + }, $options)); + + $matches = array_filter( + array_values($shorts), + function ($short) use ($input) { + return AbstractMatcher::startsWith($input, $short); + } + ); + + if (! empty($matches)) { + return array_map( + function ($opt) { + return '-' . $opt; + }, + $matches + ); + } + } + + return array_filter($this->commandNames, function ($command) use ($input) { return AbstractMatcher::startsWith($input, $command); }); } @@ -86,12 +139,19 @@ public function hasMatched(array $tokens) { /* $openTag */ array_shift($tokens); $command = array_shift($tokens); + $dash = null; + if (count($tokens) > 0) { + $dash = array_shift($tokens); + } switch (true) { + case self::tokenIs($command, self::T_STRING) && + $this->isCommand($command[1]) && + empty($tokens): case self::tokenIs($command, self::T_STRING) && !$this->isCommand($command[1]) && $this->matchCommand($command[1]) && - empty($tokens): + $dash === '-': return true; } diff --git a/src/Psy/TabCompletion/Matcher/FilesMatcher.php b/src/Psy/TabCompletion/Matcher/FilesMatcher.php new file mode 100644 index 000000000..73653883b --- /dev/null +++ b/src/Psy/TabCompletion/Matcher/FilesMatcher.php @@ -0,0 +1,71 @@ +getInput($tokens); + + $quote = '"'; + if (strlen($input) > 1) { + if (strlen($input) > 1 && $input[0] === "'" || $input[0] === '"') { + $quote = substr($input, 0, 1); + $input = substr($input, 1, strlen($input)); + } + } + + $pathFolders = explode(PATH_SEPARATOR, get_include_path()); + $relativePaths = array_map(function ($path) { + $paths = glob($path . DIRECTORY_SEPARATOR . '*.*'); + + return array_map(function ($filePath) use ($path) { + return str_replace($path . DIRECTORY_SEPARATOR, '', $filePath); + }, $paths); + }, $pathFolders); + + // flatten the array + $relativePaths = call_user_func_array('array_merge', $relativePaths); + + return array_filter( + $relativePaths, + function ($file) use ($input, $quote) { + return AbstractMatcher::startsWith($input, $file); + } + ); + } + + /** + * {@inheritDoc} + */ + public function hasMatched(array $tokens) + { + $token = array_pop($tokens); + $prevToken = array_pop($tokens); + $priorToken = array_pop($tokens); + + $fileManipulatorTokens = array( + self::T_INCLUDE, self::T_INCLUDE_ONCE, self::T_REQUIRE, self::T_REQUIRE_ONCE, + ); + + switch (true) { + case is_string($token) && $token === '"' && self::hasToken($fileManipulatorTokens, $prevToken): + case is_string($prevToken) && $prevToken === '"' && self::hasToken($fileManipulatorTokens, $priorToken): + case self::tokenIs($token, self::T_ENCAPSED_AND_WHITESPACE) && + self::hasToken($fileManipulatorTokens, $prevToken): + case self::hasToken($fileManipulatorTokens, $token): + return true; + } + + return false; + } +} diff --git a/src/Psy/TabCompletion/Matcher/OperatorsMatcher.php b/src/Psy/TabCompletion/Matcher/OperatorsMatcher.php new file mode 100644 index 000000000..e643f67a8 --- /dev/null +++ b/src/Psy/TabCompletion/Matcher/OperatorsMatcher.php @@ -0,0 +1,44 @@ +getInput($tokens); + $prevToken = array_pop($tokens); + + if (self::hasToken(array(self::T_LNUMBER, self::T_STRING), $prevToken)) { + return str_split(self::MISC_OPERATORS); + } + + return array($prevToken . $prevToken); + } + + /** + * {@inheritDoc} + */ + public function hasMatched(array $tokens) + { + $token = array_pop($tokens); + + switch (true) { + case self::tokenIs($token, self::T_LNUMBER): + case is_string($token) && in_array($token, str_split(self::MISC_OPERATORS)): + case self::tokenIs($token, self::T_VARIABLE) && $this->hasVariable($token[1]): + case self::tokenIs($token, self::T_STRING) && array_key_exists($token[1], get_defined_constants()): + return true; + } + + return false; + } +}