Rename Input to Cli. Extract CliUi as an interface. Add knowledge of cursor positioning to convert INCOMPLETE hints to INVALID if the cursor isnt in that hint. Many tidyups and tweaks as we create a test suite

This commit is contained in:
Joe Walker 2010-12-03 15:38:35 +00:00
commit 0930c37675

View file

@ -97,7 +97,7 @@ oop.inherits(ConversionHint, Hint);
* @constructor * @constructor
*/ */
function Argument(text, start, end, priorSpace) { function Argument(text, start, end, priorSpace) {
this.text = text; this.setText(text);
this.start = start; this.start = start;
this.end = end; this.end = end;
this.priorSpace = priorSpace; this.priorSpace = priorSpace;
@ -114,26 +114,55 @@ Argument.prototype = {
}, },
setText: function(text) { setText: function(text) {
if (text == null) {
throw new Error('Illegal text for Argument: ' + text);
}
this.text = text; this.text = text;
} }
}; };
/** /**
* Merge an array of arguments into a single argument. * Merge an array of arguments into a single argument.
*/ */
Argument.mergeAll = function(argArray) { Argument.merge = function(argArray, start, end) {
start = (start === undefined) ? 0 : start;
end = (end === undefined) ? argArray.length : end;
var joined; var joined;
argArray.forEach(function(arg) { for (var i = start; i < end; i++) {
var arg = argArray[i];
if (!joined) { if (!joined) {
joined = arg; joined = arg;
} }
else { else {
joined = joined.merge(arg); joined = joined.merge(arg);
} }
}); }
return joined; return joined;
}; };
/**
* CLI / UI Interface
* The Cli interacts with the UI via an instance of CliUi.
* This implementation is designed as a template rather than to be used.
* It is expected that we will have a number of implementations of this:
* - A firebug/webkit inspector cli shim
* - A simple input[type=text] version
* - A possible Cloud9 UI version
* - A possible Skywriter UI version
* This class will probably need refactoring as time goes on.
*
* TODO: Who should own the Requisition?
*/
function CliUi() {
}
CliUi.prototype = {
getSelection: function() {},
setHints: function() {},
setRequisition: function() {}
};
/** /**
* An object used during command line parsing to hold the various intermediate * An object used during command line parsing to hold the various intermediate
* data steps. * data steps.
@ -143,7 +172,7 @@ Argument.mergeAll = function(argArray) {
* <p>The other output value is input.requisition which gives access to an * <p>The other output value is input.requisition which gives access to an
* args object for use in executing the final command. * args object for use in executing the final command.
* *
* The majority of the functions in this class are called in sequence by the * <p>The majority of the functions in this class are called in sequence by the
* constructor. Their task is to add to <tt>hints</tt> fill out the requisition. * constructor. Their task is to add to <tt>hints</tt> fill out the requisition.
* <p>The general sequence is:<ul> * <p>The general sequence is:<ul>
* <li>_tokenize(): convert _typed into _parts * <li>_tokenize(): convert _typed into _parts
@ -158,23 +187,20 @@ Argument.mergeAll = function(argArray) {
* if not specified. * if not specified.
* @constructor * @constructor
*/ */
function Input(options) { function Cli(cliui, options) {
if (options) { this.cliui = cliui;
if (options.flags) { if (options && options.flags) {
this.flags = options.flags; this.flags = options.flags;
}
if (options.input) {
// TODO: implement
this.useAsInput(options.input);
}
} }
this.requisition = new Requisition(); this.requisition = new Requisition();
this.cliui.setRequisition(this.requisition);
} }
Input.prototype = { Cli.prototype = {
/** /**
* TODO: We were using a default of keyboard.buildFlags({ }); * TODO: We were using a default of keyboard.buildFlags({ });
* I think this allowed us to have commands that only existed in certain * This allowed us to have commands that only existed in certain contexts
* contexts - i.e. Javascript specific commands. * - i.e. Javascript specific commands.
*/ */
flags: {}, flags: {},
@ -184,6 +210,7 @@ Input.prototype = {
parse: function(typed) { parse: function(typed) {
if (util.none(typed)) { if (util.none(typed)) {
this.requisition.setCommand(null); this.requisition.setCommand(null);
this.cliui.setHints([]);
return; return;
} }
@ -198,17 +225,17 @@ Input.prototype = {
// until the user click a 'close' button or similar // until the user click a 'close' button or similar
this._addHint(Status.INCOMPLETE, '', 0, 0); this._addHint(Status.INCOMPLETE, '', 0, 0);
this.requisition.setCommand(null); this.requisition.setCommand(null);
this.cliui.setHints(this.hints);
return; return;
} }
var command = _split(args); var command = _split(args);
if (!command) { if (!command) {
// TODO: Should we use this technique in split?
// No command found - bail helpfully. // No command found - bail helpfully.
var commandType = types.getType('command'); var commandType = types.getType('command');
var conversion = commandType.parse(typed); var conversion = commandType.parse(typed);
var arg = Argument.mergeAll(args); var arg = Argument.merge(args);
this._addHint(new ConversionHint(conversion, arg)); this._addHint(new ConversionHint(conversion, arg));
this.requisition.setCommand(null); this.requisition.setCommand(null);
@ -224,6 +251,28 @@ Input.prototype = {
this._assign(args); this._assign(args);
this._addHint(this.requisition.getHints()); this._addHint(this.requisition.getHints());
} }
// TODO: This is the wrong place to filter this.
// It should be done by the CliUi because:
// - the cursor could move without notice
// - not all interfaces will have a notion of one cursor for the whole assignment
// Not knowing about cursor positioning, the requisition and assignments
// can't know this, but anything they mark as INCOMPLETE is actually
// INVALID unless the cursor is actually inside that argument.
var sel = this.cliui.getSelection();
this.hints.forEach(function(hint) {
var startInHint = sel.start >= hint.start && sel.start <= hint.end;
var endInHint = sel.end >= hint.start && sel.end <= hint.end;
var inHint = startInHint || endInHint;
if (!inHint && hint.status === Status.INCOMPLETE) {
hint.status = Status.INVALID;
}
}, this);
this.cliui.setHints(this.hints);
return;
}, },
/** /**
@ -269,7 +318,7 @@ Input.prototype = {
// probably given our tighter tokenize() this won't be an issue? // probably given our tighter tokenize() this won't be an issue?
this._addHint(Status.INVALID, this._addHint(Status.INVALID,
this.requisition.command.name + ' does not take any parameters', this.requisition.command.name + ' does not take any parameters',
Argument.mergeAll(args)); Argument.merge(args));
return; return;
} }
@ -278,7 +327,7 @@ Input.prototype = {
if (this.requisition.assignmentCount == 1) { if (this.requisition.assignmentCount == 1) {
var assignment = this.requisition.getAssignment(0); var assignment = this.requisition.getAssignment(0);
if (assignment.param.type.name === 'text') { if (assignment.param.type.name === 'text') {
assignment.setArgument(Argument.mergeAll(args)); assignment.setArgument(Argument.merge(args));
return; return;
} }
} }
@ -305,13 +354,15 @@ Input.prototype = {
// boolean parameters don't have values, default to false // boolean parameters don't have values, default to false
if (assignment.param.type.name === 'boolean') { if (assignment.param.type.name === 'boolean') {
assignment.setValue(true); assignment.setValue(true);
} else { }
else {
if (i + 1 < args.length) { if (i + 1 < args.length) {
// Missing value for this param // Missing value portion of this named param
this._addHint(Status.INCOMPLETE, this._addHint(Status.INCOMPLETE,
'Missing value for: ' + namedArgText, 'Missing value for: ' + namedArgText,
args[i]); args[i]);
} else { }
else {
args.splice(i + 1, 1); args.splice(i + 1, 1);
assignment.setArgument(args[i + 1]); assignment.setArgument(args[i + 1]);
} }
@ -324,31 +375,28 @@ Input.prototype = {
}, this); }, this);
// What's left are positional parameters assign in order // What's left are positional parameters assign in order
var i = 0;
names.forEach(function(name) { names.forEach(function(name) {
var assignment = this.requisition.getAssignment(name); var assignment = this.requisition.getAssignment(name);
if (i >= args.length) { if (args.length === 0) {
// No more values // No more values
assignment.setValue(undefined); // i.e. default assignment.setValue(undefined); // i.e. default
} }
else { else {
var arg = args[i]; var arg = args[0];
args.splice(i, 1); args.splice(0, 1);
assignment.setArgument(arg); assignment.setArgument(arg);
} }
i++;
}, this); }, this);
if (args.length > 0) { if (args.length > 0) {
var remaining = Argument.mergeAll(args); var remaining = Argument.merge(args);
this._addHint(Status.INVALID, this._addHint(Status.INVALID,
'Input \'' + remaining.text + '\' makes no sense.', 'Input \'' + remaining.text + '\' makes no sense.',
remaining); remaining);
} }
} }
}; };
exports.Input = Input; exports.Cli = Cli;
/** /**
* Split up the input taking into account ' and " * Split up the input taking into account ' and "
@ -545,14 +593,16 @@ function documentCommand(command) {
docs.push(' <i>'); docs.push(' <i>');
docs.push(param.name); docs.push(param.name);
docs.push('</i>'); docs.push('</i>');
} else if (param.defaultValue === null) { }
else if (param.defaultValue === null) {
docs.push(' <i>['); docs.push(' <i>[');
docs.push(param.name); docs.push(param.name);
docs.push(']</i>'); docs.push(']</i>');
} else { }
else {
optionalParamCount++; optionalParamCount++;
} }
}); }, this);
if (optionalParamCount > 3) { if (optionalParamCount > 3) {
docs.push(' [options]'); docs.push(' [options]');
} else if (optionalParamCount > 0) { } else if (optionalParamCount > 0) {
@ -562,12 +612,13 @@ function documentCommand(command) {
docs.push(param.name); docs.push(param.name);
if (param.type.name === 'boolean') { if (param.type.name === 'boolean') {
docs.push('</i>'); docs.push('</i>');
} else { }
else {
docs.push('</i> ' + param.type.name); docs.push('</i> ' + param.type.name);
} }
docs.push(']'); docs.push(']');
} }
}); }, this);
} }
docs.push('</pre>'); docs.push('</pre>');
@ -578,7 +629,7 @@ function documentCommand(command) {
if (param.type.defaultValue) { if (param.type.defaultValue) {
docs.push('<p>Default: ' + param.type.defaultValue + '</p>'); docs.push('<p>Default: ' + param.type.defaultValue + '</p>');
} }
}); }, this);
} }
return docs.join(''); return docs.join('');
@ -665,10 +716,8 @@ Requisition.prototype = {
getHints: function() { getHints: function() {
var hints = []; var hints = [];
Object.keys(this._assignments).map(function(name) { Object.keys(this._assignments).map(function(name) {
var hint = this._assignments[name].getHint(); // Append the assignments hints to our list
if (hint) { hints.push.apply(hints, this._assignments[name].getHints());
hints.push(hint);
}
}, this); }, this);
return hints; return hints;
}, },
@ -729,7 +778,7 @@ Assignment.prototype = {
/** /**
* The current value (i.e. not the string representation) * The current value (i.e. not the string representation)
* @readonly - use setValue() to mutate * Use setValue() to mutate
*/ */
value: undefined, value: undefined,
setValue: function(value) { setValue: function(value) {
@ -739,18 +788,20 @@ Assignment.prototype = {
if (value === undefined) { if (value === undefined) {
value = this.param.defaultValue; value = this.param.defaultValue;
} }
var text = (value === null) ? '' : this.param.type.stringify(value); this.value = value;
var text = (value == null) ? '' : this.param.type.stringify(value);
if (this.arg) { if (this.arg) {
this.arg.setText(text); this.arg.setText(text);
} }
this.value = value;
this.conversion = new Conversion(value, Status.VALID, '', []); this.conversion = undefined;
//this._dispatchEvent('change', { assignment: this }); //this._dispatchEvent('change', { assignment: this });
}, },
/** /**
* The textual representation of the current value * The textual representation of the current value
* @readonly - use setValue() to mutate * Use setValue() to mutate
*/ */
arg: undefined, arg: undefined,
setArgument: function(arg) { setArgument: function(arg) {
@ -763,13 +814,27 @@ Assignment.prototype = {
//this._dispatchEvent('change', { assignment: this }); //this._dispatchEvent('change', { assignment: this });
}, },
getHint: function() { /**
if (this.conversion.status === Status.VALID && * Create a list of this hints associated with this parameter assignment
this.conversion.message === '') { */
return undefined; getHints: function() {
var hints = [];
if (this.conversion != null &&
(this.conversion.status !== Status.VALID ||
this.conversion.message)) {
hints.push(new ConversionHint(this.conversion, this.arg));
} }
return new ConversionHint(this.conversion, this.arg); var argProvided = this.arg != null && this.arg.text !== '';
var dataProvided = this.value !== undefined || argProvided;
if (this.param.defaultValue === undefined && !dataProvided) {
// If the there is no data provided, we have no start/end. Use -1
hints.push(new Hint(Status.INVALID,
'Argument for ' + param.name + ' is required'
-1, -1));
}
return hints;
}, },
/** /**