mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-08 07:52:39 +01:00
[Wilds] Update "arc shell-complete" for toolsets
Summary: Ref T13098. Major changes: `arc shell-complete` now installs itself to your `~/.profile`. Running `arc shell-complete` again will update the hook and update the completion rules. Completion rules work for all toolsets, so you can install once and then autocomplete in `arc`, `phage`, etc. This code supports other shells in theory, and I developed most of it with ZSH support next to the Bash support. However, while actually testing ZSH support, I couldn't get it to even slightly work and found myself falling down a very, very deep rabbit hole of ZSH being entirely written in bash script and `${0🅰️h}` being a legitimate script construction. The existing ZSH support comes from one guy in 2012 and also does not work for me on `master`, so I ultimately removed it. Open to restoring it but I wasn't able to figure it out in 10 minutes of Googling and I'm not convinced it's worth 11 minutes of Googling. I left a few rough edges here with notes on how to improve/fix them, but the basics all work. Test Plan: - Ran `arc shell-complete` under various stages of `~/.profile`, couldn't get it to do anything bad. - Ran `arc lib<tab>`, `arc shell-complete --curr<tab>`, etc. Got sensible suggestions and completions. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13098 Differential Revision: https://secure.phabricator.com/D19700
This commit is contained in:
parent
b6f93a46d7
commit
50dfc9cc41
7 changed files with 613 additions and 156 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -19,7 +19,6 @@
|
|||
/support/xhpast/parser.yacc.output
|
||||
/support/xhpast/node_names.hpp
|
||||
/support/xhpast/xhpast
|
||||
/support/xhpast/xhpast.exe
|
||||
/src/parser/xhpast/bin/xhpast
|
||||
|
||||
## NOTE: Don't .gitignore these files! Even though they're build artifacts, we
|
||||
|
@ -32,9 +31,8 @@
|
|||
# This is an OS X build artifact.
|
||||
/support/xhpast/xhpast.dSYM
|
||||
|
||||
# libphutil
|
||||
/support/phutiltestlib/.phutil_module_cache
|
||||
|
||||
# This file overrides "default.pem" if present.
|
||||
/resources/ssl/custom.pem
|
||||
|
||||
# Generated shell completion rules.
|
||||
/support/shell/rules/*
|
||||
|
|
|
@ -77,5 +77,13 @@ final class ArcanistLogEngine
|
|||
return $trace;
|
||||
}
|
||||
|
||||
public function writeHint($label, $message) {
|
||||
return $this->writeMessage(
|
||||
$this->newMessage()
|
||||
->setColor('cyan')
|
||||
->setLabel($label)
|
||||
->setMessage($message));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ final class ArcanistWorkflowArgument
|
|||
private $key;
|
||||
private $help;
|
||||
private $wildcard;
|
||||
private $parameter;
|
||||
private $isPathArgument;
|
||||
|
||||
public function setKey($key) {
|
||||
$this->key = $key;
|
||||
|
@ -34,6 +36,11 @@ final class ArcanistWorkflowArgument
|
|||
$spec['wildcard'] = true;
|
||||
}
|
||||
|
||||
$parameter = $this->getParameter();
|
||||
if ($parameter !== null) {
|
||||
$spec['param'] = $parameter;
|
||||
}
|
||||
|
||||
return $spec;
|
||||
}
|
||||
|
||||
|
@ -46,5 +53,23 @@ final class ArcanistWorkflowArgument
|
|||
return $this->help;
|
||||
}
|
||||
|
||||
public function setParameter($parameter) {
|
||||
$this->parameter = $parameter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParameter() {
|
||||
return $this->parameter;
|
||||
}
|
||||
|
||||
public function setIsPathArgument($is_path_argument) {
|
||||
$this->isPathArgument = $is_path_argument;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsPathArgument() {
|
||||
return $this->isPathArgument;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,203 +1,623 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Powers shell-completion scripts.
|
||||
*/
|
||||
final class ArcanistShellCompleteWorkflow extends ArcanistWorkflow {
|
||||
|
||||
public function supportsToolset(ArcanistToolset $toolset) {
|
||||
return true;
|
||||
}
|
||||
final class ArcanistShellCompleteWorkflow
|
||||
extends ArcanistWorkflow {
|
||||
|
||||
public function getWorkflowName() {
|
||||
return 'shell-complete';
|
||||
}
|
||||
|
||||
public function getWorkflowSynopses() {
|
||||
return array(
|
||||
pht('**shell-complete** __--current__ __N__ -- [__argv__]'),
|
||||
);
|
||||
}
|
||||
public function getWorkflowInformation() {
|
||||
$help = pht(<<<EOTEXT
|
||||
Install shell completion so you can use the "tab" key to autocomplete
|
||||
commands and flags in your shell for Arcanist toolsets and workflows.
|
||||
|
||||
public function getWorkflowHelp() {
|
||||
return pht(<<<EOTEXT
|
||||
Implements shell completion. To use shell completion, source the appropriate
|
||||
script from 'resources/shell/' in your .shellrc.
|
||||
The **bash** shell is supported.
|
||||
|
||||
**Installing Completion**
|
||||
|
||||
To install shell completion, run the command:
|
||||
|
||||
$ arc shell-complete
|
||||
|
||||
This will install shell completion into your current shell. After installing,
|
||||
you may need to start a new shell (or open a new terminal window) to pick up
|
||||
the updated configuration.
|
||||
|
||||
Once installed, completion should work across all Arcanist toolsets.
|
||||
|
||||
**Using Completion**
|
||||
|
||||
After completion is installed, use the "tab" key to automatically complete
|
||||
workflows and flags. For example, if you type:
|
||||
|
||||
$ arc diff --draf<tab>
|
||||
|
||||
...your shell should automatically expand the flag to:
|
||||
|
||||
$ arc diff --draft
|
||||
|
||||
**Updating Completion**
|
||||
|
||||
To update shell completion, run the same command:
|
||||
|
||||
$ arc shell-complete
|
||||
|
||||
You can update shell completion without reinstalling it by running:
|
||||
|
||||
$ arc shell-complete --generate
|
||||
|
||||
You may need to update shell completion if:
|
||||
|
||||
- you install new Arcanist toolsets; or
|
||||
- you move the Arcanist directory; or
|
||||
- you upgrade Arcanist and the new version fixes shell completion bugs.
|
||||
EOTEXT
|
||||
);
|
||||
);
|
||||
|
||||
return $this->newWorkflowInformation()
|
||||
->setHelp($help);
|
||||
}
|
||||
|
||||
public function getArguments() {
|
||||
public function getWorkflowArguments() {
|
||||
return array(
|
||||
'current' => array(
|
||||
'param' => 'cursor_position',
|
||||
'paramtype' => 'int',
|
||||
'help' => pht('Current term in the argument list being completed.'),
|
||||
),
|
||||
'*' => 'argv',
|
||||
$this->newWorkflowArgument('current')
|
||||
->setParameter('cursor-position')
|
||||
->setHelp(
|
||||
pht(
|
||||
'Internal. Current term in the argument list being completed.')),
|
||||
$this->newWorkflowArgument('generate')
|
||||
->setHelp(
|
||||
pht(
|
||||
'Regenerate shell completion rules, without installing any '.
|
||||
'configuration.')),
|
||||
$this->newWorkflowArgument('shell')
|
||||
->setParameter('shell-name')
|
||||
->setHelp(
|
||||
pht(
|
||||
'Install completion support for a particular shell.')),
|
||||
$this->newWorkflowArgument('argv')
|
||||
->setWildcard(true),
|
||||
);
|
||||
}
|
||||
|
||||
protected function shouldShellComplete() {
|
||||
return false;
|
||||
public function runWorkflow() {
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
$argv = $this->getArgument('argv');
|
||||
|
||||
$is_generate = $this->getArgument('generate');
|
||||
$is_shell = (bool)strlen($this->getArgument('shell'));
|
||||
$is_current = $this->getArgument('current');
|
||||
|
||||
if ($argv) {
|
||||
$should_install = false;
|
||||
$should_generate = false;
|
||||
|
||||
if ($is_generate) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'You can not use "--generate" when completing arguments.'));
|
||||
}
|
||||
|
||||
if ($is_shell) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'You can not use "--shell" when completing arguments.'));
|
||||
}
|
||||
|
||||
} else if ($is_generate) {
|
||||
$should_install = false;
|
||||
$should_generate = true;
|
||||
|
||||
if ($is_current) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'You can not use "--current" when generating rules.'));
|
||||
}
|
||||
|
||||
if ($is_shell) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'The flags "--generate" and "--shell" are mutually exclusive. '.
|
||||
'The "--shell" flag selects which shell to install support for, '.
|
||||
'but the "--generate" suppresses installation.'));
|
||||
}
|
||||
|
||||
} else {
|
||||
$should_install = true;
|
||||
$should_generate = true;
|
||||
|
||||
if ($is_current) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'You can not use "--current" when installing support.'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($should_install) {
|
||||
$this->runInstall();
|
||||
}
|
||||
|
||||
if ($should_generate) {
|
||||
$this->runGenerate();
|
||||
}
|
||||
|
||||
if ($should_install || $should_generate) {
|
||||
$log->writeHint(
|
||||
pht('NOTE'),
|
||||
pht(
|
||||
'You may need to open a new terminal window or launch a new shell '.
|
||||
'before the changes take effect.'));
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->runAutocomplete();
|
||||
}
|
||||
|
||||
public function run() {
|
||||
$pos = $this->getArgument('current');
|
||||
$argv = $this->getArgument('argv', array());
|
||||
private function runInstall() {
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
$shells = array(
|
||||
array(
|
||||
'key' => 'bash',
|
||||
'path' => '/bin/bash',
|
||||
'file' => '.profile',
|
||||
'source' => 'hooks/bash-completion.sh',
|
||||
),
|
||||
);
|
||||
$shells = ipull($shells, null, 'key');
|
||||
|
||||
$shell = $this->getArgument('shell');
|
||||
if (!$shell) {
|
||||
$shell = $this->detectShell($shells);
|
||||
} else {
|
||||
$shell = $this->selectShell($shells, $shell);
|
||||
}
|
||||
|
||||
$spec = $shells[$shell];
|
||||
$file = $spec['file'];
|
||||
$home = getenv('HOME');
|
||||
|
||||
if (!strlen($home)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'The "HOME" environment variable is not defined, so this workflow '.
|
||||
'can not identify where to install shell completion.'));
|
||||
}
|
||||
|
||||
$file_path = getenv('HOME').'/'.$file;
|
||||
$file_display = '~/'.$file;
|
||||
|
||||
if (Filesystem::pathExists($file_path)) {
|
||||
$file_path = Filesystem::resolvePath($file_path);
|
||||
$data = Filesystem::readFile($file_path);
|
||||
$is_new = false;
|
||||
} else {
|
||||
$data = '';
|
||||
$is_new = true;
|
||||
}
|
||||
|
||||
$line = csprintf(
|
||||
'source %R # arcanist-shell-complete',
|
||||
$this->getShellPath($spec['source']));
|
||||
|
||||
$matches = null;
|
||||
$replace = preg_match(
|
||||
'/(\s*\n)?[^\n]+# arcanist-shell-complete\s*(\n\s*)?/',
|
||||
$data,
|
||||
$matches,
|
||||
PREG_OFFSET_CAPTURE);
|
||||
|
||||
$log->writeSuccess(
|
||||
pht('INSTALL'),
|
||||
pht('Installing shell completion support for "%s".', $shell));
|
||||
|
||||
if ($replace) {
|
||||
$replace_pos = $matches[0][1];
|
||||
$replace_line = $matches[0][0];
|
||||
$replace_len = strlen($replace_line);
|
||||
$replace_display = trim($replace_line);
|
||||
|
||||
if ($replace_pos === 0) {
|
||||
$new_line = $line."\n";
|
||||
} else {
|
||||
$new_line = "\n\n".$line."\n";
|
||||
}
|
||||
|
||||
$new_data = substr_replace($data, $new_line, $replace_pos, $replace_len);
|
||||
|
||||
if ($new_data === $data) {
|
||||
// If we aren't changing anything in the file, just skip the write
|
||||
// completely.
|
||||
$needs_write = false;
|
||||
|
||||
$log->writeStatus(
|
||||
pht('SKIP'),
|
||||
pht('Shell completion for "%s" is already installed.', $shell));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n\n %s\n\n%s\n\n %s\n",
|
||||
pht(
|
||||
'To update shell completion support for "%s", your existing '.
|
||||
'"%s" file will be modified. This line will be removed:',
|
||||
$shell,
|
||||
$file_display),
|
||||
$replace_display,
|
||||
pht('This line will be added:'),
|
||||
$line);
|
||||
|
||||
$prompt = pht('Rewrite this file?');
|
||||
} else {
|
||||
if ($is_new) {
|
||||
$new_data = $line."\n";
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n\n %s\n",
|
||||
pht(
|
||||
'To install shell completion support for "%s", a new "%s" file '.
|
||||
'will be created with this content:',
|
||||
$shell,
|
||||
$file_display),
|
||||
$line);
|
||||
|
||||
$prompt = pht('Create this file?');
|
||||
} else {
|
||||
$new_data = rtrim($data)."\n\n".$line."\n";
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n\n %s\n",
|
||||
pht(
|
||||
'To install shell completion support for "%s", this line will be '.
|
||||
'added to your existing "%s" file:',
|
||||
$shell,
|
||||
$file_display),
|
||||
$line);
|
||||
|
||||
$prompt = pht('Append to this file?');
|
||||
}
|
||||
}
|
||||
|
||||
// TOOLSETS: Generalize prompting.
|
||||
|
||||
if (!phutil_console_confirm($prompt, false)) {
|
||||
throw new PhutilArgumentUsageException(pht('Aborted.'));
|
||||
}
|
||||
|
||||
Filesystem::writeFile($file_path, $new_data);
|
||||
|
||||
$log->writeSuccess(
|
||||
pht('INSTALLED'),
|
||||
pht(
|
||||
'Installed shell completion support for "%s" to "%s".',
|
||||
$shell,
|
||||
$file_display));
|
||||
}
|
||||
|
||||
private function selectShell(array $shells, $shell_arg) {
|
||||
foreach ($shells as $shell) {
|
||||
if ($shell['key'] === $shell_arg) {
|
||||
return $shell_arg;
|
||||
}
|
||||
}
|
||||
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'The shell "%s" is not supported. Supported shells are: %s.',
|
||||
$shell_arg,
|
||||
implode(', ', ipull($shells, 'key'))));
|
||||
}
|
||||
|
||||
private function detectShell(array $shells) {
|
||||
// NOTE: The "BASH_VERSION" and "ZSH_VERSION" shell variables are not
|
||||
// passed to subprocesses, so we can't inspect them to figure out which
|
||||
// shell launched us. If we could figure this out in some other way, it
|
||||
// would be nice to do so.
|
||||
|
||||
// Instead, just look at "SHELL" (the user's startup shell).
|
||||
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
$detected = array();
|
||||
$log->writeStatus(
|
||||
pht('DETECT'),
|
||||
pht('Detecting current shell...'));
|
||||
|
||||
$shell_env = getenv('SHELL');
|
||||
if (!strlen($shell_env)) {
|
||||
$log->writeWarning(
|
||||
pht('SHELL'),
|
||||
pht(
|
||||
'The "SHELL" environment variable is not defined, so it can '.
|
||||
'not be used to detect the shell to install rules for.'));
|
||||
} else {
|
||||
$found = false;
|
||||
foreach ($shells as $shell) {
|
||||
if ($shell['path'] !== $shell_env) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = true;
|
||||
$detected[] = $shell['key'];
|
||||
|
||||
$log->writeSuccess(
|
||||
pht('SHELL'),
|
||||
pht(
|
||||
'The "SHELL" environment variable has value "%s", so the '.
|
||||
'target shell was detected as "%s".',
|
||||
$shell_env,
|
||||
$shell['key']));
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$log->writeStatus(
|
||||
pht('SHELL'),
|
||||
pht(
|
||||
'The "SHELL" environment variable does not match any recognized '.
|
||||
'shell.'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$detected) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Unable to detect any supported shell, so autocompletion rules '.
|
||||
'can not be installed. Use "--shell" to select a shell.'));
|
||||
} else if (count($detected) > 1) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Multiple supported shells were detected. Unable to determine '.
|
||||
'which shell to install autocompletion rules for. Use "--shell" '.
|
||||
'to select a shell.'));
|
||||
}
|
||||
|
||||
return head($detected);
|
||||
}
|
||||
|
||||
private function runGenerate() {
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
$toolsets = ArcanistToolset::newToolsetMap();
|
||||
|
||||
$log->writeStatus(
|
||||
pht('GENERATE'),
|
||||
pht('Generating shell completion rules...'));
|
||||
|
||||
$shells = array('bash');
|
||||
foreach ($shells as $shell) {
|
||||
|
||||
$rules = array();
|
||||
foreach ($toolsets as $toolset) {
|
||||
$rules[] = $this->newCompletionRules($toolset, $shell);
|
||||
}
|
||||
$rules = implode("\n", $rules);
|
||||
|
||||
$rules_path = $this->getShellPath('rules/'.$shell.'-rules.sh');
|
||||
|
||||
// If a write wouldn't change anything, skip the write. This allows
|
||||
// "arc shell-complete" to work if "arcanist/" is on a read-only NFS
|
||||
// filesystem or something unusual like that.
|
||||
|
||||
$skip_write = false;
|
||||
if (Filesystem::pathExists($rules_path)) {
|
||||
$current = Filesystem::readFile($rules_path);
|
||||
if ($current === $rules) {
|
||||
$skip_write = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($skip_write) {
|
||||
$log->writeStatus(
|
||||
pht('SKIP'),
|
||||
pht(
|
||||
'Rules are already up to date for "%s" in: %s',
|
||||
$shell,
|
||||
Filesystem::readablePath($rules_path)));
|
||||
} else {
|
||||
Filesystem::writeFile($rules_path, $rules);
|
||||
$log->writeStatus(
|
||||
pht('RULES'),
|
||||
pht(
|
||||
'Wrote updated completion rules for "%s" to: %s.',
|
||||
$shell,
|
||||
Filesystem::readablePath($rules_path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function newCompletionRules(ArcanistToolset $toolset, $shell) {
|
||||
$template_path = $this->getShellPath('templates/'.$shell.'-template.sh');
|
||||
$template = Filesystem::readFile($template_path);
|
||||
|
||||
$variables = array(
|
||||
'BIN' => $toolset->getToolsetKey(),
|
||||
);
|
||||
|
||||
foreach ($variables as $key => $value) {
|
||||
$template = str_replace('{{{'.$key.'}}}', $value, $template);
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
private function getShellPath($to_file = null) {
|
||||
$arc_root = dirname(phutil_get_library_root('arcanist'));
|
||||
return $arc_root.'/support/shell/'.$to_file;
|
||||
}
|
||||
|
||||
private function runAutocomplete() {
|
||||
$argv = $this->getArgument('argv');
|
||||
$argc = count($argv);
|
||||
if ($pos === null) {
|
||||
|
||||
$pos = $this->getArgument('current');
|
||||
if (!$pos) {
|
||||
$pos = $argc - 1;
|
||||
}
|
||||
|
||||
if ($pos > $argc) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'Specified position is greater than the number of '.
|
||||
'arguments provided.'));
|
||||
'Argument position specified with "--current" ("%s") is greater '.
|
||||
'than the number of arguments provided ("%s").',
|
||||
new PhutilNumber($pos),
|
||||
new PhutilNumber($argc)));
|
||||
}
|
||||
|
||||
// Determine which revision control system the working copy uses, so we
|
||||
// can filter out commands and flags which aren't supported. If we can't
|
||||
// figure it out, just return all flags/commands.
|
||||
$vcs = null;
|
||||
$workflows = $this->getRuntime()->getWorkflows();
|
||||
|
||||
// We have to build our own because if we requiresWorkingCopy() we'll throw
|
||||
// if we aren't in a .arcconfig directory. We probably still can't do much,
|
||||
// but commands can raise more detailed errors.
|
||||
$configuration_manager = $this->getConfigurationManager();
|
||||
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(getcwd());
|
||||
if ($working_copy->getVCSType()) {
|
||||
$configuration_manager->setWorkingCopyIdentity($working_copy);
|
||||
$repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
|
||||
$configuration_manager);
|
||||
// NOTE: This isn't quite right. For example, "arc --con<tab>" will
|
||||
// try to autocomplete workflows named "--con", but it should actually
|
||||
// autocomplete global flags and suggest "--config".
|
||||
|
||||
$vcs = $repository_api->getSourceControlSystemName();
|
||||
}
|
||||
$is_workflow = ($pos <= 1);
|
||||
|
||||
$arc_config = $this->getArcanistConfiguration();
|
||||
if ($is_workflow) {
|
||||
// NOTE: There was previously some logic to try to filter impossible
|
||||
// workflows out of the completion list based on the VCS in the current
|
||||
// working directory: for example, if you're in an SVN working directory,
|
||||
// "arc a" is unlikely to complete to "arc amend" because "amend" does
|
||||
// not support SVN. It's not clear this logic is valuable, but we could
|
||||
// consider restoring it if good use cases arise.
|
||||
|
||||
if ($pos <= 1) {
|
||||
$workflows = $arc_config->buildAllWorkflows();
|
||||
// TOOLSETS: Restore the ability for workflows to opt out of shell
|
||||
// completion. It is exceptionally unlikely that users want to shell
|
||||
// complete obscure or internal workflows, like "arc shell-complete"
|
||||
// itself. Perhaps a good behavior would be to offer these as
|
||||
// completions if they are the ONLY available completion, since a user
|
||||
// who has typed "arc shell-comp<tab>" likely does want "shell-complete".
|
||||
|
||||
$complete = array();
|
||||
foreach ($workflows as $name => $workflow) {
|
||||
if (!$workflow->shouldShellComplete()) {
|
||||
foreach ($workflows as $workflow) {
|
||||
$complete[] = $workflow->getWorkflowName();
|
||||
}
|
||||
|
||||
foreach ($this->getConfig('aliases') as $alias) {
|
||||
if ($alias->getException()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$workflow->setArcanistConfiguration($this->getArcanistConfiguration());
|
||||
$workflow->setConfigurationManager($this->getConfigurationManager());
|
||||
|
||||
if ($vcs || $workflow->requiresWorkingCopy()) {
|
||||
$supported_vcs = $workflow->getSupportedRevisionControlSystems();
|
||||
if (!in_array($vcs, $supported_vcs)) {
|
||||
continue;
|
||||
}
|
||||
if ($alias->getToolset() !== $this->getToolsetKey()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$complete[] = $name;
|
||||
$complete[] = $alias->getTrigger();
|
||||
}
|
||||
|
||||
// Also permit autocompletion of "arc alias" commands.
|
||||
$aliases = ArcanistAliasWorkflow::getAliases($configuration_manager);
|
||||
foreach ($aliases as $key => $value) {
|
||||
$complete[] = $key;
|
||||
// Remove invalid possibilities. For example, if the user has typed
|
||||
// "skun<tab>", it obviously can't complete to "zebra". We don't really
|
||||
// need to do this filtering ourselves: the shell completion will
|
||||
// automatically match things for us even if we emit impossible results.
|
||||
// However, it's a little easier to debug the raw output if we clean it
|
||||
// up here before printing it out.
|
||||
$partial = $argv[$pos];
|
||||
foreach ($complete as $key => $candidate) {
|
||||
if (strncmp($partial, $candidate, strlen($partial))) {
|
||||
unset($complete[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($complete) {
|
||||
return $this->suggestStrings($complete);
|
||||
} else {
|
||||
return $this->suggestNothing();
|
||||
}
|
||||
|
||||
echo implode(' ', $complete)."\n";
|
||||
return 0;
|
||||
} else {
|
||||
$workflow = $arc_config->buildWorkflow($argv[1]);
|
||||
// TOOLSETS: We should resolve aliases before picking a workflow, so
|
||||
// that if you alias "arc draft" to "arc diff --draft", we can suggest
|
||||
// other "diff" flags when you type "arc draft --q<tab>".
|
||||
|
||||
// TOOLSETS: It's possible the workflow isn't in position 1. The user
|
||||
// may be running "arc --trace diff --dra<tab>", for example.
|
||||
|
||||
$workflow = idx($workflows, $argv[1]);
|
||||
if (!$workflow) {
|
||||
list($new_command, $new_args) = ArcanistAliasWorkflow::resolveAliases(
|
||||
$argv[1],
|
||||
$arc_config,
|
||||
array_slice($argv, 2),
|
||||
$configuration_manager);
|
||||
if ($new_command) {
|
||||
$workflow = $arc_config->buildWorkflow($new_command);
|
||||
}
|
||||
if (!$workflow) {
|
||||
return 1;
|
||||
} else {
|
||||
$argv = array_merge(
|
||||
array($argv[0]),
|
||||
array($new_command),
|
||||
$new_args);
|
||||
}
|
||||
return $this->suggestNothing();
|
||||
}
|
||||
|
||||
$arguments = $workflow->getArguments();
|
||||
$arguments = $workflow->getWorkflowArguments();
|
||||
$arguments = mpull($arguments, null, 'getKey');
|
||||
|
||||
$argument = null;
|
||||
$prev = idx($argv, $pos - 1, null);
|
||||
if (!strncmp($prev, '--', 2)) {
|
||||
$prev = substr($prev, 2);
|
||||
} else {
|
||||
$prev = null;
|
||||
$argument = idx($arguments, $prev);
|
||||
}
|
||||
|
||||
if ($prev !== null &&
|
||||
isset($arguments[$prev]) &&
|
||||
isset($arguments[$prev]['param'])) {
|
||||
// If the last argument was a "--thing" argument, test if "--thing" is
|
||||
// a parameterized argument. If it is, the next argument should be a
|
||||
// parameter.
|
||||
|
||||
$type = idx($arguments[$prev], 'paramtype');
|
||||
switch ($type) {
|
||||
case 'file':
|
||||
echo "FILE\n";
|
||||
break;
|
||||
case 'complete':
|
||||
echo implode(' ', $workflow->getShellCompletions($argv))."\n";
|
||||
break;
|
||||
default:
|
||||
echo "ARGUMENT\n";
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
$output = array();
|
||||
foreach ($arguments as $argument => $spec) {
|
||||
if ($argument == '*') {
|
||||
continue;
|
||||
}
|
||||
if ($vcs &&
|
||||
isset($spec['supports']) &&
|
||||
!in_array($vcs, $spec['supports'])) {
|
||||
continue;
|
||||
}
|
||||
$output[] = '--'.$argument;
|
||||
if ($argument && strlen($argument->getParameter())) {
|
||||
if ($argument->getIsPathArgument()) {
|
||||
return $this->suggestPaths();
|
||||
} else {
|
||||
return $this->suggestNothing();
|
||||
}
|
||||
|
||||
$cur = idx($argv, $pos, '');
|
||||
$any_match = false;
|
||||
|
||||
if (strlen($cur)) {
|
||||
foreach ($output as $possible) {
|
||||
if (!strncmp($possible, $cur, strlen($cur))) {
|
||||
$any_match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$any_match && isset($arguments['*'])) {
|
||||
// TODO: This is mega hacktown but something else probably breaks
|
||||
// if we use a rich argument specification; fix it when we move to
|
||||
// PhutilArgumentParser since everything will need to be tested then
|
||||
// anyway.
|
||||
if ($arguments['*'] == 'branch' && isset($repository_api)) {
|
||||
$branches = $repository_api->getAllBranches();
|
||||
$branches = ipull($branches, 'name');
|
||||
$output = $branches;
|
||||
} else {
|
||||
$output = array('FILE');
|
||||
}
|
||||
}
|
||||
|
||||
echo implode(' ', $output)."\n";
|
||||
|
||||
return 0;
|
||||
// TOOLSETS: We can allow workflows and arguments to provide a specific
|
||||
// list of completeable values, like the "--shell" argument for this
|
||||
// workflow.
|
||||
}
|
||||
|
||||
$flags = array();
|
||||
$wildcard = null;
|
||||
foreach ($arguments as $argument) {
|
||||
if ($argument->getWildcard()) {
|
||||
$wildcard = $argument;
|
||||
continue;
|
||||
}
|
||||
|
||||
$flags[] = '--'.$argument->getKey();
|
||||
}
|
||||
|
||||
$current = idx($argv, $pos, '');
|
||||
$matches = array();
|
||||
if (strlen($current)) {
|
||||
foreach ($flags as $possible) {
|
||||
if (!strncmp($possible, $current, strlen($current))) {
|
||||
$matches[] = $possible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If whatever the user is completing does not match the prefix of any
|
||||
// flag, try to autcomplete a wildcard argument if it has some kind of
|
||||
// meaningful completion. For example, "arc lint READ<tab>" should
|
||||
// autocomplete a file.
|
||||
|
||||
if (!$matches && $wildcard) {
|
||||
|
||||
// TOOLSETS: There was previously some very questionable support for
|
||||
// autocompleting branches here. This could be moved into Arguments
|
||||
// and Workflows.
|
||||
|
||||
if ($wildcard->getIsPathArgument()) {
|
||||
return $this->suggestPaths();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->suggestStrings($matches);
|
||||
}
|
||||
}
|
||||
|
||||
private function suggestPaths() {
|
||||
echo "FILE\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function suggestNothing() {
|
||||
echo "ARGUMENT\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function suggestStrings(array $strings) {
|
||||
echo implode(' ', $strings)."\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
9
support/shell/hooks/bash-completion.sh
Normal file
9
support/shell/hooks/bash-completion.sh
Normal file
|
@ -0,0 +1,9 @@
|
|||
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"
|
||||
|
||||
# Try to generate the shell completion rules if they do not yet exist.
|
||||
if [ ! -f "${SCRIPTDIR}/bash-rules.sh" ]; then
|
||||
arc shell-complete --generate >/dev/null 2>/dev/null
|
||||
fi;
|
||||
|
||||
# Source the shell completion rules.
|
||||
source "${SCRIPTDIR}/../rules/bash-rules.sh"
|
0
support/shell/rules/.keep
Normal file
0
support/shell/rules/.keep
Normal file
|
@ -1,12 +1,9 @@
|
|||
if [[ -n ${ZSH_VERSION-} ]]; then
|
||||
autoload -U +X bashcompinit && bashcompinit
|
||||
fi
|
||||
|
||||
_arc ()
|
||||
_arcanist_complete_{{{BIN}}} ()
|
||||
{
|
||||
CUR="${COMP_WORDS[COMP_CWORD]}"
|
||||
COMPREPLY=()
|
||||
OPTS=$(echo | arc shell-complete --current ${COMP_CWORD} -- ${COMP_WORDS[@]})
|
||||
|
||||
CUR="${COMP_WORDS[COMP_CWORD]}"
|
||||
OPTS=$(echo | {{{BIN}}} shell-complete --current ${COMP_CWORD} -- ${COMP_WORDS[@]} 2>/dev/null)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
return $?
|
||||
|
@ -23,4 +20,4 @@ _arc ()
|
|||
|
||||
COMPREPLY=( $(compgen -W "${OPTS}" -- ${CUR}) )
|
||||
}
|
||||
complete -F _arc -o filenames arc
|
||||
complete -F _arcanist_complete_{{{BIN}}} -o filenames {{{BIN}}}
|
Loading…
Reference in a new issue