Bug 804845 - CTRL+P and CTRL+N should work the same as up arrow and down arrow respectively; r=msucan
authorzmgmoz <zmgmoz@gmail.com>
Mon, 17 Dec 2012 15:47:23 +0200
changeset 125535 a5793af7e70c262a8d00b82e3e38642516f5406f
parent 125534 c7c3914d612797c6acee4c08fb822e46a9213e4f
child 125536 315ff93a4d9046d9aa0c988513f5e64b549d905b
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmsucan
bugs804845
milestone20.0a1
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 804845 - CTRL+P and CTRL+N should work the same as up arrow and down arrow respectively; r=msucan
browser/devtools/webconsole/test/Makefile.in
browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
browser/devtools/webconsole/webconsole.js
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -112,16 +112,22 @@ MOCHITEST_BROWSER_FILES = \
 	browser_output_breaks_after_console_dir_uninspectable.js \
 	browser_console_log_inspectable_object.js \
 	browser_bug_638949_copy_link_location.js \
 	browser_output_longstring_expand.js \
 	browser_netpanel_longstring_expand.js \
 	head.js \
 	$(NULL)
 
+ifeq ($(OS_ARCH), Darwin)
+MOCHITEST_BROWSER_FILES += \
+        browser_webconsole_bug_804845_ctrl_key_nav.js \
+        $(NULL)
+endif
+
 ifndef MOZ_PER_WINDOW_PRIVATE_BROWSING
 MOCHITEST_BROWSER_FILES += \
         browser_webconsole_bug_618311_private_browsing.js \
         $(NULL)
 endif
 
 MOCHITEST_BROWSER_FILES += \
 	test-console.html \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
@@ -0,0 +1,214 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ *  zmgmoz <zmgmoz@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Test navigation of webconsole contents via ctrl-a, ctrl-e, ctrl-p, ctrl-n
+// see https://bugzilla.mozilla.org/show_bug.cgi?id=804845
+
+let jsterm, inputNode;
+function test() {
+  addTab("data:text/html;charset=utf-8,Web Console test for bug 804845 and bug 619598");
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, doTests);
+  }, true);
+}
+
+function doTests(HUD) {
+  jsterm = HUD.jsterm;
+  inputNode = jsterm.inputNode;
+  ok(!jsterm.inputNode.value, "inputNode.value is empty");
+  is(jsterm.inputNode.selectionStart, 0);
+  is(jsterm.inputNode.selectionEnd, 0);
+
+  testSingleLineInputNavNoHistory();
+  testMultiLineInputNavNoHistory();
+  testNavWithHistory();
+
+  jsterm = inputNode = null;
+  executeSoon(finishTest);
+}
+
+function testSingleLineInputNavNoHistory() {
+  // Single char input
+  EventUtils.synthesizeKey("1", {});
+  is(inputNode.selectionStart, 1, "caret location after single char input");
+
+  // nav to start/end with ctrl-a and ctrl-e;
+  EventUtils.synthesizeKey("a", { ctrlKey: true });
+  is(inputNode.selectionStart, 0, "caret location after single char input and ctrl-a");
+
+  EventUtils.synthesizeKey("e", { ctrlKey: true });
+  is(inputNode.selectionStart, 1, "caret location after single char input and ctrl-e");
+
+  // Second char input
+  EventUtils.synthesizeKey("2", {});
+  // nav to start/end with up/down keys; verify behaviour using ctrl-p/ctrl-n
+  EventUtils.synthesizeKey("VK_UP", {});
+  is(inputNode.selectionStart, 0, "caret location after two char input and VK_UP");
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  is(inputNode.selectionStart, 2, "caret location after two char input and VK_DOWN");
+
+  EventUtils.synthesizeKey("a", { ctrlKey: true });
+  is(inputNode.selectionStart, 0, "move caret to beginning of 2 char input with ctrl-a");
+  EventUtils.synthesizeKey("a", { ctrlKey: true });
+  is(inputNode.selectionStart, 0, "no change of caret location on repeat ctrl-a");
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.selectionStart, 0, "no change of caret location on ctrl-p from beginning of line");
+
+  EventUtils.synthesizeKey("e", { ctrlKey: true });
+  is(inputNode.selectionStart, 2, "move caret to end of 2 char input with ctrl-e");
+  EventUtils.synthesizeKey("e", { ctrlKey: true });
+  is(inputNode.selectionStart, 2, "no change of caret location on repeat ctrl-e");
+  EventUtils.synthesizeKey("n", { ctrlKey: true });
+  is(inputNode.selectionStart, 2, "no change of caret location on ctrl-n from end of line");
+
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.selectionStart, 0, "ctrl-p moves to start of line");
+
+  EventUtils.synthesizeKey("n", { ctrlKey: true });
+  is(inputNode.selectionStart, 2, "ctrl-n moves to end of line");
+}
+
+function testMultiLineInputNavNoHistory() {
+  let lineValues = ["one", "2", "something longer", "", "", "three!"];
+  jsterm.setInputValue("");
+  // simulate shift-return
+  for (let i = 0; i < lineValues.length; i++) {
+    jsterm.setInputValue(inputNode.value + lineValues[i]);
+    EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
+  }
+  let inputValue = inputNode.value;
+  is(inputNode.selectionStart, inputNode.selectionEnd);
+  is(inputNode.selectionStart, inputValue.length, "caret at end of multiline input");
+
+  // possibility newline is represented by one ('\r', '\n') or two ('\r\n') chars
+  let newlineString = inputValue.match(/(\r\n?|\n\r?)$/)[0];
+
+  // Ok, test navigating within the multi-line string!
+  EventUtils.synthesizeKey("VK_UP", {});
+  let expectedStringAfterCarat = lineValues[5]+newlineString;
+  is(inputNode.value.slice(inputNode.selectionStart), expectedStringAfterCarat,
+     "up arrow from end of multiline");
+
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  is(inputNode.value.slice(inputNode.selectionStart), "",
+     "down arrow from within multiline");
+
+  // navigate up through input lines
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.value.slice(inputNode.selectionStart), expectedStringAfterCarat,
+     "ctrl-p from end of multiline");
+
+  for (let i = 4; i >= 0; i--) {
+    EventUtils.synthesizeKey("p", { ctrlKey: true });
+    expectedStringAfterCarat = lineValues[i] + newlineString + expectedStringAfterCarat;
+    is(inputNode.value.slice(inputNode.selectionStart), expectedStringAfterCarat,
+       "ctrl-p from within line " + i + " of multiline input");
+  }
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.selectionStart, 0, "reached start of input");
+  is(inputNode.value, inputValue,
+     "no change to multiline input on ctrl-p from beginning of multiline");
+
+  // navigate to end of first line
+  EventUtils.synthesizeKey("e", { ctrlKey: true });
+  let caretPos = inputNode.selectionStart;
+  let expectedStringBeforeCarat = lineValues[0];
+  is(inputNode.value.slice(0, caretPos), expectedStringBeforeCarat,
+     "ctrl-e into multiline input");
+  EventUtils.synthesizeKey("e", { ctrlKey: true });
+  is(inputNode.selectionStart, caretPos,
+     "repeat ctrl-e doesn't change caret position in multiline input");
+
+  // navigate down one line; ctrl-a to the beginning; ctrl-e to end
+  for (let i = 1; i < lineValues.length; i++) {
+    EventUtils.synthesizeKey("n", { ctrlKey: true });
+    EventUtils.synthesizeKey("a", { ctrlKey: true });
+    caretPos = inputNode.selectionStart;
+    expectedStringBeforeCarat += newlineString;
+    is(inputNode.value.slice(0, caretPos), expectedStringBeforeCarat,
+       "ctrl-a to beginning of line " + (i+1) + " in multiline input");
+
+    EventUtils.synthesizeKey("e", { ctrlKey: true });
+    caretPos = inputNode.selectionStart;
+    expectedStringBeforeCarat += lineValues[i];
+    is(inputNode.value.slice(0, caretPos), expectedStringBeforeCarat,
+       "ctrl-e to end of line " + (i+1) + "in multiline input");
+  }
+}
+
+function testNavWithHistory() {
+  // NOTE: Tests does NOT currently define behaviour for ctrl-p/ctrl-n with
+  // caret placed _within_ single line input
+  let values = ['"single line input"',
+                '"a longer single-line input to check caret repositioning"',
+                ['"multi-line"', '"input"', '"here!"'].join("\n"),
+               ];
+  // submit to history
+  for (let i = 0; i < values.length; i++) {
+    jsterm.setInputValue(values[i]);
+    jsterm.execute();
+  }
+  is(inputNode.selectionStart, 0, "caret location at start of empty line");
+
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.selectionStart, values[values.length-1].length,
+     "caret location correct at end of last history input");
+
+  // Navigate backwards history with ctrl-p
+  for (let i = values.length-1; i > 0; i--) {
+    let match = values[i].match(/(\n)/g);
+    if (match) {
+      // multi-line inputs won't update from history unless caret at beginning
+      EventUtils.synthesizeKey("a", { ctrlKey: true });
+      for (let i = 0; i < match.length; i++) {
+        EventUtils.synthesizeKey("p", { ctrlKey: true });
+      }
+      EventUtils.synthesizeKey("p", { ctrlKey: true });
+    } else {
+      // single-line inputs will update from history from end of line
+      EventUtils.synthesizeKey("p", { ctrlKey: true });
+    }
+    is(inputNode.value, values[i-1],
+       "ctrl-p updates inputNode from backwards history values[" + i-1 + "]");
+  }
+  let inputValue = inputNode.value;
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.selectionStart, 0,
+     "ctrl-p at beginning of history moves caret location to beginning of line");
+  is(inputNode.value, inputValue,
+     "no change to input value on ctrl-p from beginning of line");
+
+  // Navigate forwards history with ctrl-n
+  for (let i = 1; i<values.length; i++) {
+    EventUtils.synthesizeKey("n", { ctrlKey: true });
+    is(inputNode.value, values[i],
+       "ctrl-n updates inputNode from forwards history values[" + i + "]");
+    is(inputNode.selectionStart, values[i].length,
+       "caret location correct at end of history input for values[" + i + "]");
+  }
+  EventUtils.synthesizeKey("n", { ctrlKey: true });
+  ok(!inputNode.value, "ctrl-n at end of history updates to empty input");
+
+  // Simulate editing multi-line
+  inputValue = "one\nlinebreak";
+  jsterm.setInputValue(inputValue);
+
+  // Attempt nav within input
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.value, inputValue,
+     "ctrl-p from end of multi-line does not trigger history");
+
+  EventUtils.synthesizeKey("a", { ctrlKey: true });
+  EventUtils.synthesizeKey("p", { ctrlKey: true });
+  is(inputNode.value, values[values.length-1],
+     "ctrl-p from start of multi-line triggers history");
+}
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -3176,31 +3176,82 @@ JSTerm.prototype = {
   /**
    * The inputNode "keypress" event handler.
    *
    * @param nsIDOMEvent aEvent
    */
   keyPress: function JSTF_keyPress(aEvent)
   {
     if (aEvent.ctrlKey) {
+      let inputNode = this.inputNode;
+      let closePopup = false;
       switch (aEvent.charCode) {
         case 97:
           // control-a
-          this.inputNode.setSelectionRange(0, 0);
+          let lineBeginPos = 0;
+          if (this.hasMultilineInput()) {
+            // find index of closest newline <= to cursor
+            for (let i = inputNode.selectionStart-1; i >= 0; i--) {
+              if (inputNode.value.charAt(i) == "\r" ||
+                  inputNode.value.charAt(i) == "\n") {
+                lineBeginPos = i+1;
+                break;
+              }
+            }
+          }
+          inputNode.setSelectionRange(lineBeginPos, lineBeginPos);
           aEvent.preventDefault();
+          closePopup = true;
           break;
         case 101:
           // control-e
-          this.inputNode.setSelectionRange(this.inputNode.value.length,
-                                           this.inputNode.value.length);
+          let lineEndPos = inputNode.value.length;
+          if (this.hasMultilineInput()) {
+            // find index of closest newline >= cursor
+            for (let i = inputNode.selectionEnd; i<lineEndPos; i++) {
+              if (inputNode.value.charAt(i) == "\r" ||
+                  inputNode.value.charAt(i) == "\n") {
+                lineEndPos = i;
+                break;
+              }
+            }
+          }
+          inputNode.setSelectionRange(lineEndPos, lineEndPos);
           aEvent.preventDefault();
           break;
+        case 110:
+          // Control-N differs from down arrow: it ignores autocomplete state.
+          // Note that we preserve the default 'down' navigation within
+          // multiline text.
+          if (Services.appinfo.OS == "Darwin" &&
+              this.canCaretGoNext() &&
+              this.historyPeruse(HISTORY_FORWARD)) {
+            aEvent.preventDefault();
+          }
+          closePopup = true;
+          break;
+        case 112:
+          // Control-P differs from up arrow: it ignores autocomplete state.
+          // Note that we preserve the default 'up' navigation within
+          // multiline text.
+          if (Services.appinfo.OS == "Darwin" &&
+              this.canCaretGoPrevious() &&
+              this.historyPeruse(HISTORY_BACK)) {
+            aEvent.preventDefault();
+          }
+          closePopup = true;
+          break;
         default:
           break;
       }
+      if (closePopup) {
+        if (this.autocompletePopup.isOpen) {
+          this.clearCompletion();
+        }
+      }
       return;
     }
     else if (aEvent.shiftKey &&
         aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
       // shift return
       // TODO: expand the inputNode height by one line
       return;
     }
@@ -3312,16 +3363,27 @@ JSTerm.prototype = {
     else {
       throw new Error("Invalid argument 0");
     }
 
     return true;
   },
 
   /**
+   * Test for multiline input.
+   *
+   * @return boolean
+   *         True if CR or LF found in node value; else false.
+   */
+  hasMultilineInput: function JST_hasMultilineInput()
+  {
+    return /[\r\n]/.test(this.inputNode.value);
+  },
+
+  /**
    * Check if the caret is at a location that allows selecting the previous item
    * in history when the user presses the Up arrow key.
    *
    * @return boolean
    *         True if the caret is at a location that allows selecting the
    *         previous item in history when the user presses the Up arrow key,
    *         otherwise false.
    */