From 9e2e7336bdbaf5ad396e9b1af98ebfd09b52be2e Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Mon, 13 Dec 2010 13:55:38 +0000 Subject: [PATCH] making command documentation more compact and tweaking how hints work --- plugins/cockpit/lib/cli.js | 150 +++++++++++---------- plugins/cockpit/lib/ui/plain.css | 70 +++++----- plugins/cockpit/lib/ui/plain.js | 181 +++++++++----------------- plugins/cockpit/lib/ui/requestView.js | 4 +- plugins/cockpit/lib/ui/settings.js | 33 ++++- plugins/pilot/lib/index.js | 2 + 6 files changed, 219 insertions(+), 221 deletions(-) diff --git a/plugins/cockpit/lib/cli.js b/plugins/cockpit/lib/cli.js index 01d81a34..169a5983 100644 --- a/plugins/cockpit/lib/cli.js +++ b/plugins/cockpit/lib/cli.js @@ -391,7 +391,7 @@ Requisition.prototype = { * Where parameter name == assignment names - they are the same. */ getParameterNames: function() { - return Object.keys(this._assignments); + return Object.keys(this._assignments); }, /** @@ -495,7 +495,7 @@ oop.inherits(CliRequisition, Requisition); CliRequisition.prototype.update = function(input) { this.input = input; // TODO: We only store this so getHints can work. Find a better way. - this.localHints = []; + this.hints = []; if (util.none(input.typed)) { this.setCommand(null); @@ -508,44 +508,55 @@ oop.inherits(CliRequisition, Requisition); // a complete novice a 'type help' message is very annoying, so we // need to find a way to only display this message once, or for // until the user click a 'close' button or similar - this.localHints.push(new Hint(Status.INCOMPLETE, '', 0, 0)); + this.hints.push(new Hint(Status.INCOMPLETE, '', 0, 0)); this.setCommand(null); + this._annotateHints(); return; } var conversion = _split(args); if (!conversion.value) { // No command found - bail helpfully. - this.localHints.push(new ConversionHint(conversion, conversion.arg)); + this.hints.push(new ConversionHint(conversion, conversion.arg)); this.setCommand(null); - } - else { - var message = documentCommand(conversion.value); - this.localHints.push(new Hint(Status.VALID, message, conversion.arg)); - - this.setCommand(conversion.value); - this._assign(args); + this._annotateHints(); + return; } - return; - }; + var message = documentCommand(conversion.value); + this.hints.push(new Hint(Status.VALID, message, conversion.arg)); - CliRequisition.prototype.getHints = function() { - var hints = this.localHints.slice(0); + this.setCommand(conversion.value); + this._assign(args); + // Add the hints from the assignments to those already collected Object.keys(this._assignments).map(function(name) { // Only use assignments with an argument var assignment = this._assignments[name]; if (assignment.arg) { - hints.push.apply(hints, assignment.getHints()); + this.hints.push.apply(this.hints, assignment.getHints()); } }, this); + this._annotateHints(); + return; + }; + + /** + * Marks up hints in a number of ways: + * - Makes INCOMPLETE hints that are not near the cursor INVALID since + * they can't be completed by typing + * - Finds the most severe hint, and annotates the array with it + * - Finds the hint to display, and also annotates the array with it + * TODO: I'm wondering if array annotation is evil and we should replace + * this with an object. Need to find out more. + */ + CliRequisition.prototype._annotateHints = function() { // 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 c = this.input.cursor; - hints.forEach(function(hint) { + this.hints.forEach(function(hint) { var startInHint = c.start >= hint.start && c.start <= hint.end; var endInHint = c.end >= hint.start && c.end <= hint.end; var inHint = startInHint || endInHint; @@ -554,7 +565,35 @@ oop.inherits(CliRequisition, Requisition); } }, this); - return Hint.sort(hints, this.input.cursor.start); + // Work out what the worst hint is (irrespective of the cursor). We + // return the hints in order of display importance - i.e. an INCOMPLETE + // hint under the cursor should be displayed before an INVALID hint + // somewhere else. That's good for displaying hints, but not good for + // deciding if we're good to go. + if (this.hints.length > 1) { + Hint.sort(this.hints); + this.hints.worst = this.hints[0]; + } + else if (this.hints.length > 0) { + this.hints.worst = this.hints[0]; + } + + Hint.sort(this.hints, this.input.cursor.start); + this.hints.display = this.hints[0]; + + return this.hints; + }; + + /** + * Accessor for the hints array. + * While we could just use the hints property, using getHints() is + * preferred for symmetry with Requisition where it needs a function due to + * lack of an atomic update system. + * TODO: When we use this properly (i.e. with a fancy UI) then + * CliRequisition will also not have an atomic update system. Hmmmmm + */ + CliRequisition.prototype.getHints = function() { + return this.hints; }; /** @@ -579,7 +618,7 @@ oop.inherits(CliRequisition, Requisition); // TODO: previously we were doing some extra work to avoid this if // we determined that we had args that were all whitespace, but // probably given our tighter tokenize() this won't be an issue? - this.localHints.push(new Hint(Status.INVALID, + this.hints.push(new Hint(Status.INVALID, this.command.name + ' does not take any parameters', Argument.merge(args))); return; @@ -621,7 +660,7 @@ oop.inherits(CliRequisition, Requisition); else { if (i + 1 < args.length) { // Missing value portion of this named param - this.localHints.push(new Hint(Status.INCOMPLETE, + this.hints.push(new Hint(Status.INCOMPLETE, 'Missing value for: ' + namedArgText, args[i])); } @@ -653,7 +692,7 @@ oop.inherits(CliRequisition, Requisition); if (args.length > 0) { var remaining = Argument.merge(args); - this.localHints.push(new Hint(Status.INVALID, + this.hints.push(new Hint(Status.INVALID, 'Input \'' + remaining.text + '\' makes no sense.', remaining)); } @@ -842,62 +881,41 @@ exports._split = _split; */ function documentCommand(command) { var docs = []; - docs.push('

' + command.name + '

'); - docs.push('

Summary

'); - docs.push('

' + command.description + '

'); - - if (command.manual) { - docs.push('

Description

'); - docs.push('

' + command.description + '

'); - } - + docs.push(' > '); + docs.push(command.name); if (command.params && command.params.length > 0) { - docs.push('

Synopsis

'); - docs.push('
');
-        docs.push(command.name);
-        var optionalParamCount = 0;
         command.params.forEach(function(param) {
             if (param.defaultValue === undefined) {
-                docs.push(' ');
-                docs.push(param.name);
-                docs.push('');
-            }
-            else if (param.defaultValue === null) {
-                docs.push(' [');
-                docs.push(param.name);
-                docs.push(']');
+                docs.push(' [' + param.name + ']');
             }
             else {
-                optionalParamCount++;
+                docs.push(' [' + param.name + ']');
             }
         }, this);
-        if (optionalParamCount > 3) {
-            docs.push(' [options]');
-        } else if (optionalParamCount > 0) {
-            command.params.forEach(function(param) {
-                if (param.defaultValue) {
-                    docs.push(' [--');
-                    docs.push(param.name);
-                    if (param.type.name === 'boolean') {
-                        docs.push('');
-                    }
-                    else {
-                        docs.push(' ' + param.type.name);
-                    }
-                    docs.push(']');
-                }
-            }, this);
-        }
-        docs.push('
'); + } + docs.push('

'); - docs.push('

Parameters

'); + docs.push(command.description ? command.description : '(No description)'); + docs.push('
'); + + if (command.params && command.params.length > 0) { + docs.push(''); } return docs.join(''); diff --git a/plugins/cockpit/lib/ui/plain.css b/plugins/cockpit/lib/ui/plain.css index bd1c000f..dfb47463 100644 --- a/plugins/cockpit/lib/ui/plain.css +++ b/plugins/cockpit/lib/ui/plain.css @@ -1,10 +1,37 @@ -/* Command line completion */ +#cockpit { + background: transparent; + border: none; outline: none; + font-family: consolas, courier, monospace; + font-size: 120%; +} + +.cptOutput { + border: 1px solid #AAA; + -moz-border-radius-topleft: 10px; + -moz-border-radius-topright: 10px; + border-top-left-radius: 4px; border-top-right-radius: 4px; + padding: 10px; + margin: 0 15px; + background: #DDD; color: #000; + overflow: hidden; + position: absolute; + z-index: 999; + font-family: consolas, courier, monospace; + display: none; +} + +#cockpit:focus ~ .cptOutput { + display: block; +} + .cptCompletion { color: #666; padding: 0 0 5px 2px; position: absolute; z-index: -1000; + font-family: consolas, courier, monospace; + font-size: 120%; } .cptHints { @@ -17,6 +44,17 @@ border-top-left-radius: 10px; border-top-right-radius: 10px; z-index: 1000; padding: 8px; + display: none; +} + +.cptHints ul { margin: 0; padding: 0 15px; } + +#cockpit:focus ~ .cptHints { + display: block; +} + +#cockpit:focus ~ .cptHints.cptNoHints { + display: none; } .cptCompletion.VALID { background: #FFF; } @@ -24,34 +62,8 @@ .cptCompletion.INVALID { background: #DDD; } .cptCompletion span { color: #FFF; } -.cptCompletion span.INCOMPLETE { text-shadow: 0px 0px 2px #F80; } -.cptCompletion span.INVALID { text-shadow: 0px 0px 5px #F00; } - - -#cockpit, .cptCompletion { - font-family: consolas, courier, monospace; - font-size: 120%; -} - -#cockpit { - background: transparent; - border: none; outline: none; -} - -.cptOutput { - border: 1px solid #AAA; - -moz-border-radius-topleft: 10px; - -moz-border-radius-topright: 10px; - border-top-left-radius: 4px; border-top-right-radius: 4px; - padding: 10px; - margin: 0 15px; - background: #DDD; color: #000; - font-family: consolas, courier, monospace; - font-size: 90%; - overflow: hidden; - position: absolute; - z-index: 999; -} +.cptCompletion span.INCOMPLETE { color: #DDD; border-bottom: 2px dotted #F80; } +.cptCompletion span.INVALID { color: #DDD; border-bottom: 2px dotted #F00; } diff --git a/plugins/cockpit/lib/ui/plain.js b/plugins/cockpit/lib/ui/plain.js index cb0ea8ac..fc4805d6 100644 --- a/plugins/cockpit/lib/ui/plain.js +++ b/plugins/cockpit/lib/ui/plain.js @@ -81,12 +81,11 @@ function CliView(data) { this.cli = new CliRequisition(); this.settings = data.env.settings; - this.showHint = this.settings.getSetting('showHint'); + this.hintDirection = this.settings.getSetting('hintDirection'); + this.outputDirection = this.settings.getSetting('outputDirection'); this.outputHeight = this.settings.getSetting('outputHeight'); this.hints = []; - this.shownHint; - this.worstHint; this.createElements(); } @@ -95,29 +94,35 @@ CliView.prototype = { * Create divs for completion, hints and output */ createElements: function() { + var input = this.element; + this.completer = this.doc.createElement('div'); this.completer.className = 'cptCompletion VALID'; - this.element.parentNode.insertBefore(this.completer, this.element); + input.parentNode.insertBefore(this.completer, input.nextSibling); this.hinter = this.doc.createElement('div'); this.hinter.className = 'cptHints'; - this.element.parentNode.insertBefore(this.hinter, this.element); + input.parentNode.insertBefore(this.hinter, input.nextSibling); this.output = this.doc.createElement('div'); this.output.className = 'cptOutput'; - this.element.parentNode.insertBefore(this.output, this.element); + input.parentNode.insertBefore(this.output, input.nextSibling); this.win.addEventListener('resize', this.resizer.bind(this), false); + this.hintDirection.addEventListener('change', this.resizer.bind(this)); + this.outputDirection.addEventListener('change', this.resizer.bind(this)); this.resizer(); - canon.addEventListener('output', this.showOutput.bind(this)); + canon.addEventListener('output', function(ev) { + new RequestView(ev.request, this); + }.bind(this)); - this.showHint.addEventListener('change', this.hintShower.bind(this)); - this.hintShower(); - - keyutil.addKeyDownListener(this.element, this.onKeyDown.bind(this)); - this.element.addEventListener('mouseup', this.onMouseUp.bind(this), false); - this.element.addEventListener('keyup', this.onKeyUp.bind(this), true); + keyutil.addKeyDownListener(input, this.onKeyDown.bind(this)); + input.addEventListener('keyup', this.onKeyUp.bind(this), true); + // cursor position affects hint severity. TODO: shortcuts for speed + input.addEventListener('mouseup', function(ev) { + this.update(); + }.bind(this), false); }, /** @@ -125,84 +130,34 @@ CliView.prototype = { * with the input box. */ resizer: function() { - var top, height, left, width; + var rect = this.element.getClientRects()[0]; - if (this.element.getClientRects) { - var rect = this.element.getClientRects()[0]; - top = rect.top; - height = rect.height; - left = rect.left; - width = rect.width; + this.completer.style.top = rect.top + 'px'; + this.completer.style.height = rect.height + 'px'; + this.completer.style.left = rect.left + 'px'; + this.completer.style.width = rect.width + 'px'; + + if (this.hintDirection.get() === 'below') { + this.hinter.style.top = rect.bottom + 'px'; + this.hinter.style.bottom = 'auto'; } else { - var style = this.win.getComputedStyle(this.element, null); - top = parseInt(style.getPropertyValue('top'), 10); - height = parseInt(style.getPropertyValue('height'), 10); - left = parseInt(style.getPropertyValue('left'), 10); - width = parseInt(style.getPropertyValue('width'), 10); + this.hinter.style.top = 'auto'; + this.hinter.style.bottom = (this.win.innerHeight - rect.top) + 'px'; } + this.hinter.style.left = (rect.left + 30) + 'px'; + this.hinter.style.maxWidth = (rect.width - 90) + 'px'; - this.completer.style.top = top + 'px'; - this.completer.style.height = height + 'px'; - this.completer.style.left = left + 'px'; - this.completer.style.width = width + 'px'; - - this.hinter.style.bottom = (this.win.innerHeight - top) + 'px'; - this.hinter.style.left = (left + 30) + 'px'; - - this.output.style.bottom = (this.win.innerHeight - top) + 'px'; - this.output.style.left = left + 'px'; - this.output.style.width = (width - 60) + 'px'; - }, - - /** - * Update the display of executed commands - */ - showOutput: function(ev) { - var requestOutput = new RequestView(ev.request, this); - - /* - // TODO: be less brutal in how we update this - this.output.innerHTML = ''; - - new RequestView(ev.request, this); - ev.requests.forEach(function(request) { - // this.duration.innerHTML = this.request.duration ? - // 'completed in ' + (this.request.duration / 1000) + ' sec ' : - // ''; - // this.typed.innerHTML = this.request.typed; - - request.outputs.forEach(function(output) { - var node; - if (typeof output == 'string') { - node = document.createElement('p'); - node.innerHTML = output; - } else { - node = output; - } - this.output.appendChild(node); - }, this); - - // this.cliView.scrollToBottom(); - // util.setClass(this.output, 'cmd_error', this.request.error); - // this.throb.style.display = this.request.completed ? 'none' : 'block'; - }, this); - */ - }, - - /** - * Show/hide the hint line. - * It's not clear that this is actually useful, however it does help to - * highlight some features for right now. - * TODO: remove this? - */ - hintShower: function() { - if (this.showHint.get()) { - this.hinter.style.display = 'block'; + if (this.outputDirection.get() === 'below') { + this.output.style.top = rect.bottom + 'px'; + this.output.style.bottom = 'auto'; } else { - this.hinter.style.display = 'none'; + this.output.style.top = 'auto'; + this.output.style.bottom = (this.win.innerHeight - rect.top) + 'px'; } + this.output.style.left = rect.left + 'px'; + this.output.style.width = (rect.width - 60) + 'px'; }, /** @@ -231,21 +186,17 @@ CliView.prototype = { */ if (ev.keyCode === keyutil.KeyHelper.KEY.RETURN) { - if (this.worstHint && this.worstHint.status !== Status.VALID) { - this.element.selectionStart = this.worstHint.start; - this.element.selectionEnd = this.worstHint.end; - } - else { + if (this.hints.worst || this.hints.worst.status === Status.VALID) { this.cli.exec(); this.element.value = ''; } } - if (ev.keyCode === keyutil.KeyHelper.KEY.TAB && this.shownHint && - this.shownHint.predictions && this.shownHint.predictions.length > 0) { - var prefix = this.element.value.substring(0, this.shownHint.start); - var suffix = this.element.value.substring(this.shownHint.end); - var insert = this.shownHint.predictions[0]; + 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. @@ -256,15 +207,14 @@ CliView.prototype = { this.update(); - return handled; - }, + 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; + } + } - /** - * Cause an update if the cursor changes position due to a mouse click - * TODO: there are probably some performance wins here. - */ - onMouseUp: function(ev) { - this.update(); + return handled; }, /** @@ -289,15 +239,6 @@ CliView.prototype = { this.hints = this.cli.getHints(); - // Those hints came in order of display importance - i.e. an INCOMPLETE - // hint under the cursor should be displayed before an INVALID hint - // somewhere else. That's good for displaying hints, but not good for - // deciding if we're good to go. - if (this.hints.length > 1) { - hintClone = this.hints.slice(0); - this.worstHint = Hint.sort(hintClone)[0]; - } - // Create a marked up version of the input var highlightedInput = ''; if (this.element.value.length > 0) { @@ -335,11 +276,11 @@ CliView.prototype = { } // Display the "-> prediction" at the end of the completer - this.shownHint = (this.hints.length > 0) ? this.hints[0] : NO_HINT; - var message = this.shownHint.message; - if (this.shownHint.predictions && this.shownHint.predictions.length > 0) { + var display = this.hints.display || NO_HINT; + var message = display.message; + if (display.predictions && display.predictions.length > 0) { message += ': [ '; - this.shownHint.predictions.forEach(function(prediction) { + display.predictions.forEach(function(prediction) { if (prediction.name) { message += prediction.name + ' | '; } @@ -349,16 +290,22 @@ CliView.prototype = { }, this); message = message.replace(/\| $/, ']'); - var onTab = this.shownHint.predictions[0]; + var onTab = display.predictions[0]; onTab = onTab.name ? onTab.name : onTab; - this.completer.innerHTML = highlightedInput + '  -> ' + onTab; + this.completer.innerHTML = highlightedInput + '  → ' + onTab; } else { this.completer.innerHTML = highlightedInput; } this.hinter.innerHTML = message; + if (message.length === 0) { + this.hinter.classList.add('cptNoHints'); + } + else { + this.hinter.classList.remove('cptNoHints'); + } - var status = this.worstHint ? this.worstHint.status : Status.VALID; + var status = this.hints.worst ? this.hints.worst.status : Status.VALID; this.completer.classList.add(status.toString()); // dom.addCssClass(input, status.toString()); } diff --git a/plugins/cockpit/lib/ui/requestView.js b/plugins/cockpit/lib/ui/requestView.js index b9cb3708..83aa6c7c 100644 --- a/plugins/cockpit/lib/ui/requestView.js +++ b/plugins/cockpit/lib/ui/requestView.js @@ -61,7 +61,7 @@ function RequestView(request, cliView) { this.request = request; this.cliView = cliView; - this.imagePath = '/plugins/ace/pluging/cockpit/ui'; + this.imagePath = '/plugins/ace/plugins/cockpit/lib/ui/images'; // Elements attached to this by the templater. For info only this.rowin = null; @@ -86,7 +86,7 @@ RequestView.prototype = { * the command line */ copyToInput: function() { - this.cliView.input.value = this.request.typed; + this.cliView.element.value = this.request.typed; }, /** diff --git a/plugins/cockpit/lib/ui/settings.js b/plugins/cockpit/lib/ui/settings.js index 80d95e73..dea79554 100644 --- a/plugins/cockpit/lib/ui/settings.js +++ b/plugins/cockpit/lib/ui/settings.js @@ -38,11 +38,26 @@ define(function(require, exports, module) { -var showHintSetting = { - name: "showHint", - description: "Do we display hints while we type?", - type: "bool", - defaultValue: true +var types = require("pilot/types"); +var SelectionType = require('pilot/types/basic').SelectionType; + +var direction = new SelectionType({ + name: 'direction', + data: [ 'above', 'below' ] +}); + +var hintDirectionSetting = { + name: "hintDirection", + description: "Are hints shown above or below the command line?", + type: "direction", + defaultValue: "above" +}; + +var outputDirectionSetting = { + name: "outputDirection", + description: "Is the output window shown above or below the command line?", + type: "direction", + defaultValue: "above" }; var outputHeightSetting = { @@ -53,12 +68,16 @@ var outputHeightSetting = { }; exports.startup = function(data, reason) { - data.env.settings.addSetting(showHintSetting); + types.registerType(direction); + data.env.settings.addSetting(hintDirectionSetting); + data.env.settings.addSetting(outputDirectionSetting); data.env.settings.addSetting(outputHeightSetting); }; exports.shutdown = function(data, reason) { - data.env.settings.removeSetting(showHintSetting); + types.unregisterType(direction); + data.env.settings.removeSetting(hintDirectionSetting); + data.env.settings.removeSetting(outputDirectionSetting); data.env.settings.removeSetting(outputHeightSetting); }; diff --git a/plugins/pilot/lib/index.js b/plugins/pilot/lib/index.js index f576b29c..5166a833 100644 --- a/plugins/pilot/lib/index.js +++ b/plugins/pilot/lib/index.js @@ -41,6 +41,8 @@ var deps = [ "pilot/types/command", "pilot/types/settings", "pilot/commands/settings", + "pilot/commands/basic", + // "pilot/commands/history", "pilot/settings/canon", "pilot/canon" ];