Bug 1472161 - Enter tabs in console when not at an autocomplete location;r=nchevobbe draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Mon, 09 Jul 2018 09:11:04 -0700
changeset 815646 27cbb92f2b894714a8888a108d4f83ee67223db1
parent 815500 b0111088608390a0ab4baa3727e5e6fb06cd9f31
push id115592
push userbgrinstead@mozilla.com
push dateMon, 09 Jul 2018 16:17:58 +0000
reviewersnchevobbe
bugs1472161
milestone63.0a1
Bug 1472161 - Enter tabs in console when not at an autocomplete location;r=nchevobbe This also allows Tab / Shift+Tab to unfocus the input if it is empty. MozReview-Commit-ID: C8u5GTzvA8Z
devtools/client/locales/en-US/webconsole.properties
devtools/client/webconsole/components/JSTerm.js
devtools/client/webconsole/test/mochitest/browser.ini
devtools/client/webconsole/test/mochitest/browser_jsterm_no_input_and_tab_key_pressed.js
devtools/client/webconsole/test/mochitest/browser_jsterm_no_input_change_and_tab_key_pressed.js
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -57,20 +57,16 @@ noCounterLabel=<no label>
 # LOCALIZATION NOTE (counterDoesntExist): this string is displayed when
 # console.countReset() is called with a counter that doesn't exist.
 counterDoesntExist=Counter “%S” doesn’t exist.
 
 # LOCALIZATION NOTE (noGroupLabel): this string is used to display
 # console.group messages with no label provided.
 noGroupLabel=<no group label>
 
-# LOCALIZATION NOTE (Autocomplete.blank): this string is used when inputnode
-# string containing anchor doesn't matches to any property in the content.
-Autocomplete.blank=  <- no result
-
 maxTimersExceeded=The maximum allowed number of timers in this page was exceeded.
 timerAlreadyExists=Timer “%S” already exists.
 timerDoesntExist=Timer “%S” doesn’t exist.
 timerJSError=Failed to process the timer name.
 
 # LOCALIZATION NOTE (connectionTimeout): message displayed when the Remote Web
 # Console fails to connect to the server due to a timeout.
 connectionTimeout=Connection timeout. Check the Error Console on both ends for potential error messages. Reopen the Web Console to try again.
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -92,17 +92,16 @@ class JSTerm extends Component {
     /**
      * Stores the data for the last completion.
      * @type object
      */
     this.lastCompletion = { value: null };
 
     this._keyPress = this._keyPress.bind(this);
     this._inputEventHandler = this._inputEventHandler.bind(this);
-    this._focusEventHandler = this._focusEventHandler.bind(this);
     this._blurEventHandler = this._blurEventHandler.bind(this);
 
     this.SELECTED_FRAME = -1;
 
     /**
      * Array that caches the user input suggestions received from the server.
      * @private
      * @type array
@@ -127,24 +126,16 @@ class JSTerm extends Component {
 
     /**
      * Last input value.
      * @type string
      */
     this.lastInputValue = "";
 
     /**
-     * Tells if the input node changed since the last focus.
-     *
-     * @private
-     * @type boolean
-     */
-    this._inputChanged = false;
-
-    /**
      * Tells if the autocomplete popup was navigated since the last open.
      *
      * @private
      * @type boolean
      */
     this._autocompletePopupNavigated = false;
 
     this.autocompletePopup = null;
@@ -282,17 +273,16 @@ class JSTerm extends Component {
         const cm = this.editor.codeMirror;
         cm.on("paste", (_, event) => this.props.onPaste(event));
         cm.on("drop", (_, event) => this.props.onPaste(event));
       }
     } else if (this.inputNode) {
       this.inputNode.addEventListener("keypress", this._keyPress);
       this.inputNode.addEventListener("input", this._inputEventHandler);
       this.inputNode.addEventListener("keyup", this._inputEventHandler);
-      this.inputNode.addEventListener("focus", this._focusEventHandler);
       this.focus();
     }
 
     this.hud.window.addEventListener("blur", this._blurEventHandler);
     this.lastInputValue && this.setInputValue(this.lastInputValue);
   }
 
   shouldComponentUpdate(nextProps, nextState) {
@@ -618,17 +608,16 @@ class JSTerm extends Component {
       }
 
       this.inputNode.value = newValue;
       this.completeNode.value = "";
     }
 
     this.lastInputValue = newValue;
     this.resizeInput();
