Merge branch 'completion' of git://github.com/mklabs/bower into completion

Conflicts:
	lib/commands/register.js
This commit is contained in:
André Cruz
2012-12-30 10:28:39 +00:00
19 changed files with 542 additions and 16 deletions

View File

@@ -202,6 +202,25 @@ All commands emit three types of events: `data`, `end`, and `error`.
For a better of idea how this works, you may want to check out [our bin file](https://github.com/twitter/bower/blob/master/bin/bower).
For the install command, there is an additional `package` event that is emitted for each installed/uninstalled package.
### Completion
**experimental**
Based on the completion feature and fantastic work done in
[npm](https://npmjs.org/doc/completion.html), bower now has an experimental
`completion` command that works similarly.
This command will output a bash / zsh script to put into your `~/.bashrc` or
`~/.zshrc` file.
When `COMP_CWORD`, `COMP_LINE`, and `COMP_POINT` are defined in the
environment, npm completion acts in "plumbing mode", and outputs completions
based on the arguments.
This doesn't work for Windows user, even with cygwin.
### Windows users
A lot of people are experience problems using bower on windows because [msysgit](http://code.google.com/p/msysgit/) must be installed correctly.

View File

@@ -19,8 +19,8 @@ var config = require('../core/config');
var template = require('../util/template');
var fileExists = require('../util/file-exists');
var optionTypes = { help: Boolean, force: Boolean };
var shorthand = { 'h': ['--help'], 'S': ['--save'], 'f': ['--force'] };
var optionTypes = { help: Boolean };
var shorthand = { 'h': ['--help'] };
var removePkg = function (pkg, emitter, next) {
var folder = path.join(config.cache, pkg);
@@ -70,3 +70,15 @@ module.exports.line = function (argv) {
if (options.help) return help('cache-clean');
return module.exports(pkgs, options);
};
module.exports.completion = function (opts, cb) {
glob('./*', { cwd: config.cache }, function (err, dirs) {
if (err) return cb(err);
dirs = dirs.map(function (dir) {
return dir.replace(/^\.\//, '');
});
cb(null, dirs);
});
};
module.exports.completion.options = shorthand;

111
lib/commands/completion.js Normal file
View File

@@ -0,0 +1,111 @@
// ==========================================
// BOWER: Completion API
// ==========================================
// Copyright 2012 Twitter, Inc
// Licensed under The MIT License
// http://opensource.org/licenses/MIT
// ==========================================
var Emitter = require('events').EventEmitter;
var path = require('path');
var nopt = require('nopt');
var mkdirp = require('mkdirp');
var template = require('../util/template');
var complete = require('../util/completion');
var config = require('../core/config');
var help = require('./help');
var optionTypes = { help: Boolean };
var shorthand = { 'h': ['--help'] };
module.exports = function (argv, env) {
env = env || process.env;
var emitter = new Emitter;
var commands = require('../commands');
// top level flags
var flags = ['--no-color', '--help', '--version'];
var done = function done() {
process.nextTick(function () {
emitter.emit('end');
});
return emitter;
};
// if the COMP_* isn't in the env, then just dump the script.
if (!env.COMP_CWORD) {
template('completion').on('data', emitter.emit.bind(emitter, 'end'));
return emitter;
}
// parse environment and arguments, returns a hash of completion config.
var opts = complete(argv, env);
// if only one word, complete the list of command or top level flags
if (opts.w === 1) {
if (opts.word.charAt(0) === '-') complete.log(flags, opts);
else complete.log(Object.keys(commands), opts);
return done();
}
// try to find the bower command. first thing after all the configs.
var parsed = opts.conf = nopt({}, {}, opts.partialWords, 0);
var cmd = parsed.argv.remain[0];
// unable to find a command, complete the lisf of commands
if (!cmd) {
complete.log(Object.keys(commands), opts);
return done();
}
// if words[0] is a bower command, then complete on it.
cmd = commands[cmd];
if (cmd && cmd.completion) {
// prior to that, ensure the completion cache directory is created first
mkdirp(path.join(config.completion), function (err) {
if (err) return emitter.emit('error', err);
var options = cmd.completion.options;
if (options && opts.word.charAt(0) === '-') {
complete.log(Object.keys(options).map(function (option) {
return opts.word.charAt(1) === '-' ? options[option][0] : '-' + option;
}), opts);
return done();
}
cmd.completion(opts, function (err, data) {
if (err) return emitter.emit('error', err);
// completing options, then append top level flags
if (opts.word.charAt(0) === '-') data = data.concat(flags);
complete.log(data, opts);
done();
});
});
return emitter;
}
// otherwise, do nothing
return emitter;
};
module.exports.line = function (argv) {
var emitter = new Emitter;
var options = nopt(optionTypes, shorthand, argv);
if (options.help) return help('completion');
module.exports(options.argv.remain.slice(2), process.env)
.on('data', emitter.emit.bind(emitter, 'data'))
.on('error', emitter.emit.bind(emitter, 'error'))
.on('end', emitter.emit.bind(emitter, 'end'));
return emitter;
};

View File

@@ -29,4 +29,8 @@ module.exports.line = function (argv) {
var options = nopt({}, {}, argv);
var paths = options.argv.remain.slice(1);
return module.exports(paths[0]);
};
};
module.exports.completion = function (opts, cb) {
return cb(null, Object.keys(require('../commands')));
};

View File

@@ -18,5 +18,6 @@ module.exports = {
'info': require('./info'),
'register': require('./register'),
'search': require('./search'),
'cache-clean': require('./cache-clean')
};
'cache-clean': require('./cache-clean'),
'completion': require('./completion')
};

View File

@@ -10,6 +10,7 @@ var nopt = require('nopt');
var template = require('../util/template');
var source = require('../core/source');
var install = require('./install');
var help = require('./help');
var optionTypes = { help: Boolean };
@@ -42,4 +43,7 @@ module.exports.line = function (argv) {
});
return emitter;
};
};
module.exports.completion = install.completion;
module.exports.completion.options = shorthand;

