browser/devtools/webconsole/webconsole.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
     2 /* vim: set ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 "use strict";
     9 const {Cc, Ci, Cu} = require("chrome");
    11 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
    13 loader.lazyServiceGetter(this, "clipboardHelper",
    14                          "@mozilla.org/widget/clipboardhelper;1",
    15                          "nsIClipboardHelper");
    16 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
    17 loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
    18 loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
    19 loader.lazyGetter(this, "AutocompletePopup",
    20                   () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
    21 loader.lazyGetter(this, "ToolSidebar",
    22                   () => require("devtools/framework/sidebar").ToolSidebar);
    23 loader.lazyGetter(this, "NetworkPanel",
    24                   () => require("devtools/webconsole/network-panel").NetworkPanel);
    25 loader.lazyGetter(this, "ConsoleOutput",
    26                   () => require("devtools/webconsole/console-output").ConsoleOutput);
    27 loader.lazyGetter(this, "Messages",
    28                   () => require("devtools/webconsole/console-output").Messages);
    29 loader.lazyImporter(this, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm");
    30 loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm");
    31 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
    32 loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm");
    33 loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
    34 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
    36 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
    37 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
    39 const XHTML_NS = "http://www.w3.org/1999/xhtml";
    41 const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent";
    43 const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords";
    45 const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
    47 const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
    49 const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
    51 const CONSOLE_DIR_VIEW_HEIGHT = 0.6;
    53 const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"];
    55 // The amount of time in milliseconds that we wait before performing a live
    56 // search.
    57 const SEARCH_DELAY = 200;
    59 // The number of lines that are displayed in the console output by default, for
    60 // each category. The user can change this number by adjusting the hidden
    61 // "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences.
    62 const DEFAULT_LOG_LIMIT = 200;
    64 // The various categories of messages. We start numbering at zero so we can
    65 // use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
    66 const CATEGORY_NETWORK = 0;
    67 const CATEGORY_CSS = 1;
    68 const CATEGORY_JS = 2;
    69 const CATEGORY_WEBDEV = 3;
    70 const CATEGORY_INPUT = 4;   // always on
    71 const CATEGORY_OUTPUT = 5;  // always on
    72 const CATEGORY_SECURITY = 6;
    74 // The possible message severities. As before, we start at zero so we can use
    75 // these as indexes into MESSAGE_PREFERENCE_KEYS.
    76 const SEVERITY_ERROR = 0;
    77 const SEVERITY_WARNING = 1;
    78 const SEVERITY_INFO = 2;
    79 const SEVERITY_LOG = 3;
    81 // The fragment of a CSS class name that identifies each category.
    82 const CATEGORY_CLASS_FRAGMENTS = [
    83   "network",
    84   "cssparser",
    85   "exception",
    86   "console",
    87   "input",
    88   "output",
    89   "security",
    90 ];
    92 // The fragment of a CSS class name that identifies each severity.
    93 const SEVERITY_CLASS_FRAGMENTS = [
    94   "error",
    95   "warn",
    96   "info",
    97   "log",
    98 ];
   100 // The preference keys to use for each category/severity combination, indexed
   101 // first by category (rows) and then by severity (columns).
   102 //
   103 // Most of these rather idiosyncratic names are historical and predate the
   104 // division of message type into "category" and "severity".
   105 const MESSAGE_PREFERENCE_KEYS = [
   106 //  Error         Warning       Info      Log
   107   [ "network",    "netwarn",    null,     "networkinfo", ],  // Network
   108   [ "csserror",   "cssparser",  null,     "csslog",      ],  // CSS
   109   [ "exception",  "jswarn",     null,     "jslog",       ],  // JS
   110   [ "error",      "warn",       "info",   "log",         ],  // Web Developer
   111   [ null,         null,         null,     null,          ],  // Input
   112   [ null,         null,         null,     null,          ],  // Output
   113   [ "secerror",   "secwarn",    null,     null,          ],  // Security
   114 ];
   116 // A mapping from the console API log event levels to the Web Console
   117 // severities.
   118 const LEVELS = {
   119   error: SEVERITY_ERROR,
   120   exception: SEVERITY_ERROR,
   121   assert: SEVERITY_ERROR,
   122   warn: SEVERITY_WARNING,
   123   info: SEVERITY_INFO,
   124   log: SEVERITY_LOG,
   125   trace: SEVERITY_LOG,
   126   debug: SEVERITY_LOG,
   127   dir: SEVERITY_LOG,
   128   group: SEVERITY_LOG,
   129   groupCollapsed: SEVERITY_LOG,
   130   groupEnd: SEVERITY_LOG,
   131   time: SEVERITY_LOG,
   132   timeEnd: SEVERITY_LOG,
   133   count: SEVERITY_LOG
   134 };
   136 // The lowest HTTP response code (inclusive) that is considered an error.
   137 const MIN_HTTP_ERROR_CODE = 400;
   138 // The highest HTTP response code (inclusive) that is considered an error.
   139 const MAX_HTTP_ERROR_CODE = 599;
   141 // Constants used for defining the direction of JSTerm input history navigation.
   142 const HISTORY_BACK = -1;
   143 const HISTORY_FORWARD = 1;
   145 // The indent of a console group in pixels.
   146 const GROUP_INDENT = 12;
   148 // The number of messages to display in a single display update. If we display
   149 // too many messages at once we slow the Firefox UI too much.
   150 const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT;
   152 // The delay between display updates - tells how often we should *try* to push
   153 // new messages to screen. This value is optimistic, updates won't always
   154 // happen. Keep this low so the Web Console output feels live.
   155 const OUTPUT_INTERVAL = 50; // milliseconds
   157 // When the output queue has more than MESSAGES_IN_INTERVAL items we throttle
   158 // output updates to this number of milliseconds. So during a lot of output we
   159 // update every N milliseconds given here.
   160 const THROTTLE_UPDATES = 1000; // milliseconds
   162 // The preference prefix for all of the Web Console filters.
   163 const FILTER_PREFS_PREFIX = "devtools.webconsole.filter.";
   165 // The minimum font size.
   166 const MIN_FONT_SIZE = 10;
   168 const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
   169 const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
   170 const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
   172 /**
   173  * A WebConsoleFrame instance is an interactive console initialized *per target*
   174  * that displays console log data as well as provides an interactive terminal to
   175  * manipulate the target's document content.
   176  *
   177  * The WebConsoleFrame is responsible for the actual Web Console UI
   178  * implementation.
   179  *
   180  * @constructor
   181  * @param object aWebConsoleOwner
   182  *        The WebConsole owner object.
   183  */
   184 function WebConsoleFrame(aWebConsoleOwner)
   185 {
   186   this.owner = aWebConsoleOwner;
   187   this.hudId = this.owner.hudId;
   188   this.window = this.owner.iframeWindow;
   190   this._repeatNodes = {};
   191   this._outputQueue = [];
   192   this._pruneCategoriesQueue = {};
   193   this._networkRequests = {};
   194   this.filterPrefs = {};
   196   this.output = new ConsoleOutput(this);
   198   this._toggleFilter = this._toggleFilter.bind(this);
   199   this._onPanelSelected = this._onPanelSelected.bind(this);
   200   this._flushMessageQueue = this._flushMessageQueue.bind(this);
   201   this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
   203   this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   204   this._outputTimerInitialized = false;
   206   EventEmitter.decorate(this);
   207 }
   208 exports.WebConsoleFrame = WebConsoleFrame;
   210 WebConsoleFrame.prototype = {
   211   /**
   212    * The WebConsole instance that owns this frame.
   213    * @see hudservice.js::WebConsole
   214    * @type object
   215    */
   216   owner: null,
   218   /**
   219    * Proxy between the Web Console and the remote Web Console instance. This
   220    * object holds methods used for connecting, listening and disconnecting from
   221    * the remote server, using the remote debugging protocol.
   222    *
   223    * @see WebConsoleConnectionProxy
   224    * @type object
   225    */
   226   proxy: null,
   228   /**
   229    * Getter for the xul:popupset that holds any popups we open.
   230    * @type nsIDOMElement
   231    */
   232   get popupset() this.owner.mainPopupSet,
   234   /**
   235    * Holds the initialization promise object.
   236    * @private
   237    * @type object
   238    */
   239   _initDefer: null,
   241   /**
   242    * Holds the network requests currently displayed by the Web Console. Each key
   243    * represents the connection ID and the value is network request information.
   244    * @private
   245    * @type object
   246    */
   247   _networkRequests: null,
   249   /**
   250    * Last time when we displayed any message in the output.
   251    *
   252    * @private
   253    * @type number
   254    *       Timestamp in milliseconds since the Unix epoch.
   255    */
   256   _lastOutputFlush: 0,
   258   /**
   259    * Message nodes are stored here in a queue for later display.
   260    *
   261    * @private
   262    * @type array
   263    */
   264   _outputQueue: null,
   266   /**
   267    * Keep track of the categories we need to prune from time to time.
   268    *
   269    * @private
   270    * @type array
   271    */
   272   _pruneCategoriesQueue: null,
   274   /**
   275    * Function invoked whenever the output queue is emptied. This is used by some
   276    * tests.
   277    *
   278    * @private
   279    * @type function
   280    */
   281   _flushCallback: null,
   283   /**
   284    * Timer used for flushing the messages output queue.
   285    *
   286    * @private
   287    * @type nsITimer
   288    */
   289   _outputTimer: null,
   290   _outputTimerInitialized: null,
   292   /**
   293    * Store for tracking repeated nodes.
   294    * @private
   295    * @type object
   296    */
   297   _repeatNodes: null,
   299   /**
   300    * Preferences for filtering messages by type.
   301    * @see this._initDefaultFilterPrefs()
   302    * @type object
   303    */
   304   filterPrefs: null,
   306   /**
   307    * Prefix used for filter preferences.
   308    * @private
   309    * @type string
   310    */
   311   _filterPrefsPrefix: FILTER_PREFS_PREFIX,
   313   /**
   314    * The nesting depth of the currently active console group.
   315    */
   316   groupDepth: 0,
   318   /**
   319    * The current target location.
   320    * @type string
   321    */
   322   contentLocation: "",
   324   /**
   325    * The JSTerm object that manage the console's input.
   326    * @see JSTerm
   327    * @type object
   328    */
   329   jsterm: null,
   331   /**
   332    * The element that holds all of the messages we display.
   333    * @type nsIDOMElement
   334    */
   335   outputNode: null,
   337   /**
   338    * The ConsoleOutput instance that manages all output.
   339    * @type object
   340    */
   341   output: null,
   343   /**
   344    * The input element that allows the user to filter messages by string.
   345    * @type nsIDOMElement
   346    */
   347   filterBox: null,
   349   /**
   350    * Getter for the debugger WebConsoleClient.
   351    * @type object
   352    */
   353   get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null,
   355   _destroyer: null,
   357   // Used in tests.
   358   _saveRequestAndResponseBodies: false,
   360   // Chevron width at the starting of Web Console's input box.
   361   _chevronWidth: 0,
   362   // Width of the monospace characters in Web Console's input box.
   363   _inputCharWidth: 0,
   365   /**
   366    * Tells whether to save the bodies of network requests and responses.
   367    * Disabled by default to save memory.
   368    *
   369    * @return boolean
   370    *         The saveRequestAndResponseBodies pref value.
   371    */
   372   getSaveRequestAndResponseBodies:
   373   function WCF_getSaveRequestAndResponseBodies() {
   374     let deferred = promise.defer();
   375     let toGet = [
   376       "NetworkMonitor.saveRequestAndResponseBodies"
   377     ];
   379     // Make sure the web console client connection is established first.
   380     this.webConsoleClient.getPreferences(toGet, aResponse => {
   381       if (!aResponse.error) {
   382         this._saveRequestAndResponseBodies = aResponse.preferences[toGet[0]];
   383         deferred.resolve(this._saveRequestAndResponseBodies);
   384       }
   385       else {
   386         deferred.reject(aResponse.error);
   387       }
   388     });
   390     return deferred.promise;
   391   },
   393   /**
   394    * Setter for saving of network request and response bodies.
   395    *
   396    * @param boolean aValue
   397    *        The new value you want to set.
   398    */
   399   setSaveRequestAndResponseBodies:
   400   function WCF_setSaveRequestAndResponseBodies(aValue) {
   401     if (!this.webConsoleClient) {
   402       // Don't continue if the webconsole disconnected.
   403       return promise.resolve(null);
   404     }
   406     let deferred = promise.defer();
   407     let newValue = !!aValue;
   408     let toSet = {
   409       "NetworkMonitor.saveRequestAndResponseBodies": newValue,
   410     };
   412     // Make sure the web console client connection is established first.
   413     this.webConsoleClient.setPreferences(toSet, aResponse => {
   414       if (!aResponse.error) {
   415         this._saveRequestAndResponseBodies = newValue;
   416         deferred.resolve(aResponse);
   417       }
   418       else {
   419         deferred.reject(aResponse.error);
   420       }
   421     });
   423     return deferred.promise;
   424   },
   426   /**
   427    * Getter for the persistent logging preference.
   428    * @type boolean
   429    */
   430   get persistLog() {
   431     return Services.prefs.getBoolPref(PREF_PERSISTLOG);
   432   },
   434   /**
   435    * Initialize the WebConsoleFrame instance.
   436    * @return object
   437    *         A promise object for the initialization.
   438    */
   439   init: function WCF_init()
   440   {
   441     this._initUI();
   442     return this._initConnection();
   443   },
   445   /**
   446    * Connect to the server using the remote debugging protocol.
   447    *
   448    * @private
   449    * @return object
   450    *         A promise object that is resolved/reject based on the connection
   451    *         result.
   452    */
   453   _initConnection: function WCF__initConnection()
   454   {
   455     if (this._initDefer) {
   456       return this._initDefer.promise;
   457     }
   459     this._initDefer = promise.defer();
   460     this.proxy = new WebConsoleConnectionProxy(this, this.owner.target);
   462     this.proxy.connect().then(() => { // on success
   463       this._initDefer.resolve(this);
   464     }, (aReason) => { // on failure
   465       let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
   466                                         aReason.error + ": " + aReason.message);
   467       this.outputMessage(CATEGORY_JS, node);
   468       this._initDefer.reject(aReason);
   469     }).then(() => {
   470       let id = WebConsoleUtils.supportsString(this.hudId);
   471       Services.obs.notifyObservers(id, "web-console-created", null);
   472     });
   474     return this._initDefer.promise;
   475   },
   477   /**
   478    * Find the Web Console UI elements and setup event listeners as needed.
   479    * @private
   480    */
   481   _initUI: function WCF__initUI()
   482   {
   483     this.document = this.window.document;
   484     this.rootElement = this.document.documentElement;
   486     this._initDefaultFilterPrefs();
   488     // Register the controller to handle "select all" properly.
   489     this._commandController = new CommandController(this);
   490     this.window.controllers.insertControllerAt(0, this._commandController);
   492     this._contextMenuHandler = new ConsoleContextMenu(this);
   494     let doc = this.document;
   496     this.filterBox = doc.querySelector(".hud-filter-box");
   497     this.outputNode = doc.getElementById("output-container");
   498     this.completeNode = doc.querySelector(".jsterm-complete-node");
   499     this.inputNode = doc.querySelector(".jsterm-input-node");
   501     this._setFilterTextBoxEvents();
   502     this._initFilterButtons();
   504     let fontSize = this.owner._browserConsole ?
   505                    Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0;
   507     if (fontSize != 0) {
   508       fontSize = Math.max(MIN_FONT_SIZE, fontSize);
   510       this.outputNode.style.fontSize = fontSize + "px";
   511       this.completeNode.style.fontSize = fontSize + "px";
   512       this.inputNode.style.fontSize = fontSize + "px";
   513     }
   515     if (this.owner._browserConsole) {
   516       for (let id of ["Enlarge", "Reduce", "Reset"]) {
   517         this.document.getElementById("cmd_fullZoom" + id)
   518                      .removeAttribute("disabled");
   519       }
   520     }
   522     // Update the character width and height needed for the popup offset
   523     // calculations.
   524     this._updateCharSize();
   526     let updateSaveBodiesPrefUI = (aElement) => {
   527       this.getSaveRequestAndResponseBodies().then(aValue => {
   528         aElement.setAttribute("checked", aValue);
   529         this.emit("save-bodies-ui-toggled");
   530       });
   531     }
   533     let reverseSaveBodiesPref = ({ target: aElement }) => {
   534       this.getSaveRequestAndResponseBodies().then(aValue => {
   535         this.setSaveRequestAndResponseBodies(!aValue);
   536         aElement.setAttribute("checked", aValue);
   537         this.emit("save-bodies-pref-reversed");
   538       });
   539     }
   541     let saveBodies = doc.getElementById("saveBodies");
   542     saveBodies.addEventListener("command", reverseSaveBodiesPref);
   543     saveBodies.disabled = !this.getFilterState("networkinfo") &&
   544                           !this.getFilterState("network");
   546     let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu");
   547     saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref);
   548     saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
   549                                      !this.getFilterState("network");
   551     saveBodies.parentNode.addEventListener("popupshowing", () => {
   552       updateSaveBodiesPrefUI(saveBodies);
   553       saveBodies.disabled = !this.getFilterState("networkinfo") &&
   554                             !this.getFilterState("network");
   555     });
   557     saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => {
   558       updateSaveBodiesPrefUI(saveBodiesContextMenu);
   559       saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
   560                                        !this.getFilterState("network");
   561     });
   563     let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0];
   564     clearButton.addEventListener("command", () => {
   565       this.owner._onClearButton();
   566       this.jsterm.clearOutput(true);
   567     });
   569     this.jsterm = new JSTerm(this);
   570     this.jsterm.init();
   572     let toolbox = gDevTools.getToolbox(this.owner.target);
   573     if (toolbox) {
   574       toolbox.on("webconsole-selected", this._onPanelSelected);
   575     }
   577     /*
   578      * Focus input line whenever the output area is clicked.
   579      * Reusing _addMEssageLinkCallback since it correctly filters
   580      * drag and select events.
   581      */
   582     this._addFocusCallback(this.outputNode, (evt) => {
   583       if ((evt.target.nodeName.toLowerCase() != "a") &&
   584           (evt.target.parentNode.nodeName.toLowerCase() != "a")) {
   585         this.jsterm.inputNode.focus();
   586       }
   587     });
   589     // Toggle the timestamp on preference change
   590     gDevTools.on("pref-changed", this._onToolboxPrefChanged);
   591     this._onToolboxPrefChanged("pref-changed", {
   592       pref: PREF_MESSAGE_TIMESTAMP,
   593       newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
   594     });
   596     // focus input node
   597     this.jsterm.inputNode.focus();
   598   },
   600   /**
   601    * Sets the focus to JavaScript input field when the web console tab is
   602    * selected or when there is a split console present.
   603    * @private
   604    */
   605   _onPanelSelected: function WCF__onPanelSelected(evt, id)
   606   {
   607     this.jsterm.inputNode.focus();
   608   },
   610   /**
   611    * Initialize the default filter preferences.
   612    * @private
   613    */
   614   _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs()
   615   {
   616     let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
   617                  "exception", "jswarn", "jslog", "error", "info", "warn", "log",
   618                  "secerror", "secwarn", "netwarn"];
   619     for (let pref of prefs) {
   620       this.filterPrefs[pref] = Services.prefs
   621                                .getBoolPref(this._filterPrefsPrefix + pref);
   622     }
   623   },
   625   /**
   626    * Attach / detach reflow listeners depending on the checked status
   627    * of the `CSS > Log` menuitem.
   628    *
   629    * @param function [aCallback=null]
   630    *        Optional function to invoke when the listener has been
   631    *        added/removed.
   632    *
   633    */
   634   _updateReflowActivityListener:
   635     function WCF__updateReflowActivityListener(aCallback)
   636   {
   637     if (this.webConsoleClient) {
   638       let pref = this._filterPrefsPrefix + "csslog";
   639       if (Services.prefs.getBoolPref(pref)) {
   640         this.webConsoleClient.startListeners(["ReflowActivity"], aCallback);
   641       } else {
   642         this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback);
   643       }
   644     }
   645   },
   647   /**
   648    * Sets the events for the filter input field.
   649    * @private
   650    */
   651   _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents()
   652   {
   653     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   654     let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this);
   656     let onChange = function _onChange() {
   657       // To improve responsiveness, we let the user finish typing before we
   658       // perform the search.
   659       timer.cancel();
   660       timer.initWithCallback(timerEvent, SEARCH_DELAY,
   661                              Ci.nsITimer.TYPE_ONE_SHOT);
   662     };
   664     this.filterBox.addEventListener("command", onChange, false);
   665     this.filterBox.addEventListener("input", onChange, false);
   666   },
   668   /**
   669    * Creates one of the filter buttons on the toolbar.
   670    *
   671    * @private
   672    * @param nsIDOMNode aParent
   673    *        The node to which the filter button should be appended.
   674    * @param object aDescriptor
   675    *        A descriptor that contains info about the button. Contains "name",
   676    *        "category", and "prefKey" properties, and optionally a "severities"
   677    *        property.
   678    */
   679   _initFilterButtons: function WCF__initFilterButtons()
   680   {
   681     let categories = this.document
   682                      .querySelectorAll(".webconsole-filter-button[category]");
   683     Array.forEach(categories, function(aButton) {
   684       aButton.addEventListener("click", this._toggleFilter, false);
   686       let someChecked = false;
   687       let severities = aButton.querySelectorAll("menuitem[prefKey]");
   688       Array.forEach(severities, function(aMenuItem) {
   689         aMenuItem.addEventListener("command", this._toggleFilter, false);
   691         let prefKey = aMenuItem.getAttribute("prefKey");
   692         let checked = this.filterPrefs[prefKey];
   693         aMenuItem.setAttribute("checked", checked);
   694         someChecked = someChecked || checked;
   695       }, this);
   697       aButton.setAttribute("checked", someChecked);
   698     }, this);
   700     if (!this.owner._browserConsole) {
   701       // The Browser Console displays nsIConsoleMessages which are messages that
   702       // end up in the JS category, but they are not errors or warnings, they
   703       // are just log messages. The Web Console does not show such messages.
   704       let jslog = this.document.querySelector("menuitem[prefKey=jslog]");
   705       jslog.hidden = true;
   706     }
   708     if (Services.appinfo.OS == "Darwin") {
   709       let net = this.document.querySelector("toolbarbutton[category=net]");
   710       let accesskey = net.getAttribute("accesskeyMacOSX");
   711       net.setAttribute("accesskey", accesskey);
   713       let logging = this.document.querySelector("toolbarbutton[category=logging]");
   714       logging.removeAttribute("accesskey");
   715     }
   716   },
   718   /**
   719    * Increase, decrease or reset the font size.
   720    *
   721    * @param string size
   722    *        The size of the font change. Accepted values are "+" and "-".
   723    *        An unmatched size assumes a font reset.
   724    */
   725   changeFontSize: function WCF_changeFontSize(aSize)
   726   {
   727     let fontSize = this.window
   728                    .getComputedStyle(this.outputNode, null)
   729                    .getPropertyValue("font-size").replace("px", "");
   731     if (this.outputNode.style.fontSize) {
   732       fontSize = this.outputNode.style.fontSize.replace("px", "");
   733     }
   735     if (aSize == "+" || aSize == "-") {
   736       fontSize = parseInt(fontSize, 10);
   738       if (aSize == "+") {
   739         fontSize += 1;
   740       }
   741       else {
   742         fontSize -= 1;
   743       }
   745       if (fontSize < MIN_FONT_SIZE) {
   746         fontSize = MIN_FONT_SIZE;
   747       }
   749       Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize);
   750       fontSize = fontSize + "px";
   752       this.completeNode.style.fontSize = fontSize;
   753       this.inputNode.style.fontSize = fontSize;
   754       this.outputNode.style.fontSize = fontSize;
   755     }
   756     else {
   757       this.completeNode.style.fontSize = "";
   758       this.inputNode.style.fontSize = "";
   759       this.outputNode.style.fontSize = "";
   760       Services.prefs.clearUserPref("devtools.webconsole.fontSize");
   761     }
   762     this._updateCharSize();
   763   },
   765   /**
   766    * Calculates the width and height of a single character of the input box.
   767    * This will be used in opening the popup at the correct offset.
   768    *
   769    * @private
   770    */
   771   _updateCharSize: function WCF__updateCharSize()
   772   {
   773     let doc = this.document;
   774     let tempLabel = doc.createElementNS(XHTML_NS, "span");
   775     let style = tempLabel.style;
   776     style.position = "fixed";
   777     style.padding = "0";
   778     style.margin = "0";
   779     style.width = "auto";
   780     style.color = "transparent";
   781     WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
   782     tempLabel.textContent = "x";
   783     doc.documentElement.appendChild(tempLabel);
   784     this._inputCharWidth = tempLabel.offsetWidth;
   785     tempLabel.parentNode.removeChild(tempLabel);
   786     // Calculate the width of the chevron placed at the beginning of the input
   787     // box. Remove 4 more pixels to accomodate the padding of the popup.
   788     this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
   789                              .paddingLeft.replace(/[^0-9.]/g, "") - 4;
   790   },
   792   /**
   793    * The event handler that is called whenever a user switches a filter on or
   794    * off.
   795    *
   796    * @private
   797    * @param nsIDOMEvent aEvent
   798    *        The event that triggered the filter change.
   799    */
   800   _toggleFilter: function WCF__toggleFilter(aEvent)
   801   {
   802     let target = aEvent.target;
   803     let tagName = target.tagName;
   804     if (tagName != aEvent.currentTarget.tagName) {
   805       return;
   806     }
   808     switch (tagName) {
   809       case "toolbarbutton": {
   810         let originalTarget = aEvent.originalTarget;
   811         let classes = originalTarget.classList;
   813         if (originalTarget.localName !== "toolbarbutton") {
   814           // Oddly enough, the click event is sent to the menu button when
   815           // selecting a menu item with the mouse. Detect this case and bail
   816           // out.
   817           break;
   818         }
   820         if (!classes.contains("toolbarbutton-menubutton-button") &&
   821             originalTarget.getAttribute("type") === "menu-button") {
   822           // This is a filter button with a drop-down. The user clicked the
   823           // drop-down, so do nothing. (The menu will automatically appear
   824           // without our intervention.)
   825           break;
   826         }
   828         // Toggle on the targeted filter button, and if the user alt clicked,
   829         // toggle off all other filter buttons and their associated filters.
   830         let state = target.getAttribute("checked") !== "true";
   831         if (aEvent.getModifierState("Alt")) {
   832           let buttons = this.document
   833                         .querySelectorAll(".webconsole-filter-button");
   834           Array.forEach(buttons, (button) => {
   835             if (button !== target) {
   836               button.setAttribute("checked", false);
   837               this._setMenuState(button, false);
   838             }
   839           });
   840           state = true;
   841         }
   842         target.setAttribute("checked", state);
   844         // This is a filter button with a drop-down, and the user clicked the
   845         // main part of the button. Go through all the severities and toggle
   846         // their associated filters.
   847         this._setMenuState(target, state);
   849         // CSS reflow logging can decrease web page performance.
   850         // Make sure the option is always unchecked when the CSS filter button is selected.
   851         // See bug 971798.
   852         if (target.getAttribute("category") == "css" && state) {
   853           let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]");
   854           csslogMenuItem.setAttribute("checked", false);
   855           this.setFilterState("csslog", false);
   856         }
   858         break;
   859       }
   861       case "menuitem": {
   862         let state = target.getAttribute("checked") !== "true";
   863         target.setAttribute("checked", state);
   865         let prefKey = target.getAttribute("prefKey");
   866         this.setFilterState(prefKey, state);
   868         // Disable the log response and request body if network logging is off.
   869         if (prefKey == "networkinfo" || prefKey == "network") {
   870           let checkState = !this.getFilterState("networkinfo") &&
   871                            !this.getFilterState("network");
   872           this.document.getElementById("saveBodies").disabled = checkState;
   873           this.document.getElementById("saveBodiesContextMenu").disabled = checkState;
   874         }
   876         // Adjust the state of the button appropriately.
   877         let menuPopup = target.parentNode;
   879         let someChecked = false;
   880         let menuItem = menuPopup.firstChild;
   881         while (menuItem) {
   882           if (menuItem.hasAttribute("prefKey") &&
   883               menuItem.getAttribute("checked") === "true") {
   884             someChecked = true;
   885             break;
   886           }
   887           menuItem = menuItem.nextSibling;
   888         }
   889         let toolbarButton = menuPopup.parentNode;
   890         toolbarButton.setAttribute("checked", someChecked);
   891         break;
   892       }
   893     }
   894   },
   896   /**
   897    * Set the menu attributes for a specific toggle button.
   898    *
   899    * @private
   900    * @param XULElement aTarget
   901    *        Button with drop down items to be toggled.
   902    * @param boolean aState
   903    *        True if the menu item is being toggled on, and false otherwise.
   904    */
   905   _setMenuState: function WCF__setMenuState(aTarget, aState)
   906   {
   907     let menuItems = aTarget.querySelectorAll("menuitem");
   908     Array.forEach(menuItems, (item) => {
   909       item.setAttribute("checked", aState);
   910       let prefKey = item.getAttribute("prefKey");
   911       this.setFilterState(prefKey, aState);
   912     });
   913   },
   915   /**
   916    * Set the filter state for a specific toggle button.
   917    *
   918    * @param string aToggleType
   919    * @param boolean aState
   920    * @returns void
   921    */
   922   setFilterState: function WCF_setFilterState(aToggleType, aState)
   923   {
   924     this.filterPrefs[aToggleType] = aState;
   925     this.adjustVisibilityForMessageType(aToggleType, aState);
   926     Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
   927     this._updateReflowActivityListener();
   928   },
   930   /**
   931    * Get the filter state for a specific toggle button.
   932    *
   933    * @param string aToggleType
   934    * @returns boolean
   935    */
   936   getFilterState: function WCF_getFilterState(aToggleType)
   937   {
   938     return this.filterPrefs[aToggleType];
   939   },
   941   /**
   942    * Check that the passed string matches the filter arguments.
   943    *
   944    * @param String aString
   945    *        to search for filter words in.
   946    * @param String aFilter
   947    *        is a string containing all of the words to filter on.
   948    * @returns boolean
   949    */
   950   stringMatchesFilters: function WCF_stringMatchesFilters(aString, aFilter)
   951   {
   952     if (!aFilter || !aString) {
   953       return true;
   954     }
   956     let searchStr = aString.toLowerCase();
   957     let filterStrings = aFilter.toLowerCase().split(/\s+/);
   958     return !filterStrings.some(function (f) {
   959       return searchStr.indexOf(f) == -1;
   960     });
   961   },
   963   /**
   964    * Turns the display of log nodes on and off appropriately to reflect the
   965    * adjustment of the message type filter named by @aPrefKey.
   966    *
   967    * @param string aPrefKey
   968    *        The preference key for the message type being filtered: one of the
   969    *        values in the MESSAGE_PREFERENCE_KEYS table.
   970    * @param boolean aState
   971    *        True if the filter named by @aMessageType is being turned on; false
   972    *        otherwise.
   973    * @returns void
   974    */
   975   adjustVisibilityForMessageType:
   976   function WCF_adjustVisibilityForMessageType(aPrefKey, aState)
   977   {
   978     let outputNode = this.outputNode;
   979     let doc = this.document;
   981     // Look for message nodes (".message") with the given preference key
   982     // (filter="error", filter="cssparser", etc.) and add or remove the
   983     // "filtered-by-type" class, which turns on or off the display.
   985     let xpath = ".//*[contains(@class, 'message') and " +
   986       "@filter='" + aPrefKey + "']";
   987     let result = doc.evaluate(xpath, outputNode, null,
   988       Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
   989     for (let i = 0; i < result.snapshotLength; i++) {
   990       let node = result.snapshotItem(i);
   991       if (aState) {
   992         node.classList.remove("filtered-by-type");
   993       }
   994       else {
   995         node.classList.add("filtered-by-type");
   996       }
   997     }
   998   },
  1000   /**
  1001    * Turns the display of log nodes on and off appropriately to reflect the
  1002    * adjustment of the search string.
  1003    */
  1004   adjustVisibilityOnSearchStringChange:
  1005   function WCF_adjustVisibilityOnSearchStringChange()
  1007     let nodes = this.outputNode.getElementsByClassName("message");
  1008     let searchString = this.filterBox.value;
  1010     for (let i = 0, n = nodes.length; i < n; ++i) {
  1011       let node = nodes[i];
  1013       // hide nodes that match the strings
  1014       let text = node.textContent;
  1016       // if the text matches the words in aSearchString...
  1017       if (this.stringMatchesFilters(text, searchString)) {
  1018         node.classList.remove("filtered-by-string");
  1020       else {
  1021         node.classList.add("filtered-by-string");
  1024   },
  1026   /**
  1027    * Applies the user's filters to a newly-created message node via CSS
  1028    * classes.
  1030    * @param nsIDOMNode aNode
  1031    *        The newly-created message node.
  1032    * @return boolean
  1033    *         True if the message was filtered or false otherwise.
  1034    */
  1035   filterMessageNode: function WCF_filterMessageNode(aNode)
  1037     let isFiltered = false;
  1039     // Filter by the message type.
  1040     let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity];
  1041     if (prefKey && !this.getFilterState(prefKey)) {
  1042       // The node is filtered by type.
  1043       aNode.classList.add("filtered-by-type");
  1044       isFiltered = true;
  1047     // Filter on the search string.
  1048     let search = this.filterBox.value;
  1049     let text = aNode.clipboardText;
  1051     // if string matches the filter text
  1052     if (!this.stringMatchesFilters(text, search)) {
  1053       aNode.classList.add("filtered-by-string");
  1054       isFiltered = true;
  1057     if (isFiltered && aNode.classList.contains("inlined-variables-view")) {
  1058       aNode.classList.add("hidden-message");
  1061     return isFiltered;
  1062   },
  1064   /**
  1065    * Merge the attributes of the two nodes that are about to be filtered.
  1066    * Increment the number of repeats of aOriginal.
  1068    * @param nsIDOMNode aOriginal
  1069    *        The Original Node. The one being merged into.
  1070    * @param nsIDOMNode aFiltered
  1071    *        The node being filtered out because it is repeated.
  1072    */
  1073   mergeFilteredMessageNode:
  1074   function WCF_mergeFilteredMessageNode(aOriginal, aFiltered)
  1076     let repeatNode = aOriginal.getElementsByClassName("message-repeats")[0];
  1077     if (!repeatNode) {
  1078       return; // no repeat node, return early.
  1081     let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
  1082     repeatNode.setAttribute("value", occurrences);
  1083     repeatNode.textContent = occurrences;
  1084     let str = l10n.getStr("messageRepeats.tooltip2");
  1085     repeatNode.title = PluralForm.get(occurrences, str)
  1086                        .replace("#1", occurrences);
  1087   },
  1089   /**
  1090    * Filter the message node from the output if it is a repeat.
  1092    * @private
  1093    * @param nsIDOMNode aNode
  1094    *        The message node to be filtered or not.
  1095    * @returns nsIDOMNode|null
  1096    *          Returns the duplicate node if the message was filtered, null
  1097    *          otherwise.
  1098    */
  1099   _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode)
  1101     let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
  1102     if (!repeatNode) {
  1103       return null;
  1106     let uid = repeatNode._uid;
  1107     let dupeNode = null;
  1109     if (aNode.category == CATEGORY_CSS ||
  1110         aNode.category == CATEGORY_SECURITY) {
  1111       dupeNode = this._repeatNodes[uid];
  1112       if (!dupeNode) {
  1113         this._repeatNodes[uid] = aNode;
  1116     else if ((aNode.category == CATEGORY_WEBDEV ||
  1117               aNode.category == CATEGORY_JS) &&
  1118              aNode.category != CATEGORY_NETWORK &&
  1119              !aNode.classList.contains("inlined-variables-view")) {
  1120       let lastMessage = this.outputNode.lastChild;
  1121       if (!lastMessage) {
  1122         return null;
  1125       let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0];
  1126       if (lastRepeatNode && lastRepeatNode._uid == uid) {
  1127         dupeNode = lastMessage;
  1131     if (dupeNode) {
  1132       this.mergeFilteredMessageNode(dupeNode, aNode);
  1133       return dupeNode;
  1136     return null;
  1137   },
  1139   /**
  1140    * Display cached messages that may have been collected before the UI is
  1141    * displayed.
  1143    * @param array aRemoteMessages
  1144    *        Array of cached messages coming from the remote Web Console
  1145    *        content instance.
  1146    */
  1147   displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages)
  1149     if (!aRemoteMessages.length) {
  1150       return;
  1153     aRemoteMessages.forEach(function(aMessage) {
  1154       switch (aMessage._type) {
  1155         case "PageError": {
  1156           let category = Utils.categoryForScriptError(aMessage);
  1157           this.outputMessage(category, this.reportPageError,
  1158                              [category, aMessage]);
  1159           break;
  1161         case "LogMessage":
  1162           this.handleLogMessage(aMessage);
  1163           break;
  1164         case "ConsoleAPI":
  1165           this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
  1166                              [aMessage]);
  1167           break;
  1169     }, this);
  1170   },
  1172   /**
  1173    * Logs a message to the Web Console that originates from the Web Console
  1174    * server.
  1176    * @param object aMessage
  1177    *        The message received from the server.
  1178    * @return nsIDOMElement|null
  1179    *         The message element to display in the Web Console output.
  1180    */
  1181   logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage)
  1183     let body = null;
  1184     let clipboardText = null;
  1185     let sourceURL = aMessage.filename;
  1186     let sourceLine = aMessage.lineNumber;
  1187     let level = aMessage.level;
  1188     let args = aMessage.arguments;
  1189     let objectActors = new Set();
  1190     let node = null;
  1192     // Gather the actor IDs.
  1193     args.forEach((aValue) => {
  1194       if (WebConsoleUtils.isActorGrip(aValue)) {
  1195         objectActors.add(aValue.actor);
  1197     });
  1199     switch (level) {
  1200       case "log":
  1201       case "info":
  1202       case "warn":
  1203       case "error":
  1204       case "exception":
  1205       case "assert":
  1206       case "debug": {
  1207         let msg = new Messages.ConsoleGeneric(aMessage);
  1208         node = msg.init(this.output).render().element;
  1209         break;
  1211       case "trace": {
  1212         let msg = new Messages.ConsoleTrace(aMessage);
  1213         node = msg.init(this.output).render().element;
  1214         break;
  1216       case "dir": {
  1217         body = { arguments: args };
  1218         let clipboardArray = [];
  1219         args.forEach((aValue) => {
  1220           clipboardArray.push(VariablesView.getString(aValue));
  1221         });
  1222         clipboardText = clipboardArray.join(" ");
  1223         break;
  1226       case "group":
  1227       case "groupCollapsed":
  1228         clipboardText = body = aMessage.groupName;
  1229         this.groupDepth++;
  1230         break;
  1232       case "groupEnd":
  1233         if (this.groupDepth > 0) {
  1234           this.groupDepth--;
  1236         break;
  1238       case "time": {
  1239         let timer = aMessage.timer;
  1240         if (!timer) {
  1241           return null;
  1243         if (timer.error) {
  1244           Cu.reportError(l10n.getStr(timer.error));
  1245           return null;
  1247         body = l10n.getFormatStr("timerStarted", [timer.name]);
  1248         clipboardText = body;
  1249         break;
  1252       case "timeEnd": {
  1253         let timer = aMessage.timer;
  1254         if (!timer) {
  1255           return null;
  1257         let duration = Math.round(timer.duration * 100) / 100;
  1258         body = l10n.getFormatStr("timeEnd", [timer.name, duration]);
  1259         clipboardText = body;
  1260         break;
  1263       case "count": {
  1264         let counter = aMessage.counter;
  1265         if (!counter) {
  1266           return null;
  1268         if (counter.error) {
  1269           Cu.reportError(l10n.getStr(counter.error));
  1270           return null;
  1272         let msg = new Messages.ConsoleGeneric(aMessage);
  1273         node = msg.init(this.output).render().element;
  1274         break;
  1277       default:
  1278         Cu.reportError("Unknown Console API log level: " + level);
  1279         return null;
  1282     // Release object actors for arguments coming from console API methods that
  1283     // we ignore their arguments.
  1284     switch (level) {
  1285       case "group":
  1286       case "groupCollapsed":
  1287       case "groupEnd":
  1288       case "time":
  1289       case "timeEnd":
  1290       case "count":
  1291         for (let actor of objectActors) {
  1292           this._releaseObject(actor);
  1294         objectActors.clear();
  1297     if (level == "groupEnd") {
  1298       return null; // no need to continue
  1301     if (!node) {
  1302       node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
  1303                                     sourceURL, sourceLine, clipboardText,
  1304                                     level, aMessage.timeStamp);
  1305       if (aMessage.private) {
  1306         node.setAttribute("private", true);
  1310     if (objectActors.size > 0) {
  1311       node._objectActors = objectActors;
  1313       if (!node._messageObject) {
  1314         let repeatNode = node.getElementsByClassName("message-repeats")[0];
  1315         repeatNode._uid += [...objectActors].join("-");
  1319     return node;
  1320   },
  1322   /**
  1323    * Handle ConsoleAPICall objects received from the server. This method outputs
  1324    * the window.console API call.
  1326    * @param object aMessage
  1327    *        The console API message received from the server.
  1328    */
  1329   handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage)
  1331     this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]);
  1332   },
  1334   /**
  1335    * Reports an error in the page source, either JavaScript or CSS.
  1337    * @param nsIScriptError aScriptError
  1338    *        The error message to report.
  1339    * @return nsIDOMElement|undefined
  1340    *         The message element to display in the Web Console output.
  1341    */
  1342   reportPageError: function WCF_reportPageError(aCategory, aScriptError)
  1344     // Warnings and legacy strict errors become warnings; other types become
  1345     // errors.
  1346     let severity = SEVERITY_ERROR;
  1347     if (aScriptError.warning || aScriptError.strict) {
  1348       severity = SEVERITY_WARNING;
  1351     let objectActors = new Set();
  1353     // Gather the actor IDs.
  1354     for (let prop of ["errorMessage", "lineText"]) {
  1355       let grip = aScriptError[prop];
  1356       if (WebConsoleUtils.isActorGrip(grip)) {
  1357         objectActors.add(grip.actor);
  1361     let errorMessage = aScriptError.errorMessage;
  1362     if (errorMessage.type && errorMessage.type == "longString") {
  1363       errorMessage = errorMessage.initial;
  1366     let node = this.createMessageNode(aCategory, severity,
  1367                                       errorMessage,
  1368                                       aScriptError.sourceName,
  1369                                       aScriptError.lineNumber, null, null,
  1370                                       aScriptError.timeStamp);
  1372     // Select the body of the message node that is displayed in the console
  1373     let msgBody = node.getElementsByClassName("message-body")[0];
  1374     // Add the more info link node to messages that belong to certain categories
  1375     this.addMoreInfoLink(msgBody, aScriptError);
  1377     if (aScriptError.private) {
  1378       node.setAttribute("private", true);
  1381     if (objectActors.size > 0) {
  1382       node._objectActors = objectActors;
  1385     return node;
  1386   },
  1388   /**
  1389    * Handle PageError objects received from the server. This method outputs the
  1390    * given error.
  1392    * @param nsIScriptError aPageError
  1393    *        The error received from the server.
  1394    */
  1395   handlePageError: function WCF_handlePageError(aPageError)
  1397     let category = Utils.categoryForScriptError(aPageError);
  1398     this.outputMessage(category, this.reportPageError, [category, aPageError]);
  1399   },
  1401   /**
  1402    * Handle log messages received from the server. This method outputs the given
  1403    * message.
  1405    * @param object aPacket
  1406    *        The message packet received from the server.
  1407    */
  1408   handleLogMessage: function WCF_handleLogMessage(aPacket)
  1410     if (aPacket.message) {
  1411       this.outputMessage(CATEGORY_JS, this._reportLogMessage, [aPacket]);
  1413   },
  1415   /**
  1416    * Display log messages received from the server.
  1418    * @private
  1419    * @param object aPacket
  1420    *        The message packet received from the server.
  1421    * @return nsIDOMElement
  1422    *         The message element to render for the given log message.
  1423    */
  1424   _reportLogMessage: function WCF__reportLogMessage(aPacket)
  1426     let msg = aPacket.message;
  1427     if (msg.type && msg.type == "longString") {
  1428       msg = msg.initial;
  1430     let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null,
  1431                                       null, null, null, aPacket.timeStamp);
  1432     if (WebConsoleUtils.isActorGrip(aPacket.message)) {
  1433       node._objectActors = new Set([aPacket.message.actor]);
  1435     return node;
  1436   },
  1438   /**
  1439    * Log network event.
  1441    * @param object aActorId
  1442    *        The network event actor ID to log.
  1443    * @return nsIDOMElement|null
  1444    *         The message element to display in the Web Console output.
  1445    */
  1446   logNetEvent: function WCF_logNetEvent(aActorId)
  1448     let networkInfo = this._networkRequests[aActorId];
  1449     if (!networkInfo) {
  1450       return null;
  1453     let request = networkInfo.request;
  1454     let clipboardText = request.method + " " + request.url;
  1455     let severity = SEVERITY_LOG;
  1456     let mixedRequest =
  1457       WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation);
  1458     if (mixedRequest) {
  1459       severity = SEVERITY_WARNING;
  1462     let methodNode = this.document.createElementNS(XHTML_NS, "span");
  1463     methodNode.className = "method";
  1464     methodNode.textContent = request.method + " ";
  1466     let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
  1467                                              methodNode, null, null,
  1468                                              clipboardText);
  1469     if (networkInfo.private) {
  1470       messageNode.setAttribute("private", true);
  1472     messageNode._connectionId = aActorId;
  1473     messageNode.url = request.url;
  1475     let body = methodNode.parentNode;
  1476     body.setAttribute("aria-haspopup", true);
  1478     let displayUrl = request.url;
  1479     let pos = displayUrl.indexOf("?");
  1480     if (pos > -1) {
  1481       displayUrl = displayUrl.substr(0, pos);
  1484     let urlNode = this.document.createElementNS(XHTML_NS, "a");
  1485     urlNode.className = "url";
  1486     urlNode.setAttribute("title", request.url);
  1487     urlNode.href = request.url;
  1488     urlNode.textContent = displayUrl;
  1489     urlNode.draggable = false;
  1490     body.appendChild(urlNode);
  1491     body.appendChild(this.document.createTextNode(" "));
  1493     if (mixedRequest) {
  1494       messageNode.classList.add("mixed-content");
  1495       this.makeMixedContentNode(body);
  1498     let statusNode = this.document.createElementNS(XHTML_NS, "a");
  1499     statusNode.className = "status";
  1500     body.appendChild(statusNode);
  1502     let onClick = () => {
  1503       if (!messageNode._panelOpen) {
  1504         this.openNetworkPanel(messageNode, networkInfo);
  1506     };
  1508     this._addMessageLinkCallback(urlNode, onClick);
  1509     this._addMessageLinkCallback(statusNode, onClick);
  1511     networkInfo.node = messageNode;
  1513     this._updateNetMessage(aActorId);
  1515     return messageNode;
  1516   },
  1518   /**
  1519    * Create a mixed content warning Node.
  1521    * @param aLinkNode
  1522    *        Parent to the requested urlNode.
  1523    */
  1524   makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode)
  1526     let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
  1528     // Mixed content warning message links to a Learn More page
  1529     let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a");
  1530     mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE;
  1531     mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE;
  1532     mixedContentWarningNode.className = "learn-more-link";
  1533     mixedContentWarningNode.textContent = mixedContentWarning;
  1534     mixedContentWarningNode.draggable = false;
  1536     aLinkNode.appendChild(mixedContentWarningNode);
  1538     this._addMessageLinkCallback(mixedContentWarningNode, (aEvent) => {
  1539       aEvent.stopPropagation();
  1540       this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
  1541     });
  1542   },
  1544   /**
  1545    * Adds a more info link node to messages based on the nsIScriptError object
  1546    * that we need to report to the console
  1548    * @param aNode
  1549    *        The node to which we will be adding the more info link node
  1550    * @param aScriptError
  1551    *        The script error object that we are reporting to the console
  1552    */
  1553   addMoreInfoLink: function WCF_addMoreInfoLink(aNode, aScriptError)
  1555     let url;
  1556     switch (aScriptError.category) {
  1557      case "Insecure Password Field":
  1558        url = INSECURE_PASSWORDS_LEARN_MORE;
  1559      break;
  1560      case "Mixed Content Message":
  1561      case "Mixed Content Blocker":
  1562       url = MIXED_CONTENT_LEARN_MORE;
  1563      break;
  1564      case "Invalid HSTS Headers":
  1565       url = STRICT_TRANSPORT_SECURITY_LEARN_MORE;
  1566      break;
  1567      default:
  1568       // Unknown category. Return without adding more info node.
  1569       return;
  1572     this.addLearnMoreWarningNode(aNode, url);
  1573   },
  1575   /*
  1576    * Appends a clickable warning node to the node passed
  1577    * as a parameter to the function. When a user clicks on the appended
  1578    * warning node, the browser navigates to the provided url.
  1580    * @param aNode
  1581    *        The node to which we will be adding a clickable warning node.
  1582    * @param aURL
  1583    *        The url which points to the page where the user can learn more
  1584    *        about security issues associated with the specific message that's
  1585    *        being logged.
  1586    */
  1587   addLearnMoreWarningNode:
  1588   function WCF_addLearnMoreWarningNode(aNode, aURL)
  1590     let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
  1592     let warningNode = this.document.createElementNS(XHTML_NS, "a");
  1593     warningNode.title = aURL;
  1594     warningNode.href = aURL;
  1595     warningNode.draggable = false;
  1596     warningNode.textContent = moreInfoLabel;
  1597     warningNode.className = "learn-more-link";
  1599     this._addMessageLinkCallback(warningNode, (aEvent) => {
  1600       aEvent.stopPropagation();
  1601       this.owner.openLink(aURL);
  1602     });
  1604     aNode.appendChild(warningNode);
  1605   },
  1607   /**
  1608    * Log file activity.
  1610    * @param string aFileURI
  1611    *        The file URI that was loaded.
  1612    * @return nsIDOMElement|undefined
  1613    *         The message element to display in the Web Console output.
  1614    */
  1615   logFileActivity: function WCF_logFileActivity(aFileURI)
  1617     let urlNode = this.document.createElementNS(XHTML_NS, "a");
  1618     urlNode.setAttribute("title", aFileURI);
  1619     urlNode.className = "url";
  1620     urlNode.textContent = aFileURI;
  1621     urlNode.draggable = false;
  1622     urlNode.href = aFileURI;
  1624     let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
  1625                                             urlNode, null, null, aFileURI);
  1627     this._addMessageLinkCallback(urlNode, () => {
  1628       this.owner.viewSource(aFileURI);
  1629     });
  1631     return outputNode;
  1632   },
  1634   /**
  1635    * Handle the file activity messages coming from the remote Web Console.
  1637    * @param string aFileURI
  1638    *        The file URI that was requested.
  1639    */
  1640   handleFileActivity: function WCF_handleFileActivity(aFileURI)
  1642     this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]);
  1643   },
  1645   /**
  1646    * Handle the reflow activity messages coming from the remote Web Console.
  1648    * @param object aMessage
  1649    *        An object holding information about a reflow batch.
  1650    */
  1651   logReflowActivity: function WCF_logReflowActivity(aMessage)
  1653     let {start, end, sourceURL, sourceLine} = aMessage;
  1654     let duration = Math.round((end - start) * 100) / 100;
  1655     let node = this.document.createElementNS(XHTML_NS, "span");
  1656     if (sourceURL) {
  1657       node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]);
  1658       let a = this.document.createElementNS(XHTML_NS, "a");
  1659       a.href = "#";
  1660       a.draggable = "false";
  1661       let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL);
  1662       let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction");
  1663       a.textContent = l10n.getFormatStr("reflow.messageLinkText",
  1664                          [functionName, filename, sourceLine]);
  1665       this._addMessageLinkCallback(a, () => {
  1666         this.owner.viewSourceInDebugger(sourceURL, sourceLine);
  1667       });
  1668       node.appendChild(a);
  1669     } else {
  1670       node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
  1672     return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
  1673   },
  1676   handleReflowActivity: function WCF_handleReflowActivity(aMessage)
  1678     this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]);
  1679   },
  1681   /**
  1682    * Inform user that the window.console API has been replaced by a script
  1683    * in a content page.
  1684    */
  1685   logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI()
  1687     let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
  1688                                       l10n.getStr("ConsoleAPIDisabled"));
  1689     this.outputMessage(CATEGORY_JS, node);
  1690   },
  1692   /**
  1693    * Handle the network events coming from the remote Web Console.
  1695    * @param object aActor
  1696    *        The NetworkEventActor grip.
  1697    */
  1698   handleNetworkEvent: function WCF_handleNetworkEvent(aActor)
  1700     let networkInfo = {
  1701       node: null,
  1702       actor: aActor.actor,
  1703       discardRequestBody: true,
  1704       discardResponseBody: true,
  1705       startedDateTime: aActor.startedDateTime,
  1706       request: {
  1707         url: aActor.url,
  1708         method: aActor.method,
  1709       },
  1710       response: {},
  1711       timings: {},
  1712       updates: [], // track the list of network event updates
  1713       private: aActor.private,
  1714     };
  1716     this._networkRequests[aActor.actor] = networkInfo;
  1717     this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]);
  1718   },
  1720   /**
  1721    * Handle network event updates coming from the server.
  1723    * @param string aActorId
  1724    *        The network event actor ID.
  1725    * @param string aType
  1726    *        Update type.
  1727    * @param object aPacket
  1728    *        Update details.
  1729    */
  1730   handleNetworkEventUpdate:
  1731   function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket)
  1733     let networkInfo = this._networkRequests[aActorId];
  1734     if (!networkInfo) {
  1735       return;
  1738     networkInfo.updates.push(aType);
  1740     switch (aType) {
  1741       case "requestHeaders":
  1742         networkInfo.request.headersSize = aPacket.headersSize;
  1743         break;
  1744       case "requestPostData":
  1745         networkInfo.discardRequestBody = aPacket.discardRequestBody;
  1746         networkInfo.request.bodySize = aPacket.dataSize;
  1747         break;
  1748       case "responseStart":
  1749         networkInfo.response.httpVersion = aPacket.response.httpVersion;
  1750         networkInfo.response.status = aPacket.response.status;
  1751         networkInfo.response.statusText = aPacket.response.statusText;
  1752         networkInfo.response.headersSize = aPacket.response.headersSize;
  1753         networkInfo.discardResponseBody = aPacket.response.discardResponseBody;
  1754         break;
  1755       case "responseContent":
  1756         networkInfo.response.content = {
  1757           mimeType: aPacket.mimeType,
  1758         };
  1759         networkInfo.response.bodySize = aPacket.contentSize;
  1760         networkInfo.discardResponseBody = aPacket.discardResponseBody;
  1761         break;
  1762       case "eventTimings":
  1763         networkInfo.totalTime = aPacket.totalTime;
  1764         break;
  1767     if (networkInfo.node && this._updateNetMessage(aActorId)) {
  1768       this.emit("messages-updated", new Set([networkInfo.node]));
  1771     // For unit tests we pass the HTTP activity object to the test callback,
  1772     // once requests complete.
  1773     if (this.owner.lastFinishedRequestCallback &&
  1774         networkInfo.updates.indexOf("responseContent") > -1 &&
  1775         networkInfo.updates.indexOf("eventTimings") > -1) {
  1776       this.owner.lastFinishedRequestCallback(networkInfo, this);
  1778   },
  1780   /**
  1781    * Update an output message to reflect the latest state of a network request,
  1782    * given a network event actor ID.
  1784    * @private
  1785    * @param string aActorId
  1786    *        The network event actor ID for which you want to update the message.
  1787    * @return boolean
  1788    *         |true| if the message node was updated, or |false| otherwise.
  1789    */
  1790   _updateNetMessage: function WCF__updateNetMessage(aActorId)
  1792     let networkInfo = this._networkRequests[aActorId];
  1793     if (!networkInfo || !networkInfo.node) {
  1794       return;
  1797     let messageNode = networkInfo.node;
  1798     let updates = networkInfo.updates;
  1799     let hasEventTimings = updates.indexOf("eventTimings") > -1;
  1800     let hasResponseStart = updates.indexOf("responseStart") > -1;
  1801     let request = networkInfo.request;
  1802     let response = networkInfo.response;
  1803     let updated = false;
  1805     if (hasEventTimings || hasResponseStart) {
  1806       let status = [];
  1807       if (response.httpVersion && response.status) {
  1808         status = [response.httpVersion, response.status, response.statusText];
  1810       if (hasEventTimings) {
  1811         status.push(l10n.getFormatStr("NetworkPanel.durationMS",
  1812                                       [networkInfo.totalTime]));
  1814       let statusText = "[" + status.join(" ") + "]";
  1816       let statusNode = messageNode.getElementsByClassName("status")[0];
  1817       statusNode.textContent = statusText;
  1819       messageNode.clipboardText = [request.method, request.url, statusText]
  1820                                   .join(" ");
  1822       if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE &&
  1823           response.status <= MAX_HTTP_ERROR_CODE) {
  1824         this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR);
  1827       updated = true;
  1830     if (messageNode._netPanel) {
  1831       messageNode._netPanel.update();
  1834     return updated;
  1835   },
  1837   /**
  1838    * Opens a NetworkPanel.
  1840    * @param nsIDOMNode aNode
  1841    *        The message node you want the panel to be anchored to.
  1842    * @param object aHttpActivity
  1843    *        The HTTP activity object that holds network request and response
  1844    *        information. This object is given to the NetworkPanel constructor.
  1845    * @return object
  1846    *         The new NetworkPanel instance.
  1847    */
  1848   openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity)
  1850     let actor = aHttpActivity.actor;
  1852     if (actor) {
  1853       this.webConsoleClient.getRequestHeaders(actor, function(aResponse) {
  1854         if (aResponse.error) {
  1855           Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" +
  1856                          aResponse.error);
  1857           return;
  1860         aHttpActivity.request.headers = aResponse.headers;
  1862         this.webConsoleClient.getRequestCookies(actor, onRequestCookies);
  1863       }.bind(this));
  1866     let onRequestCookies = function(aResponse) {
  1867       if (aResponse.error) {
  1868         Cu.reportError("WCF_openNetworkPanel getRequestCookies:" +
  1869                        aResponse.error);
  1870         return;
  1873       aHttpActivity.request.cookies = aResponse.cookies;
  1875       this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders);
  1876     }.bind(this);
  1878     let onResponseHeaders = function(aResponse) {
  1879       if (aResponse.error) {
  1880         Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" +
  1881                        aResponse.error);
  1882         return;
  1885       aHttpActivity.response.headers = aResponse.headers;
  1887       this.webConsoleClient.getResponseCookies(actor, onResponseCookies);
  1888     }.bind(this);
  1890     let onResponseCookies = function(aResponse) {
  1891       if (aResponse.error) {
  1892         Cu.reportError("WCF_openNetworkPanel getResponseCookies:" +
  1893                        aResponse.error);
  1894         return;
  1897       aHttpActivity.response.cookies = aResponse.cookies;
  1899       this.webConsoleClient.getRequestPostData(actor, onRequestPostData);
  1900     }.bind(this);
  1902     let onRequestPostData = function(aResponse) {
  1903       if (aResponse.error) {
  1904         Cu.reportError("WCF_openNetworkPanel getRequestPostData:" +
  1905                        aResponse.error);
  1906         return;
  1909       aHttpActivity.request.postData = aResponse.postData;
  1910       aHttpActivity.discardRequestBody = aResponse.postDataDiscarded;
  1912       this.webConsoleClient.getResponseContent(actor, onResponseContent);
  1913     }.bind(this);
  1915     let onResponseContent = function(aResponse) {
  1916       if (aResponse.error) {
  1917         Cu.reportError("WCF_openNetworkPanel getResponseContent:" +
  1918                        aResponse.error);
  1919         return;
  1922       aHttpActivity.response.content = aResponse.content;
  1923       aHttpActivity.discardResponseBody = aResponse.contentDiscarded;
  1925       this.webConsoleClient.getEventTimings(actor, onEventTimings);
  1926     }.bind(this);
  1928     let onEventTimings = function(aResponse) {
  1929       if (aResponse.error) {
  1930         Cu.reportError("WCF_openNetworkPanel getEventTimings:" +
  1931                        aResponse.error);
  1932         return;
  1935       aHttpActivity.timings = aResponse.timings;
  1937       openPanel();
  1938     }.bind(this);
  1940     let openPanel = function() {
  1941       aNode._netPanel = netPanel;
  1943       let panel = netPanel.panel;
  1944       panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
  1945       panel.sizeTo(450, 500);
  1946       panel.setAttribute("hudId", this.hudId);
  1948       panel.addEventListener("popuphiding", function WCF_netPanel_onHide() {
  1949         panel.removeEventListener("popuphiding", WCF_netPanel_onHide);
  1951         aNode._panelOpen = false;
  1952         aNode._netPanel = null;
  1953       });
  1955       aNode._panelOpen = true;
  1956     }.bind(this);
  1958     let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this);
  1959     netPanel.linkNode = aNode;
  1961     if (!actor) {
  1962       openPanel();
  1965     return netPanel;
  1966   },
  1968   /**
  1969    * Handler for page location changes.
  1971    * @param string aURI
  1972    *        New page location.
  1973    * @param string aTitle
  1974    *        New page title.
  1975    */
  1976   onLocationChange: function WCF_onLocationChange(aURI, aTitle)
  1978     this.contentLocation = aURI;
  1979     if (this.owner.onLocationChange) {
  1980       this.owner.onLocationChange(aURI, aTitle);
  1982   },
  1984   /**
  1985    * Handler for the tabNavigated notification.
  1987    * @param string aEvent
  1988    *        Event name.
  1989    * @param object aPacket
  1990    *        Notification packet received from the server.
  1991    */
  1992   handleTabNavigated: function WCF_handleTabNavigated(aEvent, aPacket)
  1994     if (aEvent == "will-navigate") {
  1995       if (this.persistLog) {
  1996         let marker = new Messages.NavigationMarker(aPacket.url, Date.now());
  1997         this.output.addMessage(marker);
  1999       else {
  2000         this.jsterm.clearOutput();
  2004     if (aPacket.url) {
  2005       this.onLocationChange(aPacket.url, aPacket.title);
  2008     if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) {
  2009       this.logWarningAboutReplacedAPI();
  2011   },
  2013   /**
  2014    * Output a message node. This filters a node appropriately, then sends it to
  2015    * the output, regrouping and pruning output as necessary.
  2017    * Note: this call is async - the given message node may not be displayed when
  2018    * you call this method.
  2020    * @param integer aCategory
  2021    *        The category of the message you want to output. See the CATEGORY_*
  2022    *        constants.
  2023    * @param function|nsIDOMElement aMethodOrNode
  2024    *        The method that creates the message element to send to the output or
  2025    *        the actual element. If a method is given it will be bound to the HUD
  2026    *        object and the arguments will be |aArguments|.
  2027    * @param array [aArguments]
  2028    *        If a method is given to output the message element then the method
  2029    *        will be invoked with the list of arguments given here.
  2030    */
  2031   outputMessage: function WCF_outputMessage(aCategory, aMethodOrNode, aArguments)
  2033     if (!this._outputQueue.length) {
  2034       // If the queue is empty we consider that now was the last output flush.
  2035       // This avoid an immediate output flush when the timer executes.
  2036       this._lastOutputFlush = Date.now();
  2039     this._outputQueue.push([aCategory, aMethodOrNode, aArguments]);
  2041     if (!this._outputTimerInitialized) {
  2042       this._initOutputTimer();
  2044   },
  2046   /**
  2047    * Try to flush the output message queue. This takes the messages in the
  2048    * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL.
  2049    * Further output is queued to happen later - see OUTPUT_INTERVAL.
  2051    * @private
  2052    */
  2053   _flushMessageQueue: function WCF__flushMessageQueue()
  2055     if (!this._outputTimer) {
  2056       return;
  2059     let timeSinceFlush = Date.now() - this._lastOutputFlush;
  2060     if (this._outputQueue.length > MESSAGES_IN_INTERVAL &&
  2061         timeSinceFlush < THROTTLE_UPDATES) {
  2062       this._initOutputTimer();
  2063       return;
  2066     // Determine how many messages we can display now.
  2067     let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL);
  2068     if (toDisplay < 1) {
  2069       this._outputTimerInitialized = false;
  2070       return;
  2073     // Try to prune the message queue.
  2074     let shouldPrune = false;
  2075     if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) {
  2076       toDisplay = Math.min(this._outputQueue.length, toDisplay);
  2077       shouldPrune = true;
  2080     let batch = this._outputQueue.splice(0, toDisplay);
  2081     if (!batch.length) {
  2082       this._outputTimerInitialized = false;
  2083       return;
  2086     let outputNode = this.outputNode;
  2087     let lastVisibleNode = null;
  2088     let scrollNode = outputNode.parentNode;
  2089     let scrolledToBottom = Utils.isOutputScrolledToBottom(outputNode);
  2090     let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
  2092     // Output the current batch of messages.
  2093     let newMessages = new Set();
  2094     let updatedMessages = new Set();
  2095     for (let item of batch) {
  2096       let result = this._outputMessageFromQueue(hudIdSupportsString, item);
  2097       if (result) {
  2098         if (result.isRepeated) {
  2099           updatedMessages.add(result.isRepeated);
  2101         else {
  2102           newMessages.add(result.node);
  2104         if (result.visible && result.node == this.outputNode.lastChild) {
  2105           lastVisibleNode = result.node;
  2110     let oldScrollHeight = 0;
  2112     // Prune messages if needed. We do not do this for every flush call to
  2113     // improve performance.
  2114     let removedNodes = 0;
  2115     if (shouldPrune || !this._outputQueue.length) {
  2116       oldScrollHeight = scrollNode.scrollHeight;
  2118       let categories = Object.keys(this._pruneCategoriesQueue);
  2119       categories.forEach(function _pruneOutput(aCategory) {
  2120         removedNodes += this.pruneOutputIfNecessary(aCategory);
  2121       }, this);
  2122       this._pruneCategoriesQueue = {};
  2125     let isInputOutput = lastVisibleNode &&
  2126                         (lastVisibleNode.category == CATEGORY_INPUT ||
  2127                          lastVisibleNode.category == CATEGORY_OUTPUT);
  2129     // Scroll to the new node if it is not filtered, and if the output node is
  2130     // scrolled at the bottom or if the new node is a jsterm input/output
  2131     // message.
  2132     if (lastVisibleNode && (scrolledToBottom || isInputOutput)) {
  2133       Utils.scrollToVisible(lastVisibleNode);
  2135     else if (!scrolledToBottom && removedNodes > 0 &&
  2136              oldScrollHeight != scrollNode.scrollHeight) {
  2137       // If there were pruned messages and if scroll is not at the bottom, then
  2138       // we need to adjust the scroll location.
  2139       scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight;
  2142     if (newMessages.size) {
  2143       this.emit("messages-added", newMessages);
  2145     if (updatedMessages.size) {
  2146       this.emit("messages-updated", updatedMessages);
  2149     // If the queue is not empty, schedule another flush.
  2150     if (this._outputQueue.length > 0) {
  2151       this._initOutputTimer();
  2153     else {
  2154       this._outputTimerInitialized = false;
  2155       if (this._flushCallback && this._flushCallback() === false) {
  2156         this._flushCallback = null;
  2160     this._lastOutputFlush = Date.now();
  2161   },
  2163   /**
  2164    * Initialize the output timer.
  2165    * @private
  2166    */
  2167   _initOutputTimer: function WCF__initOutputTimer()
  2169     if (!this._outputTimer) {
  2170       return;
  2173     this._outputTimerInitialized = true;
  2174     this._outputTimer.initWithCallback(this._flushMessageQueue,
  2175                                        OUTPUT_INTERVAL,
  2176                                        Ci.nsITimer.TYPE_ONE_SHOT);
  2177   },
  2179   /**
  2180    * Output a message from the queue.
  2182    * @private
  2183    * @param nsISupportsString aHudIdSupportsString
  2184    *        The HUD ID as an nsISupportsString.
  2185    * @param array aItem
  2186    *        An item from the output queue - this item represents a message.
  2187    * @return object
  2188    *         An object that holds the following properties:
  2189    *         - node: the DOM element of the message.
  2190    *         - isRepeated: the DOM element of the original message, if this is
  2191    *         a repeated message, otherwise null.
  2192    *         - visible: boolean that tells if the message is visible.
  2193    */
  2194   _outputMessageFromQueue:
  2195   function WCF__outputMessageFromQueue(aHudIdSupportsString, aItem)
  2197     let [category, methodOrNode, args] = aItem;
  2199     let node = typeof methodOrNode == "function" ?
  2200                methodOrNode.apply(this, args || []) :
  2201                methodOrNode;
  2202     if (!node) {
  2203       return null;
  2206     let afterNode = node._outputAfterNode;
  2207     if (afterNode) {
  2208       delete node._outputAfterNode;
  2211     let isFiltered = this.filterMessageNode(node);
  2213     let isRepeated = this._filterRepeatedMessage(node);
  2215     let visible = !isRepeated && !isFiltered;
  2216     if (!isRepeated) {
  2217       this.outputNode.insertBefore(node,
  2218                                    afterNode ? afterNode.nextSibling : null);
  2219       this._pruneCategoriesQueue[node.category] = true;
  2221       let nodeID = node.getAttribute("id");
  2222       Services.obs.notifyObservers(aHudIdSupportsString,
  2223                                    "web-console-message-created", nodeID);
  2227     if (node._onOutput) {
  2228       node._onOutput();
  2229       delete node._onOutput;
  2232     return {
  2233       visible: visible,
  2234       node: node,
  2235       isRepeated: isRepeated,
  2236     };
  2237   },
  2239   /**
  2240    * Prune the queue of messages to display. This avoids displaying messages
  2241    * that will be removed at the end of the queue anyway.
  2242    * @private
  2243    */
  2244   _pruneOutputQueue: function WCF__pruneOutputQueue()
  2246     let nodes = {};
  2248     // Group the messages per category.
  2249     this._outputQueue.forEach(function(aItem, aIndex) {
  2250       let [category] = aItem;
  2251       if (!(category in nodes)) {
  2252         nodes[category] = [];
  2254       nodes[category].push(aIndex);
  2255     }, this);
  2257     let pruned = 0;
  2259     // Loop through the categories we found and prune if needed.
  2260     for (let category in nodes) {
  2261       let limit = Utils.logLimitForCategory(category);
  2262       let indexes = nodes[category];
  2263       if (indexes.length > limit) {
  2264         let n = Math.max(0, indexes.length - limit);
  2265         pruned += n;
  2266         for (let i = n - 1; i >= 0; i--) {
  2267           this._pruneItemFromQueue(this._outputQueue[indexes[i]]);
  2268           this._outputQueue.splice(indexes[i], 1);
  2273     return pruned;
  2274   },
  2276   /**
  2277    * Prune an item from the output queue.
  2279    * @private
  2280    * @param array aItem
  2281    *        The item you want to remove from the output queue.
  2282    */
  2283   _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem)
  2285     // TODO: handle object releasing in a more elegant way once all console
  2286     // messages use the new API - bug 778766.
  2288     let [category, methodOrNode, args] = aItem;
  2289     if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
  2290       for (let actor of methodOrNode._objectActors) {
  2291         this._releaseObject(actor);
  2293       methodOrNode._objectActors.clear();
  2296     if (methodOrNode == this.output._flushMessageQueue &&
  2297         args[0]._objectActors) {
  2298       for (let arg of args) {
  2299         if (!arg._objectActors) {
  2300           continue;
  2302         for (let actor of arg._objectActors) {
  2303           this._releaseObject(actor);
  2305         arg._objectActors.clear();
  2309     if (category == CATEGORY_NETWORK) {
  2310       let connectionId = null;
  2311       if (methodOrNode == this.logNetEvent) {
  2312         connectionId = args[0];
  2314       else if (typeof methodOrNode != "function") {
  2315         connectionId = methodOrNode._connectionId;
  2317       if (connectionId && connectionId in this._networkRequests) {
  2318         delete this._networkRequests[connectionId];
  2319         this._releaseObject(connectionId);
  2322     else if (category == CATEGORY_WEBDEV &&
  2323              methodOrNode == this.logConsoleAPIMessage) {
  2324       args[0].arguments.forEach((aValue) => {
  2325         if (WebConsoleUtils.isActorGrip(aValue)) {
  2326           this._releaseObject(aValue.actor);
  2328       });
  2330     else if (category == CATEGORY_JS &&
  2331              methodOrNode == this.reportPageError) {
  2332       let pageError = args[1];
  2333       for (let prop of ["errorMessage", "lineText"]) {
  2334         let grip = pageError[prop];
  2335         if (WebConsoleUtils.isActorGrip(grip)) {
  2336           this._releaseObject(grip.actor);
  2340     else if (category == CATEGORY_JS &&
  2341              methodOrNode == this._reportLogMessage) {
  2342       if (WebConsoleUtils.isActorGrip(args[0].message)) {
  2343         this._releaseObject(args[0].message.actor);
  2346   },
  2348   /**
  2349    * Ensures that the number of message nodes of type aCategory don't exceed that
  2350    * category's line limit by removing old messages as needed.
  2352    * @param integer aCategory
  2353    *        The category of message nodes to prune if needed.
  2354    * @return number
  2355    *         The number of removed nodes.
  2356    */
  2357   pruneOutputIfNecessary: function WCF_pruneOutputIfNecessary(aCategory)
  2359     let logLimit = Utils.logLimitForCategory(aCategory);
  2360     let messageNodes = this.outputNode.querySelectorAll(".message[category=" +
  2361                        CATEGORY_CLASS_FRAGMENTS[aCategory] + "]");
  2362     let n = Math.max(0, messageNodes.length - logLimit);
  2363     let toRemove = Array.prototype.slice.call(messageNodes, 0, n);
  2364     toRemove.forEach(this.removeOutputMessage, this);
  2366     return n;
  2367   },
  2369   /**
  2370    * Remove a given message from the output.
  2372    * @param nsIDOMNode aNode
  2373    *        The message node you want to remove.
  2374    */
  2375   removeOutputMessage: function WCF_removeOutputMessage(aNode)
  2377     if (aNode._messageObject) {
  2378       aNode._messageObject.destroy();
  2381     if (aNode._objectActors) {
  2382       for (let actor of aNode._objectActors) {
  2383         this._releaseObject(actor);
  2385       aNode._objectActors.clear();
  2388     if (aNode.category == CATEGORY_CSS ||
  2389         aNode.category == CATEGORY_SECURITY) {
  2390       let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
  2391       if (repeatNode && repeatNode._uid) {
  2392         delete this._repeatNodes[repeatNode._uid];
  2395     else if (aNode._connectionId &&
  2396              aNode.category == CATEGORY_NETWORK) {
  2397       delete this._networkRequests[aNode._connectionId];
  2398       this._releaseObject(aNode._connectionId);
  2400     else if (aNode.classList.contains("inlined-variables-view")) {
  2401       let view = aNode._variablesView;
  2402       if (view) {
  2403         view.controller.releaseActors();
  2405       aNode._variablesView = null;
  2408     if (aNode.parentNode) {
  2409       aNode.parentNode.removeChild(aNode);
  2411   },
  2413   /**
  2414    * Given a category and message body, creates a DOM node to represent an
  2415    * incoming message. The timestamp is automatically added.
  2417    * @param number aCategory
  2418    *        The category of the message: one of the CATEGORY_* constants.
  2419    * @param number aSeverity
  2420    *        The severity of the message: one of the SEVERITY_* constants;
  2421    * @param string|nsIDOMNode aBody
  2422    *        The body of the message, either a simple string or a DOM node.
  2423    * @param string aSourceURL [optional]
  2424    *        The URL of the source file that emitted the error.
  2425    * @param number aSourceLine [optional]
  2426    *        The line number on which the error occurred. If zero or omitted,
  2427    *        there is no line number associated with this message.
  2428    * @param string aClipboardText [optional]
  2429    *        The text that should be copied to the clipboard when this node is
  2430    *        copied. If omitted, defaults to the body text. If `aBody` is not
  2431    *        a string, then the clipboard text must be supplied.
  2432    * @param number aLevel [optional]
  2433    *        The level of the console API message.
  2434    * @param number aTimeStamp [optional]
  2435    *        The timestamp to use for this message node. If omitted, the current
  2436    *        date and time is used.
  2437    * @return nsIDOMNode
  2438    *         The message node: a DIV ready to be inserted into the Web Console
  2439    *         output node.
  2440    */
  2441   createMessageNode:
  2442   function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL,
  2443                                  aSourceLine, aClipboardText, aLevel, aTimeStamp)
  2445     if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
  2446       aClipboardText = aBody.innerText;
  2449     let indentNode = this.document.createElementNS(XHTML_NS, "span");
  2450     indentNode.className = "indent";
  2452     // Apply the current group by indenting appropriately.
  2453     let indent = this.groupDepth * GROUP_INDENT;
  2454     indentNode.style.width = indent + "px";
  2456     // Make the icon container, which is a vertical box. Its purpose is to
  2457     // ensure that the icon stays anchored at the top of the message even for
  2458     // long multi-line messages.
  2459     let iconContainer = this.document.createElementNS(XHTML_NS, "span");
  2460     iconContainer.className = "icon";
  2462     // Create the message body, which contains the actual text of the message.
  2463     let bodyNode = this.document.createElementNS(XHTML_NS, "span");
  2464     bodyNode.className = "message-body-wrapper message-body devtools-monospace";
  2466     // Store the body text, since it is needed later for the variables view.
  2467     let body = aBody;
  2468     // If a string was supplied for the body, turn it into a DOM node and an
  2469     // associated clipboard string now.
  2470     aClipboardText = aClipboardText ||
  2471                      (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
  2472                               (aSourceLine ? ":" + aSourceLine : ""));
  2474     let timestamp = aTimeStamp || Date.now();
  2476     // Create the containing node and append all its elements to it.
  2477     let node = this.document.createElementNS(XHTML_NS, "div");
  2478     node.id = "console-msg-" + gSequenceId();
  2479     node.className = "message";
  2480     node.clipboardText = aClipboardText;
  2481     node.timestamp = timestamp;
  2482     this.setMessageType(node, aCategory, aSeverity);
  2484     if (aBody instanceof Ci.nsIDOMNode) {
  2485       bodyNode.appendChild(aBody);
  2487     else {
  2488       let str = undefined;
  2489       if (aLevel == "dir") {
  2490         str = VariablesView.getString(aBody.arguments[0]);
  2492       else {
  2493         str = aBody;
  2496       if (str !== undefined) {
  2497         aBody = this.document.createTextNode(str);
  2498         bodyNode.appendChild(aBody);
  2502     // Add the message repeats node only when needed.
  2503     let repeatNode = null;
  2504     if (aCategory != CATEGORY_INPUT &&
  2505         aCategory != CATEGORY_OUTPUT &&
  2506         aCategory != CATEGORY_NETWORK &&
  2507         !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) {
  2508       repeatNode = this.document.createElementNS(XHTML_NS, "span");
  2509       repeatNode.setAttribute("value", "1");
  2510       repeatNode.className = "message-repeats";
  2511       repeatNode.textContent = 1;
  2512       repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
  2513                          aSourceURL, aSourceLine].join(":");
  2516     // Create the timestamp.
  2517     let timestampNode = this.document.createElementNS(XHTML_NS, "span");
  2518     timestampNode.className = "timestamp devtools-monospace";
  2520     let timestampString = l10n.timestampString(timestamp);
  2521     timestampNode.textContent = timestampString + " ";
  2523     // Create the source location (e.g. www.example.com:6) that sits on the
  2524     // right side of the message, if applicable.
  2525     let locationNode;
  2526     if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) {
  2527       locationNode = this.createLocationNode(aSourceURL, aSourceLine);
  2530     node.appendChild(timestampNode);
  2531     node.appendChild(indentNode);
  2532     node.appendChild(iconContainer);
  2534     // Display the variables view after the message node.
  2535     if (aLevel == "dir") {
  2536       bodyNode.style.height = (this.window.innerHeight *
  2537                                CONSOLE_DIR_VIEW_HEIGHT) + "px";
  2539       let options = {
  2540         objectActor: body.arguments[0],
  2541         targetElement: bodyNode,
  2542         hideFilterInput: true,
  2543       };
  2544       this.jsterm.openVariablesView(options).then((aView) => {
  2545         node._variablesView = aView;
  2546         if (node.classList.contains("hidden-message")) {
  2547           node.classList.remove("hidden-message");
  2549       });
  2551       node.classList.add("inlined-variables-view");
  2554     node.appendChild(bodyNode);
  2555     if (repeatNode) {
  2556       node.appendChild(repeatNode);
  2558     if (locationNode) {
  2559       node.appendChild(locationNode);
  2561     node.appendChild(this.document.createTextNode("\n"));
  2563     return node;
  2564   },
  2566   /**
  2567    * Creates the anchor that displays the textual location of an incoming
  2568    * message.
  2570    * @param string aSourceURL
  2571    *        The URL of the source file responsible for the error.
  2572    * @param number aSourceLine [optional]
  2573    *        The line number on which the error occurred. If zero or omitted,
  2574    *        there is no line number associated with this message.
  2575    * @param string aTarget [optional]
  2576    *        Tells which tool to open the link with, on click. Supported tools:
  2577    *        jsdebugger, styleeditor, scratchpad.
  2578    * @return nsIDOMNode
  2579    *         The new anchor element, ready to be added to the message node.
  2580    */
  2581   createLocationNode:
  2582   function WCF_createLocationNode(aSourceURL, aSourceLine, aTarget)
  2584     if (!aSourceURL) {
  2585       aSourceURL = "";
  2587     let locationNode = this.document.createElementNS(XHTML_NS, "a");
  2588     let filenameNode = this.document.createElementNS(XHTML_NS, "span");
  2590     // Create the text, which consists of an abbreviated version of the URL
  2591     // Scratchpad URLs should not be abbreviated.
  2592     let filename;
  2593     let fullURL;
  2594     let isScratchpad = false;
  2596     if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
  2597       filename = aSourceURL;
  2598       fullURL = aSourceURL;
  2599       isScratchpad = true;
  2601     else {
  2602       fullURL = aSourceURL.split(" -> ").pop();
  2603       filename = WebConsoleUtils.abbreviateSourceURL(fullURL);
  2606     filenameNode.className = "filename";
  2607     filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation"));
  2608     locationNode.appendChild(filenameNode);
  2610     locationNode.href = isScratchpad || !fullURL ? "#" : fullURL;
  2611     locationNode.draggable = false;
  2612     if (aTarget) {
  2613       locationNode.target = aTarget;
  2615     locationNode.setAttribute("title", aSourceURL);
  2616     locationNode.className = "message-location theme-link devtools-monospace";
  2618     // Make the location clickable.
  2619     let onClick = () => {
  2620       let target = locationNode.target;
  2621       if (target == "scratchpad" || isScratchpad) {
  2622         this.owner.viewSourceInScratchpad(aSourceURL);
  2623         return;
  2626       let category = locationNode.parentNode.category;
  2627       if (target == "styleeditor" || category == CATEGORY_CSS) {
  2628         this.owner.viewSourceInStyleEditor(fullURL, aSourceLine);
  2630       else if (target == "jsdebugger" ||
  2631                category == CATEGORY_JS || category == CATEGORY_WEBDEV) {
  2632         this.owner.viewSourceInDebugger(fullURL, aSourceLine);
  2634       else {
  2635         this.owner.viewSource(fullURL, aSourceLine);
  2637     };
  2639     if (fullURL) {
  2640       this._addMessageLinkCallback(locationNode, onClick);
  2643     if (aSourceLine) {
  2644       let lineNumberNode = this.document.createElementNS(XHTML_NS, "span");
  2645       lineNumberNode.className = "line-number";
  2646       lineNumberNode.textContent = ":" + aSourceLine;
  2647       locationNode.appendChild(lineNumberNode);
  2648       locationNode.sourceLine = aSourceLine;
  2651     return locationNode;
  2652   },
  2654   /**
  2655    * Adjusts the category and severity of the given message.
  2657    * @param nsIDOMNode aMessageNode
  2658    *        The message node to alter.
  2659    * @param number aCategory
  2660    *        The category for the message; one of the CATEGORY_ constants.
  2661    * @param number aSeverity
  2662    *        The severity for the message; one of the SEVERITY_ constants.
  2663    * @return void
  2664    */
  2665   setMessageType:
  2666   function WCF_setMessageType(aMessageNode, aCategory, aSeverity)
  2668     aMessageNode.category = aCategory;
  2669     aMessageNode.severity = aSeverity;
  2670     aMessageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[aCategory]);
  2671     aMessageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[aSeverity]);
  2672     aMessageNode.setAttribute("filter", MESSAGE_PREFERENCE_KEYS[aCategory][aSeverity]);
  2673   },
  2675   /**
  2676    * Add the mouse event handlers needed to make a link.
  2678    * @private
  2679    * @param nsIDOMNode aNode
  2680    *        The node for which you want to add the event handlers.
  2681    * @param function aCallback
  2682    *        The function you want to invoke on click.
  2683    */
  2684   _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback)
  2686     aNode.addEventListener("mousedown", (aEvent) => {
  2687       this._mousedown = true;
  2688       this._startX = aEvent.clientX;
  2689       this._startY = aEvent.clientY;
  2690     }, false);
  2692     aNode.addEventListener("click", (aEvent) => {
  2693       let mousedown = this._mousedown;
  2694       this._mousedown = false;
  2696       aEvent.preventDefault();
  2698       // Do not allow middle/right-click or 2+ clicks.
  2699       if (aEvent.detail != 1 || aEvent.button != 0) {
  2700         return;
  2703       // If this event started with a mousedown event and it ends at a different
  2704       // location, we consider this text selection.
  2705       if (mousedown &&
  2706           (this._startX != aEvent.clientX) &&
  2707           (this._startY != aEvent.clientY))
  2709         this._startX = this._startY = undefined;
  2710         return;
  2713       this._startX = this._startY = undefined;
  2715       aCallback.call(this, aEvent);
  2716     }, false);
  2717   },
  2719   _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback)
  2721     aNode.addEventListener("mousedown", (aEvent) => {
  2722       this._mousedown = true;
  2723       this._startX = aEvent.clientX;
  2724       this._startY = aEvent.clientY;
  2725     }, false);
  2727     aNode.addEventListener("click", (aEvent) => {
  2728       let mousedown = this._mousedown;
  2729       this._mousedown = false;
  2731       // Do not allow middle/right-click or 2+ clicks.
  2732       if (aEvent.detail != 1 || aEvent.button != 0) {
  2733         return;
  2736       // If this event started with a mousedown event and it ends at a different
  2737       // location, we consider this text selection.
  2738       // Add a fuzz modifier of two pixels in any direction to account for sloppy
  2739       // clicking.
  2740       if (mousedown &&
  2741           (Math.abs(aEvent.clientX - this._startX) >= 2) &&
  2742           (Math.abs(aEvent.clientY - this._startY) >= 1))
  2744         this._startX = this._startY = undefined;
  2745         return;
  2748       this._startX = this._startY = undefined;
  2750       aCallback.call(this, aEvent);
  2751     }, false);
  2752   },
  2754   /**
  2755    * Handler for the pref-changed event coming from the toolbox.
  2756    * Currently this function only handles the timestamps preferences.
  2758    * @private
  2759    * @param object aEvent
  2760    *        This parameter is a string that holds the event name
  2761    *        pref-changed in this case.
  2762    * @param object aData
  2763    *        This is the pref-changed data object.
  2764   */
  2765   _onToolboxPrefChanged: function WCF__onToolboxPrefChanged(aEvent, aData)
  2767     if (aData.pref == PREF_MESSAGE_TIMESTAMP) {
  2768       if (aData.newValue) {
  2769         this.outputNode.classList.remove("hideTimestamps");
  2771       else {
  2772         this.outputNode.classList.add("hideTimestamps");
  2775   },
  2777   /**
  2778    * Copies the selected items to the system clipboard.
  2780    * @param object aOptions
  2781    *        - linkOnly:
  2782    *        An optional flag to copy only URL without timestamp and
  2783    *        other meta-information. Default is false.
  2784    */
  2785   copySelectedItems: function WCF_copySelectedItems(aOptions)
  2787     aOptions = aOptions || { linkOnly: false, contextmenu: false };
  2789     // Gather up the selected items and concatenate their clipboard text.
  2790     let strings = [];
  2792     let children = this.output.getSelectedMessages();
  2793     if (!children.length && aOptions.contextmenu) {
  2794       children = [this._contextMenuHandler.lastClickedMessage];
  2797     for (let item of children) {
  2798       // Ensure the selected item hasn't been filtered by type or string.
  2799       if (!item.classList.contains("filtered-by-type") &&
  2800           !item.classList.contains("filtered-by-string")) {
  2801         let timestampString = l10n.timestampString(item.timestamp);
  2802         if (aOptions.linkOnly) {
  2803           strings.push(item.url);
  2805         else {
  2806           strings.push("[" + timestampString + "] " + item.clipboardText);
  2811     clipboardHelper.copyString(strings.join("\n"), this.document);
  2812   },
  2814   /**
  2815    * Object properties provider. This function gives you the properties of the
  2816    * remote object you want.
  2818    * @param string aActor
  2819    *        The object actor ID from which you want the properties.
  2820    * @param function aCallback
  2821    *        Function you want invoked once the properties are received.
  2822    */
  2823   objectPropertiesProvider:
  2824   function WCF_objectPropertiesProvider(aActor, aCallback)
  2826     this.webConsoleClient.inspectObjectProperties(aActor,
  2827       function(aResponse) {
  2828         if (aResponse.error) {
  2829           Cu.reportError("Failed to retrieve the object properties from the " +
  2830                          "server. Error: " + aResponse.error);
  2831           return;
  2833         aCallback(aResponse.properties);
  2834       });
  2835   },
  2837   /**
  2838    * Release an actor.
  2840    * @private
  2841    * @param string aActor
  2842    *        The actor ID you want to release.
  2843    */
  2844   _releaseObject: function WCF__releaseObject(aActor)
  2846     if (this.proxy) {
  2847       this.proxy.releaseActor(aActor);
  2849   },
  2851   /**
  2852    * Open the selected item's URL in a new tab.
  2853    */
  2854   openSelectedItemInTab: function WCF_openSelectedItemInTab()
  2856     let item = this.output.getSelectedMessages(1)[0] ||
  2857                this._contextMenuHandler.lastClickedMessage;
  2859     if (!item || !item.url) {
  2860       return;
  2863     this.owner.openLink(item.url);
  2864   },
  2866   /**
  2867    * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks
  2868    * when the Web Console is closed.
  2870    * @return object
  2871    *         A promise that is resolved when the WebConsoleFrame instance is
  2872    *         destroyed.
  2873    */
  2874   destroy: function WCF_destroy()
  2876     if (this._destroyer) {
  2877       return this._destroyer.promise;
  2880     this._destroyer = promise.defer();
  2882     let toolbox = gDevTools.getToolbox(this.owner.target);
  2883     if (toolbox) {
  2884       toolbox.off("webconsole-selected", this._onPanelSelected);
  2887     gDevTools.off("pref-changed", this._onToolboxPrefChanged);
  2889     this._repeatNodes = {};
  2890     this._outputQueue = [];
  2891     this._pruneCategoriesQueue = {};
  2892     this._networkRequests = {};
  2894     if (this._outputTimerInitialized) {
  2895       this._outputTimerInitialized = false;
  2896       this._outputTimer.cancel();
  2898     this._outputTimer = null;
  2900     if (this.jsterm) {
  2901       this.jsterm.destroy();
  2902       this.jsterm = null;
  2904     this.output.destroy();
  2905     this.output = null;
  2907     if (this._contextMenuHandler) {
  2908       this._contextMenuHandler.destroy();
  2909       this._contextMenuHandler = null;
  2912     this._commandController = null;
  2914     let onDestroy = function() {
  2915       this._destroyer.resolve(null);
  2916     }.bind(this);
  2918     if (this.proxy) {
  2919       this.proxy.disconnect().then(onDestroy);
  2920       this.proxy = null;
  2922     else {
  2923       onDestroy();
  2926     return this._destroyer.promise;
  2927   },
  2928 };
  2931 /**
  2932  * @see VariablesView.simpleValueEvalMacro
  2933  */
  2934 function simpleValueEvalMacro(aItem, aCurrentString)
  2936   return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self");
  2937 };
  2940 /**
  2941  * @see VariablesView.overrideValueEvalMacro
  2942  */
  2943 function overrideValueEvalMacro(aItem, aCurrentString)
  2945   return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self");
  2946 };
  2949 /**
  2950  * @see VariablesView.getterOrSetterEvalMacro
  2951  */
  2952 function getterOrSetterEvalMacro(aItem, aCurrentString)
  2954   return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self");
  2959 /**
  2960  * Create a JSTerminal (a JavaScript command line). This is attached to an
  2961  * existing HeadsUpDisplay (a Web Console instance). This code is responsible
  2962  * with handling command line input, code evaluation and result output.
  2964  * @constructor
  2965  * @param object aWebConsoleFrame
  2966  *        The WebConsoleFrame object that owns this JSTerm instance.
  2967  */
  2968 function JSTerm(aWebConsoleFrame)
  2970   this.hud = aWebConsoleFrame;
  2971   this.hudId = this.hud.hudId;
  2973   this.lastCompletion = { value: null };
  2974   this.history = [];
  2976   // Holds the number of entries in history. This value is incremented in
  2977   // this.execute().
  2978   this.historyIndex = 0; // incremented on this.execute()
  2980   // Holds the index of the history entry that the user is currently viewing.
  2981   // This is reset to this.history.length when this.execute() is invoked.
  2982   this.historyPlaceHolder = 0;
  2983   this._objectActorsInVariablesViews = new Map();
  2985   this._keyPress = this._keyPress.bind(this);
  2986   this._inputEventHandler = this._inputEventHandler.bind(this);
  2987   this._focusEventHandler = this._focusEventHandler.bind(this);
  2988   this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this);
  2989   this._blurEventHandler = this._blurEventHandler.bind(this);
  2991   EventEmitter.decorate(this);
  2994 JSTerm.prototype = {
  2995   SELECTED_FRAME: -1,
  2997   /**
  2998    * Stores the data for the last completion.
  2999    * @type object
  3000    */
  3001   lastCompletion: null,
  3003   /**
  3004    * Array that caches the user input suggestions received from the server.
  3005    * @private
  3006    * @type array
  3007    */
  3008   _autocompleteCache: null,
  3010   /**
  3011    * The input that caused the last request to the server, whose response is
  3012    * cached in the _autocompleteCache array.
  3013    * @private
  3014    * @type string
  3015    */
  3016   _autocompleteQuery: null,
  3018   /**
  3019    * The frameActorId used in the last autocomplete query. Whenever this changes
  3020    * the autocomplete cache must be invalidated.
  3021    * @private
  3022    * @type string
  3023    */
  3024   _lastFrameActorId: null,
  3026   /**
  3027    * The Web Console sidebar.
  3028    * @see this._createSidebar()
  3029    * @see Sidebar.jsm
  3030    */
  3031   sidebar: null,
  3033   /**
  3034    * The Variables View instance shown in the sidebar.
  3035    * @private
  3036    * @type object
  3037    */
  3038   _variablesView: null,
  3040   /**
  3041    * Tells if you want the variables view UI updates to be lazy or not. Tests
  3042    * disable lazy updates.
  3044    * @private
  3045    * @type boolean
  3046    */
  3047   _lazyVariablesView: true,
  3049   /**
  3050    * Holds a map between VariablesView instances and sets of ObjectActor IDs
  3051    * that have been retrieved from the server. This allows us to release the
  3052    * objects when needed.
  3054    * @private
  3055    * @type Map
  3056    */
  3057   _objectActorsInVariablesViews: null,
  3059   /**
  3060    * Last input value.
  3061    * @type string
  3062    */
  3063   lastInputValue: "",
  3065   /**
  3066    * Tells if the input node changed since the last focus.
  3068    * @private
  3069    * @type boolean
  3070    */
  3071   _inputChanged: false,
  3073   /**
  3074    * Tells if the autocomplete popup was navigated since the last open.
  3076    * @private
  3077    * @type boolean
  3078    */
  3079   _autocompletePopupNavigated: false,
  3081   /**
  3082    * History of code that was executed.
  3083    * @type array
  3084    */
  3085   history: null,
  3086   autocompletePopup: null,
  3087   inputNode: null,
  3088   completeNode: null,
  3090   /**
  3091    * Getter for the element that holds the messages we display.
  3092    * @type nsIDOMElement
  3093    */
  3094   get outputNode() this.hud.outputNode,
  3096   /**
  3097    * Getter for the debugger WebConsoleClient.
  3098    * @type object
  3099    */
  3100   get webConsoleClient() this.hud.webConsoleClient,
  3102   COMPLETE_FORWARD: 0,
  3103   COMPLETE_BACKWARD: 1,
  3104   COMPLETE_HINT_ONLY: 2,
  3105   COMPLETE_PAGEUP: 3,
  3106   COMPLETE_PAGEDOWN: 4,
  3108   /**
  3109    * Initialize the JSTerminal UI.
  3110    */
  3111   init: function JST_init()
  3113     let autocompleteOptions = {
  3114       onSelect: this.onAutocompleteSelect.bind(this),
  3115       onClick: this.acceptProposedCompletion.bind(this),
  3116       panelId: "webConsole_autocompletePopup",
  3117       listBoxId: "webConsole_autocompletePopupListBox",
  3118       position: "before_start",
  3119       theme: "auto",
  3120       direction: "ltr",
  3121       autoSelect: true
  3122     };
  3123     this.autocompletePopup = new AutocompletePopup(this.hud.document,
  3124                                                    autocompleteOptions);
  3126     let doc = this.hud.document;
  3127     let inputContainer = doc.querySelector(".jsterm-input-container");
  3128     this.completeNode = doc.querySelector(".jsterm-complete-node");
  3129     this.inputNode = doc.querySelector(".jsterm-input-node");
  3131     if (this.hud.owner._browserConsole &&
  3132         !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
  3133       inputContainer.style.display = "none";
  3135     else {
  3136       this.inputNode.addEventListener("keypress", this._keyPress, false);
  3137       this.inputNode.addEventListener("input", this._inputEventHandler, false);
  3138       this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
  3139       this.inputNode.addEventListener("focus", this._focusEventHandler, false);
  3142     this.hud.window.addEventListener("blur", this._blurEventHandler, false);
  3143     this.lastInputValue && this.setInputValue(this.lastInputValue);
  3144   },
  3146   /**
  3147    * The JavaScript evaluation response handler.
  3149    * @private
  3150    * @param object [aAfterMessage]
  3151    *        Optional message after which the evaluation result will be
  3152    *        inserted.
  3153    * @param function [aCallback]
  3154    *        Optional function to invoke when the evaluation result is added to
  3155    *        the output.
  3156    * @param object aResponse
  3157    *        The message received from the server.
  3158    */
  3159   _executeResultCallback:
  3160   function JST__executeResultCallback(aAfterMessage, aCallback, aResponse)
  3162     if (!this.hud) {
  3163       return;
  3165     if (aResponse.error) {
  3166       Cu.reportError("Evaluation error " + aResponse.error + ": " +
  3167                      aResponse.message);
  3168       return;
  3170     let errorMessage = aResponse.exceptionMessage;
  3171     let result = aResponse.result;
  3172     let helperResult = aResponse.helperResult;
  3173     let helperHasRawOutput = !!(helperResult || {}).rawOutput;
  3175     if (helperResult && helperResult.type) {
  3176       switch (helperResult.type) {
  3177         case "clearOutput":
  3178           this.clearOutput();
  3179           break;
  3180         case "inspectObject":
  3181           if (aAfterMessage) {
  3182             if (!aAfterMessage._objectActors) {
  3183               aAfterMessage._objectActors = new Set();
  3185             aAfterMessage._objectActors.add(helperResult.object.actor);
  3187           this.openVariablesView({
  3188             label: VariablesView.getString(helperResult.object, { concise: true }),
  3189             objectActor: helperResult.object,
  3190           });
  3191           break;
  3192         case "error":
  3193           try {
  3194             errorMessage = l10n.getStr(helperResult.message);
  3196           catch (ex) {
  3197             errorMessage = helperResult.message;
  3199           break;
  3200         case "help":
  3201           this.hud.owner.openLink(HELP_URL);
  3202           break;
  3206     // Hide undefined results coming from JSTerm helper functions.
  3207     if (!errorMessage && result && typeof result == "object" &&
  3208         result.type == "undefined" &&
  3209         helperResult && !helperHasRawOutput) {
  3210       aCallback && aCallback();
  3211       return;
  3214     let msg = new Messages.JavaScriptEvalOutput(aResponse, errorMessage);
  3215     this.hud.output.addMessage(msg);
  3217     if (aCallback) {
  3218       let oldFlushCallback = this.hud._flushCallback;
  3219       this.hud._flushCallback = () => {
  3220         aCallback(msg.element);
  3221         if (oldFlushCallback) {
  3222           oldFlushCallback();
  3223           this.hud._flushCallback = oldFlushCallback;
  3224           return true;
  3227         return false;
  3228       };
  3231     msg._afterMessage = aAfterMessage;
  3232     msg._objectActors = new Set();
  3234     if (WebConsoleUtils.isActorGrip(aResponse.exception)) {
  3235       msg._objectActors.add(aResponse.exception.actor);
  3238     if (WebConsoleUtils.isActorGrip(result)) {
  3239       msg._objectActors.add(result.actor);
  3241   },
  3243   /**
  3244    * Execute a string. Execution happens asynchronously in the content process.
  3246    * @param string [aExecuteString]
  3247    *        The string you want to execute. If this is not provided, the current
  3248    *        user input is used - taken from |this.inputNode.value|.
  3249    * @param function [aCallback]
  3250    *        Optional function to invoke when the result is displayed.
  3251    */
  3252   execute: function JST_execute(aExecuteString, aCallback)
  3254     // attempt to execute the content of the inputNode
  3255     aExecuteString = aExecuteString || this.inputNode.value;
  3256     if (!aExecuteString) {
  3257       return;
  3260     let message = new Messages.Simple(aExecuteString, {
  3261       category: "input",
  3262       severity: "log",
  3263     });
  3264     this.hud.output.addMessage(message);
  3265     let onResult = this._executeResultCallback.bind(this, message, aCallback);
  3267     let options = { frame: this.SELECTED_FRAME };
  3268     this.requestEvaluation(aExecuteString, options).then(onResult, onResult);
  3270     // Append a new value in the history of executed code, or overwrite the most
  3271     // recent entry. The most recent entry may contain the last edited input
  3272     // value that was not evaluated yet.
  3273     this.history[this.historyIndex++] = aExecuteString;
  3274     this.historyPlaceHolder = this.history.length;
  3275     this.setInputValue("");
  3276     this.clearCompletion();
  3277   },
  3279   /**
  3280    * Request a JavaScript string evaluation from the server.
  3282    * @param string aString
  3283    *        String to execute.
  3284    * @param object [aOptions]
  3285    *        Options for evaluation:
  3286    *        - bindObjectActor: tells the ObjectActor ID for which you want to do
  3287    *        the evaluation. The Debugger.Object of the OA will be bound to
  3288    *        |_self| during evaluation, such that it's usable in the string you
  3289    *        execute.
  3290    *        - frame: tells the stackframe depth to evaluate the string in. If
  3291    *        the jsdebugger is paused, you can pick the stackframe to be used for
  3292    *        evaluation. Use |this.SELECTED_FRAME| to always pick the
  3293    *        user-selected stackframe.
  3294    *        If you do not provide a |frame| the string will be evaluated in the
  3295    *        global content window.
  3296    * @return object
  3297    *         A promise object that is resolved when the server response is
  3298    *         received.
  3299    */
  3300   requestEvaluation: function JST_requestEvaluation(aString, aOptions = {})
  3302     let deferred = promise.defer();
  3304     function onResult(aResponse) {
  3305       if (!aResponse.error) {
  3306         deferred.resolve(aResponse);
  3308       else {
  3309         deferred.reject(aResponse);
  3313     let frameActor = null;
  3314     if ("frame" in aOptions) {
  3315       frameActor = this.getFrameActor(aOptions.frame);
  3318     let evalOptions = {
  3319       bindObjectActor: aOptions.bindObjectActor,
  3320       frameActor: frameActor,
  3321     };
  3323     this.webConsoleClient.evaluateJS(aString, onResult, evalOptions);
  3324     return deferred.promise;
  3325   },
  3327   /**
  3328    * Retrieve the FrameActor ID given a frame depth.
  3330    * @param number aFrame
  3331    *        Frame depth.
  3332    * @return string|null
  3333    *         The FrameActor ID for the given frame depth.
  3334    */
  3335   getFrameActor: function JST_getFrameActor(aFrame)
  3337     let state = this.hud.owner.getDebuggerFrames();
  3338     if (!state) {
  3339       return null;
  3342     let grip;
  3343     if (aFrame == this.SELECTED_FRAME) {
  3344       grip = state.frames[state.selected];
  3346     else {
  3347       grip = state.frames[aFrame];
  3350     return grip ? grip.actor : null;
  3351   },
  3353   /**
  3354    * Opens a new variables view that allows the inspection of the given object.
  3356    * @param object aOptions
  3357    *        Options for the variables view:
  3358    *        - objectActor: grip of the ObjectActor you want to show in the
  3359    *        variables view.
  3360    *        - rawObject: the raw object you want to show in the variables view.
  3361    *        - label: label to display in the variables view for inspected
  3362    *        object.
  3363    *        - hideFilterInput: optional boolean, |true| if you want to hide the
  3364    *        variables view filter input.
  3365    *        - targetElement: optional nsIDOMElement to append the variables view
  3366    *        to. An iframe element is used as a container for the view. If this
  3367    *        option is not used, then the variables view opens in the sidebar.
  3368    *        - autofocus: optional boolean, |true| if you want to give focus to
  3369    *        the variables view window after open, |false| otherwise.
  3370    * @return object
  3371    *         A promise object that is resolved when the variables view has
  3372    *         opened. The new variables view instance is given to the callbacks.
  3373    */
  3374   openVariablesView: function JST_openVariablesView(aOptions)
  3376     let onContainerReady = (aWindow) => {
  3377       let container = aWindow.document.querySelector("#variables");
  3378       let view = this._variablesView;
  3379       if (!view || aOptions.targetElement) {
  3380         let viewOptions = {
  3381           container: container,
  3382           hideFilterInput: aOptions.hideFilterInput,
  3383         };
  3384         view = this._createVariablesView(viewOptions);
  3385         if (!aOptions.targetElement) {
  3386           this._variablesView = view;
  3387           aWindow.addEventListener("keypress", this._onKeypressInVariablesView);
  3390       aOptions.view = view;
  3391       this._updateVariablesView(aOptions);
  3393       if (!aOptions.targetElement && aOptions.autofocus) {
  3394         aWindow.focus();
  3397       this.emit("variablesview-open", view, aOptions);
  3398       return view;
  3399     };
  3401     let openPromise;
  3402     if (aOptions.targetElement) {
  3403       let deferred = promise.defer();
  3404       openPromise = deferred.promise;
  3405       let document = aOptions.targetElement.ownerDocument;
  3406       let iframe = document.createElementNS(XHTML_NS, "iframe");
  3408       iframe.addEventListener("load", function onIframeLoad(aEvent) {
  3409         iframe.removeEventListener("load", onIframeLoad, true);
  3410         iframe.style.visibility = "visible";
  3411         deferred.resolve(iframe.contentWindow);
  3412       }, true);
  3414       iframe.flex = 1;
  3415       iframe.style.visibility = "hidden";
  3416       iframe.setAttribute("src", VARIABLES_VIEW_URL);
  3417       aOptions.targetElement.appendChild(iframe);
  3419     else {
  3420       if (!this.sidebar) {
  3421         this._createSidebar();
  3423       openPromise = this._addVariablesViewSidebarTab();
  3426     return openPromise.then(onContainerReady);
  3427   },
  3429   /**
  3430    * Create the Web Console sidebar.
  3432    * @see devtools/framework/sidebar.js
  3433    * @private
  3434    */
  3435   _createSidebar: function JST__createSidebar()
  3437     let tabbox = this.hud.document.querySelector("#webconsole-sidebar");
  3438     this.sidebar = new ToolSidebar(tabbox, this, "webconsole");
  3439     this.sidebar.show();
  3440   },
  3442   /**
  3443    * Add the variables view tab to the sidebar.
  3445    * @private
  3446    * @return object
  3447    *         A promise object for the adding of the new tab.
  3448    */
  3449   _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab()
  3451     let deferred = promise.defer();
  3453     let onTabReady = () => {
  3454       let window = this.sidebar.getWindowForTab("variablesview");
  3455       deferred.resolve(window);
  3456     };
  3458     let tab = this.sidebar.getTab("variablesview");
  3459     if (tab) {
  3460       if (this.sidebar.getCurrentTabID() == "variablesview") {
  3461         onTabReady();
  3463       else {
  3464         this.sidebar.once("variablesview-selected", onTabReady);
  3465         this.sidebar.select("variablesview");
  3468     else {
  3469       this.sidebar.once("variablesview-ready", onTabReady);
  3470       this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
  3473     return deferred.promise;
  3474   },
  3476   /**
  3477    * The keypress event handler for the Variables View sidebar. Currently this
  3478    * is used for removing the sidebar when Escape is pressed.
  3480    * @private
  3481    * @param nsIDOMEvent aEvent
  3482    *        The keypress DOM event object.
  3483    */
  3484   _onKeypressInVariablesView: function JST__onKeypressInVariablesView(aEvent)
  3486     let tag = aEvent.target.nodeName;
  3487     if (aEvent.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || aEvent.shiftKey ||
  3488         aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey ||
  3489         ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) {
  3490         return;
  3493     this._sidebarDestroy();
  3494     this.inputNode.focus();
  3495     aEvent.stopPropagation();
  3496   },
  3498   /**
  3499    * Create a variables view instance.
  3501    * @private
  3502    * @param object aOptions
  3503    *        Options for the new Variables View instance:
  3504    *        - container: the DOM element where the variables view is inserted.
  3505    *        - hideFilterInput: boolean, if true the variables filter input is
  3506    *        hidden.
  3507    * @return object
  3508    *         The new Variables View instance.
  3509    */
  3510   _createVariablesView: function JST__createVariablesView(aOptions)
  3512     let view = new VariablesView(aOptions.container);
  3513     view.toolbox = gDevTools.getToolbox(this.hud.owner.target);
  3514     view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
  3515     view.emptyText = l10n.getStr("emptyPropertiesList");
  3516     view.searchEnabled = !aOptions.hideFilterInput;
  3517     view.lazyEmpty = this._lazyVariablesView;
  3519     VariablesViewController.attach(view, {
  3520       getEnvironmentClient: aGrip => {
  3521         return new EnvironmentClient(this.hud.proxy.client, aGrip);
  3522       },
  3523       getObjectClient: aGrip => {
  3524         return new ObjectClient(this.hud.proxy.client, aGrip);
  3525       },
  3526       getLongStringClient: aGrip => {
  3527         return this.webConsoleClient.longString(aGrip);
  3528       },
  3529       releaseActor: aActor => {
  3530         this.hud._releaseObject(aActor);
  3531       },
  3532       simpleValueEvalMacro: simpleValueEvalMacro,
  3533       overrideValueEvalMacro: overrideValueEvalMacro,
  3534       getterOrSetterEvalMacro: getterOrSetterEvalMacro,
  3535     });
  3537     // Relay events from the VariablesView.
  3538     view.on("fetched", (aEvent, aType, aVar) => {
  3539       this.emit("variablesview-fetched", aVar);
  3540     });
  3542     return view;
  3543   },
  3545   /**
  3546    * Update the variables view.
  3548    * @private
  3549    * @param object aOptions
  3550    *        Options for updating the variables view:
  3551    *        - view: the view you want to update.
  3552    *        - objectActor: the grip of the new ObjectActor you want to show in
  3553    *        the view.
  3554    *        - rawObject: the new raw object you want to show.
  3555    *        - label: the new label for the inspected object.
  3556    */
  3557   _updateVariablesView: function JST__updateVariablesView(aOptions)
  3559     let view = aOptions.view;
  3560     view.empty();
  3562     // We need to avoid pruning the object inspection starting point.
  3563     // That one is pruned when the console message is removed.
  3564     view.controller.releaseActors(aActor => {
  3565       return view._consoleLastObjectActor != aActor;
  3566     });
  3568     if (aOptions.objectActor &&
  3569         (!this.hud.owner._browserConsole ||
  3570          Services.prefs.getBoolPref("devtools.chrome.enabled"))) {
  3571       // Make sure eval works in the correct context.
  3572       view.eval = this._variablesViewEvaluate.bind(this, aOptions);
  3573       view.switch = this._variablesViewSwitch.bind(this, aOptions);
  3574       view.delete = this._variablesViewDelete.bind(this, aOptions);
  3576     else {
  3577       view.eval = null;
  3578       view.switch = null;
  3579       view.delete = null;
  3582     let { variable, expanded } = view.controller.setSingleVariable(aOptions);
  3583     variable.evaluationMacro = simpleValueEvalMacro;
  3585     if (aOptions.objectActor) {
  3586       view._consoleLastObjectActor = aOptions.objectActor.actor;
  3588     else if (aOptions.rawObject) {
  3589       view._consoleLastObjectActor = null;
  3591     else {
  3592       throw new Error("Variables View cannot open without giving it an object " +
  3593                       "display.");
  3596     expanded.then(() => {
  3597       this.emit("variablesview-updated", view, aOptions);
  3598     });
  3599   },
  3601   /**
  3602    * The evaluation function used by the variables view when editing a property
  3603    * value.
  3605    * @private
  3606    * @param object aOptions
  3607    *        The options used for |this._updateVariablesView()|.
  3608    * @param object aVar
  3609    *        The Variable object instance for the edited property.
  3610    * @param string aValue
  3611    *        The value the edited property was changed to.
  3612    */
  3613   _variablesViewEvaluate:
  3614   function JST__variablesViewEvaluate(aOptions, aVar, aValue)
  3616     let updater = this._updateVariablesView.bind(this, aOptions);
  3617     let onEval = this._silentEvalCallback.bind(this, updater);
  3618     let string = aVar.evaluationMacro(aVar, aValue);
  3620     let evalOptions = {
  3621       frame: this.SELECTED_FRAME,
  3622       bindObjectActor: aOptions.objectActor.actor,
  3623     };
  3625     this.requestEvaluation(string, evalOptions).then(onEval, onEval);
  3626   },
  3628   /**
  3629    * The property deletion function used by the variables view when a property
  3630    * is deleted.
  3632    * @private
  3633    * @param object aOptions
  3634    *        The options used for |this._updateVariablesView()|.
  3635    * @param object aVar
  3636    *        The Variable object instance for the deleted property.
  3637    */
  3638   _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar)
  3640     let onEval = this._silentEvalCallback.bind(this, null);
  3642     let evalOptions = {
  3643       frame: this.SELECTED_FRAME,
  3644       bindObjectActor: aOptions.objectActor.actor,
  3645     };
  3647     this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions)
  3648         .then(onEval, onEval);
  3649   },
  3651   /**
  3652    * The property rename function used by the variables view when a property
  3653    * is renamed.
  3655    * @private
  3656    * @param object aOptions
  3657    *        The options used for |this._updateVariablesView()|.
  3658    * @param object aVar
  3659    *        The Variable object instance for the renamed property.
  3660    * @param string aNewName
  3661    *        The new name for the property.
  3662    */
  3663   _variablesViewSwitch:
  3664   function JST__variablesViewSwitch(aOptions, aVar, aNewName)
  3666     let updater = this._updateVariablesView.bind(this, aOptions);
  3667     let onEval = this._silentEvalCallback.bind(this, updater);
  3669     let evalOptions = {
  3670       frame: this.SELECTED_FRAME,
  3671       bindObjectActor: aOptions.objectActor.actor,
  3672     };
  3674     let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]';
  3675     if (newSymbolicName == aVar.symbolicName) {
  3676       return;
  3679     let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" +
  3680                "delete _self" + aVar.symbolicName;
  3682     this.requestEvaluation(code, evalOptions).then(onEval, onEval);
  3683   },
  3685   /**
  3686    * A noop callback for JavaScript evaluation. This method releases any
  3687    * result ObjectActors that come from the server for evaluation requests. This
  3688    * is used for editing, renaming and deleting properties in the variables
  3689    * view.
  3691    * Exceptions are displayed in the output.
  3693    * @private
  3694    * @param function aCallback
  3695    *        Function to invoke once the response is received.
  3696    * @param object aResponse
  3697    *        The response packet received from the server.
  3698    */
  3699   _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse)
  3701     if (aResponse.error) {
  3702       Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" +
  3703                      aResponse.message);
  3705       aCallback && aCallback(aResponse);
  3706       return;
  3709     if (aResponse.exceptionMessage) {
  3710       let message = new Messages.Simple(aResponse.exceptionMessage, {
  3711         category: "output",
  3712         severity: "error",
  3713         timestamp: aResponse.timestamp,
  3714       });
  3715       this.hud.output.addMessage(message);
  3716       message._objectActors = new Set();
  3717       if (WebConsoleUtils.isActorGrip(aResponse.exception)) {
  3718         message._objectActors.add(aResponse.exception.actor);
  3722     let helper = aResponse.helperResult || { type: null };
  3723     let helperGrip = null;
  3724     if (helper.type == "inspectObject") {
  3725       helperGrip = helper.object;
  3728     let grips = [aResponse.result, helperGrip];
  3729     for (let grip of grips) {
  3730       if (WebConsoleUtils.isActorGrip(grip)) {
  3731         this.hud._releaseObject(grip.actor);
  3735     aCallback && aCallback(aResponse);
  3736   },
  3739   /**
  3740    * Clear the Web Console output.
  3742    * This method emits the "messages-cleared" notification.
  3744    * @param boolean aClearStorage
  3745    *        True if you want to clear the console messages storage associated to
  3746    *        this Web Console.
  3747    */
  3748   clearOutput: function JST_clearOutput(aClearStorage)
  3750     let hud = this.hud;
  3751     let outputNode = hud.outputNode;
  3752     let node;
  3753     while ((node = outputNode.firstChild)) {
  3754       hud.removeOutputMessage(node);
  3757     hud.groupDepth = 0;
  3758     hud._outputQueue.forEach(hud._pruneItemFromQueue, hud);
  3759     hud._outputQueue = [];
  3760     hud._networkRequests = {};
  3761     hud._repeatNodes = {};
  3763     if (aClearStorage) {
  3764       this.webConsoleClient.clearMessagesCache();
  3767     this.emit("messages-cleared");
  3768   },
  3770   /**
  3771    * Remove all of the private messages from the Web Console output.
  3773    * This method emits the "private-messages-cleared" notification.
  3774    */
  3775   clearPrivateMessages: function JST_clearPrivateMessages()
  3777     let nodes = this.hud.outputNode.querySelectorAll(".message[private]");
  3778     for (let node of nodes) {
  3779       this.hud.removeOutputMessage(node);
  3781     this.emit("private-messages-cleared");
  3782   },
  3784   /**
  3785    * Updates the size of the input field (command line) to fit its contents.
  3787    * @returns void
  3788    */
  3789   resizeInput: function JST_resizeInput()
  3791     let inputNode = this.inputNode;
  3793     // Reset the height so that scrollHeight will reflect the natural height of
  3794     // the contents of the input field.
  3795     inputNode.style.height = "auto";
  3797     // Now resize the input field to fit its contents.
  3798     let scrollHeight = inputNode.inputField.scrollHeight;
  3799     if (scrollHeight > 0) {
  3800       inputNode.style.height = scrollHeight + "px";
  3802   },
  3804   /**
  3805    * Sets the value of the input field (command line), and resizes the field to
  3806    * fit its contents. This method is preferred over setting "inputNode.value"
  3807    * directly, because it correctly resizes the field.
  3809    * @param string aNewValue
  3810    *        The new value to set.
  3811    * @returns void
  3812    */
  3813   setInputValue: function JST_setInputValue(aNewValue)
  3815     this.inputNode.value = aNewValue;
  3816     this.lastInputValue = aNewValue;
  3817     this.completeNode.value = "";
  3818     this.resizeInput();
  3819     this._inputChanged = true;
  3820   },
  3822   /**
  3823    * The inputNode "input" and "keyup" event handler.
  3824    * @private
  3825    */
  3826   _inputEventHandler: function JST__inputEventHandler()
  3828     if (this.lastInputValue != this.inputNode.value) {
  3829       this.resizeInput();
  3830       this.complete(this.COMPLETE_HINT_ONLY);
  3831       this.lastInputValue = this.inputNode.value;
  3832       this._inputChanged = true;
  3834   },
  3836   /**
  3837    * The window "blur" event handler.
  3838    * @private
  3839    */
  3840   _blurEventHandler: function JST__blurEventHandler()
  3842     if (this.autocompletePopup) {
  3843       this.clearCompletion();
  3845   },
  3847   /**
  3848    * The inputNode "keypress" event handler.
  3850    * @private
  3851    * @param nsIDOMEvent aEvent
  3852    */
  3853   _keyPress: function JST__keyPress(aEvent)
  3855     let inputNode = this.inputNode;
  3856     let inputUpdated = false;
  3858     if (aEvent.ctrlKey) {
  3859       switch (aEvent.charCode) {
  3860         case 101:
  3861           // control-e
  3862           if (Services.appinfo.OS == "WINNT") {
  3863             break;
  3865           let lineEndPos = inputNode.value.length;
  3866           if (this.hasMultilineInput()) {
  3867             // find index of closest newline >= cursor
  3868             for (let i = inputNode.selectionEnd; i<lineEndPos; i++) {
  3869               if (inputNode.value.charAt(i) == "\r" ||
  3870                   inputNode.value.charAt(i) == "\n") {
  3871                 lineEndPos = i;
  3872                 break;
  3876           inputNode.setSelectionRange(lineEndPos, lineEndPos);
  3877           aEvent.preventDefault();
  3878           this.clearCompletion();
  3879           break;
  3881         case 110:
  3882           // Control-N differs from down arrow: it ignores autocomplete state.
  3883           // Note that we preserve the default 'down' navigation within
  3884           // multiline text.
  3885           if (Services.appinfo.OS == "Darwin" &&
  3886               this.canCaretGoNext() &&
  3887               this.historyPeruse(HISTORY_FORWARD)) {
  3888             aEvent.preventDefault();
  3889             // Ctrl-N is also used to focus the Network category button on MacOSX.
  3890             // The preventDefault() call doesn't prevent the focus from moving
  3891             // away from the input.
  3892             inputNode.focus();
  3894           this.clearCompletion();
  3895           break;
  3897         case 112:
  3898           // Control-P differs from up arrow: it ignores autocomplete state.
  3899           // Note that we preserve the default 'up' navigation within
  3900           // multiline text.
  3901           if (Services.appinfo.OS == "Darwin" &&
  3902               this.canCaretGoPrevious() &&
  3903               this.historyPeruse(HISTORY_BACK)) {
  3904             aEvent.preventDefault();
  3905             // Ctrl-P may also be used to focus some category button on MacOSX.
  3906             // The preventDefault() call doesn't prevent the focus from moving
  3907             // away from the input.
  3908             inputNode.focus();
  3910           this.clearCompletion();
  3911           break;
  3912         default:
  3913           break;
  3915       return;
  3917     else if (aEvent.shiftKey &&
  3918         aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
  3919       // shift return
  3920       // TODO: expand the inputNode height by one line
  3921       return;
  3924     switch (aEvent.keyCode) {
  3925       case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
  3926         if (this.autocompletePopup.isOpen) {
  3927           this.clearCompletion();
  3928           aEvent.preventDefault();
  3929           aEvent.stopPropagation();
  3931         else if (this.sidebar) {
  3932           this._sidebarDestroy();
  3933           aEvent.preventDefault();
  3934           aEvent.stopPropagation();
  3936         break;
  3938       case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
  3939         if (this._autocompletePopupNavigated &&
  3940             this.autocompletePopup.isOpen &&
  3941             this.autocompletePopup.selectedIndex > -1) {
  3942           this.acceptProposedCompletion();
  3944         else {
  3945           this.execute();
  3946           this._inputChanged = false;
  3948         aEvent.preventDefault();
  3949         break;
  3951       case Ci.nsIDOMKeyEvent.DOM_VK_UP:
  3952         if (this.autocompletePopup.isOpen) {
  3953           inputUpdated = this.complete(this.COMPLETE_BACKWARD);
  3954           if (inputUpdated) {
  3955             this._autocompletePopupNavigated = true;
  3958         else if (this.canCaretGoPrevious()) {
  3959           inputUpdated = this.historyPeruse(HISTORY_BACK);
  3961         if (inputUpdated) {
  3962           aEvent.preventDefault();
  3964         break;
  3966       case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
  3967         if (this.autocompletePopup.isOpen) {
  3968           inputUpdated = this.complete(this.COMPLETE_FORWARD);
  3969           if (inputUpdated) {
  3970             this._autocompletePopupNavigated = true;
  3973         else if (this.canCaretGoNext()) {
  3974           inputUpdated = this.historyPeruse(HISTORY_FORWARD);
  3976         if (inputUpdated) {
  3977           aEvent.preventDefault();
  3979         break;
  3981       case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
  3982         if (this.autocompletePopup.isOpen) {
  3983           inputUpdated = this.complete(this.COMPLETE_PAGEUP);
  3984           if (inputUpdated) {
  3985             this._autocompletePopupNavigated = true;
  3988         else {
  3989           this.hud.outputNode.parentNode.scrollTop =
  3990             Math.max(0,
  3991               this.hud.outputNode.parentNode.scrollTop -
  3992               this.hud.outputNode.parentNode.clientHeight
  3993             );
  3995         aEvent.preventDefault();
  3996         break;
  3998       case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
  3999         if (this.autocompletePopup.isOpen) {
  4000           inputUpdated = this.complete(this.COMPLETE_PAGEDOWN);
  4001           if (inputUpdated) {
  4002             this._autocompletePopupNavigated = true;
  4005         else {
  4006           this.hud.outputNode.parentNode.scrollTop =
  4007             Math.min(this.hud.outputNode.parentNode.scrollHeight,
  4008               this.hud.outputNode.parentNode.scrollTop +
  4009               this.hud.outputNode.parentNode.clientHeight
  4010             );
  4012         aEvent.preventDefault();
  4013         break;
  4015       case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
  4016       case Ci.nsIDOMKeyEvent.DOM_VK_END:
  4017       case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
  4018         if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
  4019           this.clearCompletion();
  4021         break;
  4023       case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: {
  4024         let cursorAtTheEnd = this.inputNode.selectionStart ==
  4025                              this.inputNode.selectionEnd &&
  4026                              this.inputNode.selectionStart ==
  4027                              this.inputNode.value.length;
  4028         let haveSuggestion = this.autocompletePopup.isOpen ||
  4029                              this.lastCompletion.value;
  4030         let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated;
  4031         if (haveSuggestion && useCompletion &&
  4032             this.complete(this.COMPLETE_HINT_ONLY) &&
  4033             this.lastCompletion.value &&
  4034             this.acceptProposedCompletion()) {
  4035           aEvent.preventDefault();
  4037         if (this.autocompletePopup.isOpen) {
  4038           this.clearCompletion();
  4040         break;
  4042       case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
  4043         // Generate a completion and accept the first proposed value.
  4044         if (this.complete(this.COMPLETE_HINT_ONLY) &&
  4045             this.lastCompletion &&
  4046             this.acceptProposedCompletion()) {
  4047           aEvent.preventDefault();
  4049         else if (this._inputChanged) {
  4050           this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
  4051           aEvent.preventDefault();
  4053         break;
  4054       default:
  4055         break;
  4057   },
  4059   /**
  4060    * The inputNode "focus" event handler.
  4061    * @private
  4062    */
  4063   _focusEventHandler: function JST__focusEventHandler()
  4065     this._inputChanged = false;
  4066   },
  4068   /**
  4069    * Go up/down the history stack of input values.
  4071    * @param number aDirection
  4072    *        History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
  4074    * @returns boolean
  4075    *          True if the input value changed, false otherwise.
  4076    */
  4077   historyPeruse: function JST_historyPeruse(aDirection)
  4079     if (!this.history.length) {
  4080       return false;
  4083     // Up Arrow key
  4084     if (aDirection == HISTORY_BACK) {
  4085       if (this.historyPlaceHolder <= 0) {
  4086         return false;
  4088       let inputVal = this.history[--this.historyPlaceHolder];
  4090       // Save the current input value as the latest entry in history, only if
  4091       // the user is already at the last entry.
  4092       // Note: this code does not store changes to items that are already in
  4093       // history.
  4094       if (this.historyPlaceHolder+1 == this.historyIndex) {
  4095         this.history[this.historyIndex] = this.inputNode.value || "";
  4098       this.setInputValue(inputVal);
  4100     // Down Arrow key
  4101     else if (aDirection == HISTORY_FORWARD) {
  4102       if (this.historyPlaceHolder >= (this.history.length-1)) {
  4103         return false;
  4106       let inputVal = this.history[++this.historyPlaceHolder];
  4107       this.setInputValue(inputVal);
  4109     else {
  4110       throw new Error("Invalid argument 0");
  4113     return true;
  4114   },
  4116   /**
  4117    * Test for multiline input.
  4119    * @return boolean
  4120    *         True if CR or LF found in node value; else false.
  4121    */
  4122   hasMultilineInput: function JST_hasMultilineInput()
  4124     return /[\r\n]/.test(this.inputNode.value);
  4125   },
  4127   /**
  4128    * Check if the caret is at a location that allows selecting the previous item
  4129    * in history when the user presses the Up arrow key.
  4131    * @return boolean
  4132    *         True if the caret is at a location that allows selecting the
  4133    *         previous item in history when the user presses the Up arrow key,
  4134    *         otherwise false.
  4135    */
  4136   canCaretGoPrevious: function JST_canCaretGoPrevious()
  4138     let node = this.inputNode;
  4139     if (node.selectionStart != node.selectionEnd) {
  4140       return false;
  4143     let multiline = /[\r\n]/.test(node.value);
  4144     return node.selectionStart == 0 ? true :
  4145            node.selectionStart == node.value.length && !multiline;
  4146   },
  4148   /**
  4149    * Check if the caret is at a location that allows selecting the next item in
  4150    * history when the user presses the Down arrow key.
  4152    * @return boolean
  4153    *         True if the caret is at a location that allows selecting the next
  4154    *         item in history when the user presses the Down arrow key, otherwise
  4155    *         false.
  4156    */
  4157   canCaretGoNext: function JST_canCaretGoNext()
  4159     let node = this.inputNode;
  4160     if (node.selectionStart != node.selectionEnd) {
  4161       return false;
  4164     let multiline = /[\r\n]/.test(node.value);
  4165     return node.selectionStart == node.value.length ? true :
  4166            node.selectionStart == 0 && !multiline;
  4167   },
  4169   /**
  4170    * Completes the current typed text in the inputNode. Completion is performed
  4171    * only if the selection/cursor is at the end of the string. If no completion
  4172    * is found, the current inputNode value and cursor/selection stay.
  4174    * @param int aType possible values are
  4175    *    - this.COMPLETE_FORWARD: If there is more than one possible completion
  4176    *          and the input value stayed the same compared to the last time this
  4177    *          function was called, then the next completion of all possible
  4178    *          completions is used. If the value changed, then the first possible
  4179    *          completion is used and the selection is set from the current
  4180    *          cursor position to the end of the completed text.
  4181    *          If there is only one possible completion, then this completion
  4182    *          value is used and the cursor is put at the end of the completion.
  4183    *    - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
  4184    *          value stayed the same as the last time the function was called,
  4185    *          then the previous completion of all possible completions is used.
  4186    *    - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first
  4187    *          item.
  4188    *    - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the
  4189    *          last item.
  4190    *    - this.COMPLETE_HINT_ONLY: If there is more than one possible
  4191    *          completion and the input value stayed the same compared to the
  4192    *          last time this function was called, then the same completion is
  4193    *          used again. If there is only one possible completion, then
  4194    *          the inputNode.value is set to this value and the selection is set
  4195    *          from the current cursor position to the end of the completed text.
  4196    * @param function aCallback
  4197    *        Optional function invoked when the autocomplete properties are
  4198    *        updated.
  4199    * @returns boolean true if there existed a completion for the current input,
  4200    *          or false otherwise.
  4201    */
  4202   complete: function JSTF_complete(aType, aCallback)
  4204     let inputNode = this.inputNode;
  4205     let inputValue = inputNode.value;
  4206     let frameActor = this.getFrameActor(this.SELECTED_FRAME);
  4208     // If the inputNode has no value, then don't try to complete on it.
  4209     if (!inputValue) {
  4210       this.clearCompletion();
  4211       aCallback && aCallback(this);
  4212       this.emit("autocomplete-updated");
  4213       return false;
  4216     // Only complete if the selection is empty.
  4217     if (inputNode.selectionStart != inputNode.selectionEnd) {
  4218       this.clearCompletion();
  4219       aCallback && aCallback(this);
  4220       this.emit("autocomplete-updated");
  4221       return false;
  4224     // Update the completion results.
  4225     if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) {
  4226       this._updateCompletionResult(aType, aCallback);
  4227       return false;
  4230     let popup = this.autocompletePopup;
  4231     let accepted = false;
  4233     if (aType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
  4234       this.acceptProposedCompletion();
  4235       accepted = true;
  4237     else if (aType == this.COMPLETE_BACKWARD) {
  4238       popup.selectPreviousItem();
  4240     else if (aType == this.COMPLETE_FORWARD) {
  4241       popup.selectNextItem();
  4243     else if (aType == this.COMPLETE_PAGEUP) {
  4244       popup.selectPreviousPageItem();
  4246     else if (aType == this.COMPLETE_PAGEDOWN) {
  4247       popup.selectNextPageItem();
  4250     aCallback && aCallback(this);
  4251     this.emit("autocomplete-updated");
  4252     return accepted || popup.itemCount > 0;
  4253   },
  4255   /**
  4256    * Update the completion result. This operation is performed asynchronously by
  4257    * fetching updated results from the content process.
  4259    * @private
  4260    * @param int aType
  4261    *        Completion type. See this.complete() for details.
  4262    * @param function [aCallback]
  4263    *        Optional, function to invoke when completion results are received.
  4264    */
  4265   _updateCompletionResult:
  4266   function JST__updateCompletionResult(aType, aCallback)
  4268     let frameActor = this.getFrameActor(this.SELECTED_FRAME);
  4269     if (this.lastCompletion.value == this.inputNode.value && frameActor == this._lastFrameActorId) {
  4270       return;
  4273     let requestId = gSequenceId();
  4274     let cursor = this.inputNode.selectionStart;
  4275     let input = this.inputNode.value.substring(0, cursor);
  4276     let cache = this._autocompleteCache;
  4278     // If the current input starts with the previous input, then we already
  4279     // have a list of suggestions and we just need to filter the cached
  4280     // suggestions. When the current input ends with a non-alphanumeric
  4281     // character we ask the server again for suggestions.
  4283     // Check if last character is non-alphanumeric
  4284     if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
  4285       this._autocompleteQuery = null;
  4286       this._autocompleteCache = null;
  4289     if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
  4290       let filterBy = input;
  4291       // Find the last non-alphanumeric if exists.
  4292       let lastNonAlpha = input.match(/[^a-zA-Z0-9][a-zA-Z0-9]*$/);
  4293       // If input contains non-alphanumerics, use the part after the last one
  4294       // to filter the cache
  4295       if (lastNonAlpha) {
  4296         filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
  4299       let newList = cache.sort().filter(function(l) {
  4300         return l.startsWith(filterBy);
  4301       });
  4303       this.lastCompletion = {
  4304         requestId: null,
  4305         completionType: aType,
  4306         value: null,
  4307       };
  4309       let response = { matches: newList, matchProp: filterBy };
  4310       this._receiveAutocompleteProperties(null, aCallback, response);
  4311       return;
  4314     this._lastFrameActorId = frameActor;
  4316     this.lastCompletion = {
  4317       requestId: requestId,
  4318       completionType: aType,
  4319       value: null,
  4320     };
  4322     let callback = this._receiveAutocompleteProperties.bind(this, requestId,
  4323                                                             aCallback);
  4325     this.webConsoleClient.autocomplete(input, cursor, callback, frameActor);
  4326   },
  4328   /**
  4329    * Handler for the autocompletion results. This method takes
  4330    * the completion result received from the server and updates the UI
  4331    * accordingly.
  4333    * @param number aRequestId
  4334    *        Request ID.
  4335    * @param function [aCallback=null]
  4336    *        Optional, function to invoke when the completion result is received.
  4337    * @param object aMessage
  4338    *        The JSON message which holds the completion results received from
  4339    *        the content process.
  4340    */
  4341   _receiveAutocompleteProperties:
  4342   function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage)
  4344     let inputNode = this.inputNode;
  4345     let inputValue = inputNode.value;
  4346     if (this.lastCompletion.value == inputValue ||
  4347         aRequestId != this.lastCompletion.requestId) {
  4348       return;
  4350     // Cache whatever came from the server if the last char is alphanumeric or '.'
  4351     let cursor = inputNode.selectionStart;
  4352     let inputUntilCursor = inputValue.substring(0, cursor);
  4354     if (aRequestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) {
  4355       this._autocompleteCache = aMessage.matches;
  4356       this._autocompleteQuery = inputUntilCursor;
  4359     let matches = aMessage.matches;
  4360     let lastPart = aMessage.matchProp;
  4361     if (!matches.length) {
  4362       this.clearCompletion();
  4363       aCallback && aCallback(this);
  4364       this.emit("autocomplete-updated");
  4365       return;
  4368     let items = matches.reverse().map(function(aMatch) {
  4369       return { preLabel: lastPart, label: aMatch };
  4370     });
  4372     let popup = this.autocompletePopup;
  4373     popup.setItems(items);
  4375     let completionType = this.lastCompletion.completionType;
  4376     this.lastCompletion = {
  4377       value: inputValue,
  4378       matchProp: lastPart,
  4379     };
  4381     if (items.length > 1 && !popup.isOpen) {
  4382       let str = this.inputNode.value.substr(0, this.inputNode.selectionStart);
  4383       let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length;
  4384       let x = offset * this.hud._inputCharWidth;
  4385       popup.openPopup(inputNode, x + this.hud._chevronWidth);
  4386       this._autocompletePopupNavigated = false;
  4388     else if (items.length < 2 && popup.isOpen) {
  4389       popup.hidePopup();
  4390       this._autocompletePopupNavigated = false;
  4393     if (items.length == 1) {
  4394       popup.selectedIndex = 0;
  4397     this.onAutocompleteSelect();
  4399     if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
  4400       this.acceptProposedCompletion();
  4402     else if (completionType == this.COMPLETE_BACKWARD) {
  4403       popup.selectPreviousItem();
  4405     else if (completionType == this.COMPLETE_FORWARD) {
  4406       popup.selectNextItem();
  4409     aCallback && aCallback(this);
  4410     this.emit("autocomplete-updated");
  4411   },
  4413   onAutocompleteSelect: function JSTF_onAutocompleteSelect()
  4415     // Render the suggestion only if the cursor is at the end of the input.
  4416     if (this.inputNode.selectionStart != this.inputNode.value.length) {
  4417       return;
  4420     let currentItem = this.autocompletePopup.selectedItem;
  4421     if (currentItem && this.lastCompletion.value) {
  4422       let suffix = currentItem.label.substring(this.lastCompletion.
  4423                                                matchProp.length);
  4424       this.updateCompleteNode(suffix);
  4426     else {
  4427       this.updateCompleteNode("");
  4429   },
  4431   /**
  4432    * Clear the current completion information and close the autocomplete popup,
  4433    * if needed.
  4434    */
  4435   clearCompletion: function JSTF_clearCompletion()
  4437     this.autocompletePopup.clearItems();
  4438     this.lastCompletion = { value: null };
  4439     this.updateCompleteNode("");
  4440     if (this.autocompletePopup.isOpen) {
  4441       this.autocompletePopup.hidePopup();
  4442       this._autocompletePopupNavigated = false;
  4444   },
  4446   /**
  4447    * Accept the proposed input completion.
  4449    * @return boolean
  4450    *         True if there was a selected completion item and the input value
  4451    *         was updated, false otherwise.
  4452    */
  4453   acceptProposedCompletion: function JSTF_acceptProposedCompletion()
  4455     let updated = false;
  4457     let currentItem = this.autocompletePopup.selectedItem;
  4458     if (currentItem && this.lastCompletion.value) {
  4459       let suffix = currentItem.label.substring(this.lastCompletion.
  4460                                                matchProp.length);
  4461       let cursor = this.inputNode.selectionStart;
  4462       let value = this.inputNode.value;
  4463       this.setInputValue(value.substr(0, cursor) + suffix + value.substr(cursor));
  4464       let newCursor = cursor + suffix.length;
  4465       this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor;
  4466       updated = true;
  4469     this.clearCompletion();
  4471     return updated;
  4472   },
  4474   /**
  4475    * Update the node that displays the currently selected autocomplete proposal.
  4477    * @param string aSuffix
  4478    *        The proposed suffix for the inputNode value.
  4479    */
  4480   updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
  4482     // completion prefix = input, with non-control chars replaced by spaces
  4483     let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
  4484     this.completeNode.value = prefix + aSuffix;
  4485   },
  4488   /**
  4489    * Destroy the sidebar.
  4490    * @private
  4491    */
  4492   _sidebarDestroy: function JST__sidebarDestroy()
  4494     if (this._variablesView) {
  4495       this._variablesView.controller.releaseActors();
  4496       this._variablesView = null;
  4499     if (this.sidebar) {
  4500       this.sidebar.hide();
  4501       this.sidebar.destroy();
  4502       this.sidebar = null;
  4505     this.emit("sidebar-closed");
  4506   },
  4508   /**
  4509    * Destroy the JSTerm object. Call this method to avoid memory leaks.
  4510    */
  4511   destroy: function JST_destroy()
  4513     this._sidebarDestroy();
  4515     this.clearCompletion();
  4516     this.clearOutput();
  4518     this.autocompletePopup.destroy();
  4519     this.autocompletePopup = null;
  4521     let popup = this.hud.owner.chromeWindow.document
  4522                 .getElementById("webConsole_autocompletePopup");
  4523     if (popup) {
  4524       popup.parentNode.removeChild(popup);
  4527     this.inputNode.removeEventListener("keypress", this._keyPress, false);
  4528     this.inputNode.removeEventListener("input", this._inputEventHandler, false);
  4529     this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
  4530     this.inputNode.removeEventListener("focus", this._focusEventHandler, false);
  4531     this.hud.window.removeEventListener("blur", this._blurEventHandler, false);
  4533     this.hud = null;
  4534   },
  4535 };
  4537 /**
  4538  * Utils: a collection of globally used functions.
  4539  */
  4540 var Utils = {
  4541   /**
  4542    * Scrolls a node so that it's visible in its containing element.
  4544    * @param nsIDOMNode aNode
  4545    *        The node to make visible.
  4546    * @returns void
  4547    */
  4548   scrollToVisible: function Utils_scrollToVisible(aNode)
  4550     aNode.scrollIntoView(false);
  4551   },
  4553   /**
  4554    * Check if the given output node is scrolled to the bottom.
  4556    * @param nsIDOMNode aOutputNode
  4557    * @return boolean
  4558    *         True if the output node is scrolled to the bottom, or false
  4559    *         otherwise.
  4560    */
  4561   isOutputScrolledToBottom: function Utils_isOutputScrolledToBottom(aOutputNode)
  4563     let lastNodeHeight = aOutputNode.lastChild ?
  4564                          aOutputNode.lastChild.clientHeight : 0;
  4565     let scrollNode = aOutputNode.parentNode;
  4566     return scrollNode.scrollTop + scrollNode.clientHeight >=
  4567            scrollNode.scrollHeight - lastNodeHeight / 2;
  4568   },
  4570   /**
  4571    * Determine the category of a given nsIScriptError.
  4573    * @param nsIScriptError aScriptError
  4574    *        The script error you want to determine the category for.
  4575    * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY
  4576    *         Depending on the script error CATEGORY_JS, CATEGORY_CSS, or
  4577    *         CATEGORY_SECURITY can be returned.
  4578    */
  4579   categoryForScriptError: function Utils_categoryForScriptError(aScriptError)
  4581     let category = aScriptError.category;
  4583     if (/^(?:CSS|Layout)\b/.test(category)) {
  4584       return CATEGORY_CSS;
  4587     switch (category) {
  4588       case "Mixed Content Blocker":
  4589       case "Mixed Content Message":
  4590       case "CSP":
  4591       case "Invalid HSTS Headers":
  4592       case "Insecure Password Field":
  4593       case "SSL":
  4594       case "CORS":
  4595         return CATEGORY_SECURITY;
  4597       default:
  4598         return CATEGORY_JS;
  4600   },
  4602   /**
  4603    * Retrieve the limit of messages for a specific category.
  4605    * @param number aCategory
  4606    *        The category of messages you want to retrieve the limit for. See the
  4607    *        CATEGORY_* constants.
  4608    * @return number
  4609    *         The number of messages allowed for the specific category.
  4610    */
  4611   logLimitForCategory: function Utils_logLimitForCategory(aCategory)
  4613     let logLimit = DEFAULT_LOG_LIMIT;
  4615     try {
  4616       let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
  4617       logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
  4618       logLimit = Math.max(logLimit, 1);
  4620     catch (e) { }
  4622     return logLimit;
  4623   },
  4624 };
  4626 ///////////////////////////////////////////////////////////////////////////////
  4627 // CommandController
  4628 ///////////////////////////////////////////////////////////////////////////////
  4630 /**
  4631  * A controller (an instance of nsIController) that makes editing actions
  4632  * behave appropriately in the context of the Web Console.
  4633  */
  4634 function CommandController(aWebConsole)
  4636   this.owner = aWebConsole;
  4639 CommandController.prototype = {
  4640   /**
  4641    * Selects all the text in the HUD output.
  4642    */
  4643   selectAll: function CommandController_selectAll()
  4645     this.owner.output.selectAllMessages();
  4646   },
  4648   /**
  4649    * Open the URL of the selected message in a new tab.
  4650    */
  4651   openURL: function CommandController_openURL()
  4653     this.owner.openSelectedItemInTab();
  4654   },
  4656   copyURL: function CommandController_copyURL()
  4658     this.owner.copySelectedItems({ linkOnly: true, contextmenu: true });
  4659   },
  4661   supportsCommand: function CommandController_supportsCommand(aCommand)
  4663     if (!this.owner || !this.owner.output) {
  4664       return false;
  4666     return this.isCommandEnabled(aCommand);
  4667   },
  4669   isCommandEnabled: function CommandController_isCommandEnabled(aCommand)
  4671     switch (aCommand) {
  4672       case "consoleCmd_openURL":
  4673       case "consoleCmd_copyURL": {
  4674         // Only enable URL-related actions if node is Net Activity.
  4675         let selectedItem = this.owner.output.getSelectedMessages(1)[0] ||
  4676                            this.owner._contextMenuHandler.lastClickedMessage;
  4677         return selectedItem && "url" in selectedItem;
  4679       case "consoleCmd_clearOutput":
  4680       case "cmd_selectAll":
  4681       case "cmd_find":
  4682         return true;
  4683       case "cmd_fontSizeEnlarge":
  4684       case "cmd_fontSizeReduce":
  4685       case "cmd_fontSizeReset":
  4686       case "cmd_close":
  4687         return this.owner.owner._browserConsole;
  4689     return false;
  4690   },
  4692   doCommand: function CommandController_doCommand(aCommand)
  4694     switch (aCommand) {
  4695       case "consoleCmd_openURL":
  4696         this.openURL();
  4697         break;
  4698       case "consoleCmd_copyURL":
  4699         this.copyURL();
  4700         break;
  4701       case "consoleCmd_clearOutput":
  4702         this.owner.jsterm.clearOutput(true);
  4703         break;
  4704       case "cmd_find":
  4705         this.owner.filterBox.focus();
  4706         break;
  4707       case "cmd_selectAll":
  4708         this.selectAll();
  4709         break;
  4710       case "cmd_fontSizeEnlarge":
  4711         this.owner.changeFontSize("+");
  4712         break;
  4713       case "cmd_fontSizeReduce":
  4714         this.owner.changeFontSize("-");
  4715         break;
  4716       case "cmd_fontSizeReset":
  4717         this.owner.changeFontSize("");
  4718         break;
  4719       case "cmd_close":
  4720         this.owner.window.close();
  4721         break;
  4724 };
  4726 ///////////////////////////////////////////////////////////////////////////////
  4727 // Web Console connection proxy
  4728 ///////////////////////////////////////////////////////////////////////////////
  4730 /**
  4731  * The WebConsoleConnectionProxy handles the connection between the Web Console
  4732  * and the application we connect to through the remote debug protocol.
  4734  * @constructor
  4735  * @param object aWebConsole
  4736  *        The Web Console instance that owns this connection proxy.
  4737  * @param RemoteTarget aTarget
  4738  *        The target that the console will connect to.
  4739  */
  4740 function WebConsoleConnectionProxy(aWebConsole, aTarget)
  4742   this.owner = aWebConsole;
  4743   this.target = aTarget;
  4745   this._onPageError = this._onPageError.bind(this);
  4746   this._onLogMessage = this._onLogMessage.bind(this);
  4747   this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
  4748   this._onNetworkEvent = this._onNetworkEvent.bind(this);
  4749   this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
  4750   this._onFileActivity = this._onFileActivity.bind(this);
  4751   this._onReflowActivity = this._onReflowActivity.bind(this);
  4752   this._onTabNavigated = this._onTabNavigated.bind(this);
  4753   this._onAttachConsole = this._onAttachConsole.bind(this);
  4754   this._onCachedMessages = this._onCachedMessages.bind(this);
  4755   this._connectionTimeout = this._connectionTimeout.bind(this);
  4756   this._onLastPrivateContextExited = this._onLastPrivateContextExited.bind(this);
  4759 WebConsoleConnectionProxy.prototype = {
  4760   /**
  4761    * The owning Web Console instance.
  4763    * @see WebConsoleFrame
  4764    * @type object
  4765    */
  4766   owner: null,
  4768   /**
  4769    * The target that the console connects to.
  4770    * @type RemoteTarget
  4771    */
  4772   target: null,
  4774   /**
  4775    * The DebuggerClient object.
  4777    * @see DebuggerClient
  4778    * @type object
  4779    */
  4780   client: null,
  4782   /**
  4783    * The WebConsoleClient object.
  4785    * @see WebConsoleClient
  4786    * @type object
  4787    */
  4788   webConsoleClient: null,
  4790   /**
  4791    * Tells if the connection is established.
  4792    * @type boolean
  4793    */
  4794   connected: false,
  4796   /**
  4797    * Timer used for the connection.
  4798    * @private
  4799    * @type object
  4800    */
  4801   _connectTimer: null,
  4803   _connectDefer: null,
  4804   _disconnecter: null,
  4806   /**
  4807    * The WebConsoleActor ID.
  4809    * @private
  4810    * @type string
  4811    */
  4812   _consoleActor: null,
  4814   /**
  4815    * Tells if the window.console object of the remote web page is the native
  4816    * object or not.
  4817    * @private
  4818    * @type boolean
  4819    */
  4820   _hasNativeConsoleAPI: false,
  4822   /**
  4823    * Initialize a debugger client and connect it to the debugger server.
  4825    * @return object
  4826    *         A promise object that is resolved/rejected based on the success of
  4827    *         the connection initialization.
  4828    */
  4829   connect: function WCCP_connect()
  4831     if (this._connectDefer) {
  4832       return this._connectDefer.promise;
  4835     this._connectDefer = promise.defer();
  4837     let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT);
  4838     this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  4839     this._connectTimer.initWithCallback(this._connectionTimeout,
  4840                                         timeout, Ci.nsITimer.TYPE_ONE_SHOT);
  4842     let connPromise = this._connectDefer.promise;
  4843     connPromise.then(function _onSucess() {
  4844       this._connectTimer.cancel();
  4845       this._connectTimer = null;
  4846     }.bind(this), function _onFailure() {
  4847       this._connectTimer = null;
  4848     }.bind(this));
  4850     let client = this.client = this.target.client;
  4852     client.addListener("logMessage", this._onLogMessage);
  4853     client.addListener("pageError", this._onPageError);
  4854     client.addListener("consoleAPICall", this._onConsoleAPICall);
  4855     client.addListener("networkEvent", this._onNetworkEvent);
  4856     client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
  4857     client.addListener("fileActivity", this._onFileActivity);
  4858     client.addListener("reflowActivity", this._onReflowActivity);
  4859     client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
  4860     this.target.on("will-navigate", this._onTabNavigated);
  4861     this.target.on("navigate", this._onTabNavigated);
  4863     this._consoleActor = this.target.form.consoleActor;
  4864     if (!this.target.chrome) {
  4865       let tab = this.target.form;
  4866       this.owner.onLocationChange(tab.url, tab.title);
  4868     this._attachConsole();
  4870     return connPromise;
  4871   },
  4873   /**
  4874    * Connection timeout handler.
  4875    * @private
  4876    */
  4877   _connectionTimeout: function WCCP__connectionTimeout()
  4879     let error = {
  4880       error: "timeout",
  4881       message: l10n.getStr("connectionTimeout"),
  4882     };
  4884     this._connectDefer.reject(error);
  4885   },
  4887   /**
  4888    * Attach to the Web Console actor.
  4889    * @private
  4890    */
  4891   _attachConsole: function WCCP__attachConsole()
  4893     let listeners = ["PageError", "ConsoleAPI", "NetworkActivity",
  4894                      "FileActivity"];
  4895     this.client.attachConsole(this._consoleActor, listeners,
  4896                               this._onAttachConsole);
  4897   },
  4899   /**
  4900    * The "attachConsole" response handler.
  4902    * @private
  4903    * @param object aResponse
  4904    *        The JSON response object received from the server.
  4905    * @param object aWebConsoleClient
  4906    *        The WebConsoleClient instance for the attached console, for the
  4907    *        specific tab we work with.
  4908    */
  4909   _onAttachConsole: function WCCP__onAttachConsole(aResponse, aWebConsoleClient)
  4911     if (aResponse.error) {
  4912       Cu.reportError("attachConsole failed: " + aResponse.error + " " +
  4913                      aResponse.message);
  4914       this._connectDefer.reject(aResponse);
  4915       return;
  4918     this.webConsoleClient = aWebConsoleClient;
  4920     this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
  4922     let msgs = ["PageError", "ConsoleAPI"];
  4923     this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
  4925     this.owner._updateReflowActivityListener();
  4926   },
  4928   /**
  4929    * The "cachedMessages" response handler.
  4931    * @private
  4932    * @param object aResponse
  4933    *        The JSON response object received from the server.
  4934    */
  4935   _onCachedMessages: function WCCP__onCachedMessages(aResponse)
  4937     if (aResponse.error) {
  4938       Cu.reportError("Web Console getCachedMessages error: " + aResponse.error +
  4939                      " " + aResponse.message);
  4940       this._connectDefer.reject(aResponse);
  4941       return;
  4944     if (!this._connectTimer) {
  4945       // This happens if the promise is rejected (eg. a timeout), but the
  4946       // connection attempt is successful, nonetheless.
  4947       Cu.reportError("Web Console getCachedMessages error: invalid state.");
  4950     this.owner.displayCachedMessages(aResponse.messages);
  4952     if (!this._hasNativeConsoleAPI) {
  4953       this.owner.logWarningAboutReplacedAPI();
  4956     this.connected = true;
  4957     this._connectDefer.resolve(this);
  4958   },
  4960   /**
  4961    * The "pageError" message type handler. We redirect any page errors to the UI
  4962    * for displaying.
  4964    * @private
  4965    * @param string aType
  4966    *        Message type.
  4967    * @param object aPacket
  4968    *        The message received from the server.
  4969    */
  4970   _onPageError: function WCCP__onPageError(aType, aPacket)
  4972     if (this.owner && aPacket.from == this._consoleActor) {
  4973       this.owner.handlePageError(aPacket.pageError);
  4975   },
  4977   /**
  4978    * The "logMessage" message type handler. We redirect any message to the UI
  4979    * for displaying.
  4981    * @private
  4982    * @param string aType
  4983    *        Message type.
  4984    * @param object aPacket
  4985    *        The message received from the server.
  4986    */
  4987   _onLogMessage: function WCCP__onLogMessage(aType, aPacket)
  4989     if (this.owner && aPacket.from == this._consoleActor) {
  4990       this.owner.handleLogMessage(aPacket);
  4992   },
  4994   /**
  4995    * The "consoleAPICall" message type handler. We redirect any message to
  4996    * the UI for displaying.
  4998    * @private
  4999    * @param string aType
  5000    *        Message type.
  5001    * @param object aPacket
  5002    *        The message received from the server.
  5003    */
  5004   _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket)
  5006     if (this.owner && aPacket.from == this._consoleActor) {
  5007       this.owner.handleConsoleAPICall(aPacket.message);
  5009   },
  5011   /**
  5012    * The "networkEvent" message type handler. We redirect any message to
  5013    * the UI for displaying.
  5015    * @private
  5016    * @param string aType
  5017    *        Message type.
  5018    * @param object aPacket
  5019    *        The message received from the server.
  5020    */
  5021   _onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket)
  5023     if (this.owner && aPacket.from == this._consoleActor) {
  5024       this.owner.handleNetworkEvent(aPacket.eventActor);
  5026   },
  5028   /**
  5029    * The "networkEventUpdate" message type handler. We redirect any message to
  5030    * the UI for displaying.
  5032    * @private
  5033    * @param string aType
  5034    *        Message type.
  5035    * @param object aPacket
  5036    *        The message received from the server.
  5037    */
  5038   _onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket)
  5040     if (this.owner) {
  5041       this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType,
  5042                                           aPacket);
  5044   },
  5046   /**
  5047    * The "fileActivity" message type handler. We redirect any message to
  5048    * the UI for displaying.
  5050    * @private
  5051    * @param string aType
  5052    *        Message type.
  5053    * @param object aPacket
  5054    *        The message received from the server.
  5055    */
  5056   _onFileActivity: function WCCP__onFileActivity(aType, aPacket)
  5058     if (this.owner && aPacket.from == this._consoleActor) {
  5059       this.owner.handleFileActivity(aPacket.uri);
  5061   },
  5063   _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket)
  5065     if (this.owner && aPacket.from == this._consoleActor) {
  5066       this.owner.handleReflowActivity(aPacket);
  5068   },
  5070   /**
  5071    * The "lastPrivateContextExited" message type handler. When this message is
  5072    * received the Web Console UI is cleared.
  5074    * @private
  5075    * @param string aType
  5076    *        Message type.
  5077    * @param object aPacket
  5078    *        The message received from the server.
  5079    */
  5080   _onLastPrivateContextExited:
  5081   function WCCP__onLastPrivateContextExited(aType, aPacket)
  5083     if (this.owner && aPacket.from == this._consoleActor) {
  5084       this.owner.jsterm.clearPrivateMessages();
  5086   },
  5088   /**
  5089    * The "will-navigate" and "navigate" event handlers. We redirect any message
  5090    * to the UI for displaying.
  5092    * @private
  5093    * @param string aEvent
  5094    *        Event type.
  5095    * @param object aPacket
  5096    *        The message received from the server.
  5097    */
  5098   _onTabNavigated: function WCCP__onTabNavigated(aEvent, aPacket)
  5100     if (!this.owner) {
  5101       return;
  5104     this.owner.handleTabNavigated(aEvent, aPacket);
  5105   },
  5107   /**
  5108    * Release an object actor.
  5110    * @param string aActor
  5111    *        The actor ID to send the request to.
  5112    */
  5113   releaseActor: function WCCP_releaseActor(aActor)
  5115     if (this.client) {
  5116       this.client.release(aActor);
  5118   },
  5120   /**
  5121    * Disconnect the Web Console from the remote server.
  5123    * @return object
  5124    *         A promise object that is resolved when disconnect completes.
  5125    */
  5126   disconnect: function WCCP_disconnect()
  5128     if (this._disconnecter) {
  5129       return this._disconnecter.promise;
  5132     this._disconnecter = promise.defer();
  5134     if (!this.client) {
  5135       this._disconnecter.resolve(null);
  5136       return this._disconnecter.promise;
  5139     this.client.removeListener("logMessage", this._onLogMessage);
  5140     this.client.removeListener("pageError", this._onPageError);
  5141     this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
  5142     this.client.removeListener("networkEvent", this._onNetworkEvent);
  5143     this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
  5144     this.client.removeListener("fileActivity", this._onFileActivity);
  5145     this.client.removeListener("reflowActivity", this._onReflowActivity);
  5146     this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
  5147     this.target.off("will-navigate", this._onTabNavigated);
  5148     this.target.off("navigate", this._onTabNavigated);
  5150     this.client = null;
  5151     this.webConsoleClient = null;
  5152     this.target = null;
  5153     this.connected = false;
  5154     this.owner = null;
  5155     this._disconnecter.resolve(null);
  5157     return this._disconnecter.promise;
  5158   },
  5159 };
  5161 function gSequenceId()
  5163   return gSequenceId.n++;
  5165 gSequenceId.n = 0;
  5167 ///////////////////////////////////////////////////////////////////////////////
  5168 // Context Menu
  5169 ///////////////////////////////////////////////////////////////////////////////
  5171 /*
  5172  * ConsoleContextMenu this used to handle the visibility of context menu items.
  5174  * @constructor
  5175  * @param object aOwner
  5176  *        The WebConsoleFrame instance that owns this object.
  5177  */
  5178 function ConsoleContextMenu(aOwner)
  5180   this.owner = aOwner;
  5181   this.popup = this.owner.document.getElementById("output-contextmenu");
  5182   this.build = this.build.bind(this);
  5183   this.popup.addEventListener("popupshowing", this.build);
  5186 ConsoleContextMenu.prototype = {
  5187   lastClickedMessage: null,
  5189   /*
  5190    * Handle to show/hide context menu item.
  5191    */
  5192   build: function CCM_build(aEvent)
  5194     let metadata = this.getSelectionMetadata(aEvent.rangeParent);
  5195     for (let element of this.popup.children) {
  5196       element.hidden = this.shouldHideMenuItem(element, metadata);
  5198   },
  5200   /*
  5201    * Get selection information from the view.
  5203    * @param nsIDOMElement aClickElement
  5204    *        The DOM element the user clicked on.
  5205    * @return object
  5206    *         Selection metadata.
  5207    */
  5208   getSelectionMetadata: function CCM_getSelectionMetadata(aClickElement)
  5210     let metadata = {
  5211       selectionType: "",
  5212       selection: new Set(),
  5213     };
  5214     let selectedItems = this.owner.output.getSelectedMessages();
  5215     if (!selectedItems.length) {
  5216       let clickedItem = this.owner.output.getMessageForElement(aClickElement);
  5217       if (clickedItem) {
  5218         this.lastClickedMessage = clickedItem;
  5219         selectedItems = [clickedItem];
  5223     metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single";
  5225     let selection = metadata.selection;
  5226     for (let item of selectedItems) {
  5227       switch (item.category) {
  5228         case CATEGORY_NETWORK:
  5229           selection.add("network");
  5230           break;
  5231         case CATEGORY_CSS:
  5232           selection.add("css");
  5233           break;
  5234         case CATEGORY_JS:
  5235           selection.add("js");
  5236           break;
  5237         case CATEGORY_WEBDEV:
  5238           selection.add("webdev");
  5239           break;
  5243     return metadata;
  5244   },
  5246   /*
  5247    * Determine if an item should be hidden.
  5249    * @param nsIDOMElement aMenuItem
  5250    * @param object aMetadata
  5251    * @return boolean
  5252    *         Whether the given item should be hidden or not.
  5253    */
  5254   shouldHideMenuItem: function CCM_shouldHideMenuItem(aMenuItem, aMetadata)
  5256     let selectionType = aMenuItem.getAttribute("selectiontype");
  5257     if (selectionType && !aMetadata.selectionType == selectionType) {
  5258       return true;
  5261     let selection = aMenuItem.getAttribute("selection");
  5262     if (!selection) {
  5263       return false;
  5266     let shouldHide = true;
  5267     let itemData = selection.split("|");
  5268     for (let type of aMetadata.selection) {
  5269       // check whether this menu item should show or not.
  5270       if (itemData.indexOf(type) !== -1) {
  5271         shouldHide = false;
  5272         break;
  5276     return shouldHide;
  5277   },
  5279   /**
  5280    * Destroy the ConsoleContextMenu object instance.
  5281    */
  5282   destroy: function CCM_destroy()
  5284     this.popup.removeEventListener("popupshowing", this.build);
  5285     this.popup = null;
  5286     this.owner = null;
  5287     this.lastClickedMessage = null;
  5288   },
  5289 };

mercurial