https://github.com/mozilla/gecko-dev
Raw File
Tip revision: 1c1ac9fcff5624021cc6807a5f15ed0b1796efc6 authored by ffxbld on 06 November 2016, 03:11:57 UTC
Added FENNEC_50_0_RELEASE FENNEC_50_0_BUILD1 tag(s) for changeset 3d81a5476f96. DONTBUILD CLOSED TREE a=release
Tip revision: 1c1ac9f
inspector.js
/* 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";

/**
 * Here's the server side of the remote inspector.
 *
 * The WalkerActor is the client's view of the debuggee's DOM.  It's gives
 * the client a tree of NodeActor objects.
 *
 * The walker presents the DOM tree mostly unmodified from the source DOM
 * tree, but with a few key differences:
 *
 *  - Empty text nodes are ignored.  This is pretty typical of developer
 *    tools, but maybe we should reconsider that on the server side.
 *  - iframes with documents loaded have the loaded document as the child,
 *    the walker provides one big tree for the whole document tree.
 *
 * There are a few ways to get references to NodeActors:
 *
 *   - When you first get a WalkerActor reference, it comes with a free
 *     reference to the root document's node.
 *   - Given a node, you can ask for children, siblings, and parents.
 *   - You can issue querySelector and querySelectorAll requests to find
 *     other elements.
 *   - Requests that return arbitrary nodes from the tree (like querySelector
 *     and querySelectorAll) will also return any nodes the client hasn't
 *     seen in order to have a complete set of parents.
 *
 * Once you have a NodeFront, you should be able to answer a few questions
 * without further round trips, like the node's name, namespace/tagName,
 * attributes, etc.  Other questions (like a text node's full nodeValue)
 * might require another round trip.
 *
 * The protocol guarantees that the client will always know the parent of
 * any node that is returned by the server.  This means that some requests
 * (like querySelector) will include the extra nodes needed to satisfy this
 * requirement.  The client keeps track of this parent relationship, so the
 * node fronts form a tree that is a subset of the actual DOM tree.
 *
 *
 * We maintain this guarantee to support the ability to release subtrees on
 * the client - when a node is disconnected from the DOM tree we want to be
 * able to free the client objects for all the children nodes.
 *
 * So to be able to answer "all the children of a given node that we have
 * seen on the client side", we guarantee that every time we've seen a node,
 * we connect it up through its parents.
 */

const {Cc, Ci, Cu} = require("chrome");
const Services = require("Services");
const protocol = require("devtools/shared/protocol");
const {LongStringActor} = require("devtools/server/actors/string");
const promise = require("promise");
const {Task} = require("devtools/shared/task");
const events = require("sdk/event/core");
const {WalkerSearch} = require("devtools/server/actors/utils/walker-search");
const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles");
const {
  HighlighterActor,
  CustomHighlighterActor,
  isTypeRegistered,
  HighlighterEnvironment
} = require("devtools/server/actors/highlighters");
const {EyeDropper} = require("devtools/server/actors/highlighters/eye-dropper");
const {
  isAnonymous,
  isNativeAnonymous,
  isXBLAnonymous,
  isShadowAnonymous,
  getFrameElement
} = require("devtools/shared/layout/utils");
const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/layout");
const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");

loader.lazyRequireGetter(this, "CSS", "CSS");

const {EventParsers} = require("devtools/server/event-parsers");
const {nodeSpec, nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");

const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const IMAGE_FETCHING_TIMEOUT = 500;
const RX_FUNC_NAME =
  /((var|const|let)\s+)?([\w$.]+\s*[:=]\s*)*(function)?\s*\*?\s*([\w$]+)?\s*$/;

// The possible completions to a ':' with added score to give certain values
// some preference.
const PSEUDO_SELECTORS = [
  [":active", 1],
  [":hover", 1],
  [":focus", 1],
  [":visited", 0],
  [":link", 0],
  [":first-letter", 0],
  [":first-child", 2],
  [":before", 2],
  [":after", 2],
  [":lang(", 0],
  [":not(", 3],
  [":first-of-type", 0],
  [":last-of-type", 0],
  [":only-of-type", 0],
  [":only-child", 2],
  [":nth-child(", 3],
  [":nth-last-child(", 0],
  [":nth-of-type(", 0],
  [":nth-last-of-type(", 0],
  [":last-child", 2],
  [":root", 0],
  [":empty", 0],
  [":target", 0],
  [":enabled", 0],
  [":disabled", 0],
  [":checked", 1],
  ["::selection", 0]
];

var HELPER_SHEET = `
  .__fx-devtools-hide-shortcut__ {
    visibility: hidden !important;
  }

  :-moz-devtools-highlighted {
    outline: 2px dashed #F06!important;
    outline-offset: -2px !important;
  }
`;

loader.lazyRequireGetter(this, "DevToolsUtils",
                         "devtools/shared/DevToolsUtils");

loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");

loader.lazyGetter(this, "DOMParser", function () {
  return Cc["@mozilla.org/xmlextras/domparser;1"]
           .createInstance(Ci.nsIDOMParser);
});

loader.lazyGetter(this, "eventListenerService", function () {
  return Cc["@mozilla.org/eventlistenerservice;1"]
           .getService(Ci.nsIEventListenerService);
});

loader.lazyGetter(this, "CssLogic", () => require("devtools/server/css-logic").CssLogic);

/**
 * We only send nodeValue up to a certain size by default.  This stuff
 * controls that size.
 */
exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;

exports.getValueSummaryLength = function () {
  return gValueSummaryLength;
};

exports.setValueSummaryLength = function (val) {
  gValueSummaryLength = val;
};

// When the user selects a node to inspect in e10s, the parent process
// has a CPOW that wraps the node being inspected.  It uses the
// message manager to send this node to the child, which stores the
// node in gInspectingNode. Then a findInspectingNode request is sent
// over the remote debugging protocol, and gInspectingNode is returned
// to the parent as a NodeFront.
var gInspectingNode = null;

// We expect this function to be called from the child.js frame script
// when it receives the node to be inspected over the message manager.
exports.setInspectingNode = function (val) {
  gInspectingNode = val;
};

/**
 * Returns the properly cased version of the node's tag name, which can be
 * used when displaying said name in the UI.
 *
 * @param  {Node} rawNode
 *         Node for which we want the display name
 * @return {String}
 *         Properly cased version of the node tag name
 */
const getNodeDisplayName = function (rawNode) {
  return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
};
exports.getNodeDisplayName = getNodeDisplayName;

/**
 * Server side of the node actor.
 */
var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
  initialize: function (walker, node) {
    protocol.Actor.prototype.initialize.call(this, null);
    this.walker = walker;
    this.rawNode = node;
    this._eventParsers = new EventParsers().parsers;

    // Storing the original display of the node, to track changes when reflows
    // occur
    this.wasDisplayed = this.isDisplayed;
  },

  toString: function () {
    return "[NodeActor " + this.actorID + " for " +
      this.rawNode.toString() + "]";
  },

  /**
   * Instead of storing a connection object, the NodeActor gets its connection
   * from its associated walker.
   */
  get conn() {
    return this.walker.conn;
  },

  isDocumentElement: function () {
    return this.rawNode.ownerDocument &&
           this.rawNode.ownerDocument.documentElement === this.rawNode;
  },

  destroy: function () {
    protocol.Actor.prototype.destroy.call(this);

    if (this.mutationObserver) {
      if (!Cu.isDeadWrapper(this.mutationObserver)) {
        this.mutationObserver.disconnect();
      }
      this.mutationObserver = null;
    }
    this.rawNode = null;
    this.walker = null;
  },

  // Returns the JSON representation of this object over the wire.
  form: function (detail) {
    if (detail === "actorid") {
      return this.actorID;
    }

    let parentNode = this.walker.parentNode(this);
    let inlineTextChild = this.walker.inlineTextChild(this);

    let form = {
      actor: this.actorID,
      baseURI: this.rawNode.baseURI,
      parent: parentNode ? parentNode.actorID : undefined,
      nodeType: this.rawNode.nodeType,
      namespaceURI: this.rawNode.namespaceURI,
      nodeName: this.rawNode.nodeName,
      nodeValue: this.rawNode.nodeValue,
      displayName: getNodeDisplayName(this.rawNode),
      numChildren: this.numChildren,
      inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,

      // doctype attributes
      name: this.rawNode.name,
      publicId: this.rawNode.publicId,
      systemId: this.rawNode.systemId,

      attrs: this.writeAttrs(),
      isBeforePseudoElement: this.isBeforePseudoElement,
      isAfterPseudoElement: this.isAfterPseudoElement,
      isAnonymous: isAnonymous(this.rawNode),
      isNativeAnonymous: isNativeAnonymous(this.rawNode),
      isXBLAnonymous: isXBLAnonymous(this.rawNode),
      isShadowAnonymous: isShadowAnonymous(this.rawNode),
      pseudoClassLocks: this.writePseudoClassLocks(),

      isDisplayed: this.isDisplayed,
      isInHTMLDocument: this.rawNode.ownerDocument &&
        this.rawNode.ownerDocument.contentType === "text/html",
      hasEventListeners: this._hasEventListeners,
    };

    if (this.isDocumentElement()) {
      form.isDocumentElement = true;
    }

    // Add an extra API for custom properties added by other
    // modules/extensions.
    form.setFormProperty = (name, value) => {
      if (!form.props) {
        form.props = {};
      }
      form.props[name] = value;
    };

    // Fire an event so, other modules can create its own properties
    // that should be passed to the client (within the form.props field).
    events.emit(NodeActor, "form", {
      target: this,
      data: form
    });

    return form;
  },

  /**
   * Watch the given document node for mutations using the DOM observer
   * API.
   */
  watchDocument: function (callback) {
    let node = this.rawNode;
    // Create the observer on the node's actor.  The node will make sure
    // the observer is cleaned up when the actor is released.
    let observer = new node.defaultView.MutationObserver(callback);
    observer.mergeAttributeRecords = true;
    observer.observe(node, {
      nativeAnonymousChildList: true,
      attributes: true,
      characterData: true,
      characterDataOldValue: true,
      childList: true,
      subtree: true
    });
    this.mutationObserver = observer;
  },

  get isBeforePseudoElement() {
    return this.rawNode.nodeName === "_moz_generated_content_before";
  },

  get isAfterPseudoElement() {
    return this.rawNode.nodeName === "_moz_generated_content_after";
  },

  // Estimate the number of children that the walker will return without making
  // a call to children() if possible.
  get numChildren() {
    // For pseudo elements, childNodes.length returns 1, but the walker
    // will return 0.
    if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
      return 0;
    }

    let rawNode = this.rawNode;
    let numChildren = rawNode.childNodes.length;
    let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE &&
                          rawNode.ownerDocument.getAnonymousNodes(rawNode);

    let hasContentDocument = rawNode.contentDocument;
    let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
    if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
      // This might be an iframe with virtual children.
      numChildren = 1;
    }

    // Normal counting misses ::before/::after.  Also, some anonymous children
    // may ultimately be skipped, so we have to consult with the walker.
    if (numChildren === 0 || hasAnonChildren) {
      numChildren = this.walker.children(this).nodes.length;
    }

    return numChildren;
  },

  get computedStyle() {
    return CssLogic.getComputedStyle(this.rawNode);
  },

  /**
   * Is the node's display computed style value other than "none"
   */
  get isDisplayed() {
    // Consider all non-element nodes as displayed.
    if (isNodeDead(this) ||
        this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
        this.isAfterPseudoElement ||
        this.isBeforePseudoElement) {
      return true;
    }

    let style = this.computedStyle;
    if (!style) {
      return true;
    }

    return style.display !== "none";
  },

  /**
   * Are there event listeners that are listening on this node? This method
   * uses all parsers registered via event-parsers.js.registerEventParser() to
   * check if there are any event listeners.
   */
  get _hasEventListeners() {
    let parsers = this._eventParsers;
    for (let [, {hasListeners}] of parsers) {
      try {
        if (hasListeners && hasListeners(this.rawNode)) {
          return true;
        }
      } catch (e) {
        // An object attached to the node looked like a listener but wasn't...
        // do nothing.
      }
    }
    return false;
  },

  writeAttrs: function () {
    if (!this.rawNode.attributes) {
      return undefined;
    }

    return [...this.rawNode.attributes].map(attr => {
      return {namespace: attr.namespace, name: attr.name, value: attr.value };
    });
  },

  writePseudoClassLocks: function () {
    if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
      return undefined;
    }
    let ret = undefined;
    for (let pseudo of PSEUDO_CLASSES) {
      if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
        ret = ret || [];
        ret.push(pseudo);
      }
    }
    return ret;
  },

  /**
   * Gets event listeners and adds their information to the events array.
   *
   * @param  {Node} node
   *         Node for which we are to get listeners.
   */
  getEventListeners: function (node) {
    let parsers = this._eventParsers;
    let dbg = this.parent().tabActor.makeDebugger();
    let listeners = [];

    for (let [, {getListeners, normalizeHandler}] of parsers) {
      try {
        let eventInfos = getListeners(node);

        if (!eventInfos) {
          continue;
        }

        for (let eventInfo of eventInfos) {
          if (normalizeHandler) {
            eventInfo.normalizeHandler = normalizeHandler;
          }

          this.processHandlerForEvent(node, listeners, dbg, eventInfo);
        }
      } catch (e) {
        // An object attached to the node looked like a listener but wasn't...
        // do nothing.
      }
    }

    listeners.sort((a, b) => {
      return a.type.localeCompare(b.type);
    });

    return listeners;
  },

  /**
   * Process a handler
   *
   * @param  {Node} node
   *         The node for which we want information.
   * @param  {Array} events
   *         The events array contains all event objects that we have gathered
   *         so far.
   * @param  {Debugger} dbg
   *         JSDebugger instance.
   * @param  {Object} eventInfo
   *         See event-parsers.js.registerEventParser() for a description of the
   *         eventInfo object.
   *
   * @return {Array}
   *         An array of objects where a typical object looks like this:
   *           {
   *             type: "click",
   *             handler: function() { doSomething() },
   *             origin: "http://www.mozilla.com",
   *             searchString: 'onclick="doSomething()"',
   *             tags: tags,
   *             DOM0: true,
   *             capturing: true,
   *             hide: {
   *               dom0: true
   *             }
   *           }
   */
  processHandlerForEvent: function (node, listeners, dbg, eventInfo) {
    let type = eventInfo.type || "";
    let handler = eventInfo.handler;
    let tags = eventInfo.tags || "";
    let hide = eventInfo.hide || {};
    let override = eventInfo.override || {};
    let global = Cu.getGlobalForObject(handler);
    let globalDO = dbg.addDebuggee(global);
    let listenerDO = globalDO.makeDebuggeeValue(handler);

    if (eventInfo.normalizeHandler) {
      listenerDO = eventInfo.normalizeHandler(listenerDO);
    }

    // If the listener is an object with a 'handleEvent' method, use that.
    if (listenerDO.class === "Object" || listenerDO.class === "XULElement") {
      let desc;

      while (!desc && listenerDO) {
        desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
        listenerDO = listenerDO.proto;
      }

      if (desc && desc.value) {
        listenerDO = desc.value;
      }
    }

    if (listenerDO.isBoundFunction) {
      listenerDO = listenerDO.boundTargetFunction;
    }

    let script = listenerDO.script;
    let scriptSource = script.source.text;
    let functionSource =
      scriptSource.substr(script.sourceStart, script.sourceLength);

    /*
    The script returned is the whole script and
    scriptSource.substr(script.sourceStart, script.sourceLength) returns
    something like this:
      () { doSomething(); }

    So we need to use some regex magic to get the appropriate function info
    e.g.:
      () => { ... }
      function doit() { ... }
      doit: function() { ... }
      es6func() { ... }
      var|let|const foo = function () { ... }
      function generator*() { ... }
    */
    let scriptBeforeFunc = scriptSource.substr(0, script.sourceStart);
    let matches = scriptBeforeFunc.match(RX_FUNC_NAME);
    if (matches && matches.length > 0) {
      functionSource = matches[0].trim() + functionSource;
    }

    let dom0 = false;

    if (typeof node.hasAttribute !== "undefined") {
      dom0 = !!node.hasAttribute("on" + type);
    } else {
      dom0 = !!node["on" + type];
    }

    let line = script.startLine;
    let url = script.url;
    let origin = url + (dom0 ? "" : ":" + line);
    let searchString;

    if (dom0) {
      searchString = "on" + type + "=\"" + script.source.text + "\"";
    } else {
      scriptSource = "    " + scriptSource;
    }

    let eventObj = {
      type: typeof override.type !== "undefined" ? override.type : type,
      handler: functionSource.trim(),
      origin: typeof override.origin !== "undefined" ?
                     override.origin : origin,
      searchString: typeof override.searchString !== "undefined" ?
                           override.searchString : searchString,
      tags: tags,
      DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
      capturing: typeof override.capturing !== "undefined" ?
                        override.capturing : eventInfo.capturing,
      hide: hide
    };

    listeners.push(eventObj);

    dbg.removeDebuggee(globalDO);
  },

  /**
   * Returns a LongStringActor with the node's value.
   */
  getNodeValue: function () {
    return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
  },

  /**
   * Set the node's value to a given string.
   */
  setNodeValue: function (value) {
    this.rawNode.nodeValue = value;
  },

  /**
   * Get a unique selector string for this node.
   */
  getUniqueSelector: function () {
    return CssLogic.findCssSelector(this.rawNode);
  },

  /**
   * Scroll the selected node into view.
   */
  scrollIntoView: function () {
    this.rawNode.scrollIntoView(true);
  },

  /**
   * Get the node's image data if any (for canvas and img nodes).
   * Returns an imageData object with the actual data being a LongStringActor
   * and a size json object.
   * The image data is transmitted as a base64 encoded png data-uri.
   * The method rejects if the node isn't an image or if the image is missing
   *
   * Accepts a maxDim request parameter to resize images that are larger. This
   * is important as the resizing occurs server-side so that image-data being
   * transfered in the longstring back to the client will be that much smaller
   */
  getImageData: function (maxDim) {
    return imageToImageData(this.rawNode, maxDim).then(imageData => {
      return {
        data: LongStringActor(this.conn, imageData.data),
        size: imageData.size
      };
    });
  },

  /**
   * Get all event listeners that are listening on this node.
   */
  getEventListenerInfo: function () {
    if (this.rawNode.nodeName.toLowerCase() === "html") {
      return this.getEventListeners(this.rawNode.ownerGlobal);
    }
    return this.getEventListeners(this.rawNode);
  },

  /**
   * Modify a node's attributes.  Passed an array of modifications
   * similar in format to "attributes" mutations.
   * {
   *   attributeName: <string>
   *   attributeNamespace: <optional string>
   *   newValue: <optional string> - If null or undefined, the attribute
   *     will be removed.
   * }
   *
   * Returns when the modifications have been made.  Mutations will
   * be queued for any changes made.
   */
  modifyAttributes: function (modifications) {
    let rawNode = this.rawNode;
    for (let change of modifications) {
      if (change.newValue == null) {
        if (change.attributeNamespace) {
          rawNode.removeAttributeNS(change.attributeNamespace,
                                    change.attributeName);
        } else {
          rawNode.removeAttribute(change.attributeName);
        }
      } else if (change.attributeNamespace) {
        rawNode.setAttributeNS(change.attributeNamespace, change.attributeName,
                               change.newValue);
      } else {
        rawNode.setAttribute(change.attributeName, change.newValue);
      }
    }
  },

  /**
   * Given the font and fill style, get the image data of a canvas with the
   * preview text and font.
   * Returns an imageData object with the actual data being a LongStringActor
   * and the width of the text as a string.
   * The image data is transmitted as a base64 encoded png data-uri.
   */
  getFontFamilyDataURL: function (font, fillStyle = "black") {
    let doc = this.rawNode.ownerDocument;
    let options = {
      previewText: FONT_FAMILY_PREVIEW_TEXT,
      previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
      fillStyle: fillStyle
    };
    let { dataURL, size } = getFontPreviewData(font, doc, options);

    return { data: LongStringActor(this.conn, dataURL), size: size };
  }
});

/**
 * Server side of a node list as returned by querySelectorAll()
 */
var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
  typeName: "domnodelist",

  initialize: function (walker, nodeList) {
    protocol.Actor.prototype.initialize.call(this);
    this.walker = walker;
    this.nodeList = nodeList || [];
  },

  destroy: function () {
    protocol.Actor.prototype.destroy.call(this);
  },

  /**
   * Instead of storing a connection object, the NodeActor gets its connection
   * from its associated walker.
   */
  get conn() {
    return this.walker.conn;
  },

  /**
   * Items returned by this actor should belong to the parent walker.
   */
  marshallPool: function () {
    return this.walker;
  },

  // Returns the JSON representation of this object over the wire.
  form: function () {
    return {
      actor: this.actorID,
      length: this.nodeList ? this.nodeList.length : 0
    };
  },

  /**
   * Get a single node from the node list.
   */
  item: function (index) {
    return this.walker.attachElement(this.nodeList[index]);
  },

  /**
   * Get a range of the items from the node list.
   */
  items: function (start = 0, end = this.nodeList.length) {
    let items = Array.prototype.slice.call(this.nodeList, start, end)
      .map(item => this.walker._ref(item));
    return this.walker.attachElements(items);
  },

  release: function () {}
});