-    this._inputChanged = true;
     this.emit("set-input-value");
   }
 
   /**
    * Gets the value from the input field
    * @returns string
    */
   getInputValue() {
@@ -643,17 +632,16 @@ class JSTerm extends Component {
    * The inputNode "input" and "keyup" event handler.
    * @private
    */
   _inputEventHandler() {
     if (this.lastInputValue != this.getInputValue()) {
       this.resizeInput();
       this.complete(this.COMPLETE_HINT_ONLY);
       this.lastInputValue = this.getInputValue();
-      this._inputChanged = true;
     }
   }
 
   /**
    * The window "blur" event handler.
    * @private
    */
   _blurEventHandler() {
@@ -752,17 +740,16 @@ class JSTerm extends Component {
 
       case KeyCodes.DOM_VK_RETURN:
         if (this._autocompletePopupNavigated &&
             this.autocompletePopup.isOpen &&
             this.autocompletePopup.selectedIndex > -1) {
           this.acceptProposedCompletion();
         } else {
           this.execute();
-          this._inputChanged = false;
         }
         event.preventDefault();
         break;
 
       case KeyCodes.DOM_VK_UP:
         if (this.autocompletePopup.isOpen) {
           inputUpdated = this.complete(this.COMPLETE_BACKWARD);
           if (inputUpdated) {
@@ -870,36 +857,30 @@ class JSTerm extends Component {
         break;
 
       case KeyCodes.DOM_VK_TAB:
         // Generate a completion and accept the first proposed value.
         if (this.complete(this.COMPLETE_HINT_ONLY) &&
             this.lastCompletion &&
             this.acceptProposedCompletion()) {
           event.preventDefault();
-        } else if (this._inputChanged) {
-          this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
+        } else if (!this.hasEmptyInput()) {
+          if (!event.shiftKey) {
+            this.insertStringAtCursor("\t");
+          }
           event.preventDefault();
         }
         break;
       default:
         break;
     }
   }
   /* eslint-enable complexity */
 
   /**
-   * The inputNode "focus" event handler.
-   * @private
-   */
-  _focusEventHandler() {
-    this._inputChanged = false;
-  }
-
-  /**
    * Go up/down the history stack of input values.
    *
    * @param number direction
    *        History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
    *
    * @returns boolean
    *          True if the input value changed, false otherwise.
    */
@@ -922,16 +903,25 @@ class JSTerm extends Component {
       this.setInputValue(newInputValue);
       return true;
     }
 
     return false;
   }
 
   /**
+   * Test for empty input.
+   *
+   * @return boolean
+   */
+  hasEmptyInput() {
+    return this.getInputValue() === "";
+  }
+
+  /**
    * Test for multiline input.
    *
    * @return boolean
    *         True if CR or LF found in node value; else false.
    */
   hasMultilineInput() {
     return /[\r\n]/.test(this.getInputValue());
   }
@@ -1274,33 +1264,43 @@ class JSTerm extends Component {
    *         True if there was a selected completion item and the input value
    *         was updated, false otherwise.
    */
   acceptProposedCompletion() {
     let updated = false;
 
     const currentItem = this.autocompletePopup.selectedItem;
     if (currentItem && this.lastCompletion.value) {
-      const suffix =
-        currentItem.label.substring(this.lastCompletion.matchProp.length);
-      const cursor = this.inputNode.selectionStart;
-      const value = this.getInputValue();
-      this.setInputValue(value.substr(0, cursor) +
-        suffix + value.substr(cursor));
-      const newCursor = cursor + suffix.length;
-      this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor;
+      this.insertStringAtCursor(
+        currentItem.label.substring(this.lastCompletion.matchProp.length)
+      );
       updated = true;
     }
 
     this.clearCompletion();
 
     return updated;
   }
 
   /**
+   * Insert a string into the console at the cursor location,
+   * moving the cursor to the end of the string.
+   *
+   * @param string str
+   */
+  insertStringAtCursor(str) {
+    const cursor = this.inputNode.selectionStart;
+    const value = this.getInputValue();
+    this.setInputValue(value.substr(0, cursor) +
+      str + value.substr(cursor));
+    const newCursor = cursor + str.length;
+    this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor;
+  }
+
+  /**
    * Update the node that displays the currently selected autocomplete proposal.
    *
    * @param string suffix
    *        The proposed suffix for the inputNode value.
    */
   updateCompleteNode(suffix) {
     if (!this.completeNode) {
       return;
@@ -1354,17 +1354,16 @@ class JSTerm extends Component {
       this.autocompletePopup.destroy();
       this.autocompletePopup = null;
     }
 
     if (this.inputNode) {
       this.inputNode.removeEventListener("keypress", this._keyPress);
       this.inputNode.removeEventListener("input", this._inputEventHandler);
       this.inputNode.removeEventListener("keyup", this._inputEventHandler);
-      this.inputNode.removeEventListener("focus", this._focusEventHandler);
       this.hud.window.removeEventListener("blur", this._blurEventHandler);
     }
 
     if (this.editor) {
       this.editor.destroy();
       this.editor = null;
     }
 
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -220,17 +220,16 @@ skip-if = os != 'mac' # The tested ctrl+
 [browser_jsterm_history_nav.js]
 [browser_jsterm_history_persist.js]
 [browser_jsterm_input_expansion.js]
 [browser_jsterm_inspect.js]
 [browser_jsterm_instance_of.js]
 [browser_jsterm_multiline.js]
 [browser_jsterm_no_autocompletion_on_defined_variables.js]
 [browser_jsterm_no_input_and_tab_key_pressed.js]
-[browser_jsterm_no_input_change_and_tab_key_pressed.js]
 [browser_jsterm_null_undefined.js]
 [browser_jsterm_popup_close_on_tab_switch.js]
 [browser_jsterm_screenshot_command_clipboard.js]
 subsuite = clipboard
 [browser_jsterm_screenshot_command_file.js]
 skip-if = (os == 'win') # Bug 1464461, disabled on Win due to timeouts
 [browser_jsterm_selfxss.js]
 subsuite = clipboard
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_no_input_and_tab_key_pressed.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_no_input_and_tab_key_pressed.js
@@ -13,22 +13,22 @@ add_task(async function() {
   const hud = await openNewTabAndConsole(TEST_URI);
   testCompletion(hud);
 });
 
 function testCompletion(hud) {
   const jsterm = hud.jsterm;
   const input = jsterm.inputNode;
 
+  // With empty input, tab through
   jsterm.setInputValue("");
   EventUtils.synthesizeKey("KEY_Tab");
-  is(jsterm.completeNode.value, "<- no result", "<- no result - matched");
-  is(input.value, "", "inputnode is empty - matched");
-  ok(hasFocus(input), "input is still focused");
+  is(jsterm.getInputValue(), "", "inputnode is empty - matched");
+  ok(!hasFocus(input), "input isn't focused anymore");
+  jsterm.focus();
 
-  // Any thing which is not in property autocompleter
+  // With non-empty input, insert a tab
   jsterm.setInputValue("window.Bug583816");
   EventUtils.synthesizeKey("KEY_Tab");
-  is(jsterm.completeNode.value, "                <- no result",
-     "completenode content - matched");
-  is(input.value, "window.Bug583816", "inputnode content - matched");
+  is(jsterm.getInputValue(), "window.Bug583816\t",
+     "input content - matched");
   ok(hasFocus(input), "input is still focused");
 }
deleted file mode 100644
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_no_input_change_and_tab_key_pressed.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-// See Bug 734061.
-
-const TEST_URI = "data:text/html,Testing jsterm focus";
-
-add_task(async function() {
-  const hud = await openNewTabAndConsole(TEST_URI);
-  const jsterm = hud.jsterm;
-  const input = jsterm.inputNode;
-
-  is(hasFocus(input), true, "input has focus");
-  EventUtils.synthesizeKey("KEY_Tab");
-  is(hasFocus(input), false, "focus moved away");
-
-  // Test user changed something
-  input.focus();
-  EventUtils.sendString("A");
-  EventUtils.synthesizeKey("KEY_Tab");
-  is(hasFocus(input), true, "input is still focused");
-
-  // Test non empty input but not changed since last focus
-  input.blur();
-  input.focus();
-  EventUtils.synthesizeKey("KEY_ArrowRight");
-  EventUtils.synthesizeKey("KEY_Tab");
-  is(hasFocus(input), false, "focus moved away");
-});