diff --git a/plugins/cockpit/lib/cli.js b/plugins/cockpit/lib/cli.js index 68af68e7..a9d5acc6 100644 --- a/plugins/cockpit/lib/cli.js +++ b/plugins/cockpit/lib/cli.js @@ -49,10 +49,19 @@ var Status = require('pilot/types').Status; var Conversion = require('pilot/types').Conversion; var canon = require('pilot/canon'); +/** + * Normally type upgrade is done when the owning command is registered, but + * out commandParam isn't part of a command, so it misses out. + */ +exports.startup = function(data, reason) { + canon.upgradeType(commandParam); +}; /** * The information required to tell the user there is a problem with their * input. + * TODO: There a several places where {start,end} crop up. Perhaps we should + * have a Cursor object. */ function Hint(status, message, start, end, predictions) { this.status = status; @@ -137,9 +146,20 @@ oop.inherits(ConversionHint, Hint); /** * We record where in the input string an argument comes so we can report errors * against those string positions. + * We publish a 'change' event when-ever the text changes + * @param emitter Arguments use something else to pass on change events. + * Currently this will be the creating Requisition. This prevents dependency + * loops and prevents us from needing to merge listener lists. + * @param text The string (trimmed) that contains the argument + * @param start The position of the text in the original input string + * @param end See start + * @param priorSpace Knowledge of the whitespace used prior to this argument in + * the input string allows us to re-generate the original input from the + * arguments. * @constructor */ -function Argument(text, start, end, priorSpace) { +function Argument(emitter, text, start, end, priorSpace) { + this.emitter = emitter; this.setText(text); this.start = start; this.end = end; @@ -150,7 +170,11 @@ Argument.prototype = { * Return the result of merging these arguments */ merge: function(following) { + if (following.emitter != this.emitter) { + throw new Error('Can\'t merge Arguments from different EventEmitters'); + } return new Argument( + this.emitter, this.text + following.priorSpace + following.text, this.start, following.end, this.priorSpace); @@ -164,11 +188,14 @@ Argument.prototype = { if (text == null) { throw new Error('Illegal text for Argument: ' + text); } + var ev = { argument: this, oldText: this.text, text: text }; this.text = text; + this.emitter._dispatchEvent('argumentChange', ev); } }; /** * Merge an array of arguments into a single argument. + * All Arguments in the array are expected to have the same emitter */ Argument.merge = function(argArray, start, end) { start = (start === undefined) ? 0 : start; @@ -187,7 +214,7 @@ Argument.merge = function(argArray, start, end) { return joined; }; /** - * We sometimes need a way to say 'this error occurs whereever the cursor is' + * We sometimes need a way to say 'this error occurs where ever the cursor is' */ Argument.AT_CURSOR = -1; @@ -202,17 +229,11 @@ Argument.AT_CURSOR = -1; * Thus, null is a valid default value, and common because it identifies an * parameter that is optional. undefined means there is no value from * the command line. - * - *
TODO: We might need events in the future
- * The current hope is that a GUI and CLI that share an Assignment can be in
- * direct connection due to assignment.getValue calling arg.setText, however
- * we might need to use events if not.
- * oop.implement(Assignment, EventEmitter);
- *
* @constructor
*/
-function Assignment(param) {
+function Assignment(param, requisition) {
this.param = param;
+ this.requisition = requisition;
this.setValue(param.defaultValue);
};
Assignment.prototype = {
@@ -260,6 +281,7 @@ Assignment.prototype = {
}
this.conversion = undefined;
+ this.requisition._assignmentChanged(this);
},
/**
@@ -275,6 +297,7 @@ Assignment.prototype = {
this.conversion = this.param.type.parse(arg.text);
this.conversion.arg = arg; // TODO: make this automatic?
this.value = this.conversion.value;
+ this.requisition._assignmentChanged(this);
},
/**
@@ -285,9 +308,7 @@ Assignment.prototype = {
* from this assignment all but the most severe will ever be used. It might
* make sense with more experience to alter this to function to be getHint()
*/
- getHints: function() {
- var hints = [];
-
+ getHint: function() {
// If there is no argument, use the cursor position
var message = '' + this.param.name + ': ';
if (this.param.description) {
@@ -324,56 +345,155 @@ Assignment.prototype = {
message += 'Required<\strong>';
}
- return [ new Hint(status, message, start, end, predictions) ];
+ // Allow the parameter to provide documentation
+ // TODO: consider when we should do this
+ if (status === Status.VALID && message === '' && this.param.documentValid) {
+ message = this.param.documentValid(value);
+ }
+
+ return new Hint(status, message, start, end, predictions);
+ },
+
+ /**
+ * Basically setValue(conversion.predictions[0]) done in a safe
+ * way.
+ */
+ complete: function() {
+ if (this.conversion && this.conversion.predictions &&
+ this.conversion.predictions.length > 0) {
+ this.setValue(this.conversion.predictions[0]);
+ }
}
};
exports.Assignment = Assignment;
+/**
+ * This is a special parameter to reflect the command itself.
+ */
+var commandParam = {
+ name: 'command',
+ type: 'command',
+ description: 'The command to execute',
+
+ /**
+ * Provide some documentation for a command.
+ */
+ documentValid: function(command) {
+ var docs = [];
+ docs.push(' > ');
+ docs.push(command.name);
+ if (command.params && command.params.length > 0) {
+ command.params.forEach(function(param) {
+ if (param.defaultValue === undefined) {
+ docs.push(' [' + param.name + ']');
+ }
+ else {
+ docs.push(' [' + param.name + ']');
+ }
+ }, this);
+ }
+ docs.push('
');
+
+ docs.push(command.description ? command.description : '(No description)');
+ docs.push('
');
+
+ if (command.params && command.params.length > 0) {
+ docs.push('');
+ command.params.forEach(function(param) {
+ docs.push('
');
+ }
+
+ return docs.join('');
+ }
+};
+
/**
* A Requisition collects the information needed to execute a command.
* There is no point in a requisition for parameter-less commands because there
* is no information to collect. A Requisition is a collection of assignments
* of values to parameters, each handled by an instance of Assignment.
* CliRequisition adds functions for parsing input from a command line to this
- * class
+ * class.
+ * Events
+ * We publish the following events:
+ *
');
-
- docs.push(command.description ? command.description : '(No description)');
- docs.push('
');
-
- if (command.params && command.params.length > 0) {
- docs.push('');
- command.params.forEach(function(param) {
- docs.push('
');
- }
-
- return docs.join('');
-};
-exports.documentCommand = documentCommand;
-
});
diff --git a/plugins/cockpit/lib/index.js b/plugins/cockpit/lib/index.js
index 0691621e..5011915b 100644
--- a/plugins/cockpit/lib/index.js
+++ b/plugins/cockpit/lib/index.js
@@ -39,6 +39,7 @@ define(function(require, exports, module) {
exports.startup = function(data, reason) {
+ require('cockpit/cli').startup(data, reason);
window.testCli = require('cockpit/test/testCli');
require('cockpit/ui/settings').startup(data, reason);
diff --git a/plugins/cockpit/lib/test/testCli.js b/plugins/cockpit/lib/test/testCli.js
index 20b0cdd0..1d40520a 100644
--- a/plugins/cockpit/lib/test/testCli.js
+++ b/plugins/cockpit/lib/test/testCli.js
@@ -54,17 +54,19 @@ exports.testAll = function() {
};
exports.testTokenize = function() {
- var args = tokenize('');
+ var cli = new CliRequisition();
+
+ var args = cli._tokenize('');
test.verifyEqual(0, args.length);
- args = tokenize('s');
+ args = cli._tokenize('s');
test.verifyEqual(1, args.length);
test.verifyEqual('s', args[0].text);
test.verifyEqual(0, args[0].start);
test.verifyEqual(1, args[0].end);
test.verifyEqual('', args[0].priorSpace);
- args = tokenize('s s');
+ args = cli._tokenize('s s');
test.verifyEqual(2, args.length);
test.verifyEqual('s', args[0].text);
test.verifyEqual(0, args[0].start);
@@ -75,7 +77,7 @@ exports.testTokenize = function() {
test.verifyEqual(3, args[1].end);
test.verifyEqual(' ', args[1].priorSpace);
- args = tokenize(' 1234 \'12 34\'');
+ args = cli._tokenize(' 1234 \'12 34\'');
test.verifyEqual(2, args.length);
test.verifyEqual('1234', args[0].text);
test.verifyEqual(1, args[0].start);
@@ -86,7 +88,7 @@ exports.testTokenize = function() {
test.verifyEqual(13, args[1].end);
test.verifyEqual(' ', args[1].priorSpace);
- args = tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \
+ args = cli._tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \
test.verifyEqual(3, args.length);
test.verifyEqual('12\'34', args[0].text);
test.verifyEqual(0, args[0].start);
@@ -101,7 +103,7 @@ exports.testTokenize = function() {
test.verifyEqual(15, args[2].end);
test.verifyEqual(' ', args[2].priorSpace);
- args = tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd
+ args = cli._tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd
test.verifyEqual(4, args.length);
test.verifyEqual('a b', args[0].text);
test.verifyEqual(0, args[0].start);
@@ -124,19 +126,21 @@ exports.testTokenize = function() {
};
exports.testSplit = function() {
- var args = tokenize('s');
- var conversion = split(args);
+ var cli = new CliRequisition();
+
+ var args = cli._tokenize('s');
+ var conversion = cli._split(args);
test.verifyEqual(1, args.length);
test.verifyEqual('s', args[0].text);
test.verifyNull(conversion.value);
- var args = tokenize('set');
- var conversion = split(args);
+ var args = cli._tokenize('set');
+ var conversion = cli._split(args);
test.verifyEqual([], args);
test.verifyEqual('set', conversion.value.name);
- var args = tokenize('set a b');
- var conversion = split(args);
+ var args = cli._tokenize('set a b');
+ var conversion = cli._split(args);
test.verifyEqual('set', conversion.value.name);
test.verifyEqual(2, args.length);
test.verifyEqual('a', args[0].text);
diff --git a/plugins/cockpit/lib/ui/cliView.js b/plugins/cockpit/lib/ui/cliView.js
index f207d58f..5e34777b 100644
--- a/plugins/cockpit/lib/ui/cliView.js
+++ b/plugins/cockpit/lib/ui/cliView.js
@@ -61,13 +61,15 @@ var NO_HINT = new Hint(Status.VALID, '', 0, 0);
* 2. Attach a set of events so the command line works
*/
exports.startup = function(data, reason) {
- var cliView = new CliView(data);
+ var cli = new CliRequisition();
+ var cliView = new CliView(cli, data.env);
};
/**
* A class to handle the simplest UI implementation
*/
-function CliView(data) {
+function CliView(cli, env) {
+ this.cli = cli;
this.doc = document;
this.win = this.doc.defaultView;
@@ -78,14 +80,14 @@ function CliView(data) {
return;
}
- this.cli = new CliRequisition();
-
- this.settings = data.env.settings;
+ this.settings = env.settings;
this.hintDirection = this.settings.getSetting('hintDirection');
this.outputDirection = this.settings.getSetting('outputDirection');
this.outputHeight = this.settings.getSetting('outputHeight');
- this.hints = [];
+ // If the requisition tells us something has changed, we use this to know
+ // if we should ignore it
+ this.isUpdating = false;
this.createElements();
this.update();
@@ -128,8 +130,12 @@ CliView.prototype = {
input.addEventListener('keyup', this.onKeyUp.bind(this), true);
// cursor position affects hint severity. TODO: shortcuts for speed
input.addEventListener('mouseup', function(ev) {
+ this.isUpdating = true;
this.update();
+ this.isUpdating = false;
}.bind(this), false);
+
+ this.cli.addEventListener('argumentChange', this.onArgChange.bind(this));
},
/**
@@ -181,6 +187,7 @@ CliView.prototype = {
* Ensure that TAB isn't handled by the browser
*/
onKeyDown: function(ev) {
+ this.isUpdating = true;
var handled;
// var handled = keyboardManager.processKeyEvent(ev, this, {
// isCommandLine: true, isKeyUp: false
@@ -188,6 +195,7 @@ CliView.prototype = {
if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) {
return true;
}
+ this.isUpdating = false;
return handled;
},
@@ -195,6 +203,7 @@ CliView.prototype = {
* The main keyboard processing loop
*/
onKeyUp: function(ev) {
+ this.isUpdating = true;
var handled;
/*
var handled = keyboardManager.processKeyEvent(ev, this, {
@@ -202,35 +211,36 @@ CliView.prototype = {
});
*/
+ // RETURN does a special exec/highlight thing
if (ev.keyCode === keyutil.KeyHelper.KEY.RETURN) {
- if (this.hints.worst || this.hints.worst.status === Status.VALID) {
+ var worst = this.getWorstHint();
+ // Deny RETURN unless the command might work
+ if (worst.status === Status.VALID) {
this.cli.exec();
this.element.value = '';
}
+ else {
+ // If we've denied RETURN because the command was not VALID,
+ // select the part of the command line that is causing problems
+ // TODO: if there are 2 errors are we picking the right one?
+ this.element.selectionStart = worst.start;
+ this.element.selectionEnd = worst.end;
+ }
}
- if (ev.keyCode === keyutil.KeyHelper.KEY.TAB && this.hints.display &&
- this.hints.display.predictions && this.hints.display.predictions.length > 0) {
- var prefix = this.element.value.substring(0, this.hints.display.start);
- var suffix = this.element.value.substring(this.hints.display.end);
- var insert = this.hints.display.predictions[0];
- insert = typeof insert === 'string' ? insert : insert.name;
- this.element.value = prefix + insert + suffix;
- // Fix the cursor.
- var insertEnd = (prefix + insert).length;
- this.element.selectionStart = insertEnd;
- this.element.selectionEnd = insertEnd;
+ // TAB does a special complete thing
+ if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) {
+ var assignment = this.cli.getAssignmentAt(this.element.selectionStart);
+ if (assignment) {
+ this.isUpdating = false;
+ assignment.complete();
+ this.isUpdating = true;
+ }
}
this.update();
- if (ev.keyCode === keyutil.KeyHelper.KEY.RETURN) {
- if (this.hints.worst && this.hints.worst.status !== Status.VALID) {
- this.element.selectionStart = this.hints.worst.start;
- this.element.selectionEnd = this.hints.worst.end;
- }
- }
-
+ this.isUpdating = false;
return handled;
},
@@ -254,24 +264,10 @@ CliView.prototype = {
// dom.removeCssClass(completer, Status.INCOMPLETE.toString());
// dom.removeCssClass(completer, Status.INVALID.toString());
- this.hints = this.cli.getHints();
-
// Create a marked up version of the input
var highlightedInput = '> ';
if (this.element.value.length > 0) {
- // 'scores' is an array which tells us what chars are errors
- // Initialize with everything VALID
- var scores = this.element.value.split('').map(function(char) {
- return Status.VALID;
- });
- // For all chars in all hints, check and upgrade the score
- this.hints.forEach(function(hint) {
- for (var i = hint.start; i <= hint.end; i++) {
- if (hint.status > scores[i]) {
- scores[i] = hint.status;
- }
- }
- }, this);
+ var scores = this.cli.getInputStatusMarkup();
// Create markup
var i = 0;
var lastStatus = -1;
@@ -293,7 +289,7 @@ CliView.prototype = {
}
// Display the "-> prediction" at the end of the completer
- var display = this.hints.display || NO_HINT;
+ var display = this.cli.getAssignmentAt(this.element.selectionStart).getHint();
var message = display.message;
if (display.predictions && display.predictions.length > 0) {
message += ': [ ';
@@ -322,9 +318,26 @@ CliView.prototype = {
this.hinter.classList.remove('cptNoHints');
}
- var status = this.hints.worst ? this.hints.worst.status : Status.VALID;
- this.completer.classList.add(status.toString());
- // dom.addCssClass(input, status.toString());
+ this.completer.classList.add(this.cli.getWorstHint().status.toString());
+ // dom.addCssClass(input, this.cli.getWorstHint().status.toString());
+ },
+
+ /**
+ * Update the input element to reflect the changed argument
+ */
+ onArgChange: function(ev) {
+ if (this.isUpdating) {
+ return;
+ }
+
+ var prefix = this.element.value.substring(0, ev.argument.start);
+ var suffix = this.element.value.substring(ev.argument.end);
+ var insert = typeof ev.text === 'string' ? ev.text : ev.text.name;
+ this.element.value = prefix + insert + suffix;
+ // Fix the cursor.
+ var insertEnd = (prefix + insert).length;
+ this.element.selectionStart = insertEnd;
+ this.element.selectionEnd = insertEnd;
}
};
exports.CliView = CliView;
diff --git a/plugins/pilot/lib/canon.js b/plugins/pilot/lib/canon.js
index 0e3644d3..9e88af03 100644
--- a/plugins/pilot/lib/canon.js
+++ b/plugins/pilot/lib/canon.js
@@ -128,16 +128,20 @@ function addCommand(command) {
if (!param.name) {
throw new Error('In ' + command.name + ': all params must have a name');
}
- var lookup = param.type;
- param.type = types.getType(lookup);
- if (param.type == null) {
- throw new Error('In ' + command.name + '/' + param.name +
- ': can\'t find type for: ' + JSON.stringify(lookup));
- }
+ upgradeType(param);
}, this);
commands[command.name] = command;
};
+function upgradeType(param) {
+ var lookup = param.type;
+ param.type = types.getType(lookup);
+ if (param.type == null) {
+ throw new Error('In ' + command.name + '/' + param.name +
+ ': can\'t find type for: ' + JSON.stringify(lookup));
+ }
+}
+
function removeCommand(command) {
if (typeof command === 'string') {
delete commands[command];
@@ -187,6 +191,7 @@ exports.addCommand = addCommand;
exports.getCommand = getCommand;
exports.getCommandNames = getCommandNames;
exports.exec = exec;
+exports.upgradeType = upgradeType;
/**
@@ -228,7 +233,6 @@ var maxRequestLength = 100;
*
* var request = new Request({
* command: command,
- * commandExt: commandExt,
* args: args,
* typed: typed
* });