/**
 * Server side of the DOM walker.
 */
var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
  /**
   * Create the WalkerActor
   * @param DebuggerServerConnection conn
   *    The server connection.
   */
  initialize: function (conn, tabActor, options) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this.tabActor = tabActor;
    this.rootWin = tabActor.window;
    this.rootDoc = this.rootWin.document;
    this._refMap = new Map();
    this._pendingMutations = [];
    this._activePseudoClassLocks = new Set();
    this.showAllAnonymousContent = options.showAllAnonymousContent;

    this.walkerSearch = new WalkerSearch(this);

    // Nodes which have been removed from the client's known
    // ownership tree are considered "orphaned", and stored in
    // this set.
    this._orphaned = new Set();

    // The client can tell the walker that it is interested in a node
    // even when it is orphaned with the `retainNode` method.  This
    // list contains orphaned nodes that were so retained.
    this._retainedOrphans = new Set();

    this.onMutations = this.onMutations.bind(this);
    this.onFrameLoad = this.onFrameLoad.bind(this);
    this.onFrameUnload = this.onFrameUnload.bind(this);

    events.on(tabActor, "will-navigate", this.onFrameUnload);
    events.on(tabActor, "navigate", this.onFrameLoad);

    // Ensure that the root document node actor is ready and
    // managed.
    this.rootNode = this.document();

    this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor);
    this._onReflows = this._onReflows.bind(this);
    this.layoutChangeObserver.on("reflows", this._onReflows);
    this._onResize = this._onResize.bind(this);
    this.layoutChangeObserver.on("resize", this._onResize);

    this._onEventListenerChange = this._onEventListenerChange.bind(this);
    eventListenerService.addListenerChangeListener(this._onEventListenerChange);
  },

  /**
   * Callback for eventListenerService.addListenerChangeListener
   * @param nsISimpleEnumerator changesEnum
   *    enumerator of nsIEventListenerChange
   */
  _onEventListenerChange: function (changesEnum) {
    let changes = changesEnum.enumerate();
    while (changes.hasMoreElements()) {
      let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
      let target = current.target;

      if (this._refMap.has(target)) {
        let actor = this.getNode(target);
        let mutation = {
          type: "events",
          target: actor.actorID,
          hasEventListeners: actor._hasEventListeners
        };
        this.queueMutation(mutation);
      }
    }
  },

  // Returns the JSON representation of this object over the wire.
  form: function () {
    return {
      actor: this.actorID,
      root: this.rootNode.form(),
      traits: {
        // FF42+ Inspector starts managing the Walker, while the inspector also
        // starts cleaning itself up automatically on client disconnection.
        // So that there is no need to manually release the walker anymore.
        autoReleased: true,
        // XXX: It seems silly that we need to tell the front which capabilities
        // its actor has in this way when the target can use actorHasMethod. If
        // this was ported to the protocol (Bug 1157048) we could call that
        // inside of custom front methods and not need to do traits for this.
        multiFrameQuerySelectorAll: true,
        textSearch: true,
      }
    };
  },

  toString: function () {
    return "[WalkerActor " + this.actorID + "]";
  },

  getDocumentWalker: function (node, whatToShow) {
    // Allow native anon content (like <video> controls) if preffed on
    let nodeFilter = this.showAllAnonymousContent
                        ? allAnonymousContentTreeWalkerFilter
                        : standardTreeWalkerFilter;
    return new DocumentWalker(node, this.rootWin, whatToShow, nodeFilter);
  },

  destroy: function () {
    if (this._destroyed) {
      return;
    }
    this._destroyed = true;
    protocol.Actor.prototype.destroy.call(this);
    try {
      this.clearPseudoClassLocks();
      this._activePseudoClassLocks = null;

      this._hoveredNode = null;
      this.rootWin = null;
      this.rootDoc = null;
      this.rootNode = null;
      this.layoutHelpers = null;
      this._orphaned = null;
      this._retainedOrphans = null;
      this._refMap = null;

      events.off(this.tabActor, "will-navigate", this.onFrameUnload);
      events.off(this.tabActor, "navigate", this.onFrameLoad);

      this.onFrameLoad = null;
      this.onFrameUnload = null;

      this.walkerSearch.destroy();

      this.layoutChangeObserver.off("reflows", this._onReflows);
      this.layoutChangeObserver.off("resize", this._onResize);
      this.layoutChangeObserver = null;
      releaseLayoutChangesObserver(this.tabActor);

      eventListenerService.removeListenerChangeListener(
        this._onEventListenerChange);

      this.onMutations = null;

      this.tabActor = null;

      events.emit(this, "destroyed");
    } catch (e) {
      console.error(e);
    }
  },

  release: function () {},

  unmanage: function (actor) {
    if (actor instanceof NodeActor) {
      if (this._activePseudoClassLocks &&
          this._activePseudoClassLocks.has(actor)) {
        this.clearPseudoClassLocks(actor);
      }
      this._refMap.delete(actor.rawNode);
    }
    protocol.Actor.prototype.unmanage.call(this, actor);
  },

  /**
   * Determine if the walker has come across this DOM node before.
   * @param {DOMNode} rawNode
   * @return {Boolean}
   */
  hasNode: function (rawNode) {
    return this._refMap.has(rawNode);
  },

  /**
   * If the walker has come across this DOM node before, then get the
   * corresponding node actor.
   * @param {DOMNode} rawNode
   * @return {NodeActor}
   */
  getNode: function (rawNode) {
    return this._refMap.get(rawNode);
  },

  _ref: function (node) {
    let actor = this.getNode(node);
    if (actor) {
      return actor;
    }

    actor = new NodeActor(this, node);

    // Add the node actor as a child of this walker actor, assigning
    // it an actorID.
    this.manage(actor);
    this._refMap.set(node, actor);

    if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
      actor.watchDocument(this.onMutations);
    }
    return actor;
  },

  _onReflows: function (reflows) {
    // Going through the nodes the walker knows about, see which ones have
    // had their display changed and send a display-change event if any
    let changes = [];
    for (let [node, actor] of this._refMap) {
      if (Cu.isDeadWrapper(node)) {
        continue;
      }

      let isDisplayed = actor.isDisplayed;
      if (isDisplayed !== actor.wasDisplayed) {
        changes.push(actor);
        // Updating the original value
        actor.wasDisplayed = isDisplayed;
      }
    }

    if (changes.length) {
      events.emit(this, "display-change", changes);
    }
  },

  /**
   * When the browser window gets resized, relay the event to the front.
   */
  _onResize: function () {
    events.emit(this, "resize");
  },

  /**
   * This is kept for backward-compatibility reasons with older remote targets.
   * Targets prior to bug 916443.
   *
   * pick/cancelPick are used to pick a node on click on the content
   * document. But in their implementation prior to bug 916443, they don't allow
   * highlighting on hover.
   * The client-side now uses the highlighter actor's pick and cancelPick
   * methods instead. The client-side uses the the highlightable trait found in
   * the root actor to determine which version of pick to use.
   *
   * As for highlight, the new highlighter actor is used instead of the walker's
   * highlight method. Same here though, the client-side uses the highlightable
   * trait to dertermine which to use.
   *
   * Keeping these actor methods for now allows newer client-side debuggers to
   * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
   */
  pick: function () {},
  cancelPick: function () {},
  highlight: function (node) {},

  /**
   * Ensures that the node is attached and it can be accessed from the root.
   *
   * @param {(Node|NodeActor)} nodes The nodes
   * @return {Object} An object compatible with the disconnectedNode type.
   */
  attachElement: function (node) {
    let { nodes, newParents } = this.attachElements([node]);
    return {
      node: nodes[0],
      newParents: newParents
    };
  },

  /**
   * Ensures that the nodes are attached and they can be accessed from the root.
   *
   * @param {(Node[]|NodeActor[])} nodes The nodes
   * @return {Object} An object compatible with the disconnectedNodeArray type.
   */
  attachElements: function (nodes) {
    let nodeActors = [];
    let newParents = new Set();
    for (let node of nodes) {
      if (!(node instanceof NodeActor)) {
        // If an anonymous node was passed in and we aren't supposed to know
        // about it, then consult with the document walker as the source of
        // truth about which elements exist.
        if (!this.showAllAnonymousContent && isAnonymous(node)) {
          node = this.getDocumentWalker(node).currentNode;
        }

        node = this._ref(node);
      }

      this.ensurePathToRoot(node, newParents);
      // If nodes may be an array of raw nodes, we're sure to only have
      // NodeActors with the following array.
      nodeActors.push(node);
    }

    return {
      nodes: nodeActors,
      newParents: [...newParents]
    };
  },

  /**
   * Return the document node that contains the given node,
   * or the root node if no node is specified.
   * @param NodeActor node
   *        The node whose document is needed, or null to
   *        return the root.
   */
  document: function (node) {
    let doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
    return this._ref(doc);
  },

  /**
   * Return the documentElement for the document containing the
   * given node.
   * @param NodeActor node
   *        The node whose documentElement is requested, or null
   *        to use the root document.
   */
  documentElement: function (node) {
    let elt = isNodeDead(node)
              ? this.rootDoc.documentElement
              : nodeDocument(node.rawNode).documentElement;
    return this._ref(elt);
  },

  /**
   * Return all parents of the given node, ordered from immediate parent
   * to root.
   * @param NodeActor node
   *    The node whose parents are requested.
   * @param object options
   *    Named options, including:
   *    `sameDocument`: If true, parents will be restricted to the same
   *      document as the node.
   *    `sameTypeRootTreeItem`: If true, this will not traverse across
   *     different types of docshells.
   */
  parents: function (node, options = {}) {
    if (isNodeDead(node)) {
      return [];
    }

    let walker = this.getDocumentWalker(node.rawNode);
    let parents = [];
    let cur;
    while ((cur = walker.parentNode())) {
      if (options.sameDocument &&
          nodeDocument(cur) != nodeDocument(node.rawNode)) {
        break;
      }

      if (options.sameTypeRootTreeItem &&
          nodeDocshell(cur).sameTypeRootTreeItem !=
          nodeDocshell(node.rawNode).sameTypeRootTreeItem) {
        break;
      }

      parents.push(this._ref(cur));
    }
    return parents;
  },

  parentNode: function (node) {
    let walker = this.getDocumentWalker(node.rawNode);
    let parent = walker.parentNode();
    if (parent) {
      return this._ref(parent);
    }
    return null;
  },

  /**
   * If the given NodeActor only has a single text node as a child with a text
   * content small enough to be inlined, return that child's NodeActor.
   *
   * @param NodeActor node
   */
  inlineTextChild: function (node) {
    // Quick checks to prevent creating a new walker if possible.
    if (node.isBeforePseudoElement ||
        node.isAfterPseudoElement ||
        node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
        node.rawNode.children.length > 0) {
      return undefined;
    }

    let docWalker = this.getDocumentWalker(node.rawNode);
    let firstChild = docWalker.firstChild();

    // Bail out if:
    // - more than one child
    // - unique child is not a text node
    // - unique child is a text node, but is too long to be inlined
    if (!firstChild ||
        docWalker.nextSibling() ||
        firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
        firstChild.nodeValue.length > gValueSummaryLength
        ) {
      return undefined;
    }

    return this._ref(firstChild);
  },

  /**
   * Mark a node as 'retained'.
   *
   * A retained node is not released when `releaseNode` is called on its
   * parent, or when a parent is released with the `cleanup` option to
   * `getMutations`.
   *
   * When a retained node's parent is released, a retained mode is added to
   * the walker's "retained orphans" list.
   *
   * Retained nodes can be deleted by providing the `force` option to
   * `releaseNode`.  They will also be released when their document
   * has been destroyed.
   *
   * Retaining a node makes no promise about its children;  They can
   * still be removed by normal means.
   */
  retainNode: function (node) {
    node.retained = true;
  },

  /**
   * Remove the 'retained' mark from a node.  If the node was a
   * retained orphan, release it.
   */
  unretainNode: function (node) {
    node.retained = false;
    if (this._retainedOrphans.has(node)) {
      this._retainedOrphans.delete(node);
      this.releaseNode(node);
    }
  },

  /**
   * Release actors for a node and all child nodes.
   */
  releaseNode: function (node, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    if (node.retained && !options.force) {
      this._retainedOrphans.add(node);
      return;
    }

    if (node.retained) {
      // Forcing a retained node to go away.
      this._retainedOrphans.delete(node);
    }

    let walker = this.getDocumentWalker(node.rawNode);

    let child = walker.firstChild();
    while (child) {
      let childActor = this.getNode(child);
      if (childActor) {
        this.releaseNode(childActor, options);
      }
      child = walker.nextSibling();
    }

    node.destroy();
  },

  /**
   * Add any nodes between `node` and the walker's root node that have not
   * yet been seen by the client.
   */
  ensurePathToRoot: function (node, newParents = new Set()) {
    if (!node) {
      return newParents;
    }
    let walker = this.getDocumentWalker(node.rawNode);
    let cur;
    while ((cur = walker.parentNode())) {
      let parent = this.getNode(cur);
      if (!parent) {
        // This parent didn't exist, so hasn't been seen by the client yet.
        newParents.add(this._ref(cur));
      } else {
        // This parent did exist, so the client knows about it.
        return newParents;
      }
    }
    return newParents;
  },

  /**
   * Return children of the given node.  By default this method will return
   * all children of the node, but there are options that can restrict this
   * to a more manageable subset.
   *
   * @param NodeActor node
   *    The node whose children you're curious about.
   * @param object options
   *    Named options:
   *    `maxNodes`: The set of nodes returned by the method will be no longer
   *       than maxNodes.
   *    `start`: If a node is specified, the list of nodes will start
   *       with the given child.  Mutally exclusive with `center`.
   *    `center`: If a node is specified, the given node will be as centered
   *       as possible in the list, given how close to the ends of the child
   *       list it is.  Mutually exclusive with `start`.
   *    `whatToShow`: A bitmask of node types that should be included.  See
   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
   *
   * @returns an object with three items:
   *    hasFirst: true if the first child of the node is included in the list.
   *    hasLast: true if the last child of the node is included in the list.
   *    nodes: Child nodes returned by the request.
   */
  children: function (node, options = {}) {
    if (isNodeDead(node)) {
      return { hasFirst: true, hasLast: true, nodes: [] };
    }

    if (options.center && options.start) {
      throw Error("Can't specify both 'center' and 'start' options.");
    }
    let maxNodes = options.maxNodes || -1;
    if (maxNodes == -1) {
      maxNodes = Number.MAX_VALUE;
    }

    // We're going to create a few document walkers with the same filter,
    // make it easier.
    let getFilteredWalker = documentWalkerNode => {
      return this.getDocumentWalker(documentWalkerNode, options.whatToShow);
    };

    // Need to know the first and last child.
    let rawNode = node.rawNode;
    let firstChild = getFilteredWalker(rawNode).firstChild();
    let lastChild = getFilteredWalker(rawNode).lastChild();

    if (!firstChild) {
      // No children, we're done.
      return { hasFirst: true, hasLast: true, nodes: [] };
    }

    let start;
    if (options.center) {
      start = options.center.rawNode;
    } else if (options.start) {
      start = options.start.rawNode;
    } else {
      start = firstChild;
    }

    let nodes = [];

    // Start by reading backward from the starting point if we're centering...
    let backwardWalker = getFilteredWalker(start);
    if (start != firstChild && options.center) {
      backwardWalker.previousSibling();
      let backwardCount = Math.floor(maxNodes / 2);
      let backwardNodes = this._readBackward(backwardWalker, backwardCount);
      nodes = backwardNodes;
    }

    // Then read forward by any slack left in the max children...
    let forwardWalker = getFilteredWalker(start);
    let forwardCount = maxNodes - nodes.length;
    nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));

    // If there's any room left, it means we've run all the way to the end.
    // If we're centering, check if there are more items to read at the front.
    let remaining = maxNodes - nodes.length;
    if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
      let firstNodes = this._readBackward(backwardWalker, remaining);

      // Then put it all back together.
      nodes = firstNodes.concat(nodes);
    }

    return {
      hasFirst: nodes[0].rawNode == firstChild,
      hasLast: nodes[nodes.length - 1].rawNode == lastChild,
      nodes: nodes
    };
  },

  /**
   * Return siblings of the given node.  By default this method will return
   * all siblings of the node, but there are options that can restrict this
   * to a more manageable subset.
   *
   * If `start` or `center` are not specified, this method will center on the
   * node whose siblings are requested.
   *
   * @param NodeActor node
   *    The node whose children you're curious about.
   * @param object options
   *    Named options:
   *    `maxNodes`: The set of nodes returned by the method will be no longer
   *       than maxNodes.
   *    `start`: If a node is specified, the list of nodes will start
   *       with the given child.  Mutally exclusive with `center`.
   *    `center`: If a node is specified, the given node will be as centered
   *       as possible in the list, given how close to the ends of the child
   *       list it is.  Mutually exclusive with `start`.
   *    `whatToShow`: A bitmask of node types that should be included.  See
   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
   *
   * @returns an object with three items:
   *    hasFirst: true if the first child of the node is included in the list.
   *    hasLast: true if the last child of the node is included in the list.
   *    nodes: Child nodes returned by the request.
   */
  siblings: function (node, options = {}) {
    if (isNodeDead(node)) {
      return { hasFirst: true, hasLast: true, nodes: [] };
    }

    let parentNode = this.getDocumentWalker(node.rawNode, options.whatToShow)
                         .parentNode();
    if (!parentNode) {
      return {
        hasFirst: true,
        hasLast: true,
        nodes: [node]
      };
    }

    if (!(options.start || options.center)) {
      options.center = node;
    }

    return this.children(this._ref(parentNode), options);
  },

  /**
   * Get the next sibling of a given node.  Getting nodes one at a time
   * might be inefficient, be careful.
   *
   * @param object options
   *    Named options:
   *    `whatToShow`: A bitmask of node types that should be included.  See
   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
   */
  nextSibling: function (node, options = {}) {
    if (isNodeDead(node)) {
      return null;
    }

    let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
    let sibling = walker.nextSibling();
    return sibling ? this._ref(sibling) : null;
  },

  /**
   * Get the previous sibling of a given node.  Getting nodes one at a time
   * might be inefficient, be careful.
   *
   * @param object options
   *    Named options:
   *    `whatToShow`: A bitmask of node types that should be included.  See
   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
   */
  previousSibling: function (node, options = {}) {
    if (isNodeDead(node)) {
      return null;
    }

    let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
    let sibling = walker.previousSibling();
    return sibling ? this._ref(sibling) : null;
  },

  /**
   * Helper function for the `children` method: Read forward in the sibling
   * list into an array with `count` items, including the current node.
   */
  _readForward: function (walker, count) {
    let ret = [];
    let node = walker.currentNode;
    do {
      ret.push(this._ref(node));
      node = walker.nextSibling();
    } while (node && --count);
    return ret;
  },

  /**
   * Helper function for the `children` method: Read backward in the sibling
   * list into an array with `count` items, including the current node.
   */
  _readBackward: function (walker, count) {
    let ret = [];
    let node = walker.currentNode;
    do {
      ret.push(this._ref(node));
      node = walker.previousSibling();
    } while (node && --count);
    ret.reverse();
    return ret;
  },

  /**
   * Return the node that the parent process has asked to
   * inspect. This node is expected to be stored in gInspectingNode
   * (which is set by a message manager message to the child.js frame
   * script). The node is returned over the remote debugging protocol
   * as a NodeFront.
   */
  findInspectingNode: function () {
    let node = gInspectingNode;
    if (!node) {
      return {};
    }

    return this.attachElement(node);
  },

  /**
   * Return the first node in the document that matches the given selector.
   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
   *
   * @param NodeActor baseNode
   * @param string selector
   */
  querySelector: function (baseNode, selector) {
    if (isNodeDead(baseNode)) {
      return {};
    }

    let node = baseNode.rawNode.querySelector(selector);
    if (!node) {
      return {};
    }

    return this.attachElement(node);
  },

  /**
   * Return a NodeListActor with all nodes that match the given selector.
   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
   *
   * @param NodeActor baseNode
   * @param string selector
   */
  querySelectorAll: function (baseNode, selector) {
    let nodeList = null;

    try {
      nodeList = baseNode.rawNode.querySelectorAll(selector);
    } catch (e) {
      // Bad selector. Do nothing as the selector can come from a searchbox.
    }

    return new NodeListActor(this, nodeList);
  },

  /**
   * Get a list of nodes that match the given selector in all known frames of
   * the current content page.
   * @param {String} selector.
   * @return {Array}
   */
  _multiFrameQuerySelectorAll: function (selector) {
    let nodes = [];

    for (let {document} of this.tabActor.windows) {
      try {
        nodes = [...nodes, ...document.querySelectorAll(selector)];
      } catch (e) {
        // Bad selector. Do nothing as the selector can come from a searchbox.
      }
    }

    return nodes;
  },

  /**
   * Return a NodeListActor with all nodes that match the given selector in all
   * frames of the current content page.
   * @param {String} selector
   */
  multiFrameQuerySelectorAll: function (selector) {
    return new NodeListActor(this, this._multiFrameQuerySelectorAll(selector));
  },

  /**
   * Search the document for a given string.
   * Results will be searched with the walker-search module (searches through
   * tag names, attribute names and values, and text contents).
   *
   * @returns {searchresult}
   *            - {NodeList} list
   *            - {Array<Object>} metadata. Extra information with indices that
   *                              match up with node list.
   */
  search: function (query) {
    let results = this.walkerSearch.search(query);
    let nodeList = new NodeListActor(this, results.map(r => r.node));

    return {
      list: nodeList,
      metadata: []
    };
  },

  /**
   * Returns a list of matching results for CSS selector autocompletion.
   *
   * @param string query
   *        The selector query being completed
   * @param string completing
   *        The exact token being completed out of the query
   * @param string selectorState
   *        One of "pseudo", "id", "tag", "class", "null"
   */
  getSuggestionsForQuery: function (query, completing, selectorState) {
    let sugs = {
      classes: new Map(),
      tags: new Map(),
      ids: new Map()
    };
    let result = [];
    let nodes = null;
    // Filtering and sorting the results so that protocol transfer is miminal.
    switch (selectorState) {
      case "pseudo":
        result = PSEUDO_SELECTORS.filter(item => {
          return item[0].startsWith(":" + completing);
        });
        break;

      case "class":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("[class]");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (let node of nodes) {
          for (let className of node.classList) {
            sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
          }
        }
        sugs.classes.delete("");
        sugs.classes.delete(HIDDEN_CLASS);
        for (let [className, count] of sugs.classes) {
          if (className.startsWith(completing)) {
            result.push(["." + CSS.escape(className), count, selectorState]);
          }
        }
        break;

      case "id":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("[id]");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (let node of nodes) {
          sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
        }
        for (let [id, count] of sugs.ids) {
          if (id.startsWith(completing) && id !== "") {
            result.push(["#" + CSS.escape(id), count, selectorState]);
          }
        }
        break;

      case "tag":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("*");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (let node of nodes) {
          let tag = node.localName;
          sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
        }
        for (let [tag, count] of sugs.tags) {
          if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
            result.push([tag, count, selectorState]);
          }
        }

        // For state 'tag' (no preceding # or .) and when there's no query (i.e.
        // only one word) then search for the matching classes and ids
        if (!query) {
          result = [
            ...result,
            ...this.getSuggestionsForQuery(null, completing, "class")
                   .suggestions,
            ...this.getSuggestionsForQuery(null, completing, "id")
                   .suggestions
          ];
        }

        break;

      case "null":
        nodes = this._multiFrameQuerySelectorAll(query);
        for (let node of nodes) {
          sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
          let tag = node.localName;
          sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
          for (let className of node.classList) {
            sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
          }
        }
        for (let [tag, count] of sugs.tags) {
          tag && result.push([tag, count]);
        }
        for (let [id, count] of sugs.ids) {
          id && result.push(["#" + id, count]);
        }
        sugs.classes.delete("");
        sugs.classes.delete(HIDDEN_CLASS);
        for (let [className, count] of sugs.classes) {
          className && result.push(["." + className, count]);
        }
    }

    // Sort by count (desc) and name (asc)
    result = result.sort((a, b) => {
      // Computed a sortable string with first the inverted count, then the name
      let sortA = (10000 - a[1]) + a[0];
      let sortB = (10000 - b[1]) + b[0];

      // Prefixing ids, classes and tags, to group results
      let firstA = a[0].substring(0, 1);
      let firstB = b[0].substring(0, 1);

      if (firstA === "#") {
        sortA = "2" + sortA;
      } else if (firstA === ".") {
        sortA = "1" + sortA;
      } else {
        sortA = "0" + sortA;
      }

      if (firstB === "#") {
        sortB = "2" + sortB;
      } else if (firstB === ".") {
        sortB = "1" + sortB;
      } else {
        sortB = "0" + sortB;
      }

      // String compare
      return sortA.localeCompare(sortB);
    });

    result.slice(0, 25);

    return {
      query: query,
      suggestions: result
    };
  },

  /**
   * Add a pseudo-class lock to a node.
   *
   * @param NodeActor node
   * @param string pseudo
   *    A pseudoclass: ':hover', ':active', ':focus'
   * @param options
   *    Options object:
   *    `parents`: True if the pseudo-class should be added
   *      to parent nodes.
   *
   * @returns An empty packet.  A "pseudoClassLock" mutation will
   *    be queued for any changed nodes.
   */
  addPseudoClassLock: function (node, pseudo, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    this._addPseudoClassLock(node, pseudo);

    if (!options.parents) {
      return;
    }

    let walker = this.getDocumentWalker(node.rawNode);
    let cur;
    while ((cur = walker.parentNode())) {
      let curNode = this._ref(cur);
      this._addPseudoClassLock(curNode, pseudo);
    }
  },

  _queuePseudoClassMutation: function (node) {
    this.queueMutation({
      target: node.actorID,
      type: "pseudoClassLock",
      pseudoClassLocks: node.writePseudoClassLocks()
    });
  },

  _addPseudoClassLock: function (node, pseudo) {
    if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
      return false;
    }
    DOMUtils.addPseudoClassLock(node.rawNode, pseudo);
    this._activePseudoClassLocks.add(node);
    this._queuePseudoClassMutation(node);
    return true;
  },

  _installHelperSheet: function (node) {
    if (!this.installedHelpers) {
      this.installedHelpers = new WeakMap();
    }
    let win = node.rawNode.ownerDocument.defaultView;
    if (!this.installedHelpers.has(win)) {
      let { Style } = require("sdk/stylesheet/style");
      let { attach } = require("sdk/content/mod");
      let style = Style({source: HELPER_SHEET, type: "agent" });
      attach(style, win);
      this.installedHelpers.set(win, style);
    }
  },

  hideNode: function (node) {
    if (isNodeDead(node)) {
      return;
    }

    this._installHelperSheet(node);
    node.rawNode.classList.add(HIDDEN_CLASS);
  },

  unhideNode: function (node) {
    if (isNodeDead(node)) {
      return;
    }

    node.rawNode.classList.remove(HIDDEN_CLASS);
  },

  /**
   * Remove a pseudo-class lock from a node.
   *
   * @param NodeActor node
   * @param string pseudo
   *    A pseudoclass: ':hover', ':active', ':focus'
   * @param options
   *    Options object:
   *    `parents`: True if the pseudo-class should be removed
   *      from parent nodes.
   *
   * @returns An empty response.  "pseudoClassLock" mutations
   *    will be emitted for any changed nodes.
   */
  removePseudoClassLock: function (node, pseudo, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    this._removePseudoClassLock(node, pseudo);

    if (!options.parents) {
      return;
    }

    let walker = this.getDocumentWalker(node.rawNode);
    let cur;
    while ((cur = walker.parentNode())) {
      let curNode = this._ref(cur);
      this._removePseudoClassLock(curNode, pseudo);
    }
  },

  _removePseudoClassLock: function (node, pseudo) {
    if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
      return false;
    }
    DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
    if (!node.writePseudoClassLocks()) {
      this._activePseudoClassLocks.delete(node);
    }

    this._queuePseudoClassMutation(node);
    return true;
  },

  /**
   * Clear all the pseudo-classes on a given node or all nodes.
   * @param {NodeActor} node Optional node to clear pseudo-classes on
   */
  clearPseudoClassLocks: function (node) {
    if (node && isNodeDead(node)) {
      return;
    }

    if (node) {
      DOMUtils.clearPseudoClassLocks(node.rawNode);
      this._activePseudoClassLocks.delete(node);
      this._queuePseudoClassMutation(node);
    } else {
      for (let locked of this._activePseudoClassLocks) {
        DOMUtils.clearPseudoClassLocks(locked.rawNode);
        this._activePseudoClassLocks.delete(locked);
        this._queuePseudoClassMutation(locked);
      }
    }
  },

  /**
   * Get a node's innerHTML property.
   */
  innerHTML: function (node) {
    let html = "";
    if (!isNodeDead(node)) {
      html = node.rawNode.innerHTML;
    }
    return LongStringActor(this.conn, html);
  },

  /**
   * Set a node's innerHTML property.
   *
   * @param {NodeActor} node The node.
   * @param {string} value The piece of HTML content.
   */
  setInnerHTML: function (node, value) {
    if (isNodeDead(node)) {
      return;
    }

    let rawNode = node.rawNode;
    if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) {
      throw new Error("Can only change innerHTML to element nodes");
    }
    rawNode.innerHTML = value;
  },

  /**
   * Get a node's outerHTML property.
   *
   * @param {NodeActor} node The node.
   */
  outerHTML: function (node) {
    let outerHTML = "";
    if (!isNodeDead(node)) {
      outerHTML = node.rawNode.outerHTML;
    }
    return LongStringActor(this.conn, outerHTML);
  },

  /**
   * Set a node's outerHTML property.
   *
   * @param {NodeActor} node The node.
   * @param {string} value The piece of HTML content.
   */
  setOuterHTML: function (node, value) {
    if (isNodeDead(node)) {
      return;
    }

    let parsedDOM = DOMParser.parseFromString(value, "text/html");
    let rawNode = node.rawNode;
    let parentNode = rawNode.parentNode;

    // Special case for head and body.  Setting document.body.outerHTML
    // creates an extra <head> tag, and document.head.outerHTML creates
    // an extra <body>.  So instead we will call replaceChild with the
    // parsed DOM, assuming that they aren't trying to set both tags at once.
    if (rawNode.tagName === "BODY") {
      if (parsedDOM.head.innerHTML === "") {
        parentNode.replaceChild(parsedDOM.body, rawNode);
      } else {
        rawNode.outerHTML = value;
      }
    } else if (rawNode.tagName === "HEAD") {
      if (parsedDOM.body.innerHTML === "") {
        parentNode.replaceChild(parsedDOM.head, rawNode);
      } else {
        rawNode.outerHTML = value;
      }
    } else if (node.isDocumentElement()) {
      // Unable to set outerHTML on the document element.  Fall back by
      // setting attributes manually, then replace the body and head elements.
      let finalAttributeModifications = [];
      let attributeModifications = {};
      for (let attribute of rawNode.attributes) {
        attributeModifications[attribute.name] = null;
      }
      for (let attribute of parsedDOM.documentElement.attributes) {
        attributeModifications[attribute.name] = attribute.value;
      }
      for (let key in attributeModifications) {
        finalAttributeModifications.push({
          attributeName: key,
          newValue: attributeModifications[key]
        });
      }
      node.modifyAttributes(finalAttributeModifications);
      rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
      rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
    } else {
      rawNode.outerHTML = value;
    }
  },

  /**
   * Insert adjacent HTML to a node.
   *
   * @param {Node} node
   * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
   *                          "afterEnd" (see Element.insertAdjacentHTML).
   * @param {string} value The HTML content.
   */
  insertAdjacentHTML: function (node, position, value) {
    if (isNodeDead(node)) {
      return {node: [], newParents: []};
    }

    let rawNode = node.rawNode;
    // Don't insert anything adjacent to the document element,
    // the head or the body.
    if (node.isDocumentElement()) {
      throw new Error("Can't insert adjacent element to the root.");
    }

    let isInsertAsSibling = position === "beforeBegin" ||
      position === "afterEnd";
    if ((rawNode.tagName === "BODY" || rawNode.tagName === "HEAD") &&
      isInsertAsSibling) {
      throw new Error("Can't insert element before or after the body " +
        "or the head.");
    }

    let rawParentNode = rawNode.parentNode;
    if (!rawParentNode && isInsertAsSibling) {
      throw new Error("Can't insert as sibling without parent node.");
    }

    // We can't use insertAdjacentHTML, because we want to return the nodes
    // being created (so the front can remove them if the user undoes
    // the change). So instead, use Range.createContextualFragment().
    let range = rawNode.ownerDocument.createRange();
    if (position === "beforeBegin" || position === "afterEnd") {
      range.selectNode(rawNode);
    } else {
      range.selectNodeContents(rawNode);
    }
    let docFrag = range.createContextualFragment(value);
    let newRawNodes = Array.from(docFrag.childNodes);
    switch (position) {
      case "beforeBegin":
        rawParentNode.insertBefore(docFrag, rawNode);
        break;
      case "afterEnd":
        // Note: if the second argument is null, rawParentNode.insertBefore
        // behaves like rawParentNode.appendChild.
        rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
        break;
      case "afterBegin":
        rawNode.insertBefore(docFrag, rawNode.firstChild);
        break;
      case "beforeEnd":
        rawNode.appendChild(docFrag);
        break;
      default:
        throw new Error("Invalid position value. Must be either " +
          "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'.");
    }

    return this.attachElements(newRawNodes);
  },

  /**
   * Duplicate a specified node
   *
   * @param {NodeActor} node The node to duplicate.
   */
  duplicateNode: function ({rawNode}) {
    let clonedNode = rawNode.cloneNode(true);
    rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
  },

  /**
   * Test whether a node is a document or a document element.
   *
   * @param {NodeActor} node The node to remove.
   * @return {boolean} True if the node is a document or a document element.
   */
  isDocumentOrDocumentElementNode: function (node) {
    return ((node.rawNode.ownerDocument &&
      node.rawNode.ownerDocument.documentElement === this.rawNode) ||
      node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
  },

  /**
   * Removes a node from its parent node.
   *
   * @param {NodeActor} node The node to remove.
   * @returns The node's nextSibling before it was removed.
   */
  removeNode: function (node) {
    if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
      throw Error("Cannot remove document, document elements or dead nodes.");
    }

    let nextSibling = this.nextSibling(node);
    node.rawNode.remove();
    // Mutation events will take care of the rest.
    return nextSibling;
  },

  /**
   * Removes an array of nodes from their parent node.
   *
   * @param {NodeActor[]} nodes The nodes to remove.
   */
  removeNodes: function (nodes) {
    // Check that all nodes are valid before processing the removals.
    for (let node of nodes) {
      if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
        throw Error("Cannot remove document, document elements or dead nodes");
      }
    }

    for (let node of nodes) {
      node.rawNode.remove();
      // Mutation events will take care of the rest.
    }
  },

  /**
   * Insert a node into the DOM.
   */
  insertBefore: function (node, parent, sibling) {
    if (isNodeDead(node) ||
        isNodeDead(parent) ||
        (sibling && isNodeDead(sibling))) {
      return;
    }

    let rawNode = node.rawNode;
    let rawParent = parent.rawNode;
    let rawSibling = sibling ? sibling.rawNode : null;

    // Don't bother inserting a node if the document position isn't going
    // to change. This prevents needless iframes reloading and mutations.
    if (rawNode.parentNode === rawParent) {
      let currentNextSibling = this.nextSibling(node);
      currentNextSibling = currentNextSibling ? currentNextSibling.rawNode :
                                                null;

      if (rawNode === rawSibling || currentNextSibling === rawSibling) {
        return;
      }
    }

    rawParent.insertBefore(rawNode, rawSibling);
  },

  /**
   * Editing a node's tagname actually means creating a new node with the same
   * attributes, removing the node and inserting the new one instead.
   * This method does not return anything as mutation events are taking care of
   * informing the consumers about changes.
   */
  editTagName: function (node, tagName) {
    if (isNodeDead(node)) {
      return null;
    }

    let oldNode = node.rawNode;

    // Create a new element with the same attributes as the current element and
    // prepare to replace the current node with it.
    let newNode;
    try {
      newNode = nodeDocument(oldNode).createElement(tagName);
    } catch (x) {
      // Failed to create a new element with that tag name, ignore the change,
      // and signal the error to the front.
      return Promise.reject(new Error("Could not change node's tagName to " + tagName));
    }

    let attrs = oldNode.attributes;
    for (let i = 0; i < attrs.length; i++) {
      newNode.setAttribute(attrs[i].name, attrs[i].value);
    }

    // Insert the new node, and transfer the old node's children.
    oldNode.parentNode.insertBefore(newNode, oldNode);
    while (oldNode.firstChild) {
      newNode.appendChild(oldNode.firstChild);
    }

    oldNode.remove();
    return null;
  },

  /**
   * Get any pending mutation records.  Must be called by the client after
   * the `new-mutations` notification is received.  Returns an array of
   * mutation records.
   *
   * Mutation records have a basic structure:
   *
   * {
   *   type: attributes|characterData|childList,
   *   target: <domnode actor ID>,
   * }
   *
   * And additional attributes based on the mutation type:
   *
   * `attributes` type:
   *   attributeName: <string> - the attribute that changed
   *   attributeNamespace: <string> - the attribute's namespace URI, if any.
   *   newValue: <string> - The new value of the attribute, if any.
   *
   * `characterData` type:
   *   newValue: <string> - the new nodeValue for the node
   *
   * `childList` type is returned when the set of children for a node
   * has changed.  Includes extra data, which can be used by the client to
   * maintain its ownership subtree.
   *
   *   added: array of <domnode actor ID> - The list of actors *previously
   *     seen by the client* that were added to the target node.
   *   removed: array of <domnode actor ID> The list of actors *previously
   *     seen by the client* that were removed from the target node.
   *   inlineTextChild: If the node now has a single text child, it will
   *     be sent here.
   *
   * Actors that are included in a MutationRecord's `removed` but
   * not in an `added` have been removed from the client's ownership
   * tree (either by being moved under a node the client has seen yet
   * or by being removed from the tree entirely), and is considered
   * 'orphaned'.
   *
   * Keep in mind that if a node that the client hasn't seen is moved
   * into or out of the target node, it will not be included in the
   * removedNodes and addedNodes list, so if the client is interested
   * in the new set of children it needs to issue a `children` request.
   */
  getMutations: function (options = {}) {
    let pending = this._pendingMutations || [];
    this._pendingMutations = [];

    if (options.cleanup) {
      for (let node of this._orphaned) {
        // Release the orphaned node.  Nodes or children that have been
        // retained will be moved to this._retainedOrphans.
        this.releaseNode(node);
      }
      this._orphaned = new Set();
    }

    return pending;
  },

  queueMutation: function (mutation) {
    if (!this.actorID || this._destroyed) {
      // We've been destroyed, don't bother queueing this mutation.
      return;
    }
    // We only send the `new-mutations` notification once, until the client
    // fetches mutations with the `getMutations` packet.
    let needEvent = this._pendingMutations.length === 0;

    this._pendingMutations.push(mutation);

    if (needEvent) {
      events.emit(this, "new-mutations");
    }
  },

  /**
   * Handles mutations from the DOM mutation observer API.
   *
   * @param array[MutationRecord] mutations
   *    See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
   */
  onMutations: function (mutations) {
    // Notify any observers that want *all* mutations (even on nodes that aren't
    // referenced).  This is not sent over the protocol so can only be used by
    // scripts running in the server process.
    events.emit(this, "any-mutation");

    for (let change of mutations) {
      let targetActor = this.getNode(change.target);
      if (!targetActor) {
        continue;
      }
      let targetNode = change.target;
      let type = change.type;
      let mutation = {
        type: type,
        target: targetActor.actorID,
      };

      if (type === "attributes") {
        mutation.attributeName = change.attributeName;
        mutation.attributeNamespace = change.attributeNamespace || undefined;
        mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ?
                            targetNode.getAttribute(mutation.attributeName)
                            : null;
      } else if (type === "characterData") {
        mutation.newValue = targetNode.nodeValue;
        this._maybeQueueInlineTextChildMutation(change, targetNode);
      } else if (type === "childList" || type === "nativeAnonymousChildList") {
        // Get the list of removed and added actors that the client has seen
        // so that it can keep its ownership tree up to date.
        let removedActors = [];
        let addedActors = [];
        for (let removed of change.removedNodes) {
          let removedActor = this.getNode(removed);
          if (!removedActor) {
            // If the client never encountered this actor we don't need to
            // mention that it was removed.
            continue;
          }
          // While removed from the tree, nodes are saved as orphaned.
          this._orphaned.add(removedActor);
          removedActors.push(removedActor.actorID);
        }
        for (let added of change.addedNodes) {
          let addedActor = this.getNode(added);
          if (!addedActor) {
            // If the client never encounted this actor we don't need to tell
            // it about its addition for ownership tree purposes - if the
            // client wants to see the new nodes it can ask for children.
            continue;
          }
          // The actor is reconnected to the ownership tree, unorphan
          // it and let the client know so that its ownership tree is up
          // to date.
          this._orphaned.delete(addedActor);
          addedActors.push(addedActor.actorID);
        }

        mutation.numChildren = targetActor.numChildren;
        mutation.removed = removedActors;
        mutation.added = addedActors;

        let inlineTextChild = this.inlineTextChild(targetActor);
        if (inlineTextChild) {
          mutation.inlineTextChild = inlineTextChild.form();
        }
      }
      this.queueMutation(mutation);
    }
  },

  /**
   * Check if the provided mutation could change the way the target element is
   * inlined with its parent node. If it might, a custom mutation of type
   * "inlineTextChild" will be queued.
   *
   * @param {MutationRecord} mutation
   *        A characterData type mutation
   */
  _maybeQueueInlineTextChildMutation: function (mutation) {
    let {oldValue, target} = mutation;
    let newValue = target.nodeValue;
    let limit = gValueSummaryLength;

    if ((oldValue.length <= limit && newValue.length <= limit) ||
        (oldValue.length > limit && newValue.length > limit)) {
      // Bail out if the new & old values are both below/above the size limit.
      return;
    }

    let parentActor = this.getNode(target.parentNode);
    if (!parentActor || parentActor.rawNode.children.length > 0) {
      // If the parent node has other children, a character data mutation will
      // not change anything regarding inlining text nodes.
      return;
    }

    let inlineTextChild = this.inlineTextChild(parentActor);
    this.queueMutation({
      type: "inlineTextChild",
      target: parentActor.actorID,
      inlineTextChild:
        inlineTextChild ? inlineTextChild.form() : undefined
    });
  },

  onFrameLoad: function ({ window, isTopLevel }) {
    if (!this.rootDoc && isTopLevel) {
      this.rootDoc = window.document;
      this.rootNode = this.document();
      this.queueMutation({
        type: "newRoot",
        target: this.rootNode.form()
      });
      return;
    }
    let frame = getFrameElement(window);
    let frameActor = this.getNode(frame);
    if (!frameActor) {
      return;
    }

    this.queueMutation({
      type: "frameLoad",
      target: frameActor.actorID,
    });

    // Send a childList mutation on the frame.
    this.queueMutation({
      type: "childList",
      target: frameActor.actorID,
      added: [],
      removed: []
    });
  },

  // Returns true if domNode is in window or a subframe.
  _childOfWindow: function (window, domNode) {
    let win = nodeDocument(domNode).defaultView;
    while (win) {
      if (win === window) {
        return true;
      }
      win = getFrameElement(win);
    }
    return false;
  },

  onFrameUnload: function ({ window }) {
    // Any retained orphans that belong to this document
    // or its children need to be released, and a mutation sent
    // to notify of that.
    let releasedOrphans = [];

    for (let retained of this._retainedOrphans) {
      if (Cu.isDeadWrapper(retained.rawNode) ||
          this._childOfWindow(window, retained.rawNode)) {
        this._retainedOrphans.delete(retained);
        releasedOrphans.push(retained.actorID);
        this.releaseNode(retained, { force: true });
      }
    }

    if (releasedOrphans.length > 0) {
      this.queueMutation({
        target: this.rootNode.actorID,
        type: "unretained",
        nodes: releasedOrphans
      });
    }

    let doc = window.document;
    let documentActor = this.getNode(doc);
    if (!documentActor) {
      return;
    }

    if (this.rootDoc === doc) {
      this.rootDoc = null;
      this.rootNode = null;
    }

    this.queueMutation({
      type: "documentUnload",
      target: documentActor.actorID
    });

    let walker = this.getDocumentWalker(doc);
    let parentNode = walker.parentNode();
    if (parentNode) {
      // Send a childList mutation on the frame so that clients know
      // they should reread the children list.
      this.queueMutation({
        type: "childList",
        target: this.getNode(parentNode).actorID,
        added: [],
        removed: []
      });
    }

    // Need to force a release of this node, because those nodes can't
    // be accessed anymore.
    this.releaseNode(documentActor, { force: true });
  },

  /**
   * Check if a node is attached to the DOM tree of the current page.
   * @param {nsIDomNode} rawNode
   * @return {Boolean} false if the node is removed from the tree or within a
   * document fragment
   */
  _isInDOMTree: function (rawNode) {
    let walker = this.getDocumentWalker(rawNode);
    let current = walker.currentNode;

    // Reaching the top of tree
    while (walker.parentNode()) {
      current = walker.currentNode;
    }

    // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
    // attached
    if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
        current !== this.rootDoc) {
      return false;
    }

    // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
    return true;
  },

  /**
   * @see _isInDomTree
   */
  isInDOMTree: function (node) {
    if (isNodeDead(node)) {
      return false;
    }
    return this._isInDOMTree(node.rawNode);
  },

  /**
   * Given an ObjectActor (identified by its ID), commonly used in the debugger,
   * webconsole and variablesView, return the corresponding inspector's
   * NodeActor
   */
  getNodeActorFromObjectActor: function (objectActorID) {
    let actor = this.conn.getActor(objectActorID);
    if (!actor) {
      return null;
    }

    let debuggerObject = this.conn.getActor(objectActorID).obj;
    let rawNode = debuggerObject.unsafeDereference();

    if (!this._isInDOMTree(rawNode)) {
      return null;
    }

    // This is a special case for the document object whereby it is considered
    // as document.documentElement (the <html> node)
    if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
      rawNode = rawNode.documentElement;
    }

    return this.attachElement(rawNode);
  },

  /**
   * Given a StyleSheetActor (identified by its ID), commonly used in the
   * style-editor, get its ownerNode and return the corresponding walker's
   * NodeActor.
   * Note that getNodeFromActor was added later and can now be used instead.
   */
  getStyleSheetOwnerNode: function (styleSheetActorID) {
    return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
  },

  /**
   * This method can be used to retrieve NodeActor for DOM nodes from other
   * actors in a way that they can later be highlighted in the page, or
   * selected in the inspector.
   * If an actor has a reference to a DOM node, and the UI needs to know about
   * this DOM node (and possibly select it in the inspector), the UI should
   * first retrieve a reference to the walkerFront:
   *
   * // Make sure the inspector/walker have been initialized first.
   * toolbox.initInspector().then(() => {
   *  // Retrieve the walker.
   *  let walker = toolbox.walker;
   * });
   *
   * And then call this method:
   *
   * // Get the nodeFront from my actor, passing the ID and properties path.
   * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
   *   // Use the nodeFront, e.g. select the node in the inspector.
   *   toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
   * });
   *
   * @param {String} actorID The ID for the actor that has a reference to the
   * DOM node.
   * @param {Array} path Where, on the actor, is the DOM node stored. If in the
   * scope of the actor, the node is available as `this.data.node`, then this
   * should be ["data", "node"].
   * @return {NodeActor} The attached NodeActor, or null if it couldn't be
   * found.
   */
  getNodeFromActor: function (actorID, path) {
    let actor = this.conn.getActor(actorID);
    if (!actor) {
      return null;
    }

    let obj = actor;
    for (let name of path) {
      if (!(name in obj)) {
        return null;
      }
      obj = obj[name];
    }

    return this.attachElement(obj);
  },
});

/**
 * Server side of the inspector actor, which is used to create
 * inspector-related actors, including the walker.
 */
exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
  initialize: function (conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this.tabActor = tabActor;

    this._onColorPicked = this._onColorPicked.bind(this);
    this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
    this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
  },

  destroy: function () {
    protocol.Actor.prototype.destroy.call(this);

    this.destroyEyeDropper();

    this._highlighterPromise = null;
    this._pageStylePromise = null;
    this._walkerPromise = null;
    this.walker = null;
    this.tabActor = null;
  },

  // Forces destruction of the actor and all its children
  // like highlighter, walker and style actors.
  disconnect: function () {
    this.destroy();
  },

  get window() {
    return this.tabActor.window;
  },

  getWalker: function (options = {}) {
    if (this._walkerPromise) {
      return this._walkerPromise;
    }

    let deferred = promise.defer();
    this._walkerPromise = deferred.promise;

    let window = this.window;
    let domReady = () => {
      let tabActor = this.tabActor;
      window.removeEventListener("DOMContentLoaded", domReady, true);
      this.walker = WalkerActor(this.conn, tabActor, options);
      this.manage(this.walker);
      events.once(this.walker, "destroyed", () => {
        this._walkerPromise = null;
        this._pageStylePromise = null;
      });
      deferred.resolve(this.walker);
    };

    if (window.document.readyState === "loading") {
      window.addEventListener("DOMContentLoaded", domReady, true);
    } else {
      domReady();
    }

    return this._walkerPromise;
  },

  getPageStyle: function () {
    if (this._pageStylePromise) {
      return this._pageStylePromise;
    }

    this._pageStylePromise = this.getWalker().then(walker => {
      let pageStyle = PageStyleActor(this);
      this.manage(pageStyle);
      return pageStyle;
    });
    return this._pageStylePromise;
  },

  /**
   * The most used highlighter actor is the HighlighterActor which can be
   * conveniently retrieved by this method.
   * The same instance will always be returned by this method when called
   * several times.
   * The highlighter actor returned here is used to highlighter elements's
   * box-models from the markup-view, layout-view, console, debugger, ... as
   * well as select elements with the pointer (pick).
   *
   * @param {Boolean} autohide Optionally autohide the highlighter after an
   * element has been picked
   * @return {HighlighterActor}
   */
  getHighlighter: function (autohide) {
    if (this._highlighterPromise) {
      return this._highlighterPromise;
    }

    this._highlighterPromise = this.getWalker().then(walker => {
      let highlighter = HighlighterActor(this, autohide);
      this.manage(highlighter);
      return highlighter;
    });
    return this._highlighterPromise;
  },

  /**
   * If consumers need to display several highlighters at the same time or
   * different types of highlighters, then this method should be used, passing
   * the type name of the highlighter needed as argument.
   * A new instance will be created everytime the method is called, so it's up
   * to the consumer to release it when it is not needed anymore
   *
   * @param {String} type The type of highlighter to create
   * @return {Highlighter} The highlighter actor instance or null if the
   * typeName passed doesn't match any available highlighter
   */
  getHighlighterByType: function (typeName) {
    if (isTypeRegistered(typeName)) {
      return CustomHighlighterActor(this, typeName);
    }
    return null;
  },

  /**
   * Get the node's image data if any (for canvas and img nodes).
   * Returns an imageData object with the actual data being a LongStringActor
   * and a size json object.
   * The image data is transmitted as a base64 encoded png data-uri.
   * The method rejects if the node isn't an image or if the image is missing
   *
   * Accepts a maxDim request parameter to resize images that are larger. This
   * is important as the resizing occurs server-side so that image-data being
   * transfered in the longstring back to the client will be that much smaller
   */
  getImageDataFromURL: function (url, maxDim) {
    let img = new this.window.Image();
    img.src = url;

    // imageToImageData waits for the image to load.
    return imageToImageData(img, maxDim).then(imageData => {
      return {
        data: LongStringActor(this.conn, imageData.data),
        size: imageData.size
      };
    });
  },

  /**
   * Resolve a URL to its absolute form, in the scope of a given content window.
   * @param {String} url.
   * @param {NodeActor} node If provided, the owner window of this node will be
   * used to resolve the URL. Otherwise, the top-level content window will be
   * used instead.
   * @return {String} url.
   */
  resolveRelativeURL: function (url, node) {
    let document = isNodeDead(node)
                   ? this.window.document
                   : nodeDocument(node.rawNode);

    if (!document) {
      return url;
    }

    let baseURI = Services.io.newURI(document.location.href, null, null);
    return Services.io.newURI(url, null, baseURI).spec;
  },

  /**
   * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
   * Note that for now, a new instance is created every time to deal with page navigation.
   */
  createEyeDropper: function () {
    this.destroyEyeDropper();
    this._highlighterEnv = new HighlighterEnvironment();
    this._highlighterEnv.initFromTabActor(this.tabActor);
    this._eyeDropper = new EyeDropper(this._highlighterEnv);
  },

  /**
   * Destroy the current eye-dropper highlighter instance.
   */
  destroyEyeDropper: function () {
    if (this._eyeDropper) {
      this.cancelPickColorFromPage();
      this._eyeDropper.destroy();
      this._eyeDropper = null;
      this._highlighterEnv.destroy();
      this._highlighterEnv = null;
    }
  },

  /**
   * Pick a color from the page using the eye-dropper. This method doesn't return anything
   * but will cause events to be sent to the front when a color is picked or when the user
   * cancels the picker.
   * @param {Object} options
   */
  pickColorFromPage: function (options) {
    this.createEyeDropper();
    this._eyeDropper.show(this.window.document.documentElement, options);
    this._eyeDropper.once("selected", this._onColorPicked);
    this._eyeDropper.once("canceled", this._onColorPickCanceled);
    events.once(this.tabActor, "will-navigate", this.destroyEyeDropper);
  },

  /**
   * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
   * highlighter is for the user to click in the page and select a color. If you need to
   * dismiss the eye-dropper programatically instead, use this method.
   */
  cancelPickColorFromPage: function () {
    if (this._eyeDropper) {
      this._eyeDropper.hide();
      this._eyeDropper.off("selected", this._onColorPicked);
      this._eyeDropper.off("canceled", this._onColorPickCanceled);
      events.off(this.tabActor, "will-navigate", this.destroyEyeDropper);
    }
  },

  _onColorPicked: function (e, color) {
    events.emit(this, "color-picked", color);
  },

  _onColorPickCanceled: function () {
    events.emit(this, "color-pick-canceled");
  }
});

// Exported for test purposes.
exports._documentWalker = DocumentWalker;

function nodeDocument(node) {
  if (Cu.isDeadWrapper(node)) {
    return null;
  }
  return node.ownerDocument ||
         (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
}

function nodeDocshell(node) {
  let doc = node ? nodeDocument(node) : null;
  let win = doc ? doc.defaultView : null;
  if (win) {
    return win.QueryInterface(Ci.nsIInterfaceRequestor)
              .getInterface(Ci.nsIDocShell);
  }
  return null;
}

function isNodeDead(node) {
  return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
}

/**
 * Wrapper for inDeepTreeWalker.  Adds filtering to the traversal methods.
 * See inDeepTreeWalker for more information about the methods.
 *
 * @param {DOMNode} node
 * @param {Window} rootWin
 * @param {Int} whatToShow See nodeFilterConstants / inIDeepTreeWalker for
 * options.
 * @param {Function} filter A custom filter function Taking in a DOMNode
 *        and returning an Int. See WalkerActor.nodeFilter for an example.
 */
function DocumentWalker(node, rootWin,
    whatToShow = nodeFilterConstants.SHOW_ALL,
    filter = standardTreeWalkerFilter) {
  if (!rootWin.location) {
    throw new Error("Got an invalid root window in DocumentWalker");
  }

  this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
    .createInstance(Ci.inIDeepTreeWalker);
  this.walker.showAnonymousContent = true;
  this.walker.showSubDocuments = true;
  this.walker.showDocumentsAsNodes = true;
  this.walker.init(rootWin.document, whatToShow);
  this.filter = filter;

  // Make sure that the walker knows about the initial node (which could
  // be skipped due to a filter).  Note that simply calling parentNode()
  // causes currentNode to be updated.
  this.walker.currentNode = node;
  while (node &&
         this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
    node = this.walker.parentNode();
  }
}

DocumentWalker.prototype = {
  get node() {
    return this.walker.node;
  },
  get whatToShow() {
    return this.walker.whatToShow;
  },
  get currentNode() {
    return this.walker.currentNode;
  },
  set currentNode(val) {
    this.walker.currentNode = val;
  },

  parentNode: function () {
    return this.walker.parentNode();
  },

  nextNode: function () {
    let node = this.walker.currentNode;
    if (!node) {
      return null;
    }

    let nextNode = this.walker.nextNode();
    while (nextNode &&
           this.filter(nextNode) === nodeFilterConstants.FILTER_SKIP) {
      nextNode = this.walker.nextNode();
    }

    return nextNode;
  },

  firstChild: function () {
    let node = this.walker.currentNode;
    if (!node) {
      return null;
    }

    let firstChild = this.walker.firstChild();
    while (firstChild &&
           this.filter(firstChild) === nodeFilterConstants.FILTER_SKIP) {
      firstChild = this.walker.nextSibling();
    }

    return firstChild;
  },

  lastChild: function () {
    let node = this.walker.currentNode;
    if (!node) {
      return null;
    }

    let lastChild = this.walker.lastChild();
    while (lastChild &&
           this.filter(lastChild) === nodeFilterConstants.FILTER_SKIP) {
      lastChild = this.walker.previousSibling();
    }

    return lastChild;
  },

  previousSibling: function () {
    let node = this.walker.previousSibling();
    while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
      node = this.walker.previousSibling();
    }
    return node;
  },

  nextSibling: function () {
    let node = this.walker.nextSibling();
    while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
      node = this.walker.nextSibling();
    }
    return node;
  }
};

function isInXULDocument(el) {
  let doc = nodeDocument(el);
  return doc &&
         doc.documentElement &&
         doc.documentElement.namespaceURI === XUL_NS;
}

/**
 * This DeepTreeWalker filter skips whitespace text nodes and anonymous
 * content with the exception of ::before and ::after and anonymous content
 * in XUL document (needed to show all elements in the browser toolbox).
 */
function standardTreeWalkerFilter(node) {
  // ::before and ::after are native anonymous content, but we always
  // want to show them
  if (node.nodeName === "_moz_generated_content_before" ||
      node.nodeName === "_moz_generated_content_after") {
    return nodeFilterConstants.FILTER_ACCEPT;
  }

  // Ignore empty whitespace text nodes.
  if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
      !/[^\s]/.exec(node.nodeValue)) {
    return nodeFilterConstants.FILTER_SKIP;
  }

  // Ignore all native and XBL anonymous content inside a non-XUL document
  if (!isInXULDocument(node) && (isXBLAnonymous(node) ||
                                  isNativeAnonymous(node))) {
    // Note: this will skip inspecting the contents of feedSubscribeLine since
    // that's XUL content injected in an HTML document, but we need to because
    // this also skips many other elements that need to be skipped - like form
    // controls, scrollbars, video controls, etc (see bug 1187482).
    return nodeFilterConstants.FILTER_SKIP;
  }

  return nodeFilterConstants.FILTER_ACCEPT;
}

/**
 * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
 * it also includes all anonymous content (like internal form controls).
 */
function allAnonymousContentTreeWalkerFilter(node) {
  // Ignore empty whitespace text nodes.
  if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
      !/[^\s]/.exec(node.nodeValue)) {
    return nodeFilterConstants.FILTER_SKIP;
  }
  return nodeFilterConstants.FILTER_ACCEPT;
}

/**
 * Returns a promise that is settled once the given HTMLImageElement has
 * finished loading.
 *
 * @param {HTMLImageElement} image - The image element.
 * @param {Number} timeout - Maximum amount of time the image is allowed to load
 * before the waiting is aborted. Ignored if DevToolsUtils.testing is set.
 *
 * @return {Promise} that is fulfilled once the image has loaded. If the image
 * fails to load or the load takes too long, the promise is rejected.
 */
function ensureImageLoaded(image, timeout) {
  let { HTMLImageElement } = image.ownerDocument.defaultView;
  if (!(image instanceof HTMLImageElement)) {
    return promise.reject("image must be an HTMLImageELement");
  }

  if (image.complete) {
    // The image has already finished loading.
    return promise.resolve();
  }

  // This image is still loading.
  let onLoad = AsyncUtils.listenOnce(image, "load");

  // Reject if loading fails.
  let onError = AsyncUtils.listenOnce(image, "error").then(() => {
    return promise.reject("Image '" + image.src + "' failed to load.");
  });

  // Don't timeout when testing. This is never settled.
  let onAbort = new Promise(() => {});

  if (!DevToolsUtils.testing) {
    // Tests are not running. Reject the promise after given timeout.
    onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
      return promise.reject("Image '" + image.src + "' took too long to load.");
    });
  }

  // See which happens first.
  return promise.race([onLoad, onError, onAbort]);
}

/**
 * Given an <img> or <canvas> element, return the image data-uri. If @param node
 * is an <img> element, the method waits a while for the image to load before
 * the data is generated. If the image does not finish loading in a reasonable
 * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts.
 *
 * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas>
 * element, or Image() object. Other types cause the method to reject.
 * @param {Number} maxDim - Optionally pass a maximum size you want the longest
 * side of the image to be resized to before getting the image data.

 * @return {Promise} A promise that is fulfilled with an object containing the
 * data-uri and size-related information:
 * { data: "...",
 *   size: {
 *     naturalWidth: 400,
 *     naturalHeight: 300,
 *     resized: true }
 *  }.
 *
 * If something goes wrong, the promise is rejected.
 */
var imageToImageData = Task.async(function* (node, maxDim) {
  let { HTMLCanvasElement, HTMLImageElement } = node.ownerDocument.defaultView;

  let isImg = node instanceof HTMLImageElement;
  let isCanvas = node instanceof HTMLCanvasElement;

  if (!isImg && !isCanvas) {
    throw new Error("node is not a <canvas> or <img> element.");
  }

  if (isImg) {
    // Ensure that the image is ready.
    yield ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT);
  }

  // Get the image resize ratio if a maxDim was provided
  let resizeRatio = 1;
  let imgWidth = node.naturalWidth || node.width;
  let imgHeight = node.naturalHeight || node.height;
  let imgMax = Math.max(imgWidth, imgHeight);
  if (maxDim && imgMax > maxDim) {
    resizeRatio = maxDim / imgMax;
  }

  // Extract the image data
  let imageData;
  // The image may already be a data-uri, in which case, save ourselves the
  // trouble of converting via the canvas.drawImage.toDataURL method, but only
  // if the image doesn't need resizing
  if (isImg && node.src.startsWith("data:") && resizeRatio === 1) {
    imageData = node.src;
  } else {
    // Create a canvas to copy the rawNode into and get the imageData from
    let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
    canvas.width = imgWidth * resizeRatio;
    canvas.height = imgHeight * resizeRatio;
    let ctx = canvas.getContext("2d");

    // Copy the rawNode image or canvas in the new canvas and extract data
    ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
    imageData = canvas.toDataURL("image/png");
  }

  return {
    data: imageData,
    size: {
      naturalWidth: imgWidth,
      naturalHeight: imgHeight,
      resized: resizeRatio !== 1
    }
  };
});

loader.lazyGetter(this, "DOMUtils", function () {
  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});
back to top