Bug 601183 - Investigate Bespin/Safari-style completion styling for the Web Console a=approval2.0 r=gavin
authorJoe Walker <jwalker@mozilla.com>
Tue, 05 Oct 2010 12:01:44 +0100
changeset 58907 fc18b7041778616cace8e1ff93703d8433ab96cf
parent 58906 ae0ba1e0f094609bbd7c2d69421700dab0c99d62
child 58911 c7b784648b0252f9dc1a6f5c8225cae3eaa82e45
push id17459
push userddahl@mozilla.com
push dateWed, 08 Dec 2010 22:35:59 +0000
treeherdermozilla-central@fc18b7041778 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersapproval2.0, gavin
bugs601183
milestone2.0b8pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 601183 - Investigate Bespin/Safari-style completion styling for the Web Console a=approval2.0 r=gavin
toolkit/components/console/hudservice/HUDService.jsm
toolkit/components/console/hudservice/tests/browser/browser_webconsole_chrome.js
toolkit/components/console/hudservice/tests/browser/browser_webconsole_completion.js
toolkit/themes/gnomestripe/global/webConsole.css
toolkit/themes/pinstripe/global/webConsole.css
toolkit/themes/winstripe/global/webConsole.css
--- a/toolkit/components/console/hudservice/HUDService.jsm
+++ b/toolkit/components/console/hudservice/HUDService.jsm
@@ -3938,16 +3938,17 @@ JSTerm.prototype = {
   {
     this.createSandbox();
     this.inputNode = this.mixins.inputNode;
     let eventHandlerKeyDown = this.keyDown();
     this.inputNode.addEventListener('keypress', eventHandlerKeyDown, false);
     let eventHandlerInput = this.inputEventHandler();
     this.inputNode.addEventListener('input', eventHandlerInput, false);
     this.outputNode = this.mixins.outputNode;
+    this.completeNode = this.mixins.completeNode;
     if (this.mixins.cssClassOverride) {
       this.cssClassOverride = this.mixins.cssClassOverride;
     }
   },
 
   get codeInputString()
   {
     // TODO: filter the input for windows line breaks, conver to unix
@@ -4278,76 +4279,77 @@ JSTerm.prototype = {
               self.setInputValue(tmp);
             }, 0);
             break;
           default:
             return;
         }
         return;
       }
