lots of minor bug fixes and tweaks to the command line

This commit is contained in:
Joe Walker 2010-12-15 18:54:25 +00:00
commit 21dce2784e
5 changed files with 131 additions and 95 deletions

View file

@ -66,16 +66,17 @@ exports.startup = function(data, reason) {
function Hint(status, message, start, end, predictions) { function Hint(status, message, start, end, predictions) {
this.status = status; this.status = status;
this.message = message; this.message = message;
this.predictions = predictions;
if (typeof start === 'number') { if (typeof start === 'number') {
this.start = start; this.start = start;
this.end = end; this.end = end;
this.predictions = predictions;
} }
else { else {
var arg = start; var arg = start;
this.start = arg.start; this.start = arg.start;
this.end = arg.end; this.end = arg.end;
this.predictions = arg.predictions;
} }
} }
Hint.prototype = { Hint.prototype = {
@ -191,6 +192,13 @@ Argument.prototype = {
var ev = { argument: this, oldText: this.text, text: text }; var ev = { argument: this, oldText: this.text, text: text };
this.text = text; this.text = text;
this.emitter._dispatchEvent('argumentChange', ev); this.emitter._dispatchEvent('argumentChange', ev);
},
/**
* Helper when we're putting arguments back together
*/
toString: function() {
return this.priorSpace + this.text;
} }
}; };
/** /**
@ -309,6 +317,14 @@ Assignment.prototype = {
* make sense with more experience to alter this to function to be getHint() * make sense with more experience to alter this to function to be getHint()
*/ */
getHint: function() { getHint: function() {
// Allow the parameter to provide documentation
if (this.param.getCustomHint && this.value && this.arg) {
var hint = this.param.getCustomHint(this.value, this.arg);
if (hint) {
return hint;
}
}
// If there is no argument, use the cursor position // If there is no argument, use the cursor position
var message = '<strong>' + this.param.name + '</strong>: '; var message = '<strong>' + this.param.name + '</strong>: ';
if (this.param.description) { if (this.param.description) {
@ -345,12 +361,6 @@ Assignment.prototype = {
message += '<strong>Required<\strong>'; message += '<strong>Required<\strong>';
} }
// 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); return new Hint(status, message, start, end, predictions);
}, },
@ -385,6 +395,13 @@ Assignment.prototype = {
if (replacement != null) { if (replacement != null) {
this.setValue(replacement); this.setValue(replacement);
} }
},
/**
* Helper when we're rebuilding command lines.
*/
toString: function() {
return this.arg ? this.arg.toString() : '';
} }
}; };
exports.Assignment = Assignment; exports.Assignment = Assignment;
@ -401,7 +418,7 @@ var commandParam = {
/** /**
* Provide some documentation for a command. * Provide some documentation for a command.
*/ */
documentValid: function(command) { getCustomHint: function(command, arg) {
var docs = []; var docs = [];
docs.push('<strong><tt> &gt; '); docs.push('<strong><tt> &gt; ');
docs.push(command.name); docs.push(command.name);
@ -440,7 +457,7 @@ var commandParam = {
docs.push('</ul>'); docs.push('</ul>');
} }
return docs.join(''); return new Hint(Status.VALID, docs.join(''), arg);
} }
}; };
@ -716,11 +733,10 @@ oop.inherits(CliRequisition, Requisition);
CliRequisition.prototype.toString = function() { CliRequisition.prototype.toString = function() {
// All the params // All the params
var parts = Object.keys(this._assignments).map(function(name) { var parts = Object.keys(this._assignments).map(function(name) {
var arg = this._assignments[name].arg; return this._assignments[name].toString();
return arg ? arg.priorSpace + arg.text : '';
}, this); }, this);
// Prefix with the command // Prefix with the command
parts.unshift(this.commandAssignment.arg.text); parts.unshift(this.commandAssignment.toString());
return parts.join(''); return parts.join('');
}; };
@ -768,30 +784,33 @@ oop.inherits(CliRequisition, Requisition);
* at the given position. * at the given position.
*/ */
CliRequisition.prototype.getAssignmentAt = function(position) { CliRequisition.prototype.getAssignmentAt = function(position) {
var found;
var arg = this.commandAssignment.arg; var arg = this.commandAssignment.arg;
if (arg && arg.start <= position && arg.end >= position) { if (arg && position <= arg.end) {
found = this.commandAssignment; return this.commandAssignment;
} }
if (!found) { var names = Object.keys(this._assignments);
Object.keys(this._assignments).forEach(function(name) { for (var i = 0; i < names.length; i++) {
var assignment = this._assignments[name]; var assignment = this._assignments[names[i]];
var arg = assignment.arg; if (assignment.arg && position <= assignment.arg.end) {
if (arg && arg.start <= position && arg.end >= position) { return assignment;
found = assignment; }
}
}, this);
} }
return found; // We can only have got here if
throw new Error('position (' + position +
') is off end of requisition (' + this.toString() + ')');
}; };
/** /**
* Split up the input taking into account ' and " * Split up the input taking into account ' and "
*/ */
CliRequisition.prototype._tokenize = function(typed) { CliRequisition.prototype._tokenize = function(typed) {
// For blank input, place a dummy empty argument into the list
if (typed == null || typed.length === 0) {
return [ new Argument(this, '', 0, 0, '') ];
}
var OUTSIDE = 1; // The last character was whitespace var OUTSIDE = 1; // The last character was whitespace
var IN_SIMPLE = 2; // The last character was part of a parameter var IN_SIMPLE = 2; // The last character was part of a parameter
var IN_SINGLE_Q = 3; // We're inside a single quote: ' var IN_SINGLE_Q = 3; // We're inside a single quote: '
@ -916,12 +935,6 @@ oop.inherits(CliRequisition, Requisition);
* typed at the command line. * typed at the command line.
*/ */
CliRequisition.prototype._split = function(args) { CliRequisition.prototype._split = function(args) {
// Place a dummy empty argument into the list
// TODO: should this go into _tokenize?
if (args.length === 0) {
args.push(new Argument(this, '', 0, 0, ''));
}
var argsUsed = 1; var argsUsed = 1;
var arg; var arg;
@ -981,7 +994,7 @@ oop.inherits(CliRequisition, Requisition);
// we determined that we had args that were all whitespace, but // we determined that we had args that were all whitespace, but
// probably given our tighter tokenize() this won't be an issue? // probably given our tighter tokenize() this won't be an issue?
this._hints.push(new Hint(Status.INVALID, this._hints.push(new Hint(Status.INVALID,
this.command.name + ' does not take any parameters', this.commandAssignment.value.name + ' does not take any parameters',
Argument.merge(args))); Argument.merge(args)));
return; return;
} }

View file

@ -58,7 +58,11 @@ exports.testTokenize = function() {
var cli = new CliRequisition(); var cli = new CliRequisition();
args = cli._tokenize(''); args = cli._tokenize('');
test.verifyEqual(0, args.length); test.verifyEqual(1, args.length);
test.verifyEqual('', args[0].text);
test.verifyEqual(0, args[0].start);
test.verifyEqual(0, args[0].end);
test.verifyEqual('', args[0].priorSpace);
args = cli._tokenize('s'); args = cli._tokenize('s');
test.verifyEqual(1, args.length); test.verifyEqual(1, args.length);
@ -211,7 +215,7 @@ exports.testCli = function() {
test.verifyNull(cli.commandAssignment.value); test.verifyNull(cli.commandAssignment.value);
update({ typed: ' ', cursor: { start: 1, end: 1 } }); update({ typed: ' ', cursor: { start: 1, end: 1 } });
test.verifyEqual('1', statuses); test.verifyEqual('0', statuses);
test.verifyEqual(1, cli._hints.length); test.verifyEqual(1, cli._hints.length);
test.verifyEqual(Status.INCOMPLETE, display.status); test.verifyEqual(Status.INCOMPLETE, display.status);
test.verifyEqual(1, display.start); test.verifyEqual(1, display.start);
@ -220,7 +224,7 @@ exports.testCli = function() {
test.verifyNull(cli.commandAssignment.value); test.verifyNull(cli.commandAssignment.value);
update({ typed: ' ', cursor: { start: 0, end: 0 } }); update({ typed: ' ', cursor: { start: 0, end: 0 } });
test.verifyEqual('1', statuses); test.verifyEqual('0', statuses);
test.verifyEqual(1, cli._hints.length); test.verifyEqual(1, cli._hints.length);
test.verifyEqual(Status.INCOMPLETE, display.status); test.verifyEqual(Status.INCOMPLETE, display.status);
test.verifyEqual(1, display.start); test.verifyEqual(1, display.start);

View file

@ -187,7 +187,6 @@ CliView.prototype = {
* Ensure that TAB isn't handled by the browser * Ensure that TAB isn't handled by the browser
*/ */
onKeyDown: function(ev) { onKeyDown: function(ev) {
this.isUpdating = true;
var handled; var handled;
// var handled = keyboardManager.processKeyEvent(ev, this, { // var handled = keyboardManager.processKeyEvent(ev, this, {
// isCommandLine: true, isKeyUp: false // isCommandLine: true, isKeyUp: false
@ -197,7 +196,6 @@ CliView.prototype = {
ev.keyCode === keyutil.KeyHelper.KEY.DOWN) { ev.keyCode === keyutil.KeyHelper.KEY.DOWN) {
return true; return true;
} }
this.isUpdating = false;
return handled; return handled;
}, },
@ -205,7 +203,6 @@ CliView.prototype = {
* The main keyboard processing loop * The main keyboard processing loop
*/ */
onKeyUp: function(ev) { onKeyUp: function(ev) {
this.isUpdating = true;
var handled; var handled;
/* /*
var handled = keyboardManager.processKeyEvent(ev, this, { var handled = keyboardManager.processKeyEvent(ev, this, {
@ -230,30 +227,28 @@ CliView.prototype = {
} }
} }
this.update();
// Special actions which delegate to the assignment // Special actions which delegate to the assignment
var current = this.cli.getAssignmentAt(this.element.selectionStart); var current = this.cli.getAssignmentAt(this.element.selectionStart);
if (current) { if (current) {
this.isUpdating = false;
// TAB does a special complete thing // TAB does a special complete thing
if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) { if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) {
current.complete(); current.complete();
this.update();
} }
// UP/DOWN look for some history // UP/DOWN look for some history
if (ev.keyCode === keyutil.KeyHelper.KEY.UP) { if (ev.keyCode === keyutil.KeyHelper.KEY.UP) {
current.increment(); current.increment();
this.update();
} }
if (ev.keyCode === keyutil.KeyHelper.KEY.DOWN) { if (ev.keyCode === keyutil.KeyHelper.KEY.DOWN) {
current.decrement(); current.decrement();
this.update();
} }
this.isUpdating = true;
} }
this.update();
this.isUpdating = false;
return handled; return handled;
}, },
@ -261,82 +256,93 @@ CliView.prototype = {
* Actually parse the input and make sure we're all up to date * Actually parse the input and make sure we're all up to date
*/ */
update: function() { update: function() {
this.cli.update({ this.isUpdating = true;
var input = {
typed: this.element.value, typed: this.element.value,
cursor: { cursor: {
start: this.element.selectionStart, start: this.element.selectionStart,
end: this.element.selectionEnd end: this.element.selectionEnd
} }
}); };
this.cli.update(input);
// TODO: borked implementation? This is modern browser only. Fix var display = this.cli.getAssignmentAt(input.cursor.start).getHint();
// 1. Update the completer with prompt/error marker/TAB info
this.completer.classList.remove(Status.VALID.toString()); this.completer.classList.remove(Status.VALID.toString());
this.completer.classList.remove(Status.INCOMPLETE.toString()); this.completer.classList.remove(Status.INCOMPLETE.toString());
this.completer.classList.remove(Status.INVALID.toString()); this.completer.classList.remove(Status.INVALID.toString());
// TODO: borked implementation? This is modern browser only. Fix
// dom.removeCssClass(completer, Status.VALID.toString()); // dom.removeCssClass(completer, Status.VALID.toString());
// dom.removeCssClass(completer, Status.INCOMPLETE.toString()); // dom.removeCssClass(completer, Status.INCOMPLETE.toString());
// dom.removeCssClass(completer, Status.INVALID.toString()); // dom.removeCssClass(completer, Status.INVALID.toString());
// Create a marked up version of the input var completion = '<span class="cptPrompt">&gt;</span> ';
var highlightedInput = '<span class="cptPrompt">&gt;</span> ';
if (this.element.value.length > 0) { if (this.element.value.length > 0) {
var scores = this.cli.getInputStatusMarkup(); var scores = this.cli.getInputStatusMarkup();
// Create mark-up completion += this.markupStatusScore(scores);
var i = 0;
var lastStatus = -1;
while (true) {
if (lastStatus !== scores[i]) {
highlightedInput += '<span class=' + scores[i].toString() + '>';
lastStatus = scores[i];
}
highlightedInput += this.element.value[i];
i++;
if (i === this.element.value.length) {
highlightedInput += '</span>';
break;
}
if (lastStatus !== scores[i]) {
highlightedInput += '</span>';
}
}
} }
// Display the "-> prediction" at the end of the completer // Display the "-> prediction" at the end of the completer
var display = this.cli.getAssignmentAt(this.element.selectionStart).getHint(); if (this.element.value.length > 0 &&
var message = ''; display.predictions && display.predictions.length > 0) {
if (this.element.value.length !== 0) { var tab = display.predictions[0];
message += display.message; completion += ' &nbsp;&#x21E5; ' + (tab.name ? tab.name : tab);
if (display.predictions && display.predictions.length > 0) { }
message += ': [ '; this.completer.innerHTML = completion;
display.predictions.forEach(function(prediction) { this.completer.classList.add(this.cli.getWorstHint().status.toString());
if (prediction.name) { // dom.addCssClass(input, this.cli.getWorstHint().status.toString());
message += prediction.name + ' | ';
}
else {
message += prediction + ' | ';
}
}, this);
message = message.replace(/\| $/, ']');
var onTab = display.predictions[0]; // 2. Update the hint element
onTab = onTab.name ? onTab.name : onTab; var hint = '';
this.completer.innerHTML = highlightedInput + ' &nbsp;&#x21E5; ' + onTab; if (this.element.value.length !== 0) {
} hint += display.message;
else { if (display.predictions && display.predictions.length > 0) {
this.completer.innerHTML = highlightedInput; hint += ': [ ';
display.predictions.forEach(function(prediction) {
hint += (prediction.name ? prediction.name : prediction);
hint += ' | ';
}, this);
hint = hint.replace(/\| $/, ']');
} }
} }
this.hinter.innerHTML = message; this.hinter.innerHTML = hint;
if (message.length === 0) { if (hint.length === 0) {
this.hinter.classList.add('cptNoHints'); this.hinter.classList.add('cptNoHints');
} }
else { else {
this.hinter.classList.remove('cptNoHints'); this.hinter.classList.remove('cptNoHints');
} }
this.completer.classList.add(this.cli.getWorstHint().status.toString()); this.isUpdating = false;
// dom.addCssClass(input, this.cli.getWorstHint().status.toString()); },
/**
* Markup an array of Status values with spans
*/
markupStatusScore: function(scores) {
var completion = '';
// Create mark-up
var i = 0;
var lastStatus = -1;
while (true) {
if (lastStatus !== scores[i]) {
completion += '<span class=' + scores[i].toString() + '>';
lastStatus = scores[i];
}
completion += this.element.value[i];
i++;
if (i === this.element.value.length) {
completion += '</span>';
break;
}
if (lastStatus !== scores[i]) {
completion += '</span>';
}
}
return completion;
}, },
/** /**

View file

@ -158,8 +158,13 @@ SelectionType.prototype.parse = function(str) {
return new Conversion(matchedValue); return new Conversion(matchedValue);
} }
else { else {
// This is something of a hack. settings
if (this.noMatch) {
this.noMatch();
}
if (completions.length > 0) { if (completions.length > 0) {
var msg = 'Several possibilities' + var msg = 'Possibilities' +
(str.length === 0 ? '' : ' for \'' + str + '\''); (str.length === 0 ? '' : ' for \'' + str + '\'');
return new Conversion(null, Status.INCOMPLETE, msg, completions); return new Conversion(null, Status.INCOMPLETE, msg, completions);
} }

View file

@ -66,6 +66,9 @@ var setting = new SelectionType({
fromString: function(str) { fromString: function(str) {
lastSetting = settings.getSetting(str); lastSetting = settings.getSetting(str);
return lastSetting; return lastSetting;
},
noMatch: function() {
lastSetting = null;
} }
}); });
@ -76,7 +79,12 @@ var setting = new SelectionType({
var settingValue = new DeferredType({ var settingValue = new DeferredType({
name: 'settingValue', name: 'settingValue',
defer: function() { defer: function() {
return lastSetting.type; if (lastSetting) {
return lastSetting.type;
}
else {
return types.getType('text');
}
} }
}); });