Bug 1473923 - Autocomplete console commands; r=yulia. draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Fri, 13 Jul 2018 08:54:23 +0200
changeset 820208 2e40528ff43b76571ec59ce8af591cd71c828251
parent 820131 5a8107262015714d2907a85abc24c847ad9b32d2
push id116751
push userbmo:nchevobbe@mozilla.com
push dateThu, 19 Jul 2018 05:49:31 +0000
reviewersyulia
bugs1473923
milestone63.0a1
Bug 1473923 - Autocomplete console commands; r=yulia. MozReview-Commit-ID: LpbIzheFmeT
devtools/client/webconsole/components/JSTerm.js
devtools/client/webconsole/test/mochitest/browser.ini
devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_commands.js
devtools/server/actors/webconsole.js
devtools/server/actors/webconsole/commands.js
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -1168,20 +1168,20 @@ class JSTerm extends Component {
 
     // Check if last character is non-alphanumeric
     if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
       this._autocompleteQuery = null;
       this._autocompleteCache = null;
     }
     if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
       let filterBy = input;
-      // Find the last non-alphanumeric other than _ or $ if it exists.
-      const lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/);
+      // Find the last non-alphanumeric other than "_", ":", or "$" if it exists.
+      const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/);
       // If input contains non-alphanumerics, use the part after the last one
-      // to filter the cache
+      // to filter the cache.
       if (lastNonAlpha) {
         filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
       }
 
       const newList = cache.sort().filter(l => l.startsWith(filterBy));
 
       this.lastCompletion = {
         requestId: null,
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -180,16 +180,17 @@ skip-if = verify
 [browser_console_webconsole_ctrlw_close_tab.js]
 [browser_console_webconsole_iframe_messages.js]
 [browser_console_webconsole_private_browsing.js]
 [browser_jsterm_accessibility.js]
 [browser_jsterm_add_edited_input_to_history.js]
 [browser_jsterm_autocomplete_array_no_index.js]
 [browser_jsterm_autocomplete_arrow_keys.js]
 [browser_jsterm_autocomplete_cached_results.js]
+[browser_jsterm_autocomplete_commands.js]
 [browser_jsterm_autocomplete_crossdomain_iframe.js]
 [browser_jsterm_autocomplete_escape_key.js]
 [browser_jsterm_autocomplete_extraneous_closing_brackets.js]
 [browser_jsterm_autocomplete_helpers.js]
 [browser_jsterm_autocomplete_in_chrome_tab.js]
 [browser_jsterm_autocomplete_in_debugger_stackframe.js]
 [browser_jsterm_autocomplete_inside_text.js]
 [browser_jsterm_autocomplete_native_getters.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_commands.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that console commands are autocompleted.
+
+const TEST_URI = `data:text/html;charset=utf-8,Test command autocomplete`;
+
+add_task(async function() {
+  // Run test with legacy JsTerm
+  await performTests();
+  // And then run it with the CodeMirror-powered one.
+  await pushPref("devtools.webconsole.jsterm.codeMirror", true);
+  await performTests();
+});
+
+async function performTests() {
+  const { jsterm } = await openNewTabAndConsole(TEST_URI);
+  const { autocompletePopup } = jsterm;
+
+  const onPopUpOpen = autocompletePopup.once("popup-opened");
+
+  info(`Enter ":"`);
+  jsterm.focus();
+  EventUtils.sendString(":");
+
+  await onPopUpOpen;
+
+  const expectedCommands = [":help", ":screenshot"];
+  is(getPopupItems(autocompletePopup).join("\n"), expectedCommands.join("\n"),
+    "popup contains expected commands");
+
+  let onAutocompleUpdated = jsterm.once("autocomplete-updated");
+  EventUtils.sendString("s");
+  await onAutocompleUpdated;
+  checkJsTermCompletionValue(jsterm, "  creenshot",
+    "completion node has expected :screenshot value");
+
+  EventUtils.synthesizeKey("KEY_Tab");
+  is(jsterm.getInputValue(), ":screenshot", "Tab key correctly completed :screenshot");
+
+  ok(!autocompletePopup.isOpen, "popup is closed after Tab");
+
+  info("Test :hel completion");
+  jsterm.setInputValue(":he");
+  onAutocompleUpdated = jsterm.once("autocomplete-updated");
+  EventUtils.sendString("l");
+
+  await onAutocompleUpdated;
+  checkJsTermCompletionValue(jsterm, "    p", "completion node has expected :help value");
+
+  EventUtils.synthesizeKey("KEY_Tab");
+  is(jsterm.getInputValue(), ":help", "Tab key correctly completes :help");
+}
+
+function getPopupItems(popup) {
+  return popup.items.map(item => item.label);
+}
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -26,16 +26,17 @@ loader.lazyRequireGetter(this, "ConsoleP
 loader.lazyRequireGetter(this, "StackTraceCollector", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
 loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
 loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
 loader.lazyRequireGetter(this, "WebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "addWebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "formatCommand", "devtools/server/actors/webconsole/commands", true);
 loader.lazyRequireGetter(this, "isCommand", "devtools/server/actors/webconsole/commands", true);
+loader.lazyRequireGetter(this, "validCommands", "devtools/server/actors/webconsole/commands", true);
 loader.lazyRequireGetter(this, "CONSOLE_WORKER_IDS", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "WebConsoleUtils", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 
 // Overwrite implemented listeners for workers so that we don't attempt
 // to load an unsupported module.
 if (isWorker) {
@@ -1080,75 +1081,78 @@ WebConsoleActor.prototype =
    * @return object
    *         The response message - matched properties.
    */
   autocomplete: function(request) {
     const frameActorId = request.frameActor;
     let dbgObject = null;
     let environment = null;
     let hadDebuggee = false;
-
-    // This is the case of the paused debugger
-    if (frameActorId) {
-      const frameActor = this.conn.getActor(frameActorId);
-      try {
-        // Need to try/catch since accessing frame.environment
-        // can throw "Debugger.Frame is not live"
-        const frame = frameActor.frame;
-        environment = frame.environment;
-      } catch (e) {
-        DevToolsUtils.reportException("autocomplete",
-          Error("The frame actor was not found: " + frameActorId));
-      }
-    } else {
-      // This is the general case (non-paused debugger)
-      hadDebuggee = this.dbg.hasDebuggee(this.evalWindow);
-      dbgObject = this.dbg.addDebuggee(this.evalWindow);
-    }
-
-    const result = JSPropertyProvider(dbgObject, environment, request.text,
-                                    request.cursor, frameActorId) || {};
-
-    if (!hadDebuggee && dbgObject) {
-      this.dbg.removeDebuggee(this.evalWindow);
-    }
-
-    let matches = result.matches || [];
+    let matches = [];
+    let matchProp;
     const reqText = request.text.substr(0, request.cursor);
 
-    // We consider '$' as alphanumerc because it is used in the names of some
-    // helper functions.
-    const lastNonAlphaIsDot = /[.][a-zA-Z0-9$]*$/.test(reqText);
-    if (!lastNonAlphaIsDot) {
-      if (!this._webConsoleCommandsCache) {
-        const helpers = {
-          sandbox: Object.create(null)
-        };
-        addWebConsoleCommands(helpers);
-        this._webConsoleCommandsCache =
-          Object.getOwnPropertyNames(helpers.sandbox);
+    if (isCommand(reqText)) {
+      const commandsCache = this._getWebConsoleCommandsCache();
+      matchProp = reqText;
+      matches = validCommands
+        .filter(c => `:${c}`.startsWith(reqText)
+          && commandsCache.find(n => `:${n}`.startsWith(reqText))
+        )
+        .map(c => `:${c}`);
+    } else {
+      // This is the case of the paused debugger
+      if (frameActorId) {
+        const frameActor = this.conn.getActor(frameActorId);
+        try {
+          // Need to try/catch since accessing frame.environment
+          // can throw "Debugger.Frame is not live"
+          const frame = frameActor.frame;
+          environment = frame.environment;
+        } catch (e) {
+          DevToolsUtils.reportException("autocomplete",
+            Error("The frame actor was not found: " + frameActorId));
+        }
+      } else {
+        // This is the general case (non-paused debugger)
+        hadDebuggee = this.dbg.hasDebuggee(this.evalWindow);
+        dbgObject = this.dbg.addDebuggee(this.evalWindow);
       }
 
-      matches = matches.concat(this._webConsoleCommandsCache
-          .filter(n =>
-            // filter out `screenshot` command as it is inaccessible without
-            // the `:` prefix
-            n !== "screenshot" && n.startsWith(result.matchProp)
-          ));
+      const result = JSPropertyProvider(dbgObject, environment, request.text,
+                                      request.cursor, frameActorId) || {};
+
+      if (!hadDebuggee && dbgObject) {
+        this.dbg.removeDebuggee(this.evalWindow);
+      }
+
+      matches = result.matches || [];
+      matchProp = result.matchProp;
+
+      // We consider '$' as alphanumerc because it is used in the names of some
+      // helper functions.
+      const lastNonAlphaIsDot = /[.][a-zA-Z0-9$]*$/.test(reqText);
+      if (!lastNonAlphaIsDot) {
+        matches = matches.concat(this._getWebConsoleCommandsCache().filter(n =>
+          // filter out `screenshot` command as it is inaccessible without
+          // the `:` prefix
+          n !== "screenshot" && n.startsWith(result.matchProp)
+        ));
+      }
     }
 
     // Make sure we return an array with unique items, since `matches` can hold twice
     // the same function name if it was defined in the content page and match an helper
     // function (e.g. $, keys, …).
     matches = [...new Set(matches)].sort();
 
     return {
       from: this.actorID,
       matches,
-      matchProp: result.matchProp,
+      matchProp,
     };
   },
 
   /**
    * The "clearMessagesCache" request handler.
    */
   clearMessagesCache: function() {
     // TODO: Bug 717611 - Web Console clear button does not clear cached errors
@@ -1269,16 +1273,27 @@ WebConsoleActor.prototype =
         // Make sure the helpers can be used during eval.
         desc.value = debuggerGlobal.makeDebuggeeValue(desc.value);
       }
       Object.defineProperty(helpers.sandbox, name, desc);
     }
     return helpers;
   },
 
+  _getWebConsoleCommandsCache: function() {
+    if (!this._webConsoleCommandsCache) {
+      const helpers = {
+        sandbox: Object.create(null)
+      };
+      addWebConsoleCommands(helpers);
+      this._webConsoleCommandsCache = Object.getOwnPropertyNames(helpers.sandbox);
+    }
+    return this._webConsoleCommandsCache;
+  },
+
   /**
    * Evaluates a string using the debugger API.
    *
    * To allow the variables view to update properties from the Web Console we
    * provide the "bindObjectActor" mechanism: the Web Console tells the
    * ObjectActor ID for which it desires to evaluate an expression. The
    * Debugger.Object pointed at by the actor ID is bound such that it is
    * available during expression evaluation (executeInGlobalWithBindings()).
--- a/devtools/server/actors/webconsole/commands.js
+++ b/devtools/server/actors/webconsole/commands.js
@@ -231,8 +231,9 @@ function getTypedValue(value) {
   if (isStringChar(value[0])) {
     return value.slice(1, value.length - 1);
   }
   return value;
 }
 
 exports.formatCommand = formatCommand;
 exports.isCommand = isCommand;
+exports.validCommands = validCommands;