mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-09 16:32:39 +01:00
Port "arc shell-complete" to Toolsets
Summary: Depends on D20996. Ref T13395. Ports the updated version of this workflow from "experimental". Test Plan: Ran `arc shell-complete` to install the hook, then shell-completed commands. Maniphest Tasks: T13395 Differential Revision: https://secure.phabricator.com/D20997
This commit is contained in:
parent
70c6045c7f
commit
8cd79d38af
4 changed files with 661 additions and 202 deletions
|
@ -402,7 +402,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
|
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
|
||||||
'ArcanistSetting' => 'configuration/ArcanistSetting.php',
|
'ArcanistSetting' => 'configuration/ArcanistSetting.php',
|
||||||
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
|
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
|
||||||
'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php',
|
'ArcanistShellCompleteWorkflow' => 'toolset/workflow/ArcanistShellCompleteWorkflow.php',
|
||||||
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
|
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
|
||||||
'ArcanistSlownessXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php',
|
'ArcanistSlownessXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php',
|
||||||
'ArcanistSlownessXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSlownessXHPASTLinterRuleTestCase.php',
|
'ArcanistSlownessXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSlownessXHPASTLinterRuleTestCase.php',
|
||||||
|
|
656
src/toolset/workflow/ArcanistShellCompleteWorkflow.php
Normal file
656
src/toolset/workflow/ArcanistShellCompleteWorkflow.php
Normal file
|
@ -0,0 +1,656 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class ArcanistShellCompleteWorkflow
|
||||||
|
extends ArcanistWorkflow {
|
||||||
|
|
||||||
|
public function supportsToolset(ArcanistToolset $toolset) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWorkflowName() {
|
||||||
|
return 'shell-complete';
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 getWorkflowArguments() {
|
||||||
|
return array(
|
||||||
|
$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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function newPrompts() {
|
||||||
|
return array(
|
||||||
|
$this->newPrompt('arc.shell-complete.install')
|
||||||
|
->setDescription(
|
||||||
|
pht(
|
||||||
|
'Confirms writing to to "~/.profile" (or another similar file) '.
|
||||||
|
'to install shell completion.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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?');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getPrompt('arc.shell-complete.install')
|
||||||
|
->setQuery($prompt)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
$pos = $this->getArgument('current');
|
||||||
|
if (!$pos) {
|
||||||
|
$pos = $argc - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pos >= $argc) {
|
||||||
|
throw new ArcanistUsageException(
|
||||||
|
pht(
|
||||||
|
'Argument position specified with "--current" ("%s") is greater '.
|
||||||
|
'than the number of arguments provided ("%s").',
|
||||||
|
new PhutilNumber($pos),
|
||||||
|
new PhutilNumber($argc)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$workflows = $this->getRuntime()->getWorkflows();
|
||||||
|
|
||||||
|
// 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".
|
||||||
|
|
||||||
|
$is_workflow = ($pos <= 1);
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
// 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 $workflow) {
|
||||||
|
$complete[] = $workflow->getWorkflowName();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->getConfig('aliases') as $alias) {
|
||||||
|
if ($alias->getException()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($alias->getToolset() !== $this->getToolsetKey()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$complete[] = $alias->getTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
$partial = $argv[$pos];
|
||||||
|
$complete = $this->getMatches($complete, $partial);
|
||||||
|
|
||||||
|
if ($complete) {
|
||||||
|
return $this->suggestStrings($complete);
|
||||||
|
} else {
|
||||||
|
return $this->suggestNothing();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 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) {
|
||||||
|
return $this->suggestNothing();
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = $workflow->getWorkflowArguments();
|
||||||
|
$arguments = mpull($arguments, null, 'getKey');
|
||||||
|
$current = idx($argv, $pos, '');
|
||||||
|
|
||||||
|
$argument = null;
|
||||||
|
$prev = idx($argv, $pos - 1, null);
|
||||||
|
if (!strncmp($prev, '--', 2)) {
|
||||||
|
$prev = substr($prev, 2);
|
||||||
|
$argument = idx($arguments, $prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
if ($argument && strlen($argument->getParameter())) {
|
||||||
|
if ($argument->getIsPathArgument()) {
|
||||||
|
return $this->suggestPaths($current);
|
||||||
|
} else {
|
||||||
|
return $this->suggestNothing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
$matches = $this->getMatches($flags, $current);
|
||||||
|
|
||||||
|
// 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($current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If a command has only one flag, like "--json", don't suggest
|
||||||
|
// it if the user hasn't typed anything or has only typed "--".
|
||||||
|
|
||||||
|
// TODO: Don't suggest "--flag" arguments which aren't repeatable if
|
||||||
|
// they are already present in the argument list.
|
||||||
|
|
||||||
|
return $this->suggestStrings($matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function suggestPaths($prefix) {
|
||||||
|
// NOTE: We are returning a directive to the bash script to run "compgen"
|
||||||
|
// for us rather than running it ourselves. If we run:
|
||||||
|
//
|
||||||
|
// compgen -A file -- %s
|
||||||
|
//
|
||||||
|
// ...from this context, it fails (exits with error code 1 and no output)
|
||||||
|
// if the prefix is "foo\ ", on my machine. See T9116 for some dicussion.
|
||||||
|
echo '<compgen:file>';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function suggestNothing() {
|
||||||
|
return $this->suggestStrings(array());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function suggestStrings(array $strings) {
|
||||||
|
sort($strings);
|
||||||
|
echo implode("\n", $strings);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMatches(array $candidates, $prefix) {
|
||||||
|
$matches = array();
|
||||||
|
|
||||||
|
if (strlen($prefix)) {
|
||||||
|
foreach ($candidates as $possible) {
|
||||||
|
if (!strncmp($possible, $prefix, strlen($prefix))) {
|
||||||
|
$matches[] = $possible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we matched nothing, try a case-insensitive match.
|
||||||
|
if (!$matches) {
|
||||||
|
foreach ($candidates as $possible) {
|
||||||
|
if (!strncasecmp($possible, $prefix, strlen($prefix))) {
|
||||||
|
$matches[] = $possible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$matches = $candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,201 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Powers shell-completion scripts.
|
|
||||||
*/
|
|
||||||
final class ArcanistShellCompleteWorkflow extends ArcanistWorkflow {
|
|
||||||
|
|
||||||
public function getWorkflowName() {
|
|
||||||
return 'shell-complete';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCommandSynopses() {
|
|
||||||
return phutil_console_format(<<<EOTEXT
|
|
||||||
**shell-complete** __--current__ __N__ -- [__argv__]
|
|
||||||
EOTEXT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCommandHelp() {
|
|
||||||
return phutil_console_format(<<<EOTEXT
|
|
||||||
Supports: bash, etc.
|
|
||||||
Implements shell completion. To use shell completion, source the
|
|
||||||
appropriate script from 'resources/shell/' in your .shellrc.
|
|
||||||
EOTEXT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getArguments() {
|
|
||||||
return array(
|
|
||||||
'current' => array(
|
|
||||||
'param' => 'cursor_position',
|
|
||||||
'paramtype' => 'int',
|
|
||||||
'help' => pht('Current term in the argument list being completed.'),
|
|
||||||
),
|
|
||||||
'*' => 'argv',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function shouldShellComplete() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function run() {
|
|
||||||
$pos = $this->getArgument('current');
|
|
||||||
$argv = $this->getArgument('argv', array());
|
|
||||||
$argc = count($argv);
|
|
||||||
if ($pos === null) {
|
|
||||||
$pos = $argc - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pos > $argc) {
|
|
||||||
throw new ArcanistUsageException(
|
|
||||||
pht(
|
|
||||||
'Specified position is greater than the number of '.
|
|
||||||
'arguments provided.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
$vcs = $repository_api->getSourceControlSystemName();
|
|
||||||
}
|
|
||||||
|
|
||||||
$arc_config = $this->getArcanistConfiguration();
|
|
||||||
|
|
||||||
if ($pos <= 1) {
|
|
||||||
$workflows = $arc_config->buildAllWorkflows();
|
|
||||||
|
|
||||||
$complete = array();
|
|
||||||
foreach ($workflows as $name => $workflow) {
|
|
||||||
if (!$workflow->shouldShellComplete()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$workflow->setArcanistConfiguration($this->getArcanistConfiguration());
|
|
||||||
$workflow->setConfigurationManager($this->getConfigurationManager());
|
|
||||||
|
|
||||||
if ($vcs || $workflow->requiresWorkingCopy()) {
|
|
||||||
$supported_vcs = $workflow->getSupportedRevisionControlSystems();
|
|
||||||
if (!in_array($vcs, $supported_vcs)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$complete[] = $name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also permit autocompletion of "arc alias" commands.
|
|
||||||
$aliases = ArcanistAliasWorkflow::getAliases($configuration_manager);
|
|
||||||
foreach ($aliases as $key => $value) {
|
|
||||||
$complete[] = $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo implode(' ', $complete)."\n";
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
$workflow = $arc_config->buildWorkflow($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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments = $workflow->getArguments();
|
|
||||||
|
|
||||||
$prev = idx($argv, $pos - 1, null);
|
|
||||||
if (!strncmp($prev, '--', 2)) {
|
|
||||||
$prev = substr($prev, 2);
|
|
||||||
} else {
|
|
||||||
$prev = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($prev !== null &&
|
|
||||||
isset($arguments[$prev]) &&
|
|
||||||
isset($arguments[$prev]['param'])) {
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -2275,4 +2275,8 @@ abstract class ArcanistWorkflow extends Phobject {
|
||||||
return $this->getToolset()->getToolsetKey();
|
return $this->getToolset()->getToolsetKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final public function getConfig($key) {
|
||||||
|
return $this->getConfigurationSourceList()->getConfig($key);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue