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:
parent
2c4bfd4310
commit
0930c37675
1 changed files with 117 additions and 52 deletions
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue