Bug 1464461 - experiemental rewrite of evalWithDebugger p1; r=nchevobbe draft
authoryulia <ystartsev@mozilla.com>
Tue, 26 Jun 2018 17:56:26 +0200
changeset 810833 68f4b3610c9440e0f5369f40111ed833d8296a32
parent 810786 1398940c020c69c62f16f09dadef3fae2fc76ec6
push id114129
push userbmo:ystartsev@mozilla.com
push dateTue, 26 Jun 2018 15:56:55 +0000
reviewersnchevobbe
bugs1464461
milestone62.0a1
Bug 1464461 - experiemental rewrite of evalWithDebugger p1; r=nchevobbe MozReview-Commit-ID: LBbFJGW4KPw
devtools/server/actors/webconsole.js
devtools/server/actors/webconsole/eval-with-debugger.js
devtools/server/actors/webconsole/moz.build
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -19,25 +19,23 @@ const DevToolsUtils = require("devtools/
 const ErrorDocs = require("devtools/server/actors/errordocs");
 
 loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "NetworkEventActor", "devtools/server/actors/network-event", true);
 loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true);
 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, "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, "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");
+loader.lazyRequireGetter(this, "evalWithDebugger", "devtools/server/actors/webconsole/eval-with-debugger", true);
 
 // Overwrite implemented listeners for workers so that we don't attempt
 // to load an unsupported module.
 if (isWorker) {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/worker-listeners", true);
   loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/worker-listeners", true);
 } else {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/listeners", true);
@@ -948,17 +946,17 @@ WebConsoleActor.prototype =
     const evalOptions = {
       bindObjectActor: request.bindObjectActor,
       frameActor: request.frameActor,
       url: request.url,
       selectedNodeActor: request.selectedNodeActor,
       selectedObjectActor: request.selectedObjectActor,
     };
 
-    const evalInfo = this.evalWithDebugger(input, evalOptions);
+    const evalInfo = evalWithDebugger(input, evalOptions, this);
     const evalResult = evalInfo.result;
     const helperResult = evalInfo.helperResult;
 
     let result, errorDocURL, errorMessage, errorNotes = null, errorGrip = null,
       frame = null;
     if (evalResult) {
       if ("return" in evalResult) {
         result = evalResult.return;
@@ -1251,318 +1249,16 @@ 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;
   },
 
-  /**
-   * 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()).
-   *
-   * Example:
-   *   _self['foobar'] = 'test'
-   * where |_self| refers to the desired object.
-   *
-   * The |frameActor| property allows the Web Console client to provide the
-   * frame actor ID, such that the expression can be evaluated in the
-   * user-selected stack frame.
-   *
-   * For the above to work we need the debugger and the Web Console to share
-   * a connection, otherwise the Web Console actor will not find the frame
-   * actor.
-   *
-   * The Debugger.Frame comes from the jsdebugger's Debugger instance, which
-   * is different from the Web Console's Debugger instance. This means that
-   * for evaluation to work, we need to create a new instance for the Web
-   * Console Commands helpers - they need to be Debugger.Objects coming from the
-   * jsdebugger's Debugger instance.
-   *
-   * When |bindObjectActor| is used objects can come from different iframes,
-   * from different domains. To avoid permission-related errors when objects
-   * come from a different window, we also determine the object's own global,
-   * such that evaluation happens in the context of that global. This means that
-   * evaluation will happen in the object's iframe, rather than the top level
-   * window.
-   *
-   * @param string string
-   *        String to evaluate.
-   * @param object [options]
-   *        Options for evaluation:
-   *        - bindObjectActor: the ObjectActor ID to use for evaluation.
-   *          |evalWithBindings()| will be called with one additional binding:
-   *          |_self| which will point to the Debugger.Object of the given
-   *          ObjectActor.
-   *        - selectedObjectActor: Like bindObjectActor, but executes with the
-   *          top level window as the global.
-   *        - frameActor: the FrameActor ID to use for evaluation. The given
-   *        debugger frame is used for evaluation, instead of the global window.
-   *        - selectedNodeActor: the NodeActor ID of the currently selected node
-   *        in the Inspector (or null, if there is no selection). This is used
-   *        for helper functions that make reference to the currently selected
-   *        node, like $0.
-   *         - url: the url to evaluate the script as. Defaults to
-   *         "debugger eval code".
-   * @return object
-   *         An object that holds the following properties:
-   *         - dbg: the debugger where the string was evaluated.
-   *         - frame: (optional) the frame where the string was evaluated.
-   *         - window: the Debugger.Object for the global where the string was
-   *         evaluated.
-   *         - result: the result of the evaluation.
-   *         - helperResult: any result coming from a Web Console commands
-   *         function.
-   */
-  /* eslint-disable complexity */
-  evalWithDebugger: function(string, options = {}) {
-    const trimmedString = string.trim();
-    // The help function needs to be easy to guess, so we make the () optional.
-    if (trimmedString == "help" || trimmedString == "?") {
-      string = "help()";
-    }
-
-    const isCmd = isCommand(string);
-    // we support Unix like syntax for commands if it is preceeded by `:`
-    if (isCmd) {
-      try {
-        string = formatCommand(string);
-      } catch (e) {
-        string = `throw "${e}"`;
-      }
-    }
-
-    // Add easter egg for console.mihai().
-    if (trimmedString == "console.mihai()" || trimmedString == "console.mihai();") {
-      string = "\"http://incompleteness.me/blog/2015/02/09/console-dot-mihai/\"";
-    }
-
-    // Find the Debugger.Frame of the given FrameActor.
-    let frame = null, frameActor = null;
-    if (options.frameActor) {
-      frameActor = this.conn.getActor(options.frameActor);
-      if (frameActor) {
-        frame = frameActor.frame;
-      } else {
-        DevToolsUtils.reportException("evalWithDebugger",
-          Error("The frame actor was not found: " + options.frameActor));
-      }
-    }
-
-    // If we've been given a frame actor in whose scope we should evaluate the
-    // expression, be sure to use that frame's Debugger (that is, the JavaScript
-    // debugger's Debugger) for the whole operation, not the console's Debugger.
-    // (One Debugger will treat a different Debugger's Debugger.Object instances
-    // as ordinary objects, not as references to be followed, so mixing
-    // debuggers causes strange behaviors.)
-    const dbg = frame ? frameActor.threadActor.dbg : this.dbg;
-    let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
-
-    // If we have an object to bind to |_self|, create a Debugger.Object
-    // referring to that object, belonging to dbg.
-    let bindSelf = null;
-    if (options.bindObjectActor || options.selectedObjectActor) {
-      const objActor = this.getActorByID(options.bindObjectActor ||
-                                       options.selectedObjectActor);
-      if (objActor) {
-        const jsVal = objActor.rawValue();
-
-        if (isObject(jsVal)) {
-          // If we use the makeDebuggeeValue method of jsVal's own global, then
-          // we'll get a D.O that sees jsVal as viewed from its own compartment -
-          // that is, without wrappers. The evalWithBindings call will then wrap
-          // jsVal appropriately for the evaluation compartment.
-          bindSelf = dbgWindow.makeDebuggeeValue(jsVal);
-          if (options.bindObjectActor) {
-            const global = Cu.getGlobalForObject(jsVal);
-            try {
-              const _dbgWindow = dbg.makeGlobalObjectReference(global);
-              dbgWindow = _dbgWindow;
-            } catch (err) {
-              // The above will throw if `global` is invisible to debugger.
-            }
-          }
-        } else {
-          bindSelf = jsVal;
-        }
-      }
-    }
-
-    // Get the Web Console commands for the given debugger window.
-    const helpers = this._getWebConsoleCommands(dbgWindow);
-    const bindings = helpers.sandbox;
-    if (bindSelf) {
-      bindings._self = bindSelf;
-    }
-
-    if (options.selectedNodeActor) {
-      const actor = this.conn.getActor(options.selectedNodeActor);
-      if (actor) {
-        helpers.selectedNode = actor.rawNode;
-      }
-    }
-
-    // Check if the Debugger.Frame or Debugger.Object for the global include
-    // $ or $$. We will not overwrite these functions with the Web Console
-    // commands.
-    let found$ = false, found$$ = false, foundScreenshot = false;
-    if (!isCmd) {
-      if (frame) {
-        const env = frame.environment;
-        if (env) {
-          found$ = !!env.find("$");
-          found$$ = !!env.find("$$");
-          foundScreenshot = !!env.find("screenshot");
-        }
-      } else {
-        found$ = !!dbgWindow.getOwnPropertyDescriptor("$");
-        found$$ = !!dbgWindow.getOwnPropertyDescriptor("$$");
-        foundScreenshot = true;
-      }
-    }
-
-    let $ = null, $$ = null, screenshot = null;
-    if (found$) {
-      $ = bindings.$;
-      delete bindings.$;
-    }
-    if (found$$) {
-      $$ = bindings.$$;
-      delete bindings.$$;
-    }
-    if (foundScreenshot) {
-      screenshot = bindings.screenshot;
-      delete bindings.screenshot;
-    }
-
-    // Ready to evaluate the string.
-    helpers.evalInput = string;
-
-    let evalOptions;
-    if (typeof options.url == "string") {
-      evalOptions = { url: options.url };
-    }
-
-    // If the debugger object is changed from the last evaluation,
-    // adopt this._lastConsoleInputEvaluation value in the new debugger,
-    // to prevents "Debugger.Object belongs to a different Debugger" exceptions
-    // related to the $_ bindings.
-    if (this._lastConsoleInputEvaluation &&
-        this._lastConsoleInputEvaluation.global !== dbgWindow) {
-      this._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue(
-        this._lastConsoleInputEvaluation
-      );
-    }
-
-    let result;
-
-    if (frame) {
-      result = frame.evalWithBindings(string, bindings, evalOptions);
-    } else {
-      result = dbgWindow.executeInGlobalWithBindings(string, bindings, evalOptions);
-      // Attempt to initialize any declarations found in the evaluated string
-      // since they may now be stuck in an "initializing" state due to the
-      // error. Already-initialized bindings will be ignored.
-      if ("throw" in result) {
-        let ast;
-        // Parse errors will raise an exception. We can/should ignore the error
-        // since it's already being handled elsewhere and we are only interested
-        // in initializing bindings.
-        try {
-          ast = Parser.reflectionAPI.parse(string);
-        } catch (ex) {
-          ast = {"body": []};
-        }
-        for (const line of ast.body) {
-          // Only let and const declarations put bindings into an
-          // "initializing" state.
-          if (!(line.kind == "let" || line.kind == "const")) {
-            continue;
-          }
-
-          const identifiers = [];
-          for (const decl of line.declarations) {
-            switch (decl.id.type) {
-              case "Identifier":
-                // let foo = bar;
-                identifiers.push(decl.id.name);
-                break;
-              case "ArrayPattern":
-                // let [foo, bar]    = [1, 2];
-                // let [foo=99, bar] = [1, 2];
-                for (const e of decl.id.elements) {
-                  if (e.type == "Identifier") {
-                    identifiers.push(e.name);
-                  } else if (e.type == "AssignmentExpression") {
-                    identifiers.push(e.left.name);
-                  }
-                }
-                break;
-              case "ObjectPattern":
-                // let {bilbo, my}    = {bilbo: "baggins", my: "precious"};
-                // let {blah: foo}    = {blah: yabba()}
-                // let {blah: foo=99} = {blah: yabba()}
-                for (const prop of decl.id.properties) {
-                  // key
-                  if (prop.key.type == "Identifier") {
-                    identifiers.push(prop.key.name);
-                  }
-                  // value
-                  if (prop.value.type == "Identifier") {
-                    identifiers.push(prop.value.name);
-                  } else if (prop.value.type == "AssignmentExpression") {
-                    identifiers.push(prop.value.left.name);
-                  }
-                }
-                break;
-            }
-          }
-
-          for (const name of identifiers) {
-            dbgWindow.forceLexicalInitializationByName(name);
-          }
-        }
-      }
-    }
-
-    const helperResult = helpers.helperResult;
-    delete helpers.evalInput;
-    delete helpers.helperResult;
-    delete helpers.selectedNode;
-
-    if ($) {
-      bindings.$ = $;
-    }
-    if ($$) {
-      bindings.$$ = $$;
-    }
-    if (screenshot) {
-      bindings.screenshot = screenshot;
-    }
-
-    if (bindings._self) {
-      delete bindings._self;
-    }
-
-    return {
-      result: result,
-      helperResult: helperResult,
-      dbg: dbg,
-      frame: frame,
-      window: dbgWindow,
-    };
-  },
-  /* eslint-enable complexity */
-
   // Event handlers for various listeners.
 
   /**
    * Handler for messages received from the ConsoleServiceListener. This method
    * sends the nsIConsoleMessage to the remote Web Console client.
    *
    * @param nsIConsoleMessage message
    *        The message we need to send to the client.
copy from devtools/server/actors/webconsole.js
copy to devtools/server/actors/webconsole/eval-with-debugger.js
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole/eval-with-debugger.js
@@ -3,1956 +3,348 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* global XPCNativeWrapper */
 
-const Services = require("Services");
-const { Cc, Ci, Cu } = require("chrome");
-const { DebuggerServer, ActorPool } = require("devtools/server/main");
-const { ThreadActor } = require("devtools/server/actors/thread");
-const { ObjectActor } = require("devtools/server/actors/object");
-const { LongStringActor } = require("devtools/server/actors/object/long-string");
-const { createValueGrip, stringIsLong } = require("devtools/server/actors/object/utils");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
-const ErrorDocs = require("devtools/server/actors/errordocs");
-
-loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true);
-loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true);
-loader.lazyRequireGetter(this, "NetworkEventActor", "devtools/server/actors/network-event", true);
-loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true);
-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, "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, "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) {
-  loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/worker-listeners", true);
-  loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/worker-listeners", true);
-} else {
-  loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/listeners", true);
-  loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/listeners", true);
-  loader.lazyRequireGetter(this, "ConsoleReflowListener", "devtools/server/actors/webconsole/listeners", true);
-  loader.lazyRequireGetter(this, "ContentProcessListener", "devtools/server/actors/webconsole/listeners", true);
-  loader.lazyRequireGetter(this, "DocumentEventsListener", "devtools/server/actors/webconsole/listeners", true);
-}
 
 function isObject(value) {
   return Object(value) === value;
 }
 
 /**
- * The WebConsoleActor implements capabilities needed for the Web Console
- * feature.
+ * 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()).
+ *
+ * Example:
+ *   _self['foobar'] = 'test'
+ * where |_self| refers to the desired object.
+ *
+ * The |frameActor| property allows the Web Console client to provide the
+ * frame actor ID, such that the expression can be evaluated in the
+ * user-selected stack frame.
+ *
+ * For the above to work we need the debugger and the Web Console to share
+ * a connection, otherwise the Web Console actor will not find the frame
+ * actor.
+ *
+ * The Debugger.Frame comes from the jsdebugger's Debugger instance, which
+ * is different from the Web Console's Debugger instance. This means that
+ * for evaluation to work, we need to create a new instance for the Web
+ * Console Commands helpers - they need to be Debugger.Objects coming from the
+ * jsdebugger's Debugger instance.
  *
- * @constructor
- * @param object connection
- *        The connection to the client, DebuggerServerConnection.
- * @param object [parentActor]
- *        Optional, the parent actor.
+ * When |bindObjectActor| is used objects can come from different iframes,
+ * from different domains. To avoid permission-related errors when objects
+ * come from a different window, we also determine the object's own global,
+ * such that evaluation happens in the context of that global. This means that
+ * evaluation will happen in the object's iframe, rather than the top level
+ * window.
+ *
+ * @param string string
+ *        String to evaluate.
+ * @param object [options]
+ *        Options for evaluation:
+ *        - bindObjectActor: the ObjectActor ID to use for evaluation.
+ *          |evalWithBindings()| will be called with one additional binding:
+ *          |_self| which will point to the Debugger.Object of the given
+ *          ObjectActor.
+ *        - selectedObjectActor: Like bindObjectActor, but executes with the
+ *          top level window as the global.
+ *        - frameActor: the FrameActor ID to use for evaluation. The given
+ *        debugger frame is used for evaluation, instead of the global window.
+ *        - selectedNodeActor: the NodeActor ID of the currently selected node
+ *        in the Inspector (or null, if there is no selection). This is used
+ *        for helper functions that make reference to the currently selected
+ *        node, like $0.
+ *         - url: the url to evaluate the script as. Defaults to
+ *         "debugger eval code".
+ * @return object
+ *         An object that holds the following properties:
+ *         - dbg: the debugger where the string was evaluated.
+ *         - frame: (optional) the frame where the string was evaluated.
+ *         - window: the Debugger.Object for the global where the string was
+ *         evaluated.
+ *         - result: the result of the evaluation.
+ *         - helperResult: any result coming from a Web Console commands
+ *         function.
  */
-function WebConsoleActor(connection, parentActor) {
-  this.conn = connection;
-  this.parentActor = parentActor;
-
-  this._actorPool = new ActorPool(this.conn);
-  this.conn.addActorPool(this._actorPool);
-
-  this._prefs = {};
-
-  this.dbg = this.parentActor.makeDebugger();
 
-  this._netEvents = new Map();
-  this._networkEventActorsByURL = new Map();
-  this._gripDepth = 0;
-  this._listeners = new Set();
-  this._lastConsoleInputEvaluation = undefined;
+exports.evalWithDebugger = function(string, options = {}, webConsole) {
+  dump(`\n\nHI\n\n`);
+  const evalString = getEvalInput(string);
+  const { frame, dbg } = getFrameDbg(options, webConsole);
+  const { dbgWindow, bindSelf } = getDbgWindow(options, dbg, webConsole);
+  const { helpers, cleanupHelpers } = getHelpers(dbgWindow, options, webConsole);
+  const { bindings, cleanupBindings } = bindCommands(
+    isCommand(string),
+    dbgWindow,
+    bindSelf,
+    frame,
+    helpers
+  );
+
+  // Ready to evaluate the string.
+  helpers.evalInput = string;
+  const evalOptions = typeof options.url === "string" ? { url: options.url } : null;
+
+  updateConsoleInputEvaluation(dbg, dbgWindow, webConsole);
 
-  this.objectGrip = this.objectGrip.bind(this);
-  this._onWillNavigate = this._onWillNavigate.bind(this);
-  this._onChangedToplevelDocument = this._onChangedToplevelDocument.bind(this);
-  EventEmitter.on(this.parentActor, "changed-toplevel-document",
-            this._onChangedToplevelDocument);
-  this._onObserverNotification = this._onObserverNotification.bind(this);
-  if (this.parentActor.isRootActor) {
-    Services.obs.addObserver(this._onObserverNotification,
-                             "last-pb-context-exited");
+  const result = getEvalResult(
+    evalString,
+    evalOptions,
+    bindings,
+    frame,
+    dbgWindow
+  );
+
+  const { helperResult } = helpers;
+
+  cleanupHelpers();
+  cleanupBindings();
+
+  return {
+    result,
+    helperResult,
+    dbg,
+    frame,
+    window: dbgWindow,
+  };
+};
+
+function getEvalResult(string, evalOptions, bindings, frame, dbgWindow) {
+  if (frame) {
+    return frame.evalWithBindings(string, bindings, evalOptions);
   }
-
-  this.traits = {
-    evaluateJSAsync: true,
-    transferredResponseSize: true,
-    selectedObjectActor: true, // 44+
-    fetchCacheDescriptor: true,
-  };
+  const result = dbgWindow.executeInGlobalWithBindings(string, bindings, evalOptions);
+  // Attempt to initialize any declarations found in the evaluated string
+  // since they may now be stuck in an "initializing" state due to the
+  // error. Already-initialized bindings will be ignored.
+  if ("throw" in result) {
+    parseErrorOutput(dbgWindow, string);
+  }
+  return result;
 }
 
-WebConsoleActor.prototype =
-{
-  /**
-   * Debugger instance.
-   *
-   * @see jsdebugger.jsm
-   */
-  dbg: null,
-
-  /**
-   * This is used by the ObjectActor to keep track of the depth of grip() calls.
-   * @private
-   * @type number
-   */
-  _gripDepth: null,
-
-  /**
-   * Actor pool for all of the actors we send to the client.
-   * @private
-   * @type object
-   * @see ActorPool
-   */
-  _actorPool: null,
-
-  /**
-   * Web Console-related preferences.
-   * @private
-   * @type object
-   */
-  _prefs: null,
-
-  /**
-   * Holds a map between nsIChannel objects and NetworkEventActors for requests
-   * created with sendHTTPRequest or found via the network listener.
-   *
-   * @private
-   * @type Map
-   */
-  _netEvents: null,
-
-  /**
-   * Holds a map from URL to NetworkEventActors for requests noticed by the network
-   * listener.  Requests are added when they start, so the actor might not yet have all
-   * data for the request until it has completed.
-   *
-   * @private
-   * @type Map
-   */
-  _networkEventActorsByURL: null,
-
-  /**
-   * Holds a set of all currently registered listeners.
-   *
-   * @private
-   * @type Set
-   */
-  _listeners: null,
-
-  /**
-   * The debugger server connection instance.
-   * @type object
-   */
-  conn: null,
-
-  /**
-   * List of supported features by the console actor.
-   * @type object
-   */
-  traits: null,
-
-  /**
-   * The window or sandbox we work with.
-   * Note that even if it is named `window` it refers to the current
-   * global we are debugging, which can be a Sandbox for addons
-   * or browser content toolbox.
-   *
-   * @type nsIDOMWindow or Sandbox
-   */
-  get window() {
-    if (this.parentActor.isRootActor) {
-      return this._getWindowForBrowserConsole();
-    }
-    return this.parentActor.window;
-  },
-
-  /**
-   * Get a window to use for the browser console.
-   *
-   * @private
-   * @return nsIDOMWindow
-   *         The window to use, or null if no window could be found.
-   */
-  _getWindowForBrowserConsole: function() {
-    // Check if our last used chrome window is still live.
-    let window = this._lastChromeWindow && this._lastChromeWindow.get();
-    // If not, look for a new one.
-    if (!window || window.closed) {
-      window = this.parentActor.window;
-      if (!window) {
-        // Try to find the Browser Console window to use instead.
-        window = Services.wm.getMostRecentWindow("devtools:webconsole");
-        // We prefer the normal chrome window over the console window,
-        // so we'll look for those windows in order to replace our reference.
-        const onChromeWindowOpened = () => {
-          // We'll look for this window when someone next requests window()
-          Services.obs.removeObserver(onChromeWindowOpened, "domwindowopened");
-          this._lastChromeWindow = null;
-        };
-        Services.obs.addObserver(onChromeWindowOpened, "domwindowopened");
-      }
-
-      this._handleNewWindow(window);
-    }
-
-    return window;
-  },
-
-  /**
-   * Store a newly found window on the actor to be used in the future.
-   *
-   * @private
-   * @param nsIDOMWindow window
-   *        The window to store on the actor (can be null).
-   */
-  _handleNewWindow: function(window) {
-    if (window) {
-      if (this._hadChromeWindow) {
-        Services.console.logStringMessage("Webconsole context has changed");
-      }
-      this._lastChromeWindow = Cu.getWeakReference(window);
-      this._hadChromeWindow = true;
-    } else {
-      this._lastChromeWindow = null;
-    }
-  },
-
-  /**
-   * Whether we've been using a window before.
-   *
-   * @private
-   * @type boolean
-   */
-  _hadChromeWindow: false,
-
-  /**
-   * A weak reference to the last chrome window we used to work with.
-   *
-   * @private
-   * @type nsIWeakReference
-   */
-  _lastChromeWindow: null,
-
-  // The evalWindow is used at the scope for JS evaluation.
-  _evalWindow: null,
-  get evalWindow() {
-    return this._evalWindow || this.window;
-  },
-
-  set evalWindow(window) {
-    this._evalWindow = window;
-
-    if (!this._progressListenerActive) {
-      EventEmitter.on(this.parentActor, "will-navigate", this._onWillNavigate);
-      this._progressListenerActive = true;
-    }
-  },
-
-  /**
-   * Flag used to track if we are listening for events from the progress
-   * listener of the target actor. We use the progress listener to clear
-   * this.evalWindow on page navigation.
-   *
-   * @private
-   * @type boolean
-   */
-  _progressListenerActive: false,
-
-  /**
-   * The ConsoleServiceListener instance.
-   * @type object
-   */
-  consoleServiceListener: null,
-
-  /**
-   * The ConsoleAPIListener instance.
-   */
-  consoleAPIListener: null,
-
-  /**
-   * The NetworkMonitor instance.
-   */
-  networkMonitor: null,
-
-  /**
-   * The NetworkMonitor instance living in the same (child) process.
-   */
-  networkMonitorChild: null,
-
-  /**
-   * The ConsoleProgressListener instance.
-   */
-  consoleProgressListener: null,
-
-  /**
-   * The ConsoleReflowListener instance.
-   */
-  consoleReflowListener: null,
-
-  /**
-   * The Web Console Commands names cache.
-   * @private
-   * @type array
-   */
-  _webConsoleCommandsCache: null,
-
-  typeName: "console",
-
-  get globalDebugObject() {
-    return this.parentActor.threadActor.globalDebugObject;
-  },
-
-  grip: function() {
-    return { actor: this.actorID };
-  },
-
-  hasNativeConsoleAPI: function(window) {
-    if (isWorker) {
-      // Can't use XPCNativeWrapper as a way to check for console API in workers
-      return true;
+function parseErrorOutput(dbgWindow, string) {
+  let ast;
+  // Parse errors will raise an exception. We can/should ignore the error
+  // since it's already being handled elsewhere and we are only interested
+  // in initializing bindings.
+  try {
+    ast = Parser.reflectionAPI.parse(string);
+  } catch (ex) {
+    return;
+  }
+  for (const line of ast.body) {
+    // Only let and const declarations put bindings into an
+    // "initializing" state.
+    if (!(line.kind == "let" || line.kind == "const")) {
+      continue;
     }
 
-    let isNative = false;
-    try {
-      // We are very explicitly examining the "console" property of
-      // the non-Xrayed object here.
-      const console = window.wrappedJSObject.console;
-      isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE;
-    } catch (ex) {
-      // ignored
-    }
-    return isNative;
-  },
-
-  _findProtoChain: ThreadActor.prototype._findProtoChain,
-  _removeFromProtoChain: ThreadActor.prototype._removeFromProtoChain,
-
-  /**
-   * Destroy the current WebConsoleActor instance.
-   */
-  destroy() {
-    if (this.consoleServiceListener) {
-      this.consoleServiceListener.destroy();
-      this.consoleServiceListener = null;
-    }
-    if (this.consoleAPIListener) {
-      this.consoleAPIListener.destroy();
-      this.consoleAPIListener = null;
-    }
-    if (this.networkMonitor) {
-      this.networkMonitor.destroy();
-      this.networkMonitor = null;
-    }
-    if (this.networkMonitorChild) {
-      this.networkMonitorChild.destroy();
-      this.networkMonitorChild = null;
-    }
-    if (this.stackTraceCollector) {
-      this.stackTraceCollector.destroy();
-      this.stackTraceCollector = null;
-    }
-    if (this.consoleProgressListener) {
-      this.consoleProgressListener.destroy();
-      this.consoleProgressListener = null;
-    }
-    if (this.consoleReflowListener) {
-      this.consoleReflowListener.destroy();
-      this.consoleReflowListener = null;
-    }
-    if (this.contentProcessListener) {
-      this.contentProcessListener.destroy();
-      this.contentProcessListener = null;
-    }
-
-    EventEmitter.off(this.parentActor, "changed-toplevel-document",
-               this._onChangedToplevelDocument);
-
-    this.conn.removeActorPool(this._actorPool);
-
-    if (this.parentActor.isRootActor) {
-      Services.obs.removeObserver(this._onObserverNotification,
-                                  "last-pb-context-exited");
-    }
-
-    this._actorPool = null;
-    this._webConsoleCommandsCache = null;
-    this._lastConsoleInputEvaluation = null;
-    this._evalWindow = null;
-    this._netEvents.clear();
-    this.dbg.enabled = false;
-    this.dbg = null;
-    this.conn = null;
-  },
-
-  /**
-   * Create and return an environment actor that corresponds to the provided
-   * Debugger.Environment. This is a straightforward clone of the ThreadActor's
-   * method except that it stores the environment actor in the web console
-   * actor's pool.
-   *
-   * @param Debugger.Environment environment
-   *        The lexical environment we want to extract.
-   * @return The EnvironmentActor for |environment| or |undefined| for host
-   *         functions or functions scoped to a non-debuggee global.
-   */
-  createEnvironmentActor: function(environment) {
-    if (!environment) {
-      return undefined;
-    }
-
-    if (environment.actor) {
-      return environment.actor;
-    }
-
-    const actor = new EnvironmentActor(environment, this);
-    this._actorPool.addActor(actor);
-    environment.actor = actor;
-
-    return actor;
-  },
-
-  /**
-   * Create a grip for the given value.
-   *
-   * @param mixed value
-   * @return object
-   */
-  createValueGrip: function(value) {
-    return createValueGrip(value, this._actorPool, this.objectGrip);
-  },
-
-  /**
-   * Make a debuggee value for the given value.
-   *
-   * @param mixed value
-   *        The value you want to get a debuggee value for.
-   * @param boolean useObjectGlobal
-   *        If |true| the object global is determined and added as a debuggee,
-   *        otherwise |this.window| is used when makeDebuggeeValue() is invoked.
-   * @return object
-   *         Debuggee value for |value|.
-   */
-  makeDebuggeeValue: function(value, useObjectGlobal) {
-    if (useObjectGlobal && isObject(value)) {
-      try {
-        const global = Cu.getGlobalForObject(value);
-        const dbgGlobal = this.dbg.makeGlobalObjectReference(global);
-        return dbgGlobal.makeDebuggeeValue(value);
-      } catch (ex) {
-        // The above can throw an exception if value is not an actual object
-        // or 'Object in compartment marked as invisible to Debugger'
-      }
-    }
-    const dbgGlobal = this.dbg.makeGlobalObjectReference(this.window);
-    return dbgGlobal.makeDebuggeeValue(value);
-  },
-
-  /**
-   * Create a grip for the given object.
-   *
-   * @param object object
-   *        The object you want.
-   * @param object pool
-   *        An ActorPool where the new actor instance is added.
-   * @param object
-   *        The object grip.
-   */
-  objectGrip: function(object, pool) {
-    const actor = new ObjectActor(object, {
-      getGripDepth: () => this._gripDepth,
-      incrementGripDepth: () => this._gripDepth++,
-      decrementGripDepth: () => this._gripDepth--,
-      createValueGrip: v => this.createValueGrip(v),
-      sources: () => DevToolsUtils.reportException("WebConsoleActor",
-        Error("sources not yet implemented")),
-      createEnvironmentActor: (env) => this.createEnvironmentActor(env),
-      getGlobalDebugObject: () => this.globalDebugObject
-    });
-    pool.addActor(actor);
-    return actor.grip();
-  },
-
-  /**
-   * Create a grip for the given string.
-   *
-   * @param string string
-   *        The string you want to create the grip for.
-   * @param object pool
-   *        An ActorPool where the new actor instance is added.
-   * @return object
-   *         A LongStringActor object that wraps the given string.
-   */
-  longStringGrip: function(string, pool) {
-    const actor = new LongStringActor(string);
-    pool.addActor(actor);
-    return actor.grip();
-  },
-
-  /**
-   * Create a long string grip if needed for the given string.
-   *
-   * @private
-   * @param string string
-   *        The string you want to create a long string grip for.
-   * @return string|object
-   *         A string is returned if |string| is not a long string.
-   *         A LongStringActor grip is returned if |string| is a long string.
-   */
-  _createStringGrip: function(string) {
-    if (string && stringIsLong(string)) {
-      return this.longStringGrip(string, this._actorPool);
-    }
-    return string;
-  },
-
-  /**
-   * Get an object actor by its ID.
-   *
-   * @param string actorID
-   * @return object
-   */
-  getActorByID: function(actorID) {
-    return this._actorPool.get(actorID);
-  },
-
-  /**
-   * Release an actor.
-   *
-   * @param object actor
-   *        The actor instance you want to release.
-   */
-  releaseActor: function(actor) {
-    this._actorPool.removeActor(actor);
-  },
-
-  /**
-   * Returns the latest web console input evaluation.
-   * This is undefined if no evaluations have been completed.
-   *
-   * @return object
-   */
-  getLastConsoleInputEvaluation: function() {
-    return this._lastConsoleInputEvaluation;
-  },
-
-  /**
-   * This helper is used by the WebExtensionInspectedWindowActor to
-   * inspect an object in the developer toolbox.
-   */
-  inspectObject(dbgObj, inspectFromAnnotation) {
-    this.conn.sendActorEvent(this.actorID, "inspectObject", {
-      objectActor: this.createValueGrip(dbgObj),
-      inspectFromAnnotation,
-    });
-  },
-
-  // Request handlers for known packet types.
-
-  /**
-   * Handler for the "startListeners" request.
-   *
-   * @param object request
-   *        The JSON request object received from the Web Console client.
-   * @return object
-   *         The response object which holds the startedListeners array.
-   */
-  startListeners: function(request) {
-    const startedListeners = [];
-    const window = !this.parentActor.isRootActor ? this.window : null;
-    let messageManager = null;
-
-    // Check if the actor is running in a child process (but only if
-    // Services.appinfo exists, to prevent startListeners to fail
-    // when the target is a Worker).
-    const processBoundary = Services.appinfo && (
-      Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
-    );
-
-    // Retrieve a message manager from the parent actor if this actor is
-    // not currently running in the main process.
-    if (processBoundary) {
-      messageManager = this.parentActor.messageManager;
-    }
-
-    while (request.listeners.length > 0) {
-      const listener = request.listeners.shift();
-      switch (listener) {
-        case "PageError":
-          // Workers don't support this message type yet
-          if (isWorker) {
-            break;
-          }
-          if (!this.consoleServiceListener) {
-            this.consoleServiceListener =
-              new ConsoleServiceListener(window, this);
-            this.consoleServiceListener.init();
-          }
-          startedListeners.push(listener);
+    const identifiers = [];
+    for (const decl of line.declarations) {
+      switch (decl.id.type) {
+        case "Identifier":
+          // let foo = bar;
+          identifiers.push(decl.id.name);
           break;
-        case "ConsoleAPI":
-          if (!this.consoleAPIListener) {
-            // Create the consoleAPIListener
-            // (and apply the filtering options defined in the parent actor).
-            this.consoleAPIListener = new ConsoleAPIListener(
-              window, this, this.parentActor.consoleAPIListenerOptions);
-            this.consoleAPIListener.init();
-          }
-          startedListeners.push(listener);
-          break;
-        case "NetworkActivity":
-          // Workers don't support this message type
-          if (isWorker) {
-            break;
-          }
-          if (!this.networkMonitor) {
-            // Create a StackTraceCollector that's going to be shared both by
-            // the NetworkMonitorChild (getting messages about requests from
-            // parent) and by the NetworkMonitor that directly watches service
-            // workers requests.
-            this.stackTraceCollector = new StackTraceCollector({ window });
-            this.stackTraceCollector.init();
-
-            if (messageManager && processBoundary) {
-              // Start a network monitor in the parent process to listen to
-              // most requests than happen in parent
-              this.networkMonitor =
-                new NetworkMonitorChild(this.parentActor.outerWindowID,
-                                        messageManager, this.conn, this);
-              this.networkMonitor.init();
-              // Spawn also one in the child to listen to service workers
-              this.networkMonitorChild = new NetworkMonitor({ window }, this);
-              this.networkMonitorChild.init();
-            } else {
-              this.networkMonitor = new NetworkMonitor({ window }, this);
-              this.networkMonitor.init();
+        case "ArrayPattern":
+          // let [foo, bar]    = [1, 2];
+          // let [foo=99, bar] = [1, 2];
+          for (const e of decl.id.elements) {
+            if (e.type == "Identifier") {
+              identifiers.push(e.name);
+            } else if (e.type == "AssignmentExpression") {
+              identifiers.push(e.left.name);
             }
           }
-          startedListeners.push(listener);
           break;
-        case "FileActivity":
-          // Workers don't support this message type
-          if (isWorker) {
-            break;
-          }
-          if (this.window instanceof Ci.nsIDOMWindow) {
-            if (!this.consoleProgressListener) {
-              this.consoleProgressListener =
-                new ConsoleProgressListener(this.window, this);
+        case "ObjectPattern":
+          // let {bilbo, my}    = {bilbo: "baggins", my: "precious"};
+          // let {blah: foo}    = {blah: yabba()}
+          // let {blah: foo=99} = {blah: yabba()}
+          for (const prop of decl.id.properties) {
+            // key
+            if (prop.key.type == "Identifier") {
+              identifiers.push(prop.key.name);
             }
-            this.consoleProgressListener.startMonitor(this.consoleProgressListener
-                                                      .MONITOR_FILE_ACTIVITY);
-            startedListeners.push(listener);
-          }
-          break;
-        case "ReflowActivity":
-          // Workers don't support this message type
-          if (isWorker) {
-            break;
+            // value
+            if (prop.value.type == "Identifier") {
+              identifiers.push(prop.value.name);
+            } else if (prop.value.type == "AssignmentExpression") {
+              identifiers.push(prop.value.left.name);
+            }
           }
-          if (!this.consoleReflowListener) {
-            this.consoleReflowListener =
-              new ConsoleReflowListener(this.window, this);
-          }
-          startedListeners.push(listener);
-          break;
-        case "ContentProcessMessages":
-          // Workers don't support this message type
-          if (isWorker) {
-            break;
-          }
-          if (!this.contentProcessListener) {
-            this.contentProcessListener = new ContentProcessListener(this);
-          }
-          startedListeners.push(listener);
-          break;
-        case "DocumentEvents":
-          // Workers don't support this message type
-          if (isWorker) {
-            break;
-          }
-          if (!this.documentEventsListener) {
-            this.documentEventsListener = new DocumentEventsListener(this);
-          }
-          startedListeners.push(listener);
           break;
       }
     }
 
-    // Update the live list of running listeners
-    startedListeners.forEach(this._listeners.add, this._listeners);
-
-    return {
-      startedListeners: startedListeners,
-      nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
-      traits: this.traits,
-    };
-  },
-
-  /**
-   * Handler for the "stopListeners" request.
-   *
-   * @param object request
-   *        The JSON request object received from the Web Console client.
-   * @return object
-   *         The response packet to send to the client: holds the
-   *         stoppedListeners array.
-   */
-  stopListeners: function(request) {
-    const stoppedListeners = [];
-
-    // If no specific listeners are requested to be detached, we stop all
-    // listeners.
-    const toDetach = request.listeners ||
-      ["PageError", "ConsoleAPI", "NetworkActivity",
-       "FileActivity", "ContentProcessMessages"];
-
-    while (toDetach.length > 0) {
-      const listener = toDetach.shift();
-      switch (listener) {
-        case "PageError":
-          if (this.consoleServiceListener) {
-            this.consoleServiceListener.destroy();
-            this.consoleServiceListener = null;
-          }
-          stoppedListeners.push(listener);
-          break;
-        case "ConsoleAPI":
-          if (this.consoleAPIListener) {
-            this.consoleAPIListener.destroy();
-            this.consoleAPIListener = null;
-          }
-          stoppedListeners.push(listener);
-          break;
-        case "NetworkActivity":
-          if (this.networkMonitor) {
-            this.networkMonitor.destroy();
-            this.networkMonitor = null;
-          }
-          if (this.networkMonitorChild) {
-            this.networkMonitorChild.destroy();
-            this.networkMonitorChild = null;
-          }
-          if (this.stackTraceCollector) {
-            this.stackTraceCollector.destroy();
-            this.stackTraceCollector = null;
-          }
-          stoppedListeners.push(listener);
-          break;
-        case "FileActivity":
-          if (this.consoleProgressListener) {
-            this.consoleProgressListener.stopMonitor(this.consoleProgressListener
-                                                     .MONITOR_FILE_ACTIVITY);
-            this.consoleProgressListener = null;
-          }
-          stoppedListeners.push(listener);
-          break;
-        case "ReflowActivity":
-          if (this.consoleReflowListener) {
-            this.consoleReflowListener.destroy();
-            this.consoleReflowListener = null;
-          }
-          stoppedListeners.push(listener);
-          break;
-        case "ContentProcessMessages":
-          if (this.contentProcessListener) {
-            this.contentProcessListener.destroy();
-            this.contentProcessListener = null;
-          }
-          stoppedListeners.push(listener);
-          break;
-        case "DocumentEvents":
-          if (this.documentEventsListener) {
-            this.documentEventsListener.destroy();
-            this.documentEventsListener = null;
-          }
-          stoppedListeners.push(listener);
-          break;
-      }
+    for (const name of identifiers) {
+      dbgWindow.forceLexicalInitializationByName(name);
     }
-
-    // Update the live list of running listeners
-    stoppedListeners.forEach(this._listeners.delete, this._listeners);
-
-    return { stoppedListeners: stoppedListeners };
-  },
-
-  /**
-   * Handler for the "getCachedMessages" request. This method sends the cached
-   * error messages and the window.console API calls to the client.
-   *
-   * @param object request
-   *        The JSON request object received from the Web Console client.
-   * @return object
-   *         The response packet to send to the client: it holds the cached
-   *         messages array.
-   */
-  getCachedMessages: function(request) {
-    const types = request.messageTypes;
-    if (!types) {
-      return {
-        error: "missingParameter",
-        message: "The messageTypes parameter is missing.",
-      };
-    }
-
-    const messages = [];
-
-    while (types.length > 0) {
-      const type = types.shift();
-      switch (type) {
-        case "ConsoleAPI": {
-          if (!this.consoleAPIListener) {
-            break;
-          }
-
-          // See `window` definition. It isn't always a DOM Window.
-          const winStartTime = this.window && this.window.performance ?
-            this.window.performance.timing.navigationStart : 0;
-
-          const cache = this.consoleAPIListener
-                      .getCachedMessages(!this.parentActor.isRootActor);
-          cache.forEach((cachedMessage) => {
-            // Filter out messages that came from a ServiceWorker but happened
-            // before the page was requested.
-            if (cachedMessage.innerID === "ServiceWorker" &&
-                winStartTime > cachedMessage.timeStamp) {
-              return;
-            }
+  }
+}
 
-            const message = this.prepareConsoleMessageForRemote(cachedMessage);
-            message._type = type;
-            messages.push(message);
-          });
-          break;
-        }
-        case "PageError": {
-          if (!this.consoleServiceListener) {
-            break;
-          }
-          const cache = this.consoleServiceListener
-                      .getCachedMessages(!this.parentActor.isRootActor);
-          cache.forEach((cachedMessage) => {
-            let message = null;
-            if (cachedMessage instanceof Ci.nsIScriptError) {
-              message = this.preparePageErrorForRemote(cachedMessage);
-              message._type = type;
-            } else {
-              message = {
-                _type: "LogMessage",
-                message: this._createStringGrip(cachedMessage.message),
-                timeStamp: cachedMessage.timeStamp,
-              };
-            }
-            messages.push(message);
-          });
-          break;
-        }
-      }
-    }
-
-    return {
-      from: this.actorID,
-      messages: messages,
-    };
-  },
-
-  /**
-   * Handler for the "evaluateJSAsync" request. This method evaluates the given
-   * JavaScript string and sends back a packet with a unique ID.
-   * The result will be returned later as an unsolicited `evaluationResult`,
-   * that can be associated back to this request via the `resultID` field.
-   *
-   * @param object request
-   *        The JSON request object received from the Web Console client.
-   * @return object
-   *         The response packet to send to with the unique id in the
-   *         `resultID` field.
-   */
-  evaluateJSAsync: function(request) {
-    // We want to be able to run console commands without waiting
-    // for the first to return (see Bug 1088861).
-
-    // First, send a response packet with the id only.
-    const resultID = Date.now();
-    this.conn.send({
-      from: this.actorID,
-      resultID: resultID
-    });
-
-    // Then, execute the script that may pause.
-    const response = this.evaluateJS(request);
-    response.resultID = resultID;
-
-    this._waitForHelperResultAndSend(response);
-  },
-
-  /**
-   * In order to have asynchornous commands such as screenshot, we have to be
-   * able to handle promises in the helper result. This method handles waiting
-   * for the promise, and then dispatching the result
-   *
-   *
-   * @private
-   * @param object response
-   *         The response packet to send to with the unique id in the
-   *         `resultID` field, and potentially a promise in the helperResult
-   *         field.
-   *
-   * @return object
-   *         The response packet to send to with the unique id in the
-   *         `resultID` field, with a sanitized helperResult field.
-   */
-  _waitForHelperResultAndSend: async function(response) {
-    // wait for asynchronous command completion before sending back the response
-    if (
-      response.helperResult &&
-      typeof response.helperResult.then == "function"
-    ) {
-      response.helperResult = await response.helperResult;
-    }
-
-    // Finally, send an unsolicited evaluationResult packet with
-    // the normal return value
-    this.conn.sendActorEvent(this.actorID, "evaluationResult", response);
-  },
-
-  /**
-   * Handler for the "evaluateJS" request. This method evaluates the given
-   * JavaScript string and sends back the result.
-   *
-   * @param object request
-   *        The JSON request object received from the Web Console client.
-   * @return object
-   *         The evaluation response packet.
-   */
-  evaluateJS: function(request) {
-    const input = request.text;
-    const timestamp = Date.now();
-
-    const evalOptions = {
-      bindObjectActor: request.bindObjectActor,
-      frameActor: request.frameActor,
-      url: request.url,
-      selectedNodeActor: request.selectedNodeActor,
-      selectedObjectActor: request.selectedObjectActor,
-    };
-
-    const evalInfo = this.evalWithDebugger(input, evalOptions);
-    const evalResult = evalInfo.result;
-    const helperResult = evalInfo.helperResult;
-
-    let result, errorDocURL, errorMessage, errorNotes = null, errorGrip = null,
-      frame = null;
-    if (evalResult) {
-      if ("return" in evalResult) {
-        result = evalResult.return;
-      } else if ("yield" in evalResult) {
-        result = evalResult.yield;
-      } else if ("throw" in evalResult) {
-        const error = evalResult.throw;
-
-        errorGrip = this.createValueGrip(error);
-
-        errorMessage = String(error);
-        if (typeof error === "object" && error !== null) {
-          try {
-            errorMessage = DevToolsUtils.callPropertyOnObject(error, "toString");
-          } catch (e) {
-            // If the debuggee is not allowed to access the "toString" property
-            // of the error object, calling this property from the debuggee's
-            // compartment will fail. The debugger should show the error object
-            // as it is seen by the debuggee, so this behavior is correct.
-            //
-            // Unfortunately, we have at least one test that assumes calling the
-            // "toString" property of an error object will succeed if the
-            // debugger is allowed to access it, regardless of whether the
-            // debuggee is allowed to access it or not.
-            //
-            // To accomodate these tests, if calling the "toString" property
-            // from the debuggee compartment fails, we rewrap the error object
-            // in the debugger's compartment, and then call the "toString"
-            // property from there.
-            if (typeof error.unsafeDereference === "function") {
-              errorMessage = error.unsafeDereference().toString();
-            }
-          }
-        }
-
-        // It is possible that we won't have permission to unwrap an
-        // object and retrieve its errorMessageName.
-        try {
-          errorDocURL = ErrorDocs.GetURL(error);
-        } catch (ex) {
-          // ignored
-        }
-
-        try {
-          const line = error.errorLineNumber;
-          const column = error.errorColumnNumber;
-
-          if (typeof line === "number" && typeof column === "number") {
-            // Set frame only if we have line/column numbers.
-            frame = {
-              source: "debugger eval code",
-              line,
-              column
-            };
-          }
-        } catch (ex) {
-          // ignored
-        }
+function updateConsoleInputEvaluation(dbg, dbgWindow, webConsole) {
+  // If the debugger object is changed from the last evaluation,
+  // adopt webConsole._lastConsoleInputEvaluation value in the new debugger,
+  // to prevents "Debugger.Object belongs to a different Debugger" exceptions
+  // related to the $_ bindings.
+  if (webConsole._lastConsoleInputEvaluation &&
+      webConsole._lastConsoleInputEvaluation.global !== dbgWindow) {
+    webConsole._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue(
+      webConsole._lastConsoleInputEvaluation
+    );
+  }
+}
 
-        try {
-          const notes = error.errorNotes;
-          if (notes && notes.length) {
-            errorNotes = [];
-            for (const note of notes) {
-              errorNotes.push({
-                messageBody: this._createStringGrip(note.message),
-                frame: {
-                  source: note.fileName,
-                  line: note.lineNumber,
-                  column: note.columnNumber,
-                }
-              });
-            }
-          }
-        } catch (ex) {
-          // ignored
-        }
-      }
-    }
-
-    // If a value is encountered that the debugger server doesn't support yet,
-    // the console should remain functional.
-    let resultGrip;
-    try {
-      resultGrip = this.createValueGrip(result);
-    } catch (e) {
-      errorMessage = e;
-    }
-
-    this._lastConsoleInputEvaluation = result;
-
-    return {
-      from: this.actorID,
-      input: input,
-      result: resultGrip,
-      timestamp: timestamp,
-      exception: errorGrip,
-      exceptionMessage: this._createStringGrip(errorMessage),
-      exceptionDocURL: errorDocURL,
-      frame,
-      helperResult: helperResult,
-      notes: errorNotes,
-    };
-  },
-
-  /**
-   * The Autocomplete request handler.
-   *
-   * @param object request
-   *        The request message - what input to autocomplete.
-   * @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 || [];
-    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);
-      }
-      matches = matches.concat(this._webConsoleCommandsCache
-          .filter(n => n.startsWith(result.matchProp)));
-    }
-
-    return {
-      from: this.actorID,
-      matches: matches.sort(),
-      matchProp: result.matchProp,
-    };
-  },
-
-  /**
-   * The "clearMessagesCache" request handler.
-   */
-  clearMessagesCache: function() {
-    // TODO: Bug 717611 - Web Console clear button does not clear cached errors
-    const windowId = !this.parentActor.isRootActor ?
-                   WebConsoleUtils.getInnerWindowId(this.window) : null;
-    const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
-                              .getService(Ci.nsIConsoleAPIStorage);
-    ConsoleAPIStorage.clearEvents(windowId);
-
-    CONSOLE_WORKER_IDS.forEach((id) => {
-      ConsoleAPIStorage.clearEvents(id);
-    });
-
-    if (this.parentActor.isRootActor) {
-      Services.console.logStringMessage(null); // for the Error Console
-      Services.console.reset();
-    }
-    return {};
-  },
-
-  /**
-   * The "getPreferences" request handler.
-   *
-   * @param object request
-   *        The request message - which preferences need to be retrieved.
-   * @return object
-   *         The response message - a { key: value } object map.
-   */
-  getPreferences: function(request) {
-    const prefs = Object.create(null);
-    for (const key of request.preferences) {
-      prefs[key] = this._prefs[key];
-    }
-    return { preferences: prefs };
-  },
-
-  /**
-   * The "setPreferences" request handler.
-   *
-   * @param object request
-   *        The request message - which preferences need to be updated.
-   */
-  setPreferences: function(request) {
-    for (const key in request.preferences) {
-      this._prefs[key] = request.preferences[key];
+function getEvalInput(string) {
+  const trimmedString = string.trim();
+  // The help function needs to be easy to guess, so we make the () optional.
+  if (trimmedString === "help" || trimmedString === "?") {
+    return "help()";
+  }
 
-      if (this.networkMonitor) {
-        if (key == "NetworkMonitor.saveRequestAndResponseBodies") {
-          this.networkMonitor.saveRequestAndResponseBodies = this._prefs[key];
-          if (this.networkMonitorChild) {
-            this.networkMonitorChild.saveRequestAndResponseBodies =
-              this._prefs[key];
-          }
-        } else if (key == "NetworkMonitor.throttleData") {
-          this.networkMonitor.throttleData = this._prefs[key];
-          if (this.networkMonitorChild) {
-            this.networkMonitorChild.throttleData = this._prefs[key];
-          }
-        }
-      }
+  // we support Unix like syntax for commands if it is preceeded by `:`
+  if (isCommand(string)) {
+    try {
+      return formatCommand(string);
+    } catch (e) {
+      console.log(e);
+      return `throw "${e}"`;
     }
-    return { updated: Object.keys(request.preferences) };
-  },
-
-  // End of request handlers.
-
-  /**
-   * Create an object with the API we expose to the Web Console during
-   * JavaScript evaluation.
-   * This object inherits properties and methods from the Web Console actor.
-   *
-   * @private
-   * @param object debuggerGlobal
-   *        A Debugger.Object that wraps a content global. This is used for the
-   *        Web Console Commands.
-   * @return object
-   *         The same object as |this|, but with an added |sandbox| property.
-   *         The sandbox holds methods and properties that can be used as
-   *         bindings during JS evaluation.
-   */
-  _getWebConsoleCommands: function(debuggerGlobal) {
-    const helpers = {
-      window: this.evalWindow,
-      chromeWindow: this.chromeWindow.bind(this),
-      makeDebuggeeValue: debuggerGlobal.makeDebuggeeValue.bind(debuggerGlobal),
-      createValueGrip: this.createValueGrip.bind(this),
-      sandbox: Object.create(null),
-      helperResult: null,
-      consoleActor: this,
-    };
-    addWebConsoleCommands(helpers);
-
-    const evalWindow = this.evalWindow;
-    function maybeExport(obj, name) {
-      if (typeof obj[name] != "function") {
-        return;
-      }
-
-      // By default, chrome-implemented functions that are exposed to content
-      // refuse to accept arguments that are cross-origin for the caller. This
-      // is generally the safe thing, but causes problems for certain console
-      // helpers like cd(), where we users sometimes want to pass a cross-origin
-      // window. To circumvent this restriction, we use exportFunction along
-      // with a special option designed for this purpose. See bug 1051224.
-      obj[name] =
-        Cu.exportFunction(obj[name], evalWindow, { allowCrossOriginArguments: true });
-    }
-    for (const name in helpers.sandbox) {
-      const desc = Object.getOwnPropertyDescriptor(helpers.sandbox, name);
-
-      // Workers don't have access to Cu so won't be able to exportFunction.
-      if (!isWorker) {
-        maybeExport(desc, "get");
-        maybeExport(desc, "set");
-        maybeExport(desc, "value");
-      }
-      if (desc.value) {
-        // Make sure the helpers can be used during eval.
-        desc.value = debuggerGlobal.makeDebuggeeValue(desc.value);
-      }
-      Object.defineProperty(helpers.sandbox, name, desc);
-    }
-    return helpers;
-  },
+  }
 
-  /**
-   * 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()).
-   *
-   * Example:
-   *   _self['foobar'] = 'test'
-   * where |_self| refers to the desired object.
-   *
-   * The |frameActor| property allows the Web Console client to provide the
-   * frame actor ID, such that the expression can be evaluated in the
-   * user-selected stack frame.
-   *
-   * For the above to work we need the debugger and the Web Console to share
-   * a connection, otherwise the Web Console actor will not find the frame
-   * actor.
-   *
-   * The Debugger.Frame comes from the jsdebugger's Debugger instance, which
-   * is different from the Web Console's Debugger instance. This means that
-   * for evaluation to work, we need to create a new instance for the Web
-   * Console Commands helpers - they need to be Debugger.Objects coming from the
-   * jsdebugger's Debugger instance.
-   *
-   * When |bindObjectActor| is used objects can come from different iframes,
-   * from different domains. To avoid permission-related errors when objects
-   * come from a different window, we also determine the object's own global,
-   * such that evaluation happens in the context of that global. This means that
-   * evaluation will happen in the object's iframe, rather than the top level
-   * window.
-   *
-   * @param string string
-   *        String to evaluate.
-   * @param object [options]
-   *        Options for evaluation:
-   *        - bindObjectActor: the ObjectActor ID to use for evaluation.
-   *          |evalWithBindings()| will be called with one additional binding:
-   *          |_self| which will point to the Debugger.Object of the given
-   *          ObjectActor.
-   *        - selectedObjectActor: Like bindObjectActor, but executes with the
-   *          top level window as the global.
-   *        - frameActor: the FrameActor ID to use for evaluation. The given
-   *        debugger frame is used for evaluation, instead of the global window.
-   *        - selectedNodeActor: the NodeActor ID of the currently selected node
-   *        in the Inspector (or null, if there is no selection). This is used
-   *        for helper functions that make reference to the currently selected
-   *        node, like $0.
-   *         - url: the url to evaluate the script as. Defaults to
-   *         "debugger eval code".
-   * @return object
-   *         An object that holds the following properties:
-   *         - dbg: the debugger where the string was evaluated.
-   *         - frame: (optional) the frame where the string was evaluated.
-   *         - window: the Debugger.Object for the global where the string was
-   *         evaluated.
-   *         - result: the result of the evaluation.
-   *         - helperResult: any result coming from a Web Console commands
-   *         function.
-   */
-  /* eslint-disable complexity */
-  evalWithDebugger: function(string, options = {}) {
-    const trimmedString = string.trim();
-    // The help function needs to be easy to guess, so we make the () optional.
-    if (trimmedString == "help" || trimmedString == "?") {
-      string = "help()";
-    }
+  // Add easter egg for console.mihai().
+  if (trimmedString == "console.mihai()" || trimmedString == "console.mihai();") {
+    return "\"http://incompleteness.me/blog/2015/02/09/console-dot-mihai/\"";
+  }
+  return string;
+}
 
-    const isCmd = isCommand(string);
-    // we support Unix like syntax for commands if it is preceeded by `:`
-    if (isCmd) {
-      try {
-        string = formatCommand(string);
-      } catch (e) {
-        string = `throw "${e}"`;
-      }
-    }
-
-    // Add easter egg for console.mihai().
-    if (trimmedString == "console.mihai()" || trimmedString == "console.mihai();") {
-      string = "\"http://incompleteness.me/blog/2015/02/09/console-dot-mihai/\"";
-    }
-
-    // Find the Debugger.Frame of the given FrameActor.
-    let frame = null, frameActor = null;
-    if (options.frameActor) {
-      frameActor = this.conn.getActor(options.frameActor);
-      if (frameActor) {
-        frame = frameActor.frame;
-      } else {
-        DevToolsUtils.reportException("evalWithDebugger",
-          Error("The frame actor was not found: " + options.frameActor));
-      }
-    }
-
+function getFrameDbg(options, webConsole) {
+  if (!options.frameActor) {
+    return { frame: null, dbg: webConsole.dbg };
+  }
+  // Find the Debugger.Frame of the given FrameActor.
+  const frameActor = webConsole.conn.getActor(options.frameActor);
+  if (frameActor) {
     // If we've been given a frame actor in whose scope we should evaluate the
     // expression, be sure to use that frame's Debugger (that is, the JavaScript
     // debugger's Debugger) for the whole operation, not the console's Debugger.
     // (One Debugger will treat a different Debugger's Debugger.Object instances
     // as ordinary objects, not as references to be followed, so mixing
     // debuggers causes strange behaviors.)
-    const dbg = frame ? frameActor.threadActor.dbg : this.dbg;
-    let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
-
-    // If we have an object to bind to |_self|, create a Debugger.Object
-    // referring to that object, belonging to dbg.
-    let bindSelf = null;
-    if (options.bindObjectActor || options.selectedObjectActor) {
-      const objActor = this.getActorByID(options.bindObjectActor ||
-                                       options.selectedObjectActor);
-      if (objActor) {
-        const jsVal = objActor.rawValue();
+    return { frame: frameActor.frame, dbg: frameActor.threadActor.dbg };
+  }
+  return DevToolsUtils.reportException("evalWithDebugger",
+    Error("The frame actor was not found: " + options.frameActor));
+}
 
-        if (isObject(jsVal)) {
-          // If we use the makeDebuggeeValue method of jsVal's own global, then
-          // we'll get a D.O that sees jsVal as viewed from its own compartment -
-          // that is, without wrappers. The evalWithBindings call will then wrap
-          // jsVal appropriately for the evaluation compartment.
-          bindSelf = dbgWindow.makeDebuggeeValue(jsVal);
-          if (options.bindObjectActor) {
-            const global = Cu.getGlobalForObject(jsVal);
-            try {
-              const _dbgWindow = dbg.makeGlobalObjectReference(global);
-              dbgWindow = _dbgWindow;
-            } catch (err) {
-              // The above will throw if `global` is invisible to debugger.
-            }
-          }
-        } else {
-          bindSelf = jsVal;
-        }
-      }
-    }
-
-    // Get the Web Console commands for the given debugger window.
-    const helpers = this._getWebConsoleCommands(dbgWindow);
-    const bindings = helpers.sandbox;
-    if (bindSelf) {
-      bindings._self = bindSelf;
-    }
+function getDbgWindow(options, dbg, webConsole) {
+  const dbgWindow = dbg.makeGlobalObjectReference(webConsole.evalWindow);
+  // If we have an object to bind to |_self|, create a Debugger.Object
+  // referring to that object, belonging to dbg.
+  if (!options.bindObjectActor && !options.selectedObjectActor) {
+    return { bindSelf: null, dbgWindow };
+  }
 
-    if (options.selectedNodeActor) {
-      const actor = this.conn.getActor(options.selectedNodeActor);
-      if (actor) {
-        helpers.selectedNode = actor.rawNode;
-      }
-    }
+  const objActor = webConsole.getActorByID(
+    options.bindObjectActor || options.selectedObjectActor
+  );
 
-    // Check if the Debugger.Frame or Debugger.Object for the global include
-    // $ or $$. We will not overwrite these functions with the Web Console
-    // commands.
-    let found$ = false, found$$ = false, foundScreenshot = false;
-    if (!isCmd) {
-      if (frame) {
-        const env = frame.environment;
-        if (env) {
-          found$ = !!env.find("$");
-          found$$ = !!env.find("$$");
-          foundScreenshot = !!env.find("screenshot");
-        }
-      } else {
-        found$ = !!dbgWindow.getOwnPropertyDescriptor("$");
-        found$$ = !!dbgWindow.getOwnPropertyDescriptor("$$");
-        foundScreenshot = true;
-      }
-    }
+  if (!objActor) {
+    return { bindSelf: null, dbgWindow };
+  }
 
-    let $ = null, $$ = null, screenshot = null;
-    if (found$) {
-      $ = bindings.$;
-      delete bindings.$;
-    }
-    if (found$$) {
-      $$ = bindings.$$;
-      delete bindings.$$;
-    }
-    if (foundScreenshot) {
-      screenshot = bindings.screenshot;
-      delete bindings.screenshot;
-    }
+  const jsVal = objActor.rawValue();
 
-    // Ready to evaluate the string.
-    helpers.evalInput = string;
+  if (!isObject(jsVal)) {
+    return { bindSelf: jsVal, dbgWindow };
+  }
 
-    let evalOptions;
-    if (typeof options.url == "string") {
-      evalOptions = { url: options.url };
-    }
-
-    // If the debugger object is changed from the last evaluation,
-    // adopt this._lastConsoleInputEvaluation value in the new debugger,
-    // to prevents "Debugger.Object belongs to a different Debugger" exceptions
-    // related to the $_ bindings.
-    if (this._lastConsoleInputEvaluation &&
-        this._lastConsoleInputEvaluation.global !== dbgWindow) {
-      this._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue(
-        this._lastConsoleInputEvaluation
-      );
+  // If we use the makeDebuggeeValue method of jsVal's own global, then
+  // we'll get a D.O that sees jsVal as viewed from its own compartment -
+  // that is, without wrappers. The evalWithBindings call will then wrap
+  // jsVal appropriately for the evaluation compartment.
+  const bindSelf = dbgWindow.makeDebuggeeValue(jsVal);
+  if (options.bindObjectActor) {
+    const global = Cu.getGlobalForObject(jsVal);
+    try {
+      const _dbgWindow = dbg.makeGlobalObjectReference(global);
+      return { bindSelf, dbgWindow: _dbgWindow };
+    } catch (err) {
+      // The above will throw if `global` is invisible to debugger.
     }
-
-    let result;
-
-    if (frame) {
-      result = frame.evalWithBindings(string, bindings, evalOptions);
-    } else {
-      result = dbgWindow.executeInGlobalWithBindings(string, bindings, evalOptions);
-      // Attempt to initialize any declarations found in the evaluated string
-      // since they may now be stuck in an "initializing" state due to the
-      // error. Already-initialized bindings will be ignored.
-      if ("throw" in result) {
-        let ast;
-        // Parse errors will raise an exception. We can/should ignore the error
-        // since it's already being handled elsewhere and we are only interested
-        // in initializing bindings.
-        try {
-          ast = Parser.reflectionAPI.parse(string);
-        } catch (ex) {
-          ast = {"body": []};
-        }
-        for (const line of ast.body) {
-          // Only let and const declarations put bindings into an
-          // "initializing" state.
-          if (!(line.kind == "let" || line.kind == "const")) {
-            continue;
-          }
+  }
+  return { bindSelf, dbgWindow };
+}
 
-          const identifiers = [];
-          for (const decl of line.declarations) {
-            switch (decl.id.type) {
-              case "Identifier":
-                // let foo = bar;
-                identifiers.push(decl.id.name);
-                break;
-              case "ArrayPattern":
-                // let [foo, bar]    = [1, 2];
-                // let [foo=99, bar] = [1, 2];
-                for (const e of decl.id.elements) {
-                  if (e.type == "Identifier") {
-                    identifiers.push(e.name);
-                  } else if (e.type == "AssignmentExpression") {
-                    identifiers.push(e.left.name);
-                  }
-                }
-                break;
-              case "ObjectPattern":
-                // let {bilbo, my}    = {bilbo: "baggins", my: "precious"};
-                // let {blah: foo}    = {blah: yabba()}
-                // let {blah: foo=99} = {blah: yabba()}
-                for (const prop of decl.id.properties) {
-                  // key
-                  if (prop.key.type == "Identifier") {
-                    identifiers.push(prop.key.name);
-                  }
-                  // value
-                  if (prop.value.type == "Identifier") {
-                    identifiers.push(prop.value.name);
-                  } else if (prop.value.type == "AssignmentExpression") {
-                    identifiers.push(prop.value.left.name);
-                  }
-                }
-                break;
-            }
-          }
+function getHelpers(dbgWindow, options, webConsole) {
+  // Get the Web Console commands for the given debugger window.
+  const helpers = webConsole._getWebConsoleCommands(dbgWindow);
+  if (options.selectedNodeActor) {
+    const actor = webConsole.conn.getActor(options.selectedNodeActor);
+    if (actor) {
+      helpers.selectedNode = actor.rawNode;
+    }
+  }
 
-          for (const name of identifiers) {
-            dbgWindow.forceLexicalInitializationByName(name);
-          }
-        }
-      }
-    }
-
-    const helperResult = helpers.helperResult;
+  function cleanupHelpers() {
     delete helpers.evalInput;
     delete helpers.helperResult;
     delete helpers.selectedNode;
+  }
 
-    if ($) {
-      bindings.$ = $;
+  return { helpers, cleanupHelpers };
+}
+
+function bindCommands(isCmd, dbgWindow, bindSelf, frame, helpers) {
+  const bindings = helpers.sandbox;
+  if (bindSelf) {
+    bindings._self = bindSelf;
+  }
+  // Check if the Debugger.Frame or Debugger.Object for the global include
+  // $ or $$. We will not overwrite these functions with the Web Console
+  // commands.
+  const overrides = ["$", "$$", "screenshot"];
+  const backups = [];
+  const env = frame && frame.environment;
+
+  overrides.forEach(override => {
+    const shouldOverride = (
+      !isCmd && override === "screenshot" ||
+      env && !!env.find(override) ||
+      !!dbgWindow.getOwnPropertyDescriptor(override)
+    );
+
+    if (shouldOverride) {
+      backups[override] = bindings[override];
+      delete bindings[override];
     }
-    if ($$) {
-      bindings.$$ = $$;
-    }
-    if (screenshot) {
-      bindings.screenshot = screenshot;
-    }
+  });
 
+  function cleanupBindings() {
+    Object.entries(backups).forEach(([name, value]) => {
+      bindings[name] = value;
+    });
     if (bindings._self) {
       delete bindings._self;
     }
-
-    return {
-      result: result,
-      helperResult: helperResult,
-      dbg: dbg,
-      frame: frame,
-      window: dbgWindow,
-    };
-  },
-  /* eslint-enable complexity */
-
-  // Event handlers for various listeners.
-
-  /**
-   * Handler for messages received from the ConsoleServiceListener. This method
-   * sends the nsIConsoleMessage to the remote Web Console client.
-   *
-   * @param nsIConsoleMessage message
-   *        The message we need to send to the client.
-   */
-  onConsoleServiceMessage: function(message) {
-    let packet;
-    if (message instanceof Ci.nsIScriptError) {
-      packet = {
-        from: this.actorID,
-        type: "pageError",
-        pageError: this.preparePageErrorForRemote(message),
-      };
-    } else {
-      packet = {
-        from: this.actorID,
-        type: "logMessage",
-        message: this._createStringGrip(message.message),
-        timeStamp: message.timeStamp,
-      };
-    }
-    this.conn.send(packet);
-  },
-
-  /**
-   * Prepare an nsIScriptError to be sent to the client.
-   *
-   * @param nsIScriptError pageError
-   *        The page error we need to send to the client.
-   * @return object
-   *         The object you can send to the remote client.
-   */
-  preparePageErrorForRemote: function(pageError) {
-    let stack = null;
-    // Convert stack objects to the JSON attributes expected by client code
-    // Bug 1348885: If the global from which this error came from has been
-    // nuked, stack is going to be a dead wrapper.
-    if (pageError.stack && !Cu.isDeadWrapper(pageError.stack)) {
-      stack = [];
-      let s = pageError.stack;
-      while (s !== null) {
-        stack.push({
-          filename: s.source,
-          lineNumber: s.line,
-          columnNumber: s.column,
-          functionName: s.functionDisplayName
-        });
-        s = s.parent;
-      }
-    }
-    let lineText = pageError.sourceLine;
-    if (lineText && lineText.length > DebuggerServer.LONG_STRING_INITIAL_LENGTH) {
-      lineText = lineText.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
-    }
-
-    let notesArray = null;
-    const notes = pageError.notes;
-    if (notes && notes.length) {
-      notesArray = [];
-      for (let i = 0, len = notes.length; i < len; i++) {
-        const note = notes.queryElementAt(i, Ci.nsIScriptErrorNote);
-        notesArray.push({
-          messageBody: this._createStringGrip(note.errorMessage),
-          frame: {
-            source: note.sourceName,
-            line: note.lineNumber,
-            column: note.columnNumber,
-          }
-        });
-      }
-    }
-
-    return {
-      errorMessage: this._createStringGrip(pageError.errorMessage),
-      errorMessageName: pageError.errorMessageName,
-      exceptionDocURL: ErrorDocs.GetURL(pageError),
-      sourceName: pageError.sourceName,
-      lineText: lineText,
-      lineNumber: pageError.lineNumber,
-      columnNumber: pageError.columnNumber,
-      category: pageError.category,
-      timeStamp: pageError.timeStamp,
-      warning: !!(pageError.flags & pageError.warningFlag),
-      error: !!(pageError.flags & pageError.errorFlag),
-      exception: !!(pageError.flags & pageError.exceptionFlag),
-      strict: !!(pageError.flags & pageError.strictFlag),
-      info: !!(pageError.flags & pageError.infoFlag),
-      private: pageError.isFromPrivateWindow,
-      stacktrace: stack,
-      notes: notesArray,
-    };
-  },
-
-  /**
-   * Handler for window.console API calls received from the ConsoleAPIListener.
-   * This method sends the object to the remote Web Console client.
-   *
-   * @see ConsoleAPIListener
-   * @param object message
-   *        The console API call we need to send to the remote client.
-   */
-  onConsoleAPICall: function(message) {
-    const packet = {
-      from: this.actorID,
-      type: "consoleAPICall",
-      message: this.prepareConsoleMessageForRemote(message),
-    };
-    this.conn.send(packet);
-  },
-
-  /**
-   * Handler for network events. This method is invoked when a new network event
-   * is about to be recorded.
-   *
-   * @see NetworkEventActor
-   * @see NetworkMonitor from webconsole/utils.js
-   *
-   * @param object event
-   *        The initial network request event information.
-   * @return object
-   *         A new NetworkEventActor is returned. This is used for tracking the
-   *         network request and response.
-   */
-  onNetworkEvent: function(event) {
-    const actor = this.getNetworkEventActor(event.channelId);
-    actor.init(event);
-
-    this._networkEventActorsByURL.set(actor._request.url, actor);
-
-    const packet = {
-      from: this.actorID,
-      type: "networkEvent",
-      eventActor: actor.form()
-    };
-
-    this.conn.send(packet);
-
-    return actor;
-  },
-
-  /**
-   * Get the NetworkEventActor for a nsIHttpChannel, if it exists,
-   * otherwise create a new one.
-   *
-   * @param string channelId
-   *        The id of the channel for the network event.
-   * @return object
-   *         The NetworkEventActor for the given channel.
-   */
-  getNetworkEventActor: function(channelId) {
-    let actor = this._netEvents.get(channelId);
-    if (actor) {
-      // delete from map as we should only need to do this check once
-      this._netEvents.delete(channelId);
-      return actor;
-    }
-
-    actor = new NetworkEventActor(this);
-    this._actorPool.addActor(actor);
-    return actor;
-  },
-
-  /**
-   * Get the NetworkEventActor for a given URL that may have been noticed by the network
-   * listener.  Requests are added when they start, so the actor might not yet have all
-   * data for the request until it has completed.
-   *
-   * @param string url
-   *        The URL of the request to search for.
-   */
-  getNetworkEventActorForURL(url) {
-    return this._networkEventActorsByURL.get(url);
-  },
-
-  /**
-   * Send a new HTTP request from the target's window.
-   *
-   * @param object message
-   *        Object with 'request' - the HTTP request details.
-   */
-  sendHTTPRequest(message) {
-    const { url, method, headers, body } = message.request;
-
-    // Set the loadingNode and loadGroup to the target document - otherwise the
-    // request won't show up in the opened netmonitor.
-    const doc = this.window.document;
+  }
 
-    const channel = NetUtil.newChannel({
-      uri: NetUtil.newURI(url),
-      loadingNode: doc,
-      securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-      contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
-    });
-
-    channel.QueryInterface(Ci.nsIHttpChannel);
-
-    channel.loadGroup = doc.documentLoadGroup;
-    channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE |
-                         Ci.nsIRequest.INHIBIT_CACHING |
-                         Ci.nsIRequest.LOAD_ANONYMOUS;
-
-    channel.requestMethod = method;
-
-    for (const {name, value} of headers) {
-      channel.setRequestHeader(name, value, false);
-    }
-
-    if (body) {
-      channel.QueryInterface(Ci.nsIUploadChannel2);
-      const bodyStream = Cc["@mozilla.org/io/string-input-stream;1"]
-        .createInstance(Ci.nsIStringInputStream);
-      bodyStream.setData(body, body.length);
-      channel.explicitSetUploadStream(bodyStream, null, -1, method, false);
-    }
-
-    NetUtil.asyncFetch(channel, () => {});
-
-    const actor = this.getNetworkEventActor(channel.channelId);
-
-    // map channel to actor so we can associate future events with it
-    this._netEvents.set(channel.channelId, actor);
-
-    return {
-      from: this.actorID,
-      eventActor: actor.form()
-    };
-  },
-
-  /**
-   * Handler for file activity. This method sends the file request information
-   * to the remote Web Console client.
-   *
-   * @see ConsoleProgressListener
-   * @param string fileURI
-   *        The requested file URI.
-   */
-  onFileActivity: function(fileURI) {
-    const packet = {
-      from: this.actorID,
-      type: "fileActivity",
-      uri: fileURI,
-    };
-    this.conn.send(packet);
-  },
-
-  /**
-   * Handler for reflow activity. This method forwards reflow events to the
-   * remote Web Console client.
-   *
-   * @see ConsoleReflowListener
-   * @param Object reflowInfo
-   */
-  onReflowActivity: function(reflowInfo) {
-    const packet = {
-      from: this.actorID,
-      type: "reflowActivity",
-      interruptible: reflowInfo.interruptible,
-      start: reflowInfo.start,
-      end: reflowInfo.end,
-      sourceURL: reflowInfo.sourceURL,
-      sourceLine: reflowInfo.sourceLine,
-      functionName: reflowInfo.functionName
-    };
-
-    this.conn.send(packet);
-  },
-
-  // End of event handlers for various listeners.
-
-  /**
-   * Prepare a message from the console API to be sent to the remote Web Console
-   * instance.
-   *
-   * @param object message
-   *        The original message received from console-api-log-event.
-   * @param boolean aUseObjectGlobal
-   *        If |true| the object global is determined and added as a debuggee,
-   *        otherwise |this.window| is used when makeDebuggeeValue() is invoked.
-   * @return object
-   *         The object that can be sent to the remote client.
-   */
-  prepareConsoleMessageForRemote: function(message, useObjectGlobal = true) {
-    const result = WebConsoleUtils.cloneObject(message);
-
-    result.workerType = WebConsoleUtils.getWorkerType(result) || "none";
-
-    delete result.wrappedJSObject;
-    delete result.ID;
-    delete result.innerID;
-    delete result.consoleID;
+  return { bindings, cleanupBindings };
+}
 
-    result.arguments = Array.map(message.arguments || [], (obj) => {
-      const dbgObj = this.makeDebuggeeValue(obj, useObjectGlobal);
-      return this.createValueGrip(dbgObj);
-    });
-
-    result.styles = Array.map(message.styles || [], (string) => {
-      return this.createValueGrip(string);
-    });
-
-    result.category = message.category || "webdev";
-
-    return result;
-  },
-
-  /**
-   * Find the XUL window that owns the content window.
-   *
-   * @return Window
-   *         The XUL window that owns the content window.
-   */
-  chromeWindow: function() {
-    let window = null;
-    try {
-      window = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
-             .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell)
-             .chromeEventHandler.ownerGlobal;
-    } catch (ex) {
-      // The above can fail because chromeEventHandler is not available for all
-      // kinds of |this.window|.
-    }
-
-    return window;
-  },
-
-  /**
-   * Notification observer for the "last-pb-context-exited" topic.
-   *
-   * @private
-   * @param object subject
-   *        Notification subject - in this case it is the inner window ID that
-   *        was destroyed.
-   * @param string topic
-   *        Notification topic.
-   */
-  _onObserverNotification: function(subject, topic) {
-    switch (topic) {
-      case "last-pb-context-exited":
-        this.conn.send({
-          from: this.actorID,
-          type: "lastPrivateContextExited",
-        });
-        break;
-    }
-  },
-
-  /**
-   * The "will-navigate" progress listener. This is used to clear the current
-   * eval scope.
-   */
-  _onWillNavigate: function({ window, isTopLevel }) {
-    if (isTopLevel) {
-      this._evalWindow = null;
-      EventEmitter.off(this.parentActor, "will-navigate", this._onWillNavigate);
-      this._progressListenerActive = false;
-    }
-  },
-
-  /**
-   * This listener is called when we switch to another frame,
-   * mostly to unregister previous listeners and start listening on the new document.
-   */
-  _onChangedToplevelDocument: function() {
-    // Convert the Set to an Array
-    const listeners = [...this._listeners];
-
-    // Unregister existing listener on the previous document
-    // (pass a copy of the array as it will shift from it)
-    this.stopListeners({listeners: listeners.slice()});
-
-    // This method is called after this.window is changed,
-    // so we register new listener on this new window
-    this.startListeners({listeners: listeners});
-
-    // Also reset the cached top level chrome window being targeted
-    this._lastChromeWindow = null;
-  },
-};
-
-WebConsoleActor.prototype.requestTypes =
-{
-  startListeners: WebConsoleActor.prototype.startListeners,
-  stopListeners: WebConsoleActor.prototype.stopListeners,
-  getCachedMessages: WebConsoleActor.prototype.getCachedMessages,
-  evaluateJS: WebConsoleActor.prototype.evaluateJS,
-  evaluateJSAsync: WebConsoleActor.prototype.evaluateJSAsync,
-  autocomplete: WebConsoleActor.prototype.autocomplete,
-  clearMessagesCache: WebConsoleActor.prototype.clearMessagesCache,
-  getPreferences: WebConsoleActor.prototype.getPreferences,
-  setPreferences: WebConsoleActor.prototype.setPreferences,
-  sendHTTPRequest: WebConsoleActor.prototype.sendHTTPRequest
-};
-
-exports.WebConsoleActor = WebConsoleActor;
--- a/devtools/server/actors/webconsole/moz.build
+++ b/devtools/server/actors/webconsole/moz.build
@@ -2,13 +2,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'commands.js',
     'content-process-forward.js',
+    'eval-with-debugger.js',
     'listeners.js',
     'screenshot.js',
     'utils.js',
     'worker-listeners.js',
 )