From 21dce2784e0b54fa9583f8a7537cc62d3a8dfb7a Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Wed, 15 Dec 2010 18:54:25 +0000 Subject: [PATCH] lots of minor bug fixes and tweaks to the command line --- plugins/cockpit/lib/cli.js | 77 ++++++++++-------- plugins/cockpit/lib/test/testCli.js | 10 ++- plugins/cockpit/lib/ui/cliView.js | 122 +++++++++++++++------------- plugins/pilot/lib/types/basic.js | 7 +- plugins/pilot/lib/types/settings.js | 10 ++- 5 files changed, 131 insertions(+), 95 deletions(-) diff --git a/plugins/cockpit/lib/cli.js b/plugins/cockpit/lib/cli.js index 780408c2..06e3642a 100644 --- a/plugins/cockpit/lib/cli.js +++ b/plugins/cockpit/lib/cli.js @@ -66,16 +66,17 @@ exports.startup = function(data, reason) { function Hint(status, message, start, end, predictions) { this.status = status; this.message = message; - this.predictions = predictions; if (typeof start === 'number') { this.start = start; this.end = end; + this.predictions = predictions; } else { var arg = start; this.start = arg.start; this.end = arg.end; + this.predictions = arg.predictions; } } Hint.prototype = { @@ -191,6 +192,13 @@ Argument.prototype = { var ev = { argument: this, oldText: this.text, text: text }; this.text = text; 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() */ 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 var message = '' + this.param.name + ': '; if (this.param.description) { @@ -345,12 +361,6 @@ Assignment.prototype = { message += '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); }, @@ -385,6 +395,13 @@ Assignment.prototype = { if (replacement != null) { this.setValue(replacement); } + }, + + /** + * Helper when we're rebuilding command lines. + */ + toString: function() { + return this.arg ? this.arg.toString() : ''; } }; exports.Assignment = Assignment; @@ -401,7 +418,7 @@ var commandParam = { /** * Provide some documentation for a command. */ - documentValid: function(command) { + getCustomHint: function(command, arg) { var docs = []; docs.push(' > '); docs.push(command.name); @@ -440,7 +457,7 @@ var commandParam = { docs.push(''); } - return docs.join(''); + return new Hint(Status.VALID, docs.join(''), arg); } }; @@ -716,11 +733,10 @@ oop.inherits(CliRequisition, Requisition); CliRequisition.prototype.toString = function() { // All the params var parts = Object.keys(this._assignments).map(function(name) { - var arg = this._assignments[name].arg; - return arg ? arg.priorSpace + arg.text : ''; + return this._assignments[name].toString(); }, this); // Prefix with the command - parts.unshift(this.commandAssignment.arg.text); + parts.unshift(this.commandAssignment.toString()); return parts.join(''); }; @@ -768,30 +784,33 @@ oop.inherits(CliRequisition, Requisition); * at the given position. */ CliRequisition.prototype.getAssignmentAt = function(position) { - var found; - var arg = this.commandAssignment.arg; - if (arg && arg.start <= position && arg.end >= position) { - found = this.commandAssignment; + if (arg && position <= arg.end) { + return this.commandAssignment; } - if (!found) { - Object.keys(this._assignments).forEach(function(name) { - var assignment = this._assignments[name]; - var arg = assignment.arg; - if (arg && arg.start <= position && arg.end >= position) { - found = assignment; - } - }, this); + var names = Object.keys(this._assignments); + for (var i = 0; i < names.length; i++) { + var assignment = this._assignments[names[i]]; + if (assignment.arg && position <= assignment.arg.end) { + return assignment; + } } - 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 " */ 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 IN_SIMPLE = 2; // The last character was part of a parameter var IN_SINGLE_Q = 3; // We're inside a single quote: ' @@ -916,12 +935,6 @@ oop.inherits(CliRequisition, Requisition); * typed at the command line. */ 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 arg; @@ -981,7 +994,7 @@ oop.inherits(CliRequisition, Requisition); // we determined that we had args that were all whitespace, but // probably given our tighter tokenize() this won't be an issue? 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))); return; } diff --git a/plugins/cockpit/lib/test/testCli.js b/plugins/cockpit/lib/test/testCli.js index 809df6b1..26caf643 100644 --- a/plugins/cockpit/lib/test/testCli.js +++ b/plugins/cockpit/lib/test/testCli.js @@ -58,7 +58,11 @@ exports.testTokenize = function() { var cli = new CliRequisition(); 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'); test.verifyEqual(1, args.length); @@ -211,7 +215,7 @@ exports.testCli = function() { test.verifyNull(cli.commandAssignment.value); update({ typed: ' ', cursor: { start: 1, end: 1 } }); - test.verifyEqual('1', statuses); + test.verifyEqual('0', statuses); test.verifyEqual(1, cli._hints.length); test.verifyEqual(Status.INCOMPLETE, display.status); test.verifyEqual(1, display.start); @@ -220,7 +224,7 @@ exports.testCli = function() { test.verifyNull(cli.commandAssignment.value); update({ typed: ' ', cursor: { start: 0, end: 0 } }); - test.verifyEqual('1', statuses); + test.verifyEqual('0', statuses); test.verifyEqual(1, cli._hints.length); test.verifyEqual(Status.INCOMPLETE, display.status); test.verifyEqual(1, display.start); diff --git a/plugins/cockpit/lib/ui/cliView.js b/plugins/cockpit/lib/ui/cliView.js index bd2c047f..5a9c1a3b 100644 --- a/plugins/cockpit/lib/ui/cliView.js +++ b/plugins/cockpit/lib/ui/cliView.js @@ -187,7 +187,6 @@ 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 @@ -197,7 +196,6 @@ CliView.prototype = { ev.keyCode === keyutil.KeyHelper.KEY.DOWN) { return true; } - this.isUpdating = false; return handled; }, @@ -205,7 +203,6 @@ CliView.prototype = { * The main keyboard processing loop */ onKeyUp: function(ev) { - this.isUpdating = true; var handled; /* var handled = keyboardManager.processKeyEvent(ev, this, { @@ -230,30 +227,28 @@ CliView.prototype = { } } + this.update(); + // Special actions which delegate to the assignment var current = this.cli.getAssignmentAt(this.element.selectionStart); if (current) { - this.isUpdating = false; - // TAB does a special complete thing if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) { current.complete(); + this.update(); } // UP/DOWN look for some history if (ev.keyCode === keyutil.KeyHelper.KEY.UP) { current.increment(); + this.update(); } if (ev.keyCode === keyutil.KeyHelper.KEY.DOWN) { current.decrement(); + this.update(); } - - this.isUpdating = true; } - this.update(); - - this.isUpdating = false; return handled; }, @@ -261,82 +256,93 @@ CliView.prototype = { * Actually parse the input and make sure we're all up to date */ update: function() { - this.cli.update({ + this.isUpdating = true; + var input = { typed: this.element.value, cursor: { start: this.element.selectionStart, 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.INCOMPLETE.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.INCOMPLETE.toString()); // dom.removeCssClass(completer, Status.INVALID.toString()); - // Create a marked up version of the input - var highlightedInput = '> '; + var completion = '> '; if (this.element.value.length > 0) { var scores = this.cli.getInputStatusMarkup(); - // Create mark-up - var i = 0; - var lastStatus = -1; - while (true) { - if (lastStatus !== scores[i]) { - highlightedInput += ''; - lastStatus = scores[i]; - } - highlightedInput += this.element.value[i]; - i++; - if (i === this.element.value.length) { - highlightedInput += ''; - break; - } - if (lastStatus !== scores[i]) { - highlightedInput += ''; - } - } + completion += this.markupStatusScore(scores); } // Display the "-> prediction" at the end of the completer - var display = this.cli.getAssignmentAt(this.element.selectionStart).getHint(); - var message = ''; - if (this.element.value.length !== 0) { - message += display.message; - if (display.predictions && display.predictions.length > 0) { - message += ': [ '; - display.predictions.forEach(function(prediction) { - if (prediction.name) { - message += prediction.name + ' | '; - } - else { - message += prediction + ' | '; - } - }, this); - message = message.replace(/\| $/, ']'); + if (this.element.value.length > 0 && + display.predictions && display.predictions.length > 0) { + var tab = display.predictions[0]; + completion += '  ⇥ ' + (tab.name ? tab.name : tab); + } + this.completer.innerHTML = completion; + this.completer.classList.add(this.cli.getWorstHint().status.toString()); + // dom.addCssClass(input, this.cli.getWorstHint().status.toString()); - var onTab = display.predictions[0]; - onTab = onTab.name ? onTab.name : onTab; - this.completer.innerHTML = highlightedInput + '  ⇥ ' + onTab; - } - else { - this.completer.innerHTML = highlightedInput; + // 2. Update the hint element + var hint = ''; + if (this.element.value.length !== 0) { + hint += display.message; + if (display.predictions && display.predictions.length > 0) { + hint += ': [ '; + display.predictions.forEach(function(prediction) { + hint += (prediction.name ? prediction.name : prediction); + hint += ' | '; + }, this); + hint = hint.replace(/\| $/, ']'); } } - this.hinter.innerHTML = message; - if (message.length === 0) { + this.hinter.innerHTML = hint; + if (hint.length === 0) { this.hinter.classList.add('cptNoHints'); } else { this.hinter.classList.remove('cptNoHints'); } - this.completer.classList.add(this.cli.getWorstHint().status.toString()); - // dom.addCssClass(input, this.cli.getWorstHint().status.toString()); + this.isUpdating = false; + }, + + /** + * 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 += ''; + lastStatus = scores[i]; + } + completion += this.element.value[i]; + i++; + if (i === this.element.value.length) { + completion += ''; + break; + } + if (lastStatus !== scores[i]) { + completion += ''; + } + } + + return completion; }, /** diff --git a/plugins/pilot/lib/types/basic.js b/plugins/pilot/lib/types/basic.js index b119e879..9d775f6a 100644 --- a/plugins/pilot/lib/types/basic.js +++ b/plugins/pilot/lib/types/basic.js @@ -158,8 +158,13 @@ SelectionType.prototype.parse = function(str) { return new Conversion(matchedValue); } else { + // This is something of a hack. settings + if (this.noMatch) { + this.noMatch(); + } + if (completions.length > 0) { - var msg = 'Several possibilities' + + var msg = 'Possibilities' + (str.length === 0 ? '' : ' for \'' + str + '\''); return new Conversion(null, Status.INCOMPLETE, msg, completions); } diff --git a/plugins/pilot/lib/types/settings.js b/plugins/pilot/lib/types/settings.js index 4b64d069..065bdca5 100644 --- a/plugins/pilot/lib/types/settings.js +++ b/plugins/pilot/lib/types/settings.js @@ -66,6 +66,9 @@ var setting = new SelectionType({ fromString: function(str) { lastSetting = settings.getSetting(str); return lastSetting; + }, + noMatch: function() { + lastSetting = null; } }); @@ -76,7 +79,12 @@ var setting = new SelectionType({ var settingValue = new DeferredType({ name: 'settingValue', defer: function() { - return lastSetting.type; + if (lastSetting) { + return lastSetting.type; + } + else { + return types.getType('text'); + } } });