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

Add support for autocomplete descriptions #90

Open
wants to merge 3 commits into
base: master
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Expand Up @@ -198,6 +198,25 @@ $handler->addHandler(
);
```

##### Option descriptions (zsh)

You can add custom descriptions for options by returning a `DescriptiveCompletion`:

```php
$handler->addHandler(
new DescriptiveCompletion(
Completion::ALL_COMMANDS,
'weather',
Completion::TYPE_ARGUMENT,
[
'gale' => 'winds exceeding 34 kts',
'storm' => 'winds exceeding 48 kts',
'hurricane' => 'winds exceeding 64 kts'
]
)
);
```

##### Completing the for both arguments and options

To have a completion run for both options and arguments matching the specified name, you can use the type `Completion::ALL_TYPES`. Combining this with `Completion::ALL_COMMANDS` and consistent option/argument naming throughout your application, it's easy to share completion behaviour between commands, options and arguments:
Expand Down
10 changes: 5 additions & 5 deletions src/Completion.php
@@ -1,9 +1,9 @@
<?php


namespace Stecman\Component\Symfony\Console\BashCompletion;

use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionResult;

class Completion implements CompletionInterface
{
Expand Down Expand Up @@ -57,7 +57,7 @@ public static function makeGlobalHandler($targetName, $type, $completion)
* @param string $commandName
* @param string $targetName
* @param string $type
* @param array|callable $completion
* @param array|\Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionResultInterface|callable $completion
*/
public function __construct($commandName, $targetName, $type, $completion)
{
Expand All @@ -70,15 +70,15 @@ public function __construct($commandName, $targetName, $type, $completion)
/**
* Return the stored completion, or the results returned from the completion callback
*
* @return array
* @return \Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionResultInterface
*/
public function run()
{
if ($this->isCallable()) {
return call_user_func($this->completion);
return new CompletionResult(call_user_func($this->completion));
}

return $this->completion;
return new CompletionResult($this->completion);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Completion/CompletionAwareInterface.php
Expand Up @@ -12,7 +12,7 @@ interface CompletionAwareInterface
*
* @param string $optionName
* @param CompletionContext $context
* @return array
* @return array|CompletionResultInterface
*/
public function completeOptionValues($optionName, CompletionContext $context);

Expand All @@ -21,7 +21,7 @@ public function completeOptionValues($optionName, CompletionContext $context);
*
* @param string $argumentName
* @param CompletionContext $context
* @return array
* @return array|CompletionResultInterface
*/
public function completeArgumentValues($argumentName, CompletionContext $context);
}
2 changes: 1 addition & 1 deletion src/Completion/CompletionInterface.php
Expand Up @@ -42,7 +42,7 @@ public function getTargetName();
/**
* Execute the completion
*
* @return string[] - an array of possible completion values
* @return CompletionResultInterface
*/
public function run();
}
40 changes: 40 additions & 0 deletions src/Completion/CompletionResult.php
@@ -0,0 +1,40 @@
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;

class CompletionResult implements CompletionResultInterface
{
/**
* @var string[]
*/
private $values;

/**
* @var bool
*/
private $descriptive;

public function __construct(
$values,
$descriptive = false
) {
$this->values = $values;
$this->descriptive = $descriptive;
}

/**
* @return bool
*/
public function isDescriptive()
{
return $this->descriptive;
}

/**
* @return string[]
*/
public function getValues()
{
return $this->values;
}
}
16 changes: 16 additions & 0 deletions src/Completion/CompletionResultInterface.php
@@ -0,0 +1,16 @@
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;

interface CompletionResultInterface
{
/**
* @return bool
*/
public function isDescriptive();

/**
* @return string[]
*/
public function getValues();
}
25 changes: 25 additions & 0 deletions src/Completion/DescriptiveCompletion.php
@@ -0,0 +1,25 @@
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;

use Stecman\Component\Symfony\Console\BashCompletion\Completion;

/**
* A completion that allows zsh-compatible descriptions
*/
class DescriptiveCompletion extends Completion
{
/**
* Return the stored completion, or the results returned from the completion callback
*
* @return \Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionResultInterface
*/
public function run()
{
if ($this->isCallable()) {
return new CompletionResult(call_user_func($this->completion), true);
}

return new CompletionResult($this->completion, true);
}
}
86 changes: 49 additions & 37 deletions src/CompletionCommand.php
Expand Up @@ -123,16 +123,13 @@ protected function execute(InputInterface $input, OutputInterface $output)
$handler->setContext(new EnvironmentCompletionContext());

// Get completion results
$results = $this->runCompletion();
$this->configureCompletion($handler);
$results = $this->handler->runCompletion();

// Escape results for the current shell
$shellType = $input->getOption('shell-type') ?: $this->getShellType();

foreach ($results as &$result) {
$result = $this->escapeForShell($result, $shellType);
}

$output->write($results, true);
return $this->writeForShell($results, $shellType, $output);
}

return 0;
Expand All @@ -141,56 +138,71 @@ protected function execute(InputInterface $input, OutputInterface $output)
/**
* Escape each completion result for the specified shell
*
* @param string $result - Completion results that should appear in the shell
* @param Completion\CompletionResultInterface $result - Completion results that should appear in the shell
* @param string $shellType - Valid shell type from HookFactory
* @return string
* @param OutputInterface $output
* @return int
*/
protected function escapeForShell($result, $shellType)
protected function writeForShell($result, $shellType, $output)
{
$desc = $result->isDescriptive();
$values = $result->getValues();
switch ($shellType) {
// BASH requires special escaping for multi-word and special character results
// This emulates registering completion with`-o filenames`, without side-effects like dir name slashes
case 'bash':
$context = $this->handler->getContext();
$wordStart = substr($context->getRawCurrentWord(), 0, 1);

if ($wordStart == "'") {
// If the current word is single-quoted, escape any single quotes in the result
$result = str_replace("'", "\\'", $result);
} else if ($wordStart == '"') {
// If the current word is double-quoted, escape any double quotes in the result
$result = str_replace('"', '\\"', $result);
} else {
// Otherwise assume the string is unquoted and word breaks should be escaped
$result = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $result);
if ($desc) {
// BASH does not support autocompletion descriptions, so we just want the actual suggestions
$values = array_keys($values);
}
foreach ($values as &$value) {
$context = $this->handler->getContext();
$wordStart = substr($context->getRawCurrentWord(), 0, 1);

if ($wordStart == "'") {
// If the current word is single-quoted, escape any single quotes in the result
$value = str_replace("'", "\\'", $value);
} else if ($wordStart == '"') {
// If the current word is double-quoted, escape any double quotes in the result
$value = str_replace('"', '\\"', $value);
} else {
// Otherwise assume the string is unquoted and word breaks should be escaped
$value = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $value);
}

// Escape output to prevent special characters being lost when passing results to compgen
$value = escapeshellarg($value);
}
$output->write($values, true);

// Escape output to prevent special characters being lost when passing results to compgen
return escapeshellarg($result);
return 0;

case 'zsh':
if ($desc) {
$out = array();
foreach ($values as $cmd => $description) {
$out[] = sprintf("'%s:%s'", $cmd, $description);
}

$output->write(sprintf("(%s)", implode(" ", $out)), true);
return 100;
}

// No transformation by default
default:
return $result;
if ($desc) {
$values = array_keys($values);
}
$output->write($values, true);
return 0;
}
}

/**
* Run the completion handler and return a filtered list of results
*
* @deprecated - This will be removed in 1.0.0 in favour of CompletionCommand::configureCompletion
*
* @return string[]
*/
protected function runCompletion()
{
$this->configureCompletion($this->handler);
return $this->handler->runCompletion();
}

/**
* Configure the CompletionHandler instance before it is run
*
* @param CompletionHandler $handler
* @return void
*/
protected function configureCompletion(CompletionHandler $handler)
{
Expand Down