Changeset View
Standalone View
src/toolset/workflow/ArcanistShellCompleteWorkflow.php
| <?php | <?php | ||||
| /** | final class ArcanistShellCompleteWorkflow | ||||
| * Powers shell-completion scripts. | extends ArcanistWorkflow { | ||||
| */ | |||||
| final class ArcanistShellCompleteWorkflow extends ArcanistWorkflow { | |||||
| public function supportsToolset(ArcanistToolset $toolset) { | |||||
| return true; | |||||
| } | |||||
| public function getWorkflowName() { | public function getWorkflowName() { | ||||
| return 'shell-complete'; | return 'shell-complete'; | ||||
| } | } | ||||
| public function getWorkflowSynopses() { | public function getWorkflowInformation() { | ||||
| return array( | $help = pht(<<<EOTEXT | ||||
| pht('**shell-complete** __--current__ __N__ -- [__argv__]'), | 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** | |||||
| public function getWorkflowHelp() { | After completion is installed, use the "tab" key to automatically complete | ||||
| return pht(<<<EOTEXT | workflows and flags. For example, if you type: | ||||
| Implements shell completion. To use shell completion, source the appropriate | |||||
| script from 'resources/shell/' in your .shellrc. | $ 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 | EOTEXT | ||||
| ); | ); | ||||
| return $this->newWorkflowInformation() | |||||
| ->setHelp($help); | |||||
| } | } | ||||
| public function getArguments() { | public function getWorkflowArguments() { | ||||
| return array( | return array( | ||||
| 'current' => array( | $this->newWorkflowArgument('current') | ||||
| 'param' => 'cursor_position', | ->setParameter('cursor-position') | ||||
| 'paramtype' => 'int', | ->setHelp( | ||||
| 'help' => pht('Current term in the argument list being completed.'), | pht( | ||||
| ), | 'Internal. Current term in the argument list being completed.')), | ||||
| '*' => 'argv', | $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() { | public function runWorkflow() { | ||||
| return false; | $log = $this->getLogEngine(); | ||||
| } | |||||
| public function run() { | $argv = $this->getArgument('argv'); | ||||
| $pos = $this->getArgument('current'); | |||||
| $argv = $this->getArgument('argv', array()); | $is_generate = $this->getArgument('generate'); | ||||
| $argc = count($argv); | $is_shell = (bool)strlen($this->getArgument('shell')); | ||||
| if ($pos === null) { | $is_current = $this->getArgument('current'); | ||||
| $pos = $argc - 1; | |||||
| if ($argv) { | |||||
| $should_install = false; | |||||
| $should_generate = false; | |||||
| if ($is_generate) { | |||||
| throw new PhutilArgumentUsageException( | |||||
| pht( | |||||
| 'You can not use "--generate" when completing arguments.')); | |||||
| } | } | ||||
| if ($pos > $argc) { | if ($is_shell) { | ||||
| throw new ArcanistUsageException( | throw new PhutilArgumentUsageException( | ||||
| pht( | pht( | ||||
| 'Specified position is greater than the number of '. | 'You can not use "--shell" when completing arguments.')); | ||||
| 'arguments provided.')); | |||||
| } | } | ||||
| // Determine which revision control system the working copy uses, so we | } else if ($is_generate) { | ||||
| // can filter out commands and flags which aren't supported. If we can't | $should_install = false; | ||||
| // figure it out, just return all flags/commands. | $should_generate = true; | ||||
| $vcs = null; | |||||
| // We have to build our own because if we requiresWorkingCopy() we'll throw | if ($is_current) { | ||||
| // if we aren't in a .arcconfig directory. We probably still can't do much, | throw new PhutilArgumentUsageException( | ||||
| // but commands can raise more detailed errors. | pht( | ||||
| $configuration_manager = $this->getConfigurationManager(); | 'You can not use "--current" when generating rules.')); | ||||
| $working_copy = ArcanistWorkingCopyIdentity::newFromPath(getcwd()); | } | ||||
| if ($working_copy->getVCSType()) { | |||||
| $configuration_manager->setWorkingCopyIdentity($working_copy); | |||||
| $repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager( | |||||
| $configuration_manager); | |||||
| $vcs = $repository_api->getSourceControlSystemName(); | 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.')); | |||||
| } | } | ||||
| $arc_config = $this->getArcanistConfiguration(); | } else { | ||||
| $should_install = true; | |||||
| $should_generate = true; | |||||
| if ($pos <= 1) { | if ($is_current) { | ||||
| $workflows = $arc_config->buildAllWorkflows(); | throw new PhutilArgumentUsageException( | ||||
| pht( | |||||
| 'You can not use "--current" when installing support.')); | |||||
| } | |||||
| } | |||||
| $complete = array(); | if ($should_install) { | ||||
| foreach ($workflows as $name => $workflow) { | $this->runInstall(); | ||||
| if (!$workflow->shouldShellComplete()) { | |||||
| continue; | |||||
| } | } | ||||
| $workflow->setArcanistConfiguration($this->getArcanistConfiguration()); | if ($should_generate) { | ||||
| $workflow->setConfigurationManager($this->getConfigurationManager()); | $this->runGenerate(); | ||||
| } | |||||
| if ($vcs || $workflow->requiresWorkingCopy()) { | if ($should_install || $should_generate) { | ||||
| $supported_vcs = $workflow->getSupportedRevisionControlSystems(); | $log->writeHint( | ||||
| if (!in_array($vcs, $supported_vcs)) { | pht('NOTE'), | ||||
| continue; | pht( | ||||
| 'You may need to open a new terminal window or launch a new shell '. | |||||
| 'before the changes take effect.')); | |||||
| return 0; | |||||
| } | } | ||||
| $this->runAutocomplete(); | |||||
| } | } | ||||
| $complete[] = $name; | 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); | |||||
| } | } | ||||
| // Also permit autocompletion of "arc alias" commands. | $spec = $shells[$shell]; | ||||
| $aliases = ArcanistAliasWorkflow::getAliases($configuration_manager); | $file = $spec['file']; | ||||
| foreach ($aliases as $key => $value) { | $home = getenv('HOME'); | ||||
| $complete[] = $key; | |||||
| 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.')); | |||||
| } | } | ||||
| echo implode(' ', $complete)."\n"; | $file_path = getenv('HOME').'/'.$file; | ||||
| return 0; | $file_display = '~/'.$file; | ||||
| if (Filesystem::pathExists($file_path)) { | |||||
| $file_path = Filesystem::resolvePath($file_path); | |||||
| $data = Filesystem::readFile($file_path); | |||||
| $is_new = false; | |||||
| } else { | } else { | ||||
| $workflow = $arc_config->buildWorkflow($argv[1]); | $data = ''; | ||||
| if (!$workflow) { | $is_new = true; | ||||
| 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; | $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 { | } else { | ||||
| $argv = array_merge( | $new_line = "\n\n".$line."\n"; | ||||
| array($argv[0]), | |||||
| array($new_command), | |||||
| $new_args); | |||||
| } | } | ||||
| $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; | |||||
| } | } | ||||
| $arguments = $workflow->getArguments(); | 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); | |||||
| $prev = idx($argv, $pos - 1, null); | $prompt = pht('Rewrite this file?'); | ||||
| if (!strncmp($prev, '--', 2)) { | |||||
| $prev = substr($prev, 2); | |||||
| } else { | } else { | ||||
| $prev = null; | 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?'); | |||||
| } | |||||
| } | } | ||||
| if ($prev !== null && | // TOOLSETS: Generalize prompting. | ||||
| isset($arguments[$prev]) && | |||||
| isset($arguments[$prev]['param'])) { | |||||
| $type = idx($arguments[$prev], 'paramtype'); | if (!phutil_console_confirm($prompt, false)) { | ||||
| switch ($type) { | throw new PhutilArgumentUsageException(pht('Aborted.')); | ||||
| case 'file': | |||||
| echo "FILE\n"; | |||||
| break; | |||||
| case 'complete': | |||||
| echo implode(' ', $workflow->getShellCompletions($argv))."\n"; | |||||
| break; | |||||
| default: | |||||
| echo "ARGUMENT\n"; | |||||
| break; | |||||
| } | } | ||||
| return 0; | |||||
| 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 { | } else { | ||||
| $output = array(); | Filesystem::writeFile($rules_path, $rules); | ||||
| foreach ($arguments as $argument => $spec) { | $log->writeStatus( | ||||
| if ($argument == '*') { | 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; | continue; | ||||
| } | } | ||||
| if ($vcs && | |||||
| isset($spec['supports']) && | if ($alias->getToolset() !== $this->getToolsetKey()) { | ||||
| !in_array($vcs, $spec['supports'])) { | |||||
| continue; | continue; | ||||
| } | } | ||||
| $output[] = '--'.$argument; | |||||
| $complete[] = $alias->getTrigger(); | |||||
| } | } | ||||
| $cur = idx($argv, $pos, ''); | // Remove invalid possibilities. For example, if the user has typed | ||||
| $any_match = false; | // "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 (strlen($cur)) { | if ($complete) { | ||||
| foreach ($output as $possible) { | return $this->suggestStrings($complete); | ||||
| if (!strncmp($possible, $cur, strlen($cur))) { | } else { | ||||
| $any_match = true; | 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'); | |||||
| $argument = null; | |||||
| $prev = idx($argv, $pos - 1, null); | |||||
| if (!strncmp($prev, '--', 2)) { | |||||
| $prev = substr($prev, 2); | |||||
| $argument = idx($arguments, $prev); | |||||
| } | } | ||||
| if (!$any_match && isset($arguments['*'])) { | // If the last argument was a "--thing" argument, test if "--thing" is | ||||
| // TODO: This is mega hacktown but something else probably breaks | // a parameterized argument. If it is, the next argument should be a | ||||
| // if we use a rich argument specification; fix it when we move to | // parameter. | ||||
| // PhutilArgumentParser since everything will need to be tested then | |||||
| // anyway. | if ($argument && strlen($argument->getParameter())) { | ||||
| if ($arguments['*'] == 'branch' && isset($repository_api)) { | if ($argument->getIsPathArgument()) { | ||||
| $branches = $repository_api->getAllBranches(); | return $this->suggestPaths(); | ||||
| $branches = ipull($branches, 'name'); | |||||
| $output = $branches; | |||||
| } else { | } else { | ||||
| $output = array('FILE'); | 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. | |||||
| } | } | ||||
| echo implode(' ', $output)."\n"; | $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"; | |||||
amckinley: `//TODO` | |||||
| return 0; | return 0; | ||||
| } | } | ||||
| private function suggestNothing() { | |||||
| echo "ARGUMENT\n"; | |||||
Not Done Inline Actions//TODO amckinley: `//TODO` | |||||
| return 0; | |||||
| } | } | ||||
| private function suggestStrings(array $strings) { | |||||
| echo implode(' ', $strings)."\n"; | |||||
| return 0; | |||||
Not Done Inline ActionsShouldn't all of these be handled in the main executeWorkflow by returning 0 there? Or is this a "let eventual complications in these methods bubble up automatically through executeWorkflow situation? amckinley: Shouldn't all of these be handled in the main `executeWorkflow` by returning 0 there? Or is… | |||||
Done Inline ActionsYeah, the latter. I imagine maybe we'll return some kind of list or object or something eventually and then do one echo + return code at top level (perhaps if this gets unit testing, for example). No specific plans for why we'd do that, but this seemed a little better/more readable than having multiple echo "FILE\n" lines in the code, I just didn't go quite as far as to pulling the side effects out completely and restoring them at top level. The "FILE" and "ARGUMENT" things are actually how this works "properly" right now -- the Bash script looks for "FILE" and calls some kind of Bash magic (compgen -f -- <directory>) to mean "complete files in current directory". This is weird, but in the realm of Bash scripting it seems pretty mild. Since ZSH completion theoretically is the same as Bash completion with the addition of some "zsh, work like Bash" incantation but maybe if/when we write Powershell completion or whatever it will make sense to do file completion in-process. "ARGUMENT" means "nothing can be completed" and should maybe be renamed or otherwise made more obvious. epriestley: Yeah, the latter. I imagine maybe we'll return some kind of list or object or something… | |||||
| } | } | ||||
| } | } | ||||
//TODO