Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Completion: Silent install, better handling of descriptions #442

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ var list = require('cli-list');
var pkg = require('../package.json');
var Router = require('./router');
var gens = list(process.argv.slice(2));

/* eslint new-cap: 0, no-extra-parens: 0 */
var tabtab = new (require('tabtab').Commands.default)({
var TabtabCommands = require('tabtab').Commands;
var tabtab = new TabtabCommands({
name: 'yo',
completer: 'yo-complete'
});
Expand Down
149 changes: 99 additions & 50 deletions lib/completion/completer.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
'use strict';

var path = require('path');
var execFile = require('child_process').execFile;
var parseHelp = require('parse-help');
var assign = require('lodash').assign;
var debug = require('tabtab/lib/debug')('yo:completer');

var YO_OPTS = [
{name: '-f', description: 'Overwrite files that already exist'},
{name: '--force', description: 'Overwrite files that already exist'},
{name: '--generators', description: 'Print available generators'},
{name: '--help', description: 'Print this info and generator\'s options and usage'},
{name: '--insight', description: 'Toggle anonymous tracking'},
{name: '--no-color', description: 'Disable colors'},
{name: '--no-insight', description: 'Toggle anonymous tracking'},
{name: '--version', description: 'Print version'}
];

/**
* The Completer is in charge of handling `yo-complete` behavior.
Expand All @@ -15,53 +23,98 @@ var Completer = module.exports = function (env) {
};

/**
* Completion event done
* This is the completion handler.
*
* The first word must be a generator or flags for the yo cli itself.
* If the first word is a generator then subsequent words will be
* generator specific.
* If the first word is not a generator then remaining words will
* simply be arguments and options provided by the yo clie itself.
*
* @param {String} data Environment object as parsed by tabtab
* @param {Function} done Callback to invoke with completion results
*/
Completer.prototype.complete = function (data, done) {
if (data.last !== 'yo' && !/^-/.test(data.last)) {
return this.generator(data, done);
}

this.env.lookup(function (err) {
if (err) {
return done(err);
}
//debug('XXX yo complete data %j', data);
var hasFirstArg = (data && data.args && data.args.length > 3);
//debug('FFF %s', (hasFirstArg ? 'true' : 'false'));
var last = data.last.trim();

this.env.lookup(function () {
var meta = this.env.getGeneratorsMeta();
var results = Object.keys(meta).map(this.item('yo'), this);
done(null, results);
//debug('MMM yo complete meta %j', meta);
var generator = (hasFirstArg ? this.getGeneratorFromMeta(data.args[3], meta) : undefined);
//debug('GGG %s', generator);

// Completing on `yo <tab>`
if (data.words == 1) {
var results = this.metaItems(meta);
done(null, results.concat(YO_OPTS));
}
// Completing for a sub/generator
else if (generator) {
return this.handleGenerator(generator, data, done);
}
// Completing on yo options
else {
done(null, YO_OPTS);
}
}.bind(this));

};

Completer.prototype.getGeneratorFromMeta = function (name, meta) {
var genName = name;
if (name.indexOf(':') === -1) {
genName += ':app';
}
return meta[genName];
}

/**
* Generator completion event done
* Invoked when tabbing after a yeoman generator.
*
* @param {String} data Environment object as parsed by tabtab
* @param {Function} done Callback to invoke with completion results
* @param {Object} generator Generator meta to provide completion for
* @param {String} data Environment object as parsed by tabtab
* @param {Function} done Callback to invoke with completion results
*/
Completer.prototype.generator = function (data, done) {
var last = data.last;
var bin = path.join(__dirname, '../cli.js');

execFile('node', [bin, last, '--help'], function (err, out) {
if (err) {
return done(err);
}
Completer.prototype.handleGenerator = function (generator, data, done) {
debug('Looking for generator %s > %s', generator.namespace, generator.resolved);
var gen = this.env.create(generator.namespace);
//debug('AAA %j', gen);

var results = this.parseHelp(last, out);
done(null, results);
}.bind(this));
var opts = [];
if (gen) {
Object.keys(gen._options).forEach(function (key) {
var option = gen._options[key];
var name = option.name;
if (name.indexOf('--') !== 0) {
name = '--' + name;
}
opts.push({
name: name,
description: option.desc
});
if (option.alias) {
var alias = option.alias;
if (alias.indexOf('-') !== 0) {
alias = '-' + alias;
}
opts.push({
name: alias,
description: option.desc
});
}
});
}
return done(null, opts);
};

/**
* Helper to format completion results into { name, description } objects
*
* @param {String} data Environment object as parsed by tabtab
* @param {Function} done Callback to invoke with completion results
* @param {String} desc When defined, can be used to set a static description
* @param {String} prefix Used to prefix completion results (ex: --)
*/
Completer.prototype.item = function (desc, prefix) {
prefix = prefix || '';
Expand All @@ -80,28 +133,24 @@ Completer.prototype.item = function (desc, prefix) {
};

/**
* parse-help wrapper. Invokes parse-help with stdout result, returning the
* list of completion items for flags / alias.
* Helper to format completion results for generator meta result into { name, description } objects
*
* @param {String} last Last word in COMP_LINE (completed line in command line)
* @param {String} out Help output
* @param {String} desc When defined, can be used to set a static description
*/
Completer.prototype.parseHelp = function (last, out) {
var help = parseHelp(out);
var alias = [];
var results = Object.keys(help.flags).map(function (key) {
var flag = help.flags[key];
if (flag.alias) {
alias.push(assign({}, flag, { name: flag.alias }));
Completer.prototype.metaItems = function (metas) {
return Object.keys(metas).map(function (key) {
var meta = metas[key];
var parts = meta.namespace.split(':');
var generator = parts[0];
var subgenerator = parts[1];

if (subgenerator === 'app') {
key = generator;
}
flag.name = key;
return flag;
}).map(this.item(last, '--'), this);

results = results.concat(alias.map(this.item(last.replace(':', '_'), '-'), this));
results = results.filter(function (r) {
return r.name !== '--help' && r.name !== '-h';
return {
name: key,
description: subgenerator + ' from ' + generator + ' generator'
};
});

return results;
};
2 changes: 1 addition & 1 deletion lib/usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Install a generator:

Completion:

To enable shell completion for the yo command, try running
Run the following to enable shell completion

$ yo completion

Expand Down
19 changes: 4 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@
},
"scripts": {
"test": "gulp",
"postinstall": "yodoctor",
"postinstall": "yodoctor && tabtab install --auto --name=yo --completer=yo-complete",
"postupdate": "yodoctor",
"prepublish": "gulp prepublish"
"prepublish": "gulp prepublish",
"postuninstall": "tabtab uninstall --auto"
},
"dependencies": {
"async": "^1.0.0",
Expand All @@ -57,13 +58,12 @@
"npm-keyword": "^4.1.0",
"opn": "^3.0.2",
"package-json": "^2.1.0",
"parse-help": "^0.1.1",
"read-pkg-up": "^1.0.1",
"repeating": "^2.0.0",
"root-check": "^1.0.0",
"sort-on": "^1.0.0",
"string-length": "^1.0.0",
"tabtab": "^1.3.0",
"tabtab": "file:../node-tabtab",
"titleize": "^1.0.0",
"update-notifier": "^0.6.0",
"user-home": "^2.0.0",
Expand All @@ -88,16 +88,5 @@
"proxyquire": "^1.0.1",
"registry-url": "^3.0.0",
"sinon": "^1.12.1"
},
"tabtab": {
"yo": [
"-f",
"--force",
"--version",
"--no-color",
"--no-insight",
"--insight",
"--generators"
]
}
}
64 changes: 4 additions & 60 deletions test/completion.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
'use strict';

var path = require('path');
var assert = require('assert');
var events = require('events');
var execFile = require('child_process').execFile;
var Completer = require('../lib/completion/completer');
var completion = require('../lib/completion');
var find = require('lodash').find;

var help = [
' Usage:',
' yo backbone:app [options] [<app_name>]',
'',
' Options:',
' -h, --help # Print the generator\'s options and usage',
' --skip-cache # Do not remember prompt answers Default: false',
' --skip-install # Do not automatically install dependencies Default: false',
' --appPath # Name of application directory Default: app',
' --requirejs # Support requirejs Default: false',
' --template-framework # Choose template framework. lodash/handlebars/mustache Default: lodash',
' --test-framework # Choose test framework. mocha/jasmine Default: mocha',
'',
' Arguments:',
' app_name Type: String Required: false'
].join('\n');

describe('Completion', function () {

before(function () {
Expand All @@ -33,11 +14,8 @@ describe('Completion', function () {

describe('Test completion STDOUT output', function () {
it('Returns the completion candidates for both options and installed generators', function (done) {
var yocomplete = path.join(__dirname, '../lib/completion/index.js');
var yo = path.join(__dirname, '../lib/cli');

var cmd = 'export cmd=\"yo\" && DEBUG=\"tabtab*\" COMP_POINT=\"4\" COMP_LINE=\"$cmd\" COMP_CWORD=\"$cmd\"';
cmd += 'node ' + yocomplete + ' completion -- ' + yo + ' $cmd';
var cmd = 'DEBUG="tabtab*" SHELL=zsh COMP_POINT="4" COMP_LINE="yo" COMP_CWORD="yo" ';
cmd += 'node lib/completion/index.js completion -- yo';

execFile('bash', ['-c', cmd], function (err, out) {
if (err) {
Expand Down Expand Up @@ -69,7 +47,7 @@ describe('Completion', function () {
// instance directly to completer.
this.getGeneratorsMeta = this.env.getGeneratorsMeta;

this.env.getGeneratorsMeta = function () {
this.env.getGeneratorsMeta = function () {
return {
'dummy:app': {
resolved: '/home/user/.nvm/versions/node/v6.1.0/lib/node_modules/generator-dummy/app/index.js',
Expand All @@ -89,19 +67,6 @@ describe('Completion', function () {
this.env.getGeneratorsMeta = this.getGeneratorsMeta;
});

describe('#parseHelp', function () {
it('Returns completion items based on help output', function () {
var results = this.completer.parseHelp('backbone:app', help);
var first = results[0];

assert.equal(results.length, 6);
assert.deepEqual(first, {
name: '--skip-cache',
description: 'Do not remember prompt answers Default-> false'
});
});
});

describe('#item', function () {
it('Format results into { name, description }', function () {
var list = ['foo', 'bar'];
Expand All @@ -126,27 +91,6 @@ describe('Completion', function () {
});
});

describe('#generator', function () {
it('Returns completion candidates from generator help output', function (done) {
// Here we test against yo --help (could use dummy:yo --help)
this.completer.complete({ last: '' }, function (err, results) {
if (err) {
return done(err);
}

/* eslint no-multi-spaces: 0 */
assert.deepEqual(results, [
{ name: '--force', description: 'Overwrite files that already exist' },
{ name: '--version', description: 'Print version' },
{ name: '--no-color', description: 'Disable colors' },
{ name: '-f', description: 'Overwrite files that already exist' }
]);

done();
});
});
});

describe('#complete', function () {
it('Returns the list of user installed generators as completion candidates', function (done) {
this.completer.complete({ last: 'yo' }, function (err, results) {
Expand All @@ -159,7 +103,7 @@ describe('Completion', function () {
});

assert.equal(dummy.name, 'dummy:yo');
assert.equal(dummy.description, 'yo');
assert.equal(dummy.description, 'yo from dummy generator');

done();
});
Expand Down