View File

@@ -13,8 +13,12 @@
var Emitter = require('events').EventEmitter;
var nopt = require('nopt');
var fs = require('fs');
var path = require('path');
var Manager = require('../core/manager');
var config = require('../core/config');
var source = require('../core/source');
var save = require('../util/save');
var help = require('./help');
@@ -45,3 +49,29 @@ module.exports.line = function (argv) {
if (options.help) return help('install');
return module.exports(paths, options);
};
module.exports.completion = function (opts, cb) {
var cache = path.join(config.completion, 'install.json');
var done = function done(err, results) {
if (err) return cb(err);
var names = results.map(function (pkg) {
return pkg.name;
});
return cb(null, names);
};
fs.readFile(cache, function (err, body) {
if (!err) return done(null, JSON.parse(body));
// expected error, do the first request and cache the results
source.all(function (err, results) {
if (err) return cb(err);
fs.writeFile(cache, JSON.stringify(results, null, 2), function (err) {
done(err, results);
});
});
});
};
module.exports.completion.options = shorthand;

View File

@@ -108,4 +108,14 @@ module.exports.line = function (argv) {
if (options.help) return help('link');
return module.exports(name);
};
};
module.exports.completion = function (opts, cb) {
fs.readdir(config.links, function (err, dirs) {
// ignore ENOENT, ~/.bower/links not created yet
if (err && err.code === 'ENOENT') return cb(null, []);
cb(err, dirs);
});
};
module.exports.completion.options = shorthand;

View File

@@ -177,4 +177,16 @@ module.exports.line = function (argv) {
var options = nopt(optionTypes, shorthand, argv);
if (options.help) return help('list');
return module.exports(options);
};
};
module.exports.completion = function (opts, cb) {
if (!/^-/.test(opts.word)) return cb(null, []);
var results = Object.keys(optionTypes).map(function (option) {
return '--' + option;
});
cb(null, results);
};
module.exports.completion.options = shorthand;

View File

@@ -11,6 +11,7 @@ var nopt = require('nopt');
var template = require('../util/template');
var source = require('../core/source');
var install = require('./install');
var help = require('./help');
var optionTypes = { help: Boolean };
@@ -46,4 +47,7 @@ module.exports.line = function (argv) {
if (options.help || !names.length) return help('lookup');
return module.exports(names[0]);
};
};
module.exports.completion = install.completion;
module.exports.completion.options = shorthand;

View File

@@ -64,4 +64,4 @@ module.exports.completion = function (opts, cb) {
return '--' + option;
}));
}
};
};

View File

@@ -11,6 +11,7 @@ var nopt = require('nopt');
var template = require('../util/template');
var source = require('../core/source');
var install = require('./install');
var help = require('./help');
var optionTypes = { help: Boolean };
@@ -46,4 +47,7 @@ module.exports.line = function (argv) {
if (options.help) return help('search');
return module.exports(names[0]);
};
};
module.exports.completion = install.completion;
module.exports.completion.options = shorthand;

View File

@@ -175,4 +175,23 @@ module.exports.line = function (argv) {
var names = options.argv.remain.slice(1);
return module.exports(names, options);
};
};
module.exports.completion = function (opts, cb) {
var word = opts.word;
// completing options?
if (opts.words[0] === 'uninstall' && word.charAt(0) === '-') {
return cb(null, Object.keys(optionTypes).map(function (option) {
return '--' + option;
}));
}
fs.readdir(config.directory, function (err, dirs) {
// ignore ENOENT, ./components not created yet
if (err && err.code === 'ENOENT') return cb(null, []);
cb(err, dirs);
});
};
module.exports.completion.options = shorthand;

View File

@@ -11,8 +11,9 @@ var async = require('async');
var nopt = require('nopt');
var _ = require('lodash');
var Manager = require('../core/manager');
var help = require('./help');
var Manager = require('../core/manager');
var help = require('./help');
var uninstall = require('./uninstall');
var shorthand = { 'h': ['--help'], 'f': ['--force'] };
var optionTypes = { help: Boolean, force: Boolean };
@@ -77,4 +78,7 @@ module.exports.line = function (argv) {
var paths = options.argv.remain.slice(1);
return module.exports(paths, options);
};
};
module.exports.completion = uninstall.completion;
module.exports.completion.options = shorthand;

View File

@@ -26,6 +26,7 @@ var folder = process.platform === 'win32'
var config = require('rc') ('bower', {
cache : path.join(roaming, folder, 'cache'),
links : path.join(roaming, folder, 'links'),
completion : path.join(roaming, folder, 'cache/__bowercompletion__'),
json : 'component.json',
endpoint : 'https://bower.herokuapp.com',
directory : 'components'
@@ -41,4 +42,4 @@ if (fileExists(localFile)) {
// If an uncaught exception occurs, the temporary directories will be deleted nevertheless
tmp.setGracefulCleanup();
module.exports = config;
module.exports = config;

65
lib/util/completion.js Normal file
View File

@@ -0,0 +1,65 @@
// ==========================================
// BOWER: completion
// ==========================================
// Copyright 2012 Twitter, Inc
// Licensed under The MIT License
// http://opensource.org/licenses/MIT
// ==========================================
// This module exposes a simple helper to parse the environment variables in
// case of a tab completion command. It parses the provided `argv` (nopt's
// remain arguments after `--`) and `env` (should be process.env)
//
// It is inspired and based off Isaac's work on npm.
module.exports = function (argv, env) {
var opts = {};
// w is the words number, based on the cursor position
opts.w = +env.COMP_CWORD;
// words is the escaped sequence of words following `bower`
opts.words = argv.map(function (word) {
return word.charAt(0) === "\"" ?
word.replace(/^"|"$/g, "") :
word.replace(/\\ /g, " ");
});
// word is a shortcut to the last word in the line
opts.word = opts.words[opts.w - 1];
// line is the sequence of tab completed words.
opts.line = env.COMP_LINE;
// point is the cursor position in the line
opts.point = +env.COMP_POINT;
// length is the whole line's length.
opts.length = opts.line.length;
// partialLine is the line ignoring the sequence of characters after
// cursor position, ie. tabbing at: bower install j|qu
// gives back a partialLine: bower install j
opts.partialLine = opts.line.slice(0, opts.point);
// partialWords is only returning the words based on cursor position,
// ie tabbing at: bower install ze|pto backbone
// gives back a partialWords array: ['install', 'zepto']
opts.partialWords = opts.words.slice(0, opts.w);
return opts;
};
module.exports.log = function (arr, opts, prefix) {
arr = Array.isArray(arr) ? arr : [arr];
arr.filter(module.exports.abbrev(opts)).forEach(function (word) {
console.log(word);
});
};
module.exports.abbrev = function abbrev(opts) {
var word = opts.word.replace(/\./g, '\\.');
return function (it) {
return new RegExp('^' + word).test(it);
};
};

View File

@@ -0,0 +1,55 @@
# Credits to npm's. Awesome completion utility.
#
# Bower completion script, based on npm completion script.
###-begin-bower-completion-###
#
# Installation: bower completion >> ~/.bashrc (or ~/.zshrc)
# Or, maybe: bower completion > /usr/local/etc/bash_completion.d/npm
#
COMP_WORDBREAKS=${COMP_WORDBREAKS/=/}
COMP_WORDBREAKS=${COMP_WORDBREAKS/@/}
export COMP_WORDBREAKS
if type complete &>/dev/null; then
_bower_completion () {
local si="$IFS"
IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \
COMP_LINE="$COMP_LINE" \
COMP_POINT="$COMP_POINT" \
bower completion -- "${COMP_WORDS[@]}" \
2>/dev/null)) || return $?
IFS="$si"
}
complete -F _bower_completion bower
elif type compdef &>/dev/null; then
_bower_completion() {
si=$IFS
compadd -- $(COMP_CWORD=$((CURRENT-1)) \
COMP_LINE=$BUFFER \
COMP_POINT=0 \
bower completion -- "${words[@]}" \
2>/dev/null)
IFS=$si
}
compdef _bower_completion bower
elif type compctl &>/dev/null; then
_bower_completion () {
local cword line point words si
read -Ac words
read -cn cword
let cword-=1
read -l line
read -ln point
si="$IFS"
IFS=$'\n' reply=($(COMP_CWORD="$cword" \
COMP_LINE="$line" \
COMP_POINT="$point" \
bower completion -- "${words[@]}" \
2>/dev/null)) || return $?
IFS="$si"
}
compctl -K _bower_completion bower
fi
###-end-bower-completion-###

View File

@@ -0,0 +1,12 @@
Usage:
{{#cyan}}bower{{/cyan}} completion
Synopsis:
. <(bower completion)
Description:
Tab Completion for bower

159
test/completion.js Normal file
View File

@@ -0,0 +1,159 @@
/*jshint plusplus:false*/
var fs = require('fs');
var path = require('path');
var assert = require('assert');
var complete = require('../lib/util/completion');
var config = require('../lib/core/config');
var command = require('../lib/commands/completion');
var commands = require('../lib/commands');
describe('completion', function () {
before(function () {
this.opts = complete(['bower', 'install'], {
COMP_CWORD: '2',
COMP_LINE: 'bower install',
COMP_POINT: '14'
});
});
beforeEach(function() {
this.log = complete.log;
});
afterEach(function () {
complete.log = this.log;
});
it('parses COMP_* in the env', function () {
assert.deepEqual(this.opts, {
w: 2,
words: [ 'bower', 'install' ],
word: 'install',
line: 'bower install',
point: 14,
length: 13,
partialLine: 'bower install',
partialWords: [ 'bower', 'install' ]
});
});
it('filters out completion results', function () {
var completed = [
'backbone',
'backbone-forms',
'backbone-paginator',
'backbone.forms',
'backbone.paginator'
];
var all = complete.abbrev({ word: 'ba' });
var none = complete.abbrev({ word: 'foobar' });
var form = complete.abbrev({ word: 'backbone-form' });
var dots = complete.abbrev({ word: 'backbone.' });
var dashed = complete.abbrev({ word: 'backbone-' });
assert.deepEqual(completed.filter(all), completed);
assert.deepEqual(completed.filter(none), []);
assert.deepEqual(completed.filter(form), ['backbone-forms']);
assert.deepEqual(completed.filter(dashed), ['backbone-forms', 'backbone-paginator']);
assert.deepEqual(completed.filter(dots), ['backbone.forms', 'backbone.paginator']);
});
it('dumps the script when COMP_* aren\'t in the env', function (done) {
command().on('end', function (data) {
var script = fs.readFileSync(path.join(__dirname, '../templates/completion.mustache'), 'utf8');
assert.equal(data, script);
done();
});
});
it('completes the list of command on first word', function () {
complete.log = function (results, opts) {
assert.deepEqual(results, Object.keys(commands));
};
command([''], {
COMP_CWORD: '1',
COMP_LINE: 'bower ',
COMP_POINT: '6'
});
});
it('completes the list of options on first word', function () {
complete.log = function (results, opts) {
assert.deepEqual(results, ['--no-color', '--help', '--version']);
};
command(['-'], {
COMP_CWORD: '1',
COMP_LINE: 'bower -',
COMP_POINT: '7'
});
});
it('completes the list of command on invalid command', function () {
complete.log = function (results, opts) {
assert.deepEqual(results, Object.keys(commands));
};
command(['foobar'], {
COMP_CWORD: '1',
COMP_LINE: 'bower foobar',
COMP_POINT: '12'
});
});
it('delegates to command.completion for each bower command', function (done) {
complete.log = function (results, opts) {
assert.ok(results.length);
var jq = results.filter(function (res) {
return res === 'jquery';
});
assert.equal(jq.length, 1);
};
var cmd = command(['install', 'jquery-'], {
COMP_CWORD: '2',
COMP_LINE: 'bower install jquery-',
COMP_POINT: '14'
});
cmd.on('end', done);
});
describe('completion cache clean', function() {
it('caches the result of <endpoint>/packages to ~/.bower/cache/__bowercompletion__', function(done) {
complete.log = function() {};
var cmd = command(['install', 'jquery-'], {
COMP_CWORD: '2',
COMP_LINE: 'bower install jquery-',
COMP_POINT: '14'
});
cmd.on('end', function() {
var cache = path.join(config.completion, 'install.json');
fs.stat(cache, done);
});
})
it('is cleared with cache-clean command', function(done) {
commands['cache-clean']().on('end', function() {
var cache = path.join(config.completion, 'install.json');
fs.stat(cache, function(err) {
done(err ? null : new Error('completion results wasn\'t cleaned'));
});
});
});
});
});