Skip to content

Commit

Permalink
feat(@angular/cli): Generate completion.sh automatically.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
catull authored and hansl committed Feb 3, 2017
1 parent 3b62a93 commit d2f8ca7
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 93 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
174 changes: 169 additions & 5 deletions packages/@angular/cli/commands/completion.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});

Expand Down
85 changes: 0 additions & 85 deletions packages/@angular/cli/utilities/completion.sh

This file was deleted.

0 comments on commit d2f8ca7

Please sign in to comment.