Tab completion, handler for install command

- results of the first lookup is cached to
  ~/.bower/completion/install.json
- consider caching in ~/.bower/cache instead, so that cache-clean
  command also removes completion cache.
This commit is contained in:
Mickael Daniel
2012-12-01 21:18:01 +01:00
parent f9f3ee1d06
commit 174c37d75d
7 changed files with 402 additions and 1 deletions

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

@@ -0,0 +1,102 @@
// ==========================================
// 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);
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

@@ -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');
@@ -43,3 +47,36 @@ module.exports.line = function (argv) {
if (options.help) return help('install');
return module.exports(paths, options);
};
module.exports.completion = function (opts, cb) {
var word = opts.word;
// completing options?
if (opts.words[0] === 'install' && word.charAt(0) === '-') {
return cb(null, Object.keys(optionTypes).map(function (option) {
return '--' + option;
}));
}
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);
});
});
});
};

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, 'completion'),
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);
};
};