-      else if (aEvent.shiftKey && aEvent.keyCode == 13) {
+      else if (aEvent.shiftKey &&
+          aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
         // shift return
         // TODO: expand the inputNode height by one line
         return;
       }
       else {
         switch(aEvent.keyCode) {
-          case 13:
-            // return
+          case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
             self.execute();
             aEvent.preventDefault();
             break;
-          case 38:
-            // up arrow: history previous
+
+          case Ci.nsIDOMKeyEvent.DOM_VK_UP:
+            // history previous
             if (self.caretAtStartOfInput()) {
               let updated = self.historyPeruse(HISTORY_BACK);
               if (updated && aEvent.cancelable) {
                 self.inputNode.setSelectionRange(0, 0);
                 aEvent.preventDefault();
               }
             }
             break;
-          case 40:
-            // down arrow: history next
+
+          case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
+            // history next
             if (self.caretAtEndOfInput()) {
               let updated = self.historyPeruse(HISTORY_FORWARD);
               if (updated && aEvent.cancelable) {
                 let inputEnd = self.inputNode.value.length;
                 self.inputNode.setSelectionRange(inputEnd, inputEnd);
                 aEvent.preventDefault();
               }
             }
             break;
-          case 9:
-            // tab key
+
+          case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
+            // accept proposed completion
+            self.acceptProposedCompletion();
+            break;
+
+          case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
             // If there are more than one possible completion, pressing tab
             // means taking the next completion, shift_tab means taking
             // the previous completion.
             var completionResult;
             if (aEvent.shiftKey) {
               completionResult = self.complete(self.COMPLETE_BACKWARD);
             }
             else {
               completionResult = self.complete(self.COMPLETE_FORWARD);
             }
             if (completionResult) {
               if (aEvent.cancelable) {
-              aEvent.preventDefault();
-            }
-            aEvent.target.focus();
+                aEvent.preventDefault();
+              }
+              aEvent.target.focus();
             }
             break;
-          case 8:
-            // backspace key
-          case 46:
-            // delete key
-            // necessary so that default is not reached.
-            break;
+
           default:
-            // all not handled keys
             // Store the current inputNode value. If the value is the same
             // after keyDown event was handled (after 0ms) then the user
             // moved the cursor. If the value changed, then call the complete
             // function to show completion on new value.
             var value = self.inputNode.value;
             setTimeout(function() {
               if (self.inputNode.value !== value) {
                 self.complete(self.COMPLETE_HINT_ONLY);
@@ -4473,40 +4475,32 @@ JSTerm.prototype = {
    *          or false otherwise.
    */
   complete: function JSTF_complete(type)
   {
     let inputNode = this.inputNode;
     let inputValue = inputNode.value;
     // If the inputNode has no value, then don't try to complete on it.
     if (!inputValue) {
+      this.updateCompleteNode("");
       return false;
     }
-    let selStart = inputNode.selectionStart, selEnd = inputNode.selectionEnd;
-
-    // 'Normalize' the selection so that end is always after start.
-    if (selStart > selEnd) {
-      let newSelEnd = selStart;
-      selStart = selEnd;
-      selEnd = newSelEnd;
-    }
-
-    // Only complete if the selection is at the end of the input.
-    if (selEnd != inputValue.length) {
+
+    // Only complete if the selection is empty and at the end of the input.
+    if (inputNode.selectionStart == inputNode.selectionEnd &&
+        inputNode.selectionEnd != inputValue.length) {
+      // TODO: shouldnt we do this in the other 'bail' cases?
       this.lastCompletion = null;
+      this.updateCompleteNode("");
       return false;
     }
 
-    // Remove the selected text from the inputValue.
-    inputValue = inputValue.substring(0, selStart);
-
     let matches;
     let matchIndexToUse;
     let matchOffset;
-    let completionStr;
 
     // If there is a saved completion from last time and the used value for
     // completion stayed the same, then use the stored completion.
     if (this.lastCompletion && inputValue == this.lastCompletion.value) {
       matches = this.lastCompletion.matches;
       matchOffset = this.lastCompletion.matchOffset;
       if (type === this.COMPLETE_BACKWARD) {
         this.lastCompletion.index --;
@@ -4515,62 +4509,71 @@ JSTerm.prototype = {
         this.lastCompletion.index ++;
       }
       matchIndexToUse = this.lastCompletion.index;
     }
     else {
       // Look up possible completion values.
       let completion = this.propertyProvider(this.sandbox.window, inputValue);
       if (!completion) {
+        this.updateCompleteNode("");
         return false;
       }
       matches = completion.matches;
       matchIndexToUse = 0;
       matchOffset = completion.matchProp.length;
       // Store this match;
       this.lastCompletion = {
         index: 0,
         value: inputValue,
         matches: matches,
         matchOffset: matchOffset
       };
     }
 
-    if (matches.length != 0) {
+    if (type != this.COMPLETE_HINT_ONLY && matches.length == 1) {
+      this.acceptProposedCompletion();
+      return true;
+    }
+    else if (matches.length != 0) {
       // Ensure that the matchIndexToUse is always a valid array index.
       if (matchIndexToUse < 0) {
         matchIndexToUse = matches.length + (matchIndexToUse % matches.length);
         if (matchIndexToUse == matches.length) {
           matchIndexToUse = 0;
         }
       }
       else {
         matchIndexToUse = matchIndexToUse % matches.length;
       }
 
-      completionStr = matches[matchIndexToUse].substring(matchOffset);
-      this.setInputValue(inputValue + completionStr);
-
-      selEnd = inputValue.length + completionStr.length;
-
-      // If there is more than one possible completion or the completed part
-      // should get displayed only without moving the cursor at the end of the
-      // completion.
-      if (matches.length > 1 || type === this.COMPLETE_HINT_ONLY) {
-        inputNode.setSelectionRange(selStart, selEnd);
-      }
-      else {
-        inputNode.setSelectionRange(selEnd, selEnd);
-      }
-
+      let completionStr = matches[matchIndexToUse].substring(matchOffset);
+      this.updateCompleteNode(completionStr);
       return completionStr ? true : false;
     }
+    else {
+      this.updateCompleteNode("");
+    }
 
     return false;
-  }
+  },
+
+  acceptProposedCompletion: function JSTF_acceptProposedCompletion()
+  {
+    this.setInputValue(this.inputNode.value + this.completionValue);
+    this.updateCompleteNode("");
+  },
+
+  updateCompleteNode: function JSTF_updateCompleteNode(suffix)
+  {
+    this.completionValue = suffix;
+
+    let prefix = new Array(this.inputNode.value.length + 1).join(" ");
+    this.completeNode.value = prefix + this.completionValue;
+  },
 };
 
 /**
  * JSTermFirefoxMixin
  *
  * JavaScript Terminal Firefox Mixin
  *
  */
@@ -4606,48 +4609,48 @@ JSTermFirefoxMixin.prototype = {
   /**
    * Generates and attaches the UI for an entire JS Workspace or
    * just the input node used under the console output
    *
    * @returns void
    */
   generateUI: function JSTF_generateUI()
   {
-    let inputContainer = this.xulElementFactory("hbox");
-    inputContainer.setAttribute("class", "jsterm-input-container");
-    inputContainer.setAttribute("style", "direction: ltr;");
-
-    let inputNode = this.xulElementFactory("textbox");
-    inputNode.setAttribute("class", "jsterm-input-node");
-    inputNode.setAttribute("flex", "1");
-    inputNode.setAttribute("multiline", "true");
-    inputNode.setAttribute("rows", "1");
-    inputContainer.appendChild(inputNode);
+    this.completeNode = this.xulElementFactory("textbox");
+    this.completeNode.setAttribute("class", "jsterm-complete-node");
+    this.completeNode.setAttribute("multiline", "true");
+    this.completeNode.setAttribute("rows", "1");
+
+    this.inputNode = this.xulElementFactory("textbox");
+    this.inputNode.setAttribute("class", "jsterm-input-node");
+    this.inputNode.setAttribute("multiline", "true");
+    this.inputNode.setAttribute("rows", "1");
+
+    let inputStack = this.xulElementFactory("stack");
+    inputStack.setAttribute("class", "jsterm-stack-node");
+    inputStack.setAttribute("flex", "1");
+    inputStack.appendChild(this.completeNode);
+    inputStack.appendChild(this.inputNode);
 
     if (this.existingConsoleNode == undefined) {
-      // create elements
-      let term = this.xulElementFactory("vbox");
-      term.setAttribute("class", "jsterm-wrapper-node");
-      term.setAttribute("flex", "1");
-
-      let outputNode = this.xulElementFactory("vbox");
-      outputNode.setAttribute("class", "jsterm-output-node");
-
-      // construction
-      term.appendChild(outputNode);
-      term.appendChild(inputNode);
-
-      this.outputNode = outputNode;
-      this.inputNode = inputNode;
-      this.term = term;
+      this.outputNode = this.xulElementFactory("vbox");
+      this.outputNode.setAttribute("class", "jsterm-output-node");
+
+      this.term = this.xulElementFactory("vbox");
+      this.term.setAttribute("class", "jsterm-wrapper-node");
+      this.term.setAttribute("flex", "1");
+      this.term.appendChild(this.outputNode);
     }
     else {
-      this.inputNode = inputNode;
-      this.term = inputContainer;
       this.outputNode = this.existingConsoleNode;
+
+      this.term = this.xulElementFactory("hbox");
+      this.term.setAttribute("class", "jsterm-input-container");
+      this.term.setAttribute("style", "direction: ltr;");
+      this.term.appendChild(inputStack);
     }
   },
 
   get inputValue()
   {
     return this.inputNode.value;
   },
 
--- a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_chrome.js
+++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_chrome.js
@@ -64,16 +64,14 @@ function testChrome() {
   let input = jsterm.inputNode;
   let outputNode = hudBox.querySelector(".jsterm-input-node");
   ok(outputNode, "we have an output node");
 
   // Test typing 'docu'.
   input.value = "docu";
   input.setSelectionRange(4, 4);
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
-  is(input.value, "document", "'docu' completion");
-  is(input.selectionStart, 4, "start selection is alright");
-  is(input.selectionEnd, 8, "end selection is alright");
+  is(jsterm.completeNode.value, "    ment", "'docu' completion");
 
   HUD = jsterm = input = null;
   finishTest();
 }
 
--- a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_completion.js
+++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_completion.js
@@ -57,47 +57,44 @@ function testCompletion() {
   var HUD = HUDService.hudReferences[hudId];
   var jsterm = HUD.jsterm;
   var input = jsterm.inputNode;
 
   // Test typing 'docu'.
   input.value = "docu";
   input.setSelectionRange(4, 4);
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
-  is(input.value, "document", "'docu' completion");
-  is(input.selectionStart, 4, "start selection is alright");
-  is(input.selectionEnd, 8, "end selection is alright");
+  is(input.value, "docu", "'docu' completion");
+  is(jsterm.completeNode.value, "    ment", "'docu' completion");
 
   // Test typing 'docu' and press tab.
   input.value = "docu";
   input.setSelectionRange(4, 4);
   jsterm.complete(jsterm.COMPLETE_FORWARD);
   is(input.value, "document", "'docu' tab completion");
   is(input.selectionStart, 8, "start selection is alright");
   is(input.selectionEnd, 8, "end selection is alright");
+  is(jsterm.completeNode.value.replace(/ /g, ""), "", "'docu' completed");
 
   // Test typing 'document.getElem'.
   input.value = "document.getElem";
   input.setSelectionRange(16, 16);
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
-  is(input.value, "document.getElementById", "'document.getElem' completion");
-  is(input.selectionStart, 16, "start selection is alright");
-  is(input.selectionEnd, 23, "end selection is alright");
+  is(input.value, "document.getElem", "'document.getElem' completion");
+  is(jsterm.completeNode.value, "                entById", "'document.getElem' completion");
 
   // Test pressing tab another time.
   jsterm.complete(jsterm.COMPLETE_FORWARD);
-  is(input.value, "document.getElementsByClassName", "'document.getElem' another tab completion");
-  is(input.selectionStart, 16, "start selection is alright");
-  is(input.selectionEnd, 31, "end selection is alright");
+  is(input.value, "document.getElem", "'document.getElem' completion");
+  is(jsterm.completeNode.value, "                entsByClassName", "'document.getElem' another tab completion");
 
   // Test pressing shift_tab.
   jsterm.complete(jsterm.COMPLETE_BACKWARD);
-  is(input.value, "document.getElementById", "'document.getElem' untab completion");
-  is(input.selectionStart, 16, "start selection is alright");
-  is(input.selectionEnd, 23, "end selection is alright");
+  is(input.value, "document.getElem", "'document.getElem' untab completion");
+  is(jsterm.completeNode.value, "                entById", "'document.getElem' completion");
 
   jsterm.clearOutput();
   jsterm.history.splice(0);   // workaround for bug 592552
 
   HUD = jsterm = input = null;
   finishTest();
 }
 
--- a/toolkit/themes/gnomestripe/global/webConsole.css
+++ b/toolkit/themes/gnomestripe/global/webConsole.css
@@ -196,12 +196,25 @@
   padding: 0px 0px 0px 16px;
   -moz-appearance: none;
 }
 
 .jsterm-input-node > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
+.jsterm-complete-node {
+    font-family: monospace;
+    font-size: 9pt;
+    border: none;
+    padding: 0px 0px 0px 16px;
+    -moz-appearance: none;
+}
+
+.jsterm-complete-node textarea {
+    overflow-x: hidden;
+    color: #AAA;
+}
+
 .jsterm-output-line {
   font-size: 1em;
 }
 
--- a/toolkit/themes/pinstripe/global/webConsole.css
+++ b/toolkit/themes/pinstripe/global/webConsole.css
@@ -263,16 +263,29 @@
   padding: 0px 0px 0px 16px;
   -moz-appearance: none;
 }
 
 .jsterm-input-node > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
+.jsterm-complete-node {
+    font-family: monospace;
+    font-size: 9pt;
+    border: none;
+    padding: 0px 0px 0px 16px;
+    -moz-appearance: none;
+}
+
+.jsterm-complete-node textarea {
+    overflow-x: hidden;
+    color: #AAA;
+}
+
 .jsterm-output-line {
   font-size: 1em;
 }
 
 .hud-console-filter-toolbar {
   background: @scopeBarBackground@;
   border-bottom: @scopeBarSeparatorBorder@;
   padding: 0px 1px;
--- a/toolkit/themes/winstripe/global/webConsole.css
+++ b/toolkit/themes/winstripe/global/webConsole.css
@@ -205,16 +205,29 @@
   padding: 0px 0px 0px 16px;
   -moz-appearance: none;
 }
 
 .jsterm-input-node > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
+.jsterm-complete-node {
+    font-family: monospace;
+    font-size: 9pt;
+    border: none;
+    padding: 0px 0px 0px 16px;
+    -moz-appearance: none;
+}
+
+.jsterm-complete-node textarea {
+    overflow-x: hidden;
+    color: #AAA;
+}
+
 .jsterm-output-line {
   font-size: 1em;
 }
 
 .hud-console-filter-toolbar {
   padding: 1px 0px;
   -moz-box-align: center;
   -moz-appearance: none;