Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] tab completion #167

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Psy/TabCompletion/Matcher/AbstractContextAwareMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions src/Psy/TabCompletion/Matcher/AbstractMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
77 changes: 69 additions & 8 deletions src/Psy/TabCompletion/Matcher/CommandsMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Psy\TabCompletion\Matcher;

use Psy\Command\Command;
use Symfony\Component\Console\Input\InputOption;

/**
* A Psy Command tab completion Matcher.
Expand All @@ -24,6 +25,9 @@
class CommandsMatcher extends AbstractMatcher
{
/** @var string[] */
protected $commandNames = array();

/** @var Command[] */
protected $commands = array();

/**
Expand All @@ -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;
}
Expand All @@ -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);
});
}
Expand All @@ -86,12 +139,20 @@ 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]) &&
$this->matchCommand($command[1]) &&
empty($tokens):
case self::tokenIs($command, self::T_STRING) &&
!$this->isCommand($command[1]) &&
$this->matchCommand($command[1]) &&
$dash === '-':
return true;
}

Expand Down
71 changes: 71 additions & 0 deletions src/Psy/TabCompletion/Matcher/FilesMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace Psy\TabCompletion\Matcher;

class FilesMatcher extends AbstractMatcher
{
/**
* Provide tab completion matches for readline input.
*
* @param array $tokens information substracted with get_token_all
* @param array $info readline_info object
*
* @return array The matches resulting from the query
*/
public function getMatches(array $tokens, array $info = array())
{
$input = $this->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;
}
}
44 changes: 44 additions & 0 deletions src/Psy/TabCompletion/Matcher/OperatorsMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Psy\TabCompletion\Matcher;

class OperatorsMatcher extends AbstractContextAwareMatcher
{
/**
* Provide tab completion matches for readline input.
*
* @param array $tokens information substracted with get_token_all
* @param array $info readline_info object
*
* @return array The matches resulting from the query
*/
public function getMatches(array $tokens, array $info = array())
{
$input = $this->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;
}
}