From d2f8ca7215d109d60f63b1d1b17c77a10a6f9922 Mon Sep 17 00:00:00 2001 From: Carlo Dapor Date: Fri, 13 Jan 2017 04:20:23 +0100 Subject: [PATCH] feat(@angular/cli): Generate completion.sh automatically. It requires little tweaking in the case-block. Now the completion shell script is generated out of TypeScript code entirely. The options and aliases are generated dynamically. There are options to only produce bash- or zsh-specific code. Closes #3981. --- README.md | 6 +- packages/@angular/cli/commands/completion.ts | 174 +++++++++++++++++- packages/@angular/cli/utilities/completion.sh | 85 --------- 3 files changed, 172 insertions(+), 93 deletions(-) delete mode 100644 packages/@angular/cli/utilities/completion.sh diff --git a/README.md b/README.md index 154fb4d0b58f..1f8651cd9fdc 100644 --- a/README.md +++ b/README.md @@ -285,19 +285,19 @@ To turn on auto completion use the following commands: For bash: ```bash -ng completion 1>> ~/.bashrc 2>>&1 +ng completion --bash >> ~/.bashrc source ~/.bashrc ``` For zsh: ```bash -ng completion 1>> ~/.zshrc 2>>&1 +ng completion --zsh >> ~/.zshrc source ~/.zshrc ``` Windows users using gitbash: ```bash -ng completion 1>> ~/.bash_profile 2>>&1 +ng completion --bash >> ~/.bash_profile source ~/.bash_profile ``` diff --git a/packages/@angular/cli/commands/completion.ts b/packages/@angular/cli/commands/completion.ts index 0a272ad996b0..0751ee186c5f 100644 --- a/packages/@angular/cli/commands/completion.ts +++ b/packages/@angular/cli/commands/completion.ts @@ -1,17 +1,181 @@ -import * as path from 'path'; import * as fs from 'fs'; +import * as path from 'path'; + +import { oneLine, stripIndent } from 'common-tags'; +const stringUtils = require('ember-cli-string-utils'); const Command = require('../ember-cli/lib/models/command'); +const lookupCommand = require('../ember-cli/lib/cli/lookup-command'); + +function extractOptions(opts: any): String { + const output: String[] = []; + + for (let index = 0; index < opts.length; index++) { + const element = opts[index]; + output.push('--' + element.name); + if (element.aliases) { + output.push('-' + element.aliases[0]); + } + } + + return output.sort().join(' '); +} + +export interface CompletionCommandOptions { + all?: boolean; + bash?: boolean; + zsh?: boolean; +}; + +const commandsToIgnore = [ + 'easter-egg', + 'destroy', + 'github-pages-deploy' // errors because there is no base github-pages command +]; + +const optsNg: String[] = []; const CompletionCommand = Command.extend({ name: 'completion', description: 'Adds autocomplete functionality to `ng` commands and subcommands', works: 'everywhere', - run: function() { - const scriptPath = path.resolve(__dirname, '..', 'utilities', 'completion.sh'); - const scriptOutput = fs.readFileSync(scriptPath, 'utf8'); + availableOptions: [ + { name: 'all', type: Boolean, default: true, aliases: ['a'] }, + { name: 'bash', type: Boolean, default: false, aliases: ['b'] }, + { name: 'zsh', type: Boolean, default: false, aliases: ['z'] } + ], + + run: function (commandOptions: CompletionCommandOptions) { + commandOptions.all = !commandOptions.bash && !commandOptions.zsh; + + const commandFiles = fs.readdirSync(__dirname) + .filter(file => file.match(/\.ts$/) && !file.match(/\.run.ts$/)) + .map(file => path.parse(file).name) + .filter(file => { + return commandsToIgnore.indexOf(file) < 0; + }) + .map(file => file.toLowerCase()); + + const commandMap = commandFiles.reduce((acc: any, curr: string) => { + let classifiedName = stringUtils.classify(curr); + let defaultImport = require(`./${curr}`).default; + + acc[classifiedName] = defaultImport; + + return acc; + }, {}); + + let caseBlock = ''; + + commandFiles.forEach(cmd => { + const Command = lookupCommand(commandMap, cmd); + const com: String[] = []; + + const command = new Command({ + ui: this.ui, + project: this.project, + commands: this.commands, + tasks: this.tasks + }); + + optsNg.push(command.name); + com.push(command.name); + + if (command.aliases) { + command.aliases.forEach((element: String) => { + optsNg.push(element); + com.push(element); + }); + } + + if (command.availableOptions && command.availableOptions[0]) { + let opts = extractOptions (command.availableOptions); + caseBlock = caseBlock + ' ' + com.sort().join('|') + ') opts="' + opts + '" ;;\n'; + } + }); + + caseBlock = 'ng|help) opts="' + optsNg.sort().join(' ') + '" ;;\n' + + caseBlock + + ' *) opts="" ;;'; + + console.log(stripIndent` + ###-begin-ng-completion### + # + + # ng command completion script + # This command supports 3 cases. + # 1. (Default case) It prints a common completion initialisation for both Bash and Zsh. + # It is the result of either calling "ng completion" or "ng completion -a". + # 2. Produce Bash-only completion: "ng completion -b" or "ng completion --bash". + # 3. Produce Zsh-only completion: "ng completion -z" or "ng completion --zsh". + # + # Installation: ng completion -b >> ~/.bashrc + # or ng completion -z >> ~/.zshrc + #`); + + if (commandOptions.all && !commandOptions.bash) { + console.log('if test ".$(type -t complete 2>/dev/null || true)" = ".builtin"; then'); + } + + if (commandOptions.all || commandOptions.bash) { + console.log(stripIndent` + _ng_completion() { + local cword pword opts + + COMPREPLY=() + cword=\${COMP_WORDS[COMP_CWORD]} + pword=\${COMP_WORDS[COMP_CWORD - 1]} + + case \${pword} in + ${caseBlock} + esac + + COMPREPLY=( $(compgen -W '\${opts}' -- $cword) ) + + return 0 + } + + complete -o default -F _ng_completion ng + `); + } + + if (commandOptions.all) { + console.log(stripIndent` + elif test ".$(type -w compctl 2>/dev/null || true)" = ".compctl: builtin" ; then + `); + } + + if (commandOptions.all || commandOptions.zsh) { + console.log(stripIndent` + _ng_completion () { + local words cword opts + read -Ac words + read -cn cword + let cword-=1 + + case $words[cword] in + ${caseBlock} + esac + + setopt shwordsplit + reply=($opts) + unset shwordsplit + } + + compctl -K _ng_completion ng + `); + } + + if (commandOptions.all) { + console.log(stripIndent` + else + echo "Builtin command 'complete' or 'compctl' is redefined; cannot produce completion." + return 1 + fi`); + } + + console.log('###-end-ng-completion###'); - console.log(scriptOutput); } }); diff --git a/packages/@angular/cli/utilities/completion.sh b/packages/@angular/cli/utilities/completion.sh deleted file mode 100644 index 36751a31e8f9..000000000000 --- a/packages/@angular/cli/utilities/completion.sh +++ /dev/null @@ -1,85 +0,0 @@ -###-begin-ng-completion### -# -# ng command completion script -# -# Installation: ng completion 1>> ~/.bashrc 2>>&1 -# or ng completion 1>> ~/.zshrc 2>>&1 -# - -ng_opts='b build completion doc e2e g generate get github-pages:deploy gh-pages:deploy h help i init install lint make-this-awesome new s serve server set t test v version' - -build_opts='--aot --base-href --environment --i18n-file --i18n-format --locale --output-path --progress --sourcemap --suppress-sizes --target --vendor-chunk --verbose --watch --watcher -bh -dev -e -o -prod -sm -t -w' -generate_opts='class component directive enum module pipe route service c cl d e m p r s --help' -github_pages_deploy_opts='--base-href --environment --gh-token --gh-username --message --skip-build --target --user-page --custom-domain -cd -bh -e -t' -help_opts='--json --verbose -v' -init_opts='--dry-run inline-style inline-template --link-cli --name --prefix --routing --skip-npm --source-dir --style --verbose -d -is -it -lc -n -p -sb -sd -sn -v' -new_opts='--directory --dry-run inline-style inline-template --link-cli --prefix --routing --skip-git --skip-npm --source-dir --style --verbose -d -dir -is -it -lc -p -sb -sd -sg -sn -v' -serve_opts='--aot --environment --hmr --host --i18n-file --i18n-format --live-reload --live-reload-base-url --live-reload-host --live-reload-live-css --live-reload-port --locale --open --port --proxy-config --sourcemap --ssl --ssl-cert --ssl-key --target --watcher -H -e -lr -lrbu -lrh -lrp -o -p -pc -sm -t -w' -set_opts='--global -g' -test_opts='--browsers --build --code-coverage --colors --lint --log-level --port --reporters --single-run --sourcemap --watch -cc -l -sm -sr -w' - -version_opts='--verbose' - -if test ".$(type -t complete 2>/dev/null || true)" = ".builtin"; then - _ng_completion() { - local cword pword opts - - COMPREPLY=() - cword=${COMP_WORDS[COMP_CWORD]} - pword=${COMP_WORDS[COMP_CWORD - 1]} - - case ${pword} in - ng) opts=$ng_opts ;; - b|build) opts=$build_opts ;; - g|generate) opts=$generate_opts ;; - gh-pages:deploy|github-pages:deploy) opts=$github_pages_deploy_opts ;; - h|help|-h|--help) opts=$help_opts ;; - init) opts=$init_opts ;; - new) opts=$new_opts ;; - s|serve|server) opts=$serve_opts ;; - set) opts=$set_opts ;; - t|test) opts=$test_opts ;; - v|version) opts=$version_opts ;; - *) opts='' ;; - esac - - COMPREPLY=( $(compgen -W '${opts}' -- $cword) ) - - return 0 - } - - complete -o default -F _ng_completion ng -elif test ".$(type -w compctl 2>/dev/null || true)" = ".compctl: builtin" ; then - _ng_completion () { - local words cword opts - read -Ac words - read -cn cword - let cword-=1 - - case $words[cword] in - ng) opts=$ng_opts ;; - b|build) opts=$build_opts ;; - g|generate) opts=$generate_opts ;; - gh-pages:deploy|github-pages:deploy) opts=$github_pages_deploy_opts ;; - h|help|-h|--help) opts=$help_opts ;; - init) opts=$init_opts ;; - new) opts=$new_opts ;; - s|serve|server) opts=$serve_opts ;; - set) opts=$set_opts ;; - t|test) opts=$test_opts ;; - v|version) opts=$version_opts ;; - *) opts='' ;; - esac - - setopt shwordsplit - reply=($opts) - unset shwordsplit - } - - compctl -K _ng_completion ng -else - echo "Shell builtin command 'complete' or 'compctl' is redefined; cannot perform ng completion." - return 1 -fi - -###-end-ng-completion###