Skip to content

Commit

Permalink
Merge a087cc9 into f9bf583
Browse files Browse the repository at this point in the history
  • Loading branch information
mklabs committed May 2, 2016
2 parents f9bf583 + a087cc9 commit a962454
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 6 deletions.
106 changes: 106 additions & 0 deletions lib/completion/completer.js
@@ -0,0 +1,106 @@
'use strict';

var path = require('path');
var execFile = require('child_process').execFile;
var parseHelp = require('parse-help');

/**
* The Completer is in charge of handling `yo-complete` behavior.
* @constructor
* @param {Environment} env A yeoman environment instance
*/
var Completer = module.exports = function (env) {
this.env = env;
};

/**
* Completion event done
*
* @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);
}

var meta = this.env.getGeneratorsMeta();
var results = Object.keys(meta).map(this.item('yo'), this);
done(null, results);
}.bind(this));
};


/**
* Generator completion event done
*
* @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);
}

var results = this.parseHelp(last, out);
done(null, results);
}.bind(this));
};

/**
* 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
*/
Completer.prototype.item = function (desc, prefix) {
prefix = prefix || '';
return function (item) {
var name = typeof item === 'string' ? item : item.name;
desc = typeof item !== 'string' && item.description ? item.description : desc;
desc = desc.replace(/^#?\s*/g, '');
desc = desc.replace(/:/g, '->');
desc = desc.replace(/'/g, ' ');

return {
name: prefix + name,
description: desc
};
};
};

/**
* parse-help wrapper. Invokes parse-help with stdout result, returning the
* list of completion items for flags / alias.
*
* @param {String} last Last word in COMP_LINE (completed line in command line)
* @param {String} out Help output
*/
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(Object.assign({}, flag, { name: flag.alias }));
}
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 results;
};
18 changes: 18 additions & 0 deletions lib/completion/index.js
@@ -0,0 +1,18 @@
#! /usr/bin/env node
'use strict';

var env = require('yeoman-environment').createEnv();
var Completer = require('./completer');

var tabtab = require('tabtab')({
name: 'yo'
});

var completer = new Completer(env);

// Lookup installed generator in yeoman environment, respond completion results
// with each generator.
tabtab.on('yo', completer.complete.bind(completer));

// Register complete command
tabtab.start();
24 changes: 21 additions & 3 deletions package.json
Expand Up @@ -27,15 +27,20 @@
],
"license": "BSD-2-Clause",
"repository": "yeoman/yo",
"bin": "lib/cli.js",
"bin": {
"yo": "lib/cli.js",
"yo-complete": "lib/completion/index.js"
},
"engines": {
"node": ">=0.12.0"
},
"scripts": {
"test": "gulp",
"postinstall": "yodoctor",
"postinstall": "yodoctor && npm run tabtab",
"postupdate": "yodoctor",
"prepublish": "gulp prepublish"
"prepublish": "gulp prepublish",
"tabtab": "[ $CI ] || tabtab install --name=yo --completer=yo-complete",
"test-completion": "export cmd=\"yo\" && DEBUG=\"tabtab*\" COMP_POINT=\"4\" COMP_LINE=\"$cmd\" COMP_CWORD=\"$cmd\" yo-complete completion -- yo $cmd"
},
"dependencies": {
"async": "^1.0.0",
Expand All @@ -54,11 +59,13 @@
"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.1.1",
"titleize": "^1.0.0",
"update-notifier": "^0.6.0",
"user-home": "^2.0.0",
Expand All @@ -83,5 +90,16 @@
"gulp-plumber": "^1.0.0",
"gulp-nsp": "^2.1.0",
"gulp-coveralls": "^0.1.0"
},
"tabtab": {
"yo": [
"-f",
"--force",
"--version",
"--no-color",
"--no-insight",
"--insight",
"--generators"
]
}
}
6 changes: 3 additions & 3 deletions test/cli.js
Expand Up @@ -42,7 +42,7 @@ describe('bin', function () {
done();
};

process.argv = ['node', path.join(__dirname, '../', pkg.bin), 'notexisting'];
process.argv = ['node', path.join(__dirname, '../', pkg.bin.yo), 'notexisting'];

this.env.lookup = function (cb) {
cb();
Expand All @@ -53,7 +53,7 @@ describe('bin', function () {
});

it('should return the version', function (cb) {
var cp = execFile('node', [path.join(__dirname, '../', pkg.bin), '--version', '--no-insight', '--no-update-notifier']);
var cp = execFile('node', [path.join(__dirname, '../', pkg.bin.yo), '--version', '--no-insight', '--no-update-notifier']);
var expected = pkg.version;

cp.stdout.on('data', function (data) {
Expand All @@ -63,7 +63,7 @@ describe('bin', function () {
});

it('should output available generators when `--generators` flag is supplied', function (cb) {
var cp = execFile('node', [path.join(__dirname, '../', pkg.bin), '--generators', '--no-insight', '--no-update-notifier']);
var cp = execFile('node', [path.join(__dirname, '../', pkg.bin.yo), '--generators', '--no-insight', '--no-update-notifier']);

cp.stdout.once('data', function (data) {
assert(data.length > 0);
Expand Down
144 changes: 144 additions & 0 deletions test/completion.js
@@ -0,0 +1,144 @@
'use strict';

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

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 () {
this.env = require('yeoman-environment').createEnv();
});

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';

execFile('bash', ['-c', cmd], function (err, out) {
if (err) {
return done(err);
}

assert.ok(/-f/.test(out));
assert.ok(/--force/.test(out));
assert.ok(/--version/.test(out));
assert.ok(/--no-color/.test(out));
assert.ok(/--no-insight/.test(out));
assert.ok(/--insight/.test(out));
assert.ok(/--generators/.test(out));

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

describe('Completer', function () {
beforeEach(function () {
this.completer = new Completer(this.env);
});

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'];
var results = list.map(this.completer.item('yo!', '--'));
assert.deepEqual(results, [{
name: '--foo',
description: 'yo!'
}, {
name: '--bar',
description: 'yo!'
}]);
});

it('Escapes certain characters before consumption by shell scripts', function () {
var list = ['foo'];

var desc = '# yo I\'m a very subtle description, with chars that likely will break your Shell: yeah I\'m mean';
var expected = 'yo I m a very subtle description, with chars that likely will break your Shell-> yeah I m mean';
var results = list.map(this.completer.item(desc, '-'));

assert.equal(results[0].description, expected);
});
});

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.generator({ 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 () {
// SKipping on CI right now, otherwise might introduce an `npm install
// generator-dummy` as a pretest script

it.skip('Returns the list of user installed generators as completion candidates', function (done) {
this.completer.complete({ last: 'yo' }, function (err, results) {
if (err) {
return done(err);
}

console.log(results);
var dummy = results.find(function (result) {
return result.name === 'dummy:yo';
});

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

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

});

0 comments on commit a962454

Please sign in to comment.