Skip to content

Commit

Permalink
feat: basic bash/zsh completion
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtajina committed Jul 30, 2013
1 parent 9c5840d commit 9dc1cf6
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 5 deletions.
4 changes: 3 additions & 1 deletion bin/karma
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ if (!fs.existsSync(dir)) {
}

var cli = require(path.join(dir, 'cli'));

var config = cli.process();

switch (config.cmd) {
Expand All @@ -25,4 +24,7 @@ switch (config.cmd) {
case 'init':
require(path.join(dir, 'init')).init(config);
break;
case 'completion':
require(path.join(dir, 'completion')).completion(config);
break;
}
50 changes: 50 additions & 0 deletions karma-completion.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
###-begin-karma-completion-###
#
# karma command completion script
# This is stolen from NPM. Thanks @isaac!
#
# Installation: karma completion >> ~/.bashrc (or ~/.zshrc)
# Or, maybe: karma completion > /usr/local/etc/bash_completion.d/npm
#

if type complete &>/dev/null; then
__karma_completion () {
local si="$IFS"
IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \
COMP_LINE="$COMP_LINE" \
COMP_POINT="$COMP_POINT" \
karma completion -- "${COMP_WORDS[@]}" \
2>/dev/null)) || return $?
IFS="$si"
}
complete -F __karma_completion karma
elif type compdef &>/dev/null; then
__karma_completion() {
si=$IFS
compadd -- $(COMP_CWORD=$((CURRENT-1)) \
COMP_LINE=$BUFFER \
COMP_POINT=0 \
karma completion -- "${words[@]}" \
2>/dev/null)
IFS=$si
}
compdef __karma_completion karma
elif type compctl &>/dev/null; then
__karma_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" \
karma completion -- "${words[@]}" \
2>/dev/null)) || return $?
IFS="$si"
}
compctl -K __karma_completion karma
fi
###-end-karma-completion-###
20 changes: 16 additions & 4 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ var describeShared = function() {
'Commands:\n' +
' start [<configFile>] [<options>] Start the server / do single run.\n' +
' init [<configFile>] Initialize a config file.\n' +
' run [<options>] [ -- <clientArgs>] Trigger a test run.\n\n' +
' run [<options>] [ -- <clientArgs>] Trigger a test run.\n' +
' completion Shell completion for karma.\n\n' +
'Run --help with particular command to see its description and available options.')
.describe('help', 'Print usage and options.')
.describe('version', 'Print current version.');
Expand All @@ -93,7 +94,6 @@ var describeInit = function() {
.describe('colors', 'Use colors when reporting and printing logs.')
.describe('no-colors', 'Do not use colors when reporting or printing logs.')
.describe('help', 'Print usage and options.')
.describe('version', 'Print current version.');
};


Expand All @@ -116,7 +116,6 @@ var describeStart = function() {
.describe('no-single-run', 'Disable single-run.')
.describe('report-slower-than', '<integer> Report tests that are slower than given time [ms].')
.describe('help', 'Print usage and options.')
.describe('version', 'Print current version.');
};


Expand All @@ -128,7 +127,16 @@ var describeRun = function() {
' $0 run [<options>]')
.describe('port', '<integer> Port where the server is listening.')
.describe('help', 'Print usage.')
.describe('version', 'Print current version.');
};


var describeCompletion = function() {
optimist
.usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
'COMPLETION - Bash/ZSH completion for karma.\n\n' +
'Installation:\n' +
' $0 completion >> ~/.bashrc\n')
.describe('help', 'Print usage.')
};


Expand All @@ -153,6 +161,10 @@ exports.process = function() {
describeInit();
break;

case 'completion':
describeCompletion();
break;

default:
describeShared();
if (!options.cmd) {
Expand Down
160 changes: 160 additions & 0 deletions lib/completion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
var CUSTOM = [''];
var BOOLEAN = false;

var options = {
start: {
'--port': CUSTOM,
'--auto-watch': BOOLEAN,
'--no-auto-watch': BOOLEAN,
'--log-level': ['disable', 'debug', 'info', 'warn', 'error'],
'--colors': BOOLEAN,
'--no-colors': BOOLEAN,
'--reporters': ['dots', 'progress'],
'--no-reporters': BOOLEAN,
'--browsers': ['Chrome', 'ChromeCanary', 'Firefox', 'PhantomJS', 'Safari', 'Opera'],
'--no-browsers': BOOLEAN,
'--single-run': BOOLEAN,
'--no-single-run': BOOLEAN,
'--help': BOOLEAN
},
init: {
'--colors': BOOLEAN,
'--no-colors': BOOLEAN,
'--help': BOOLEAN
},
run: {
'--port': CUSTOM,
'--help': BOOLEAN
}
};

var parseEnv = function(argv, env) {
var words = argv.slice(5);

return {
words: words,
count: parseInt(env.COMP_CWORD, 10),
last: words[words.length - 1],
prev: words[words.length - 2]
};
};

var opositeWord = function(word) {
if (word.charAt(0) !== '-') {
return null;
}

return word.substr(0, 5) === '--no-' ? '--' + word.substr(5) : '--no-' + word.substr(2);
};

var sendCompletion = function(possibleWords, env) {
var regexp = new RegExp('^' + env.last);
var filteredWords = possibleWords.filter(function(word) {
return regexp.test(word) && env.words.indexOf(word) === -1 && env.words.indexOf(opositeWord(word)) === -1;
});

if (!filteredWords.length) {
return sendCompletionNoOptions(env);
}

filteredWords.forEach(function(word) {
console.log(word);
});
};


var glob = require('glob');
var globOpts = {
mark: true,
nocase: true
};

var sendCompletionFiles = function(env) {
glob(env.last + '*', globOpts, function(err, files) {
if (files.length === 1 && files[0].charAt(files[0].length - 1) === '/') {
sendCompletionFiles({last: files[0]});
} else {
console.log(files.join('\n'));
}
});
};

var sendCompletionConfirmLast = function(env) {
console.log(env.last);
};

var sendCompletionNoOptions = function() {};

var complete = function(env) {
if (env.count === 1) {
if (env.words[0].charAt(0) === '-') {
return sendCompletion(['--help', '--version'], env);
}

return sendCompletion(Object.keys(options), env);
}

if (env.count === 2 && env.words[1].charAt(0) !== '-') {
// complete files (probably karma.conf.js)
return sendCompletionFiles(env);
}

var cmdOptions = options[env.words[0]];
var previousOption = cmdOptions[env.prev];

if (!cmdOptions) {
// no completion, wrong command
return sendCompletionNoOptions();
}

if (previousOption === CUSTOM && env.last) {
// custom value with already filled something
return sendCompletionConfirmLast(env);
}

if (previousOption) {
// custom options
return sendCompletion(previousOption, env);
}

return sendCompletion(Object.keys(cmdOptions), env);
};


var completion = function() {
if (process.argv[3] == '--') {
return complete(parseEnv(process.argv, process.env));
}

// just print out the karma-completion.sh
var fs = require('fs');
var path = require('path');

fs.readFile(path.resolve(__dirname, '../karma-completion.sh'), 'utf8', function (err, data) {
process.stdout.write(data);
process.stdout.on('error', function (error) {
// Darwin is a real dick sometimes.
//
// This is necessary because the "source" or "." program in
// bash on OS X closes its file argument before reading
// from it, meaning that you get exactly 1 write, which will
// work most of the time, and will always raise an EPIPE.
//
// Really, one should not be tossing away EPIPE errors, or any
// errors, so casually. But, without this, `. <(karma completion)`
// can never ever work on OS X.
if (error.errno === 'EPIPE') {
error = null;
}
});
});
};


// PUBLIC API
exports.completion = completion;

// for testing
exports.opositeWord = opositeWord;
exports.sendCompletion = sendCompletion;
exports.complete = complete;
59 changes: 59 additions & 0 deletions test/unit/completion.spec.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#==============================================================================
# lib/completion.js module
#==============================================================================
describe 'completion', ->
c = require '../../lib/completion'
completion = null

mockEnv = (line) ->
words = line.split ' '

words: words
count: words.length
last: words[words.length - 1]
prev: words[words.length - 2]

beforeEach ->
sinon.stub console, 'log', (msg) -> completion.push msg
completion = []

describe 'opositeWord', ->

it 'should handle --no-x args', ->
expect(c.opositeWord '--no-single-run').to.equal '--single-run'


it 'should handle --x args', ->
expect(c.opositeWord '--browsers').to.equal '--no-browsers'


it 'should ignore args without --', ->
expect(c.opositeWord 'start').to.equal null


describe 'sendCompletion', ->

it 'should filter only words matching last typed partial', ->
c.sendCompletion ['start', 'init', 'run'], mockEnv 'in'
expect(completion).to.deep.equal ['init']


it 'should filter out already used words/args', ->
c.sendCompletion ['--single-run', '--port', '--xxx'], mockEnv 'start --single-run '
expect(completion).to.deep.equal ['--port', '--xxx']


it 'should filter out already used oposite words', ->
c.sendCompletion ['--auto-watch', '--port'], mockEnv 'start --no-auto-watch '
expect(completion).to.deep.equal ['--port']


describe 'complete', ->

it 'should complete the basic commands', ->
c.complete mockEnv ''
expect(completion).to.deep.equal ['start', 'init', 'run']

completion.length = 0 # reset
c.complete mockEnv 's'
expect(completion).to.deep.equal ['start']

0 comments on commit 9dc1cf6

Please sign in to comment.