browser/devtools/debugger/debugger-panes.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/debugger/debugger-panes.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,3315 @@
     1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +"use strict";
    1.11 +
    1.12 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
    1.13 +                                  "resource://gre/modules/Task.jsm");
    1.14 +
    1.15 +// Used to detect minification for automatic pretty printing
    1.16 +const SAMPLE_SIZE = 50; // no of lines
    1.17 +const INDENT_COUNT_THRESHOLD = 5; // percentage
    1.18 +const CHARACTER_LIMIT = 250; // line character limit
    1.19 +
    1.20 +// Maps known URLs to friendly source group names
    1.21 +const KNOWN_SOURCE_GROUPS = {
    1.22 +  "Add-on SDK": "resource://gre/modules/commonjs/",
    1.23 +};
    1.24 +
    1.25 +/**
    1.26 + * Functions handling the sources UI.
    1.27 + */
    1.28 +function SourcesView() {
    1.29 +  dumpn("SourcesView was instantiated");
    1.30 +
    1.31 +  this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
    1.32 +  this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this);
    1.33 +  this.toggleBreakpoints = this.toggleBreakpoints.bind(this);
    1.34 +
    1.35 +  this._onEditorLoad = this._onEditorLoad.bind(this);
    1.36 +  this._onEditorUnload = this._onEditorUnload.bind(this);
    1.37 +  this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this);
    1.38 +  this._onSourceSelect = this._onSourceSelect.bind(this);
    1.39 +  this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this);
    1.40 +  this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
    1.41 +  this._onBreakpointClick = this._onBreakpointClick.bind(this);
    1.42 +  this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this);
    1.43 +  this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this);
    1.44 +  this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
    1.45 +  this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
    1.46 +  this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
    1.47 +
    1.48 +  this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this);
    1.49 +}
    1.50 +
    1.51 +SourcesView.prototype = Heritage.extend(WidgetMethods, {
    1.52 +  /**
    1.53 +   * Initialization function, called when the debugger is started.
    1.54 +   */
    1.55 +  initialize: function() {
    1.56 +    dumpn("Initializing the SourcesView");
    1.57 +
    1.58 +    this.widget = new SideMenuWidget(document.getElementById("sources"), {
    1.59 +      showArrows: true
    1.60 +    });
    1.61 +
    1.62 +    // Sort known source groups towards the end of the list
    1.63 +    this.widget.groupSortPredicate = function(a, b) {
    1.64 +      if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
    1.65 +        return a.localeCompare(b);
    1.66 +      }
    1.67 +
    1.68 +      return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
    1.69 +    };
    1.70 +
    1.71 +    this.emptyText = L10N.getStr("noSourcesText");
    1.72 +    this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
    1.73 +
    1.74 +    this._commandset = document.getElementById("debuggerCommands");
    1.75 +    this._popupset = document.getElementById("debuggerPopupset");
    1.76 +    this._cmPopup = document.getElementById("sourceEditorContextMenu");
    1.77 +    this._cbPanel = document.getElementById("conditional-breakpoint-panel");
    1.78 +    this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox");
    1.79 +    this._blackBoxButton = document.getElementById("black-box");
    1.80 +    this._stopBlackBoxButton = document.getElementById("black-boxed-message-button");
    1.81 +    this._prettyPrintButton = document.getElementById("pretty-print");
    1.82 +    this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints");
    1.83 +
    1.84 +    if (Prefs.prettyPrintEnabled) {
    1.85 +      this._prettyPrintButton.removeAttribute("hidden");
    1.86 +    }
    1.87 +
    1.88 +    window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false);
    1.89 +    window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false);
    1.90 +    this.widget.addEventListener("select", this._onSourceSelect, false);
    1.91 +    this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false);
    1.92 +    this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false);
    1.93 +    this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false);
    1.94 +    this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false);
    1.95 +    this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false);
    1.96 +
    1.97 +    this.autoFocusOnSelection = false;
    1.98 +
    1.99 +    // Sort the contents by the displayed label.
   1.100 +    this.sortContents((aFirst, aSecond) => {
   1.101 +      return +(aFirst.attachment.label.toLowerCase() >
   1.102 +               aSecond.attachment.label.toLowerCase());
   1.103 +    });
   1.104 +  },
   1.105 +
   1.106 +  /**
   1.107 +   * Destruction function, called when the debugger is closed.
   1.108 +   */
   1.109 +  destroy: function() {
   1.110 +    dumpn("Destroying the SourcesView");
   1.111 +
   1.112 +    window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false);
   1.113 +    window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false);
   1.114 +    this.widget.removeEventListener("select", this._onSourceSelect, false);
   1.115 +    this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false);
   1.116 +    this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
   1.117 +    this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false);
   1.118 +    this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
   1.119 +    this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);
   1.120 +  },
   1.121 +
   1.122 +  /**
   1.123 +   * Sets the preferred location to be selected in this sources container.
   1.124 +   * @param string aUrl
   1.125 +   */
   1.126 +  set preferredSource(aUrl) {
   1.127 +    this._preferredValue = aUrl;
   1.128 +
   1.129 +    // Selects the element with the specified value in this sources container,
   1.130 +    // if already inserted.
   1.131 +    if (this.containsValue(aUrl)) {
   1.132 +      this.selectedValue = aUrl;
   1.133 +    }
   1.134 +  },
   1.135 +
   1.136 +  /**
   1.137 +   * Adds a source to this sources container.
   1.138 +   *
   1.139 +   * @param object aSource
   1.140 +   *        The source object coming from the active thread.
   1.141 +   * @param object aOptions [optional]
   1.142 +   *        Additional options for adding the source. Supported options:
   1.143 +   *        - staged: true to stage the item to be appended later
   1.144 +   */
   1.145 +  addSource: function(aSource, aOptions = {}) {
   1.146 +    let fullUrl = aSource.url;
   1.147 +    let url = fullUrl.split(" -> ").pop();
   1.148 +    let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url);
   1.149 +    let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url);
   1.150 +    let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl));
   1.151 +
   1.152 +    let contents = document.createElement("label");
   1.153 +    contents.className = "plain dbg-source-item";
   1.154 +    contents.setAttribute("value", label);
   1.155 +    contents.setAttribute("crop", "start");
   1.156 +    contents.setAttribute("flex", "1");
   1.157 +    contents.setAttribute("tooltiptext", unicodeUrl);
   1.158 +
   1.159 +    // Append a source item to this container.
   1.160 +    this.push([contents, fullUrl], {
   1.161 +      staged: aOptions.staged, /* stage the item to be appended later? */
   1.162 +      attachment: {
   1.163 +        label: label,
   1.164 +        group: group,
   1.165 +        checkboxState: !aSource.isBlackBoxed,
   1.166 +        checkboxTooltip: this._blackBoxCheckboxTooltip,
   1.167 +        source: aSource
   1.168 +      }
   1.169 +    });
   1.170 +  },
   1.171 +
   1.172 +  /**
   1.173 +   * Adds a breakpoint to this sources container.
   1.174 +   *
   1.175 +   * @param object aBreakpointData
   1.176 +   *        Information about the breakpoint to be shown.
   1.177 +   *        This object must have the following properties:
   1.178 +   *          - location: the breakpoint's source location and line number
   1.179 +   *          - disabled: the breakpoint's disabled state, boolean
   1.180 +   *          - text: the breakpoint's line text to be displayed
   1.181 +   * @param object aOptions [optional]
   1.182 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.183 +   */
   1.184 +  addBreakpoint: function(aBreakpointData, aOptions = {}) {
   1.185 +    let { location, disabled } = aBreakpointData;
   1.186 +
   1.187 +    // Make sure we're not duplicating anything. If a breakpoint at the
   1.188 +    // specified source url and line already exists, just toggle it.
   1.189 +    if (this.getBreakpoint(location)) {
   1.190 +      this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location);
   1.191 +      return;
   1.192 +    }
   1.193 +
   1.194 +    // Get the source item to which the breakpoint should be attached.
   1.195 +    let sourceItem = this.getItemByValue(location.url);
   1.196 +
   1.197 +    // Create the element node and menu popup for the breakpoint item.
   1.198 +    let breakpointArgs = Heritage.extend(aBreakpointData, aOptions);
   1.199 +    let breakpointView = this._createBreakpointView.call(this, breakpointArgs);
   1.200 +    let contextMenu = this._createContextMenu.call(this, breakpointArgs);
   1.201 +
   1.202 +    // Append a breakpoint child item to the corresponding source item.
   1.203 +    sourceItem.append(breakpointView.container, {
   1.204 +      attachment: Heritage.extend(breakpointArgs, {
   1.205 +        url: location.url,
   1.206 +        line: location.line,
   1.207 +        view: breakpointView,
   1.208 +        popup: contextMenu
   1.209 +      }),
   1.210 +      attributes: [
   1.211 +        ["contextmenu", contextMenu.menupopupId]
   1.212 +      ],
   1.213 +      // Make sure that when the breakpoint item is removed, the corresponding
   1.214 +      // menupopup and commandset are also destroyed.
   1.215 +      finalize: this._onBreakpointRemoved
   1.216 +    });
   1.217 +
   1.218 +    // Highlight the newly appended breakpoint child item if necessary.
   1.219 +    if (aOptions.openPopup || !aOptions.noEditorUpdate) {
   1.220 +      this.highlightBreakpoint(location, aOptions);
   1.221 +    }
   1.222 +  },
   1.223 +
   1.224 +  /**
   1.225 +   * Removes a breakpoint from this sources container.
   1.226 +   * It does not also remove the breakpoint from the controller. Be careful.
   1.227 +   *
   1.228 +   * @param object aLocation
   1.229 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.230 +   */
   1.231 +  removeBreakpoint: function(aLocation) {
   1.232 +    // When a parent source item is removed, all the child breakpoint items are
   1.233 +    // also automagically removed.
   1.234 +    let sourceItem = this.getItemByValue(aLocation.url);
   1.235 +    if (!sourceItem) {
   1.236 +      return;
   1.237 +    }
   1.238 +    let breakpointItem = this.getBreakpoint(aLocation);
   1.239 +    if (!breakpointItem) {
   1.240 +      return;
   1.241 +    }
   1.242 +
   1.243 +    // Clear the breakpoint view.
   1.244 +    sourceItem.remove(breakpointItem);
   1.245 +  },
   1.246 +
   1.247 +  /**
   1.248 +   * Returns the breakpoint at the specified source url and line.
   1.249 +   *
   1.250 +   * @param object aLocation
   1.251 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.252 +   * @return object
   1.253 +   *         The corresponding breakpoint item if found, null otherwise.
   1.254 +   */
   1.255 +  getBreakpoint: function(aLocation) {
   1.256 +    return this.getItemForPredicate(aItem =>
   1.257 +      aItem.attachment.url == aLocation.url &&
   1.258 +      aItem.attachment.line == aLocation.line);
   1.259 +  },
   1.260 +
   1.261 +  /**
   1.262 +   * Returns all breakpoints for all sources.
   1.263 +   *
   1.264 +   * @return array
   1.265 +   *         The breakpoints for all sources if any, an empty array otherwise.
   1.266 +   */
   1.267 +  getAllBreakpoints: function(aStore = []) {
   1.268 +    return this.getOtherBreakpoints(undefined, aStore);
   1.269 +  },
   1.270 +
   1.271 +  /**
   1.272 +   * Returns all breakpoints which are not at the specified source url and line.
   1.273 +   *
   1.274 +   * @param object aLocation [optional]
   1.275 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.276 +   * @param array aStore [optional]
   1.277 +   *        A list in which to store the corresponding breakpoints.
   1.278 +   * @return array
   1.279 +   *         The corresponding breakpoints if found, an empty array otherwise.
   1.280 +   */
   1.281 +  getOtherBreakpoints: function(aLocation = {}, aStore = []) {
   1.282 +    for (let source of this) {
   1.283 +      for (let breakpointItem of source) {
   1.284 +        let { url, line } = breakpointItem.attachment;
   1.285 +        if (url != aLocation.url || line != aLocation.line) {
   1.286 +          aStore.push(breakpointItem);
   1.287 +        }
   1.288 +      }
   1.289 +    }
   1.290 +    return aStore;
   1.291 +  },
   1.292 +
   1.293 +  /**
   1.294 +   * Enables a breakpoint.
   1.295 +   *
   1.296 +   * @param object aLocation
   1.297 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.298 +   * @param object aOptions [optional]
   1.299 +   *        Additional options or flags supported by this operation:
   1.300 +   *          - silent: pass true to not update the checkbox checked state;
   1.301 +   *                    this is usually necessary when the checked state will
   1.302 +   *                    be updated automatically (e.g: on a checkbox click).
   1.303 +   * @return object
   1.304 +   *         A promise that is resolved after the breakpoint is enabled, or
   1.305 +   *         rejected if no breakpoint was found at the specified location.
   1.306 +   */
   1.307 +  enableBreakpoint: function(aLocation, aOptions = {}) {
   1.308 +    let breakpointItem = this.getBreakpoint(aLocation);
   1.309 +    if (!breakpointItem) {
   1.310 +      return promise.reject(new Error("No breakpoint found."));
   1.311 +    }
   1.312 +
   1.313 +    // Breakpoint will now be enabled.
   1.314 +    let attachment = breakpointItem.attachment;
   1.315 +    attachment.disabled = false;
   1.316 +
   1.317 +    // Update the corresponding menu items to reflect the enabled state.
   1.318 +    let prefix = "bp-cMenu-"; // "breakpoints context menu"
   1.319 +    let identifier = DebuggerController.Breakpoints.getIdentifier(attachment);
   1.320 +    let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
   1.321 +    let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
   1.322 +    document.getElementById(enableSelfId).setAttribute("hidden", "true");
   1.323 +    document.getElementById(disableSelfId).removeAttribute("hidden");
   1.324 +
   1.325 +    // Update the breakpoint toggle button checked state.
   1.326 +    this._toggleBreakpointsButton.removeAttribute("checked");
   1.327 +
   1.328 +    // Update the checkbox state if necessary.
   1.329 +    if (!aOptions.silent) {
   1.330 +      attachment.view.checkbox.setAttribute("checked", "true");
   1.331 +    }
   1.332 +
   1.333 +    return DebuggerController.Breakpoints.addBreakpoint(aLocation, {
   1.334 +      // No need to update the pane, since this method is invoked because
   1.335 +      // a breakpoint's view was interacted with.
   1.336 +      noPaneUpdate: true
   1.337 +    });
   1.338 +  },
   1.339 +
   1.340 +  /**
   1.341 +   * Disables a breakpoint.
   1.342 +   *
   1.343 +   * @param object aLocation
   1.344 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.345 +   * @param object aOptions [optional]
   1.346 +   *        Additional options or flags supported by this operation:
   1.347 +   *          - silent: pass true to not update the checkbox checked state;
   1.348 +   *                    this is usually necessary when the checked state will
   1.349 +   *                    be updated automatically (e.g: on a checkbox click).
   1.350 +   * @return object
   1.351 +   *         A promise that is resolved after the breakpoint is disabled, or
   1.352 +   *         rejected if no breakpoint was found at the specified location.
   1.353 +   */
   1.354 +  disableBreakpoint: function(aLocation, aOptions = {}) {
   1.355 +    let breakpointItem = this.getBreakpoint(aLocation);
   1.356 +    if (!breakpointItem) {
   1.357 +      return promise.reject(new Error("No breakpoint found."));
   1.358 +    }
   1.359 +
   1.360 +    // Breakpoint will now be disabled.
   1.361 +    let attachment = breakpointItem.attachment;
   1.362 +    attachment.disabled = true;
   1.363 +
   1.364 +    // Update the corresponding menu items to reflect the disabled state.
   1.365 +    let prefix = "bp-cMenu-"; // "breakpoints context menu"
   1.366 +    let identifier = DebuggerController.Breakpoints.getIdentifier(attachment);
   1.367 +    let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
   1.368 +    let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
   1.369 +    document.getElementById(enableSelfId).removeAttribute("hidden");
   1.370 +    document.getElementById(disableSelfId).setAttribute("hidden", "true");
   1.371 +
   1.372 +    // Update the checkbox state if necessary.
   1.373 +    if (!aOptions.silent) {
   1.374 +      attachment.view.checkbox.removeAttribute("checked");
   1.375 +    }
   1.376 +
   1.377 +    return DebuggerController.Breakpoints.removeBreakpoint(aLocation, {
   1.378 +      // No need to update this pane, since this method is invoked because
   1.379 +      // a breakpoint's view was interacted with.
   1.380 +      noPaneUpdate: true,
   1.381 +      // Mark this breakpoint as being "disabled", not completely removed.
   1.382 +      // This makes sure it will not be forgotten across target navigations.
   1.383 +      rememberDisabled: true
   1.384 +    });
   1.385 +  },
   1.386 +
   1.387 +  /**
   1.388 +   * Highlights a breakpoint in this sources container.
   1.389 +   *
   1.390 +   * @param object aLocation
   1.391 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.392 +   * @param object aOptions [optional]
   1.393 +   *        An object containing some of the following boolean properties:
   1.394 +   *          - openPopup: tells if the expression popup should be shown.
   1.395 +   *          - noEditorUpdate: tells if you want to skip editor updates.
   1.396 +   */
   1.397 +  highlightBreakpoint: function(aLocation, aOptions = {}) {
   1.398 +    let breakpointItem = this.getBreakpoint(aLocation);
   1.399 +    if (!breakpointItem) {
   1.400 +      return;
   1.401 +    }
   1.402 +
   1.403 +    // Breakpoint will now be selected.
   1.404 +    this._selectBreakpoint(breakpointItem);
   1.405 +
   1.406 +    // Update the editor location if necessary.
   1.407 +    if (!aOptions.noEditorUpdate) {
   1.408 +      DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true });
   1.409 +    }
   1.410 +
   1.411 +    // If the breakpoint requires a new conditional expression, display
   1.412 +    // the panel to input the corresponding expression.
   1.413 +    if (aOptions.openPopup) {
   1.414 +      this._openConditionalPopup();
   1.415 +    } else {
   1.416 +      this._hideConditionalPopup();
   1.417 +    }
   1.418 +  },
   1.419 +
   1.420 +  /**
   1.421 +   * Unhighlights the current breakpoint in this sources container.
   1.422 +   */
   1.423 +  unhighlightBreakpoint: function() {
   1.424 +    this._unselectBreakpoint();
   1.425 +    this._hideConditionalPopup();
   1.426 +  },
   1.427 +
   1.428 +  /**
   1.429 +   * Update the checked/unchecked and enabled/disabled states of the buttons in
   1.430 +   * the sources toolbar based on the currently selected source's state.
   1.431 +   */
   1.432 +  updateToolbarButtonsState: function() {
   1.433 +    const { source } = this.selectedItem.attachment;
   1.434 +    const sourceClient = gThreadClient.source(source);
   1.435 +
   1.436 +    if (sourceClient.isBlackBoxed) {
   1.437 +      this._prettyPrintButton.setAttribute("disabled", true);
   1.438 +      this._blackBoxButton.setAttribute("checked", true);
   1.439 +    } else {
   1.440 +      this._prettyPrintButton.removeAttribute("disabled");
   1.441 +      this._blackBoxButton.removeAttribute("checked");
   1.442 +    }
   1.443 +
   1.444 +    if (sourceClient.isPrettyPrinted) {
   1.445 +      this._prettyPrintButton.setAttribute("checked", true);
   1.446 +    } else {
   1.447 +      this._prettyPrintButton.removeAttribute("checked");
   1.448 +    }
   1.449 +  },
   1.450 +
   1.451 +  /**
   1.452 +   * Toggle the pretty printing of the selected source.
   1.453 +   */
   1.454 +  togglePrettyPrint: function() {
   1.455 +    if (this._prettyPrintButton.hasAttribute("disabled")) {
   1.456 +      return;
   1.457 +    }
   1.458 +
   1.459 +    const resetEditor = ([{ url }]) => {
   1.460 +      // Only set the text when the source is still selected.
   1.461 +      if (url == this.selectedValue) {
   1.462 +        DebuggerView.setEditorLocation(url, 0, { force: true });
   1.463 +      }
   1.464 +    };
   1.465 +
   1.466 +    const printError = ([{ url }, error]) => {
   1.467 +      DevToolsUtils.reportException("togglePrettyPrint", error);
   1.468 +    };
   1.469 +
   1.470 +    DebuggerView.showProgressBar();
   1.471 +    const { source } = this.selectedItem.attachment;
   1.472 +    const sourceClient = gThreadClient.source(source);
   1.473 +    const shouldPrettyPrint = !sourceClient.isPrettyPrinted;
   1.474 +
   1.475 +    if (shouldPrettyPrint) {
   1.476 +      this._prettyPrintButton.setAttribute("checked", true);
   1.477 +    } else {
   1.478 +      this._prettyPrintButton.removeAttribute("checked");
   1.479 +    }
   1.480 +
   1.481 +    DebuggerController.SourceScripts.togglePrettyPrint(source)
   1.482 +      .then(resetEditor, printError)
   1.483 +      .then(DebuggerView.showEditor)
   1.484 +      .then(this.updateToolbarButtonsState);
   1.485 +  },
   1.486 +
   1.487 +  /**
   1.488 +   * Toggle the black boxed state of the selected source.
   1.489 +   */
   1.490 +  toggleBlackBoxing: function() {
   1.491 +    const { source } = this.selectedItem.attachment;
   1.492 +    const sourceClient = gThreadClient.source(source);
   1.493 +    const shouldBlackBox = !sourceClient.isBlackBoxed;
   1.494 +
   1.495 +    // Be optimistic that the (un-)black boxing will succeed, so enable/disable
   1.496 +    // the pretty print button and check/uncheck the black box button
   1.497 +    // immediately. Then, once we actually get the results from the server, make
   1.498 +    // sure that it is in the correct state again by calling
   1.499 +    // `updateToolbarButtonsState`.
   1.500 +
   1.501 +    if (shouldBlackBox) {
   1.502 +      this._prettyPrintButton.setAttribute("disabled", true);
   1.503 +      this._blackBoxButton.setAttribute("checked", true);
   1.504 +    } else {
   1.505 +      this._prettyPrintButton.removeAttribute("disabled");
   1.506 +      this._blackBoxButton.removeAttribute("checked");
   1.507 +    }
   1.508 +
   1.509 +    DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox)
   1.510 +      .then(this.updateToolbarButtonsState,
   1.511 +            this.updateToolbarButtonsState);
   1.512 +  },
   1.513 +
   1.514 +  /**
   1.515 +   * Toggles all breakpoints enabled/disabled.
   1.516 +   */
   1.517 +  toggleBreakpoints: function() {
   1.518 +    let breakpoints = this.getAllBreakpoints();
   1.519 +    let hasBreakpoints = breakpoints.length > 0;
   1.520 +    let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled);
   1.521 +
   1.522 +    if (hasBreakpoints && hasEnabledBreakpoints) {
   1.523 +      this._toggleBreakpointsButton.setAttribute("checked", true);
   1.524 +      this._onDisableAll();
   1.525 +    } else {
   1.526 +      this._toggleBreakpointsButton.removeAttribute("checked");
   1.527 +      this._onEnableAll();
   1.528 +    }
   1.529 +  },
   1.530 +
   1.531 +  /**
   1.532 +   * Marks a breakpoint as selected in this sources container.
   1.533 +   *
   1.534 +   * @param object aItem
   1.535 +   *        The breakpoint item to select.
   1.536 +   */
   1.537 +  _selectBreakpoint: function(aItem) {
   1.538 +    if (this._selectedBreakpointItem == aItem) {
   1.539 +      return;
   1.540 +    }
   1.541 +    this._unselectBreakpoint();
   1.542 +    this._selectedBreakpointItem = aItem;
   1.543 +    this._selectedBreakpointItem.target.classList.add("selected");
   1.544 +
   1.545 +    // Ensure the currently selected breakpoint is visible.
   1.546 +    this.widget.ensureElementIsVisible(aItem.target);
   1.547 +  },
   1.548 +
   1.549 +  /**
   1.550 +   * Marks the current breakpoint as unselected in this sources container.
   1.551 +   */
   1.552 +  _unselectBreakpoint: function() {
   1.553 +    if (!this._selectedBreakpointItem) {
   1.554 +      return;
   1.555 +    }
   1.556 +    this._selectedBreakpointItem.target.classList.remove("selected");
   1.557 +    this._selectedBreakpointItem = null;
   1.558 +  },
   1.559 +
   1.560 +  /**
   1.561 +   * Opens a conditional breakpoint's expression input popup.
   1.562 +   */
   1.563 +  _openConditionalPopup: function() {
   1.564 +    let breakpointItem = this._selectedBreakpointItem;
   1.565 +    let attachment = breakpointItem.attachment;
   1.566 +    // Check if this is an enabled conditional breakpoint, and if so,
   1.567 +    // retrieve the current conditional epression.
   1.568 +    let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
   1.569 +    if (breakpointPromise) {
   1.570 +      breakpointPromise.then(aBreakpointClient => {
   1.571 +        let isConditionalBreakpoint = aBreakpointClient.hasCondition();
   1.572 +        let condition = aBreakpointClient.getCondition();
   1.573 +        doOpen.call(this, isConditionalBreakpoint ? condition : "")
   1.574 +      });
   1.575 +    } else {
   1.576 +      doOpen.call(this, "")
   1.577 +    }
   1.578 +
   1.579 +    function doOpen(aConditionalExpression) {
   1.580 +      // Update the conditional expression textbox. If no expression was
   1.581 +      // previously set, revert to using an empty string by default.
   1.582 +      this._cbTextbox.value = aConditionalExpression;
   1.583 +
   1.584 +      // Show the conditional expression panel. The popup arrow should be pointing
   1.585 +      // at the line number node in the breakpoint item view.
   1.586 +      this._cbPanel.hidden = false;
   1.587 +      this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber,
   1.588 +        BREAKPOINT_CONDITIONAL_POPUP_POSITION,
   1.589 +        BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
   1.590 +        BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
   1.591 +    }
   1.592 +  },
   1.593 +
   1.594 +  /**
   1.595 +   * Hides a conditional breakpoint's expression input popup.
   1.596 +   */
   1.597 +  _hideConditionalPopup: function() {
   1.598 +    this._cbPanel.hidden = true;
   1.599 +
   1.600 +    // Sometimes this._cbPanel doesn't have hidePopup method which doesn't
   1.601 +    // break anything but simply outputs an exception to the console.
   1.602 +    if (this._cbPanel.hidePopup) {
   1.603 +      this._cbPanel.hidePopup();
   1.604 +    }
   1.605 +  },
   1.606 +
   1.607 +  /**
   1.608 +   * Customization function for creating a breakpoint item's UI.
   1.609 +   *
   1.610 +   * @param object aOptions
   1.611 +   *        A couple of options or flags supported by this operation:
   1.612 +   *          - location: the breakpoint's source location and line number
   1.613 +   *          - disabled: the breakpoint's disabled state, boolean
   1.614 +   *          - text: the breakpoint's line text to be displayed
   1.615 +   * @return object
   1.616 +   *         An object containing the breakpoint container, checkbox,
   1.617 +   *         line number and line text nodes.
   1.618 +   */
   1.619 +  _createBreakpointView: function(aOptions) {
   1.620 +    let { location, disabled, text } = aOptions;
   1.621 +    let identifier = DebuggerController.Breakpoints.getIdentifier(location);
   1.622 +
   1.623 +    let checkbox = document.createElement("checkbox");
   1.624 +    checkbox.setAttribute("checked", !disabled);
   1.625 +    checkbox.className = "dbg-breakpoint-checkbox";
   1.626 +
   1.627 +    let lineNumberNode = document.createElement("label");
   1.628 +    lineNumberNode.className = "plain dbg-breakpoint-line";
   1.629 +    lineNumberNode.setAttribute("value", location.line);
   1.630 +
   1.631 +    let lineTextNode = document.createElement("label");
   1.632 +    lineTextNode.className = "plain dbg-breakpoint-text";
   1.633 +    lineTextNode.setAttribute("value", text);
   1.634 +    lineTextNode.setAttribute("crop", "end");
   1.635 +    lineTextNode.setAttribute("flex", "1");
   1.636 +
   1.637 +    let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH);
   1.638 +    lineTextNode.setAttribute("tooltiptext", tooltip);
   1.639 +
   1.640 +    let container = document.createElement("hbox");
   1.641 +    container.id = "breakpoint-" + identifier;
   1.642 +    container.className = "dbg-breakpoint side-menu-widget-item-other";
   1.643 +    container.classList.add("devtools-monospace");
   1.644 +    container.setAttribute("align", "center");
   1.645 +    container.setAttribute("flex", "1");
   1.646 +
   1.647 +    container.addEventListener("click", this._onBreakpointClick, false);
   1.648 +    checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false);
   1.649 +
   1.650 +    container.appendChild(checkbox);
   1.651 +    container.appendChild(lineNumberNode);
   1.652 +    container.appendChild(lineTextNode);
   1.653 +
   1.654 +    return {
   1.655 +      container: container,
   1.656 +      checkbox: checkbox,
   1.657 +      lineNumber: lineNumberNode,
   1.658 +      lineText: lineTextNode
   1.659 +    };
   1.660 +  },
   1.661 +
   1.662 +  /**
   1.663 +   * Creates a context menu for a breakpoint element.
   1.664 +   *
   1.665 +   * @param object aOptions
   1.666 +   *        A couple of options or flags supported by this operation:
   1.667 +   *          - location: the breakpoint's source location and line number
   1.668 +   *          - disabled: the breakpoint's disabled state, boolean
   1.669 +   * @return object
   1.670 +   *         An object containing the breakpoint commandset and menu popup ids.
   1.671 +   */
   1.672 +  _createContextMenu: function(aOptions) {
   1.673 +    let { location, disabled } = aOptions;
   1.674 +    let identifier = DebuggerController.Breakpoints.getIdentifier(location);
   1.675 +
   1.676 +    let commandset = document.createElement("commandset");
   1.677 +    let menupopup = document.createElement("menupopup");
   1.678 +    commandset.id = "bp-cSet-" + identifier;
   1.679 +    menupopup.id = "bp-mPop-" + identifier;
   1.680 +
   1.681 +    createMenuItem.call(this, "enableSelf", !disabled);
   1.682 +    createMenuItem.call(this, "disableSelf", disabled);
   1.683 +    createMenuItem.call(this, "deleteSelf");
   1.684 +    createMenuSeparator();
   1.685 +    createMenuItem.call(this, "setConditional");
   1.686 +    createMenuSeparator();
   1.687 +    createMenuItem.call(this, "enableOthers");
   1.688 +    createMenuItem.call(this, "disableOthers");
   1.689 +    createMenuItem.call(this, "deleteOthers");
   1.690 +    createMenuSeparator();
   1.691 +    createMenuItem.call(this, "enableAll");
   1.692 +    createMenuItem.call(this, "disableAll");
   1.693 +    createMenuSeparator();
   1.694 +    createMenuItem.call(this, "deleteAll");
   1.695 +
   1.696 +    this._popupset.appendChild(menupopup);
   1.697 +    this._commandset.appendChild(commandset);
   1.698 +
   1.699 +    return {
   1.700 +      commandsetId: commandset.id,
   1.701 +      menupopupId: menupopup.id
   1.702 +    };
   1.703 +
   1.704 +    /**
   1.705 +     * Creates a menu item specified by a name with the appropriate attributes
   1.706 +     * (label and handler).
   1.707 +     *
   1.708 +     * @param string aName
   1.709 +     *        A global identifier for the menu item.
   1.710 +     * @param boolean aHiddenFlag
   1.711 +     *        True if this menuitem should be hidden.
   1.712 +     */
   1.713 +    function createMenuItem(aName, aHiddenFlag) {
   1.714 +      let menuitem = document.createElement("menuitem");
   1.715 +      let command = document.createElement("command");
   1.716 +
   1.717 +      let prefix = "bp-cMenu-"; // "breakpoints context menu"
   1.718 +      let commandId = prefix + aName + "-" + identifier + "-command";
   1.719 +      let menuitemId = prefix + aName + "-" + identifier + "-menuitem";
   1.720 +
   1.721 +      let label = L10N.getStr("breakpointMenuItem." + aName);
   1.722 +      let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1);
   1.723 +
   1.724 +      command.id = commandId;
   1.725 +      command.setAttribute("label", label);
   1.726 +      command.addEventListener("command", () => this[func](location), false);
   1.727 +
   1.728 +      menuitem.id = menuitemId;
   1.729 +      menuitem.setAttribute("command", commandId);
   1.730 +      aHiddenFlag && menuitem.setAttribute("hidden", "true");
   1.731 +
   1.732 +      commandset.appendChild(command);
   1.733 +      menupopup.appendChild(menuitem);
   1.734 +    }
   1.735 +
   1.736 +    /**
   1.737 +     * Creates a simple menu separator element and appends it to the current
   1.738 +     * menupopup hierarchy.
   1.739 +     */
   1.740 +    function createMenuSeparator() {
   1.741 +      let menuseparator = document.createElement("menuseparator");
   1.742 +      menupopup.appendChild(menuseparator);
   1.743 +    }
   1.744 +  },
   1.745 +
   1.746 +  /**
   1.747 +   * Function called each time a breakpoint item is removed.
   1.748 +   *
   1.749 +   * @param object aItem
   1.750 +   *        The corresponding item.
   1.751 +   */
   1.752 +  _onBreakpointRemoved: function(aItem) {
   1.753 +    dumpn("Finalizing breakpoint item: " + aItem);
   1.754 +
   1.755 +    // Destroy the context menu for the breakpoint.
   1.756 +    let contextMenu = aItem.attachment.popup;
   1.757 +    document.getElementById(contextMenu.commandsetId).remove();
   1.758 +    document.getElementById(contextMenu.menupopupId).remove();
   1.759 +
   1.760 +    // Clear the breakpoint selection.
   1.761 +    if (this._selectedBreakpointItem == aItem) {
   1.762 +      this._selectedBreakpointItem = null;
   1.763 +    }
   1.764 +  },
   1.765 +
   1.766 +  /**
   1.767 +   * The load listener for the source editor.
   1.768 +   */
   1.769 +  _onEditorLoad: function(aName, aEditor) {
   1.770 +    aEditor.on("cursorActivity", this._onEditorCursorActivity);
   1.771 +  },
   1.772 +
   1.773 +  /**
   1.774 +   * The unload listener for the source editor.
   1.775 +   */
   1.776 +  _onEditorUnload: function(aName, aEditor) {
   1.777 +    aEditor.off("cursorActivity", this._onEditorCursorActivity);
   1.778 +  },
   1.779 +
   1.780 +  /**
   1.781 +   * The selection listener for the source editor.
   1.782 +   */
   1.783 +  _onEditorCursorActivity: function(e) {
   1.784 +    let editor = DebuggerView.editor;
   1.785 +    let start  = editor.getCursor("start").line + 1;
   1.786 +    let end    = editor.getCursor().line + 1;
   1.787 +    let url    = this.selectedValue;
   1.788 +
   1.789 +    let location = { url: url, line: start };
   1.790 +
   1.791 +    if (this.getBreakpoint(location) && start == end) {
   1.792 +      this.highlightBreakpoint(location, { noEditorUpdate: true });
   1.793 +    } else {
   1.794 +      this.unhighlightBreakpoint();
   1.795 +    }
   1.796 +  },
   1.797 +
   1.798 +  /**
   1.799 +   * The select listener for the sources container.
   1.800 +   */
   1.801 +  _onSourceSelect: function({ detail: sourceItem }) {
   1.802 +    if (!sourceItem) {
   1.803 +      return;
   1.804 +    }
   1.805 +    const { source } = sourceItem.attachment;
   1.806 +    const sourceClient = gThreadClient.source(source);
   1.807 +
   1.808 +    // The container is not empty and an actual item was selected.
   1.809 +    DebuggerView.setEditorLocation(sourceItem.value);
   1.810 +
   1.811 +    if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) {
   1.812 +      DebuggerController.SourceScripts.getText(source).then(([, aText]) => {
   1.813 +        if (SourceUtils.isMinified(sourceClient, aText)) {
   1.814 +          this.togglePrettyPrint();
   1.815 +        }
   1.816 +      }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e));
   1.817 +    }
   1.818 +
   1.819 +    // Set window title. No need to split the url by " -> " here, because it was
   1.820 +    // already sanitized when the source was added.
   1.821 +    document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value);
   1.822 +
   1.823 +    DebuggerView.maybeShowBlackBoxMessage();
   1.824 +    this.updateToolbarButtonsState();
   1.825 +  },
   1.826 +
   1.827 +  /**
   1.828 +   * The click listener for the "stop black boxing" button.
   1.829 +   */
   1.830 +  _onStopBlackBoxing: function() {
   1.831 +    const { source } = this.selectedItem.attachment;
   1.832 +
   1.833 +    DebuggerController.SourceScripts.setBlackBoxing(source, false)
   1.834 +      .then(this.updateToolbarButtonsState,
   1.835 +            this.updateToolbarButtonsState);
   1.836 +  },
   1.837 +
   1.838 +  /**
   1.839 +   * The click listener for a breakpoint container.
   1.840 +   */
   1.841 +  _onBreakpointClick: function(e) {
   1.842 +    let sourceItem = this.getItemForElement(e.target);
   1.843 +    let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
   1.844 +    let attachment = breakpointItem.attachment;
   1.845 +
   1.846 +    // Check if this is an enabled conditional breakpoint.
   1.847 +    let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
   1.848 +    if (breakpointPromise) {
   1.849 +      breakpointPromise.then(aBreakpointClient => {
   1.850 +        doHighlight.call(this, aBreakpointClient.hasCondition());
   1.851 +      });
   1.852 +    } else {
   1.853 +      doHighlight.call(this, false);
   1.854 +    }
   1.855 +
   1.856 +    function doHighlight(aConditionalBreakpointFlag) {
   1.857 +      // Highlight the breakpoint in this pane and in the editor.
   1.858 +      this.highlightBreakpoint(attachment, {
   1.859 +        // Don't show the conditional expression popup if this is not a
   1.860 +        // conditional breakpoint, or the right mouse button was pressed (to
   1.861 +        // avoid clashing the popup with the context menu).
   1.862 +        openPopup: aConditionalBreakpointFlag && e.button == 0
   1.863 +      });
   1.864 +    }
   1.865 +  },
   1.866 +
   1.867 +  /**
   1.868 +   * The click listener for a breakpoint checkbox.
   1.869 +   */
   1.870 +  _onBreakpointCheckboxClick: function(e) {
   1.871 +    let sourceItem = this.getItemForElement(e.target);
   1.872 +    let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
   1.873 +    let attachment = breakpointItem.attachment;
   1.874 +
   1.875 +    // Toggle the breakpoint enabled or disabled.
   1.876 +    this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, {
   1.877 +      // Do this silently (don't update the checkbox checked state), since
   1.878 +      // this listener is triggered because a checkbox was already clicked.
   1.879 +      silent: true
   1.880 +    });
   1.881 +
   1.882 +    // Don't update the editor location (avoid propagating into _onBreakpointClick).
   1.883 +    e.preventDefault();
   1.884 +    e.stopPropagation();
   1.885 +  },
   1.886 +
   1.887 +  /**
   1.888 +   * The popup showing listener for the breakpoints conditional expression panel.
   1.889 +   */
   1.890 +  _onConditionalPopupShowing: function() {
   1.891 +    this._conditionalPopupVisible = true; // Used in tests.
   1.892 +    window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
   1.893 +  },
   1.894 +
   1.895 +  /**
   1.896 +   * The popup shown listener for the breakpoints conditional expression panel.
   1.897 +   */
   1.898 +  _onConditionalPopupShown: function() {
   1.899 +    this._cbTextbox.focus();
   1.900 +    this._cbTextbox.select();
   1.901 +  },
   1.902 +
   1.903 +  /**
   1.904 +   * The popup hiding listener for the breakpoints conditional expression panel.
   1.905 +   */
   1.906 +  _onConditionalPopupHiding: Task.async(function*() {
   1.907 +    this._conditionalPopupVisible = false; // Used in tests.
   1.908 +    let breakpointItem = this._selectedBreakpointItem;
   1.909 +    let attachment = breakpointItem.attachment;
   1.910 +
   1.911 +    // Check if this is an enabled conditional breakpoint, and if so,
   1.912 +    // save the current conditional epression.
   1.913 +    let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
   1.914 +    if (breakpointPromise) {
   1.915 +      let breakpointClient = yield breakpointPromise;
   1.916 +      yield DebuggerController.Breakpoints.updateCondition(
   1.917 +        breakpointClient.location,
   1.918 +        this._cbTextbox.value
   1.919 +      );
   1.920 +    }
   1.921 +
   1.922 +    window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
   1.923 +  }),
   1.924 +
   1.925 +  /**
   1.926 +   * The keypress listener for the breakpoints conditional expression textbox.
   1.927 +   */
   1.928 +  _onConditionalTextboxKeyPress: function(e) {
   1.929 +    if (e.keyCode == e.DOM_VK_RETURN) {
   1.930 +      this._hideConditionalPopup();
   1.931 +    }
   1.932 +  },
   1.933 +
   1.934 +  /**
   1.935 +   * Called when the add breakpoint key sequence was pressed.
   1.936 +   */
   1.937 +  _onCmdAddBreakpoint: function(e) {
   1.938 +    let url = DebuggerView.Sources.selectedValue;
   1.939 +    let line = DebuggerView.editor.getCursor().line + 1;
   1.940 +    let location = { url: url, line: line };
   1.941 +    let breakpointItem = this.getBreakpoint(location);
   1.942 +
   1.943 +    // If a breakpoint already existed, remove it now.
   1.944 +    if (breakpointItem) {
   1.945 +      DebuggerController.Breakpoints.removeBreakpoint(location);
   1.946 +    }
   1.947 +    // No breakpoint existed at the required location, add one now.
   1.948 +    else {
   1.949 +      DebuggerController.Breakpoints.addBreakpoint(location);
   1.950 +    }
   1.951 +  },
   1.952 +
   1.953 +  /**
   1.954 +   * Called when the add conditional breakpoint key sequence was pressed.
   1.955 +   */
   1.956 +  _onCmdAddConditionalBreakpoint: function() {
   1.957 +    let url =  DebuggerView.Sources.selectedValue;
   1.958 +    let line = DebuggerView.editor.getCursor().line + 1;
   1.959 +    let location = { url: url, line: line };
   1.960 +    let breakpointItem = this.getBreakpoint(location);
   1.961 +
   1.962 +    // If a breakpoint already existed or wasn't a conditional, morph it now.
   1.963 +    if (breakpointItem) {
   1.964 +      this.highlightBreakpoint(location, { openPopup: true });
   1.965 +    }
   1.966 +    // No breakpoint existed at the required location, add one now.
   1.967 +    else {
   1.968 +      DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true });
   1.969 +    }
   1.970 +  },
   1.971 +
   1.972 +  /**
   1.973 +   * Function invoked on the "setConditional" menuitem command.
   1.974 +   *
   1.975 +   * @param object aLocation
   1.976 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.977 +   */
   1.978 +  _onSetConditional: function(aLocation) {
   1.979 +    // Highlight the breakpoint and show a conditional expression popup.
   1.980 +    this.highlightBreakpoint(aLocation, { openPopup: true });
   1.981 +  },
   1.982 +
   1.983 +  /**
   1.984 +   * Function invoked on the "enableSelf" menuitem command.
   1.985 +   *
   1.986 +   * @param object aLocation
   1.987 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.988 +   */
   1.989 +  _onEnableSelf: function(aLocation) {
   1.990 +    // Enable the breakpoint, in this container and the controller store.
   1.991 +    this.enableBreakpoint(aLocation);
   1.992 +  },
   1.993 +
   1.994 +  /**
   1.995 +   * Function invoked on the "disableSelf" menuitem command.
   1.996 +   *
   1.997 +   * @param object aLocation
   1.998 +   *        @see DebuggerController.Breakpoints.addBreakpoint
   1.999 +   */
  1.1000 +  _onDisableSelf: function(aLocation) {
  1.1001 +    // Disable the breakpoint, in this container and the controller store.
  1.1002 +    this.disableBreakpoint(aLocation);
  1.1003 +  },
  1.1004 +
  1.1005 +  /**
  1.1006 +   * Function invoked on the "deleteSelf" menuitem command.
  1.1007 +   *
  1.1008 +   * @param object aLocation
  1.1009 +   *        @see DebuggerController.Breakpoints.addBreakpoint
  1.1010 +   */
  1.1011 +  _onDeleteSelf: function(aLocation) {
  1.1012 +    // Remove the breakpoint, from this container and the controller store.
  1.1013 +    this.removeBreakpoint(aLocation);
  1.1014 +    DebuggerController.Breakpoints.removeBreakpoint(aLocation);
  1.1015 +  },
  1.1016 +
  1.1017 +  /**
  1.1018 +   * Function invoked on the "enableOthers" menuitem command.
  1.1019 +   *
  1.1020 +   * @param object aLocation
  1.1021 +   *        @see DebuggerController.Breakpoints.addBreakpoint
  1.1022 +   */
  1.1023 +  _onEnableOthers: function(aLocation) {
  1.1024 +    let enableOthers = aCallback => {
  1.1025 +      let other = this.getOtherBreakpoints(aLocation);
  1.1026 +      let outstanding = other.map(e => this.enableBreakpoint(e.attachment));
  1.1027 +      promise.all(outstanding).then(aCallback);
  1.1028 +    }
  1.1029 +
  1.1030 +    // Breakpoints can only be set while the debuggee is paused. To avoid
  1.1031 +    // an avalanche of pause/resume interrupts of the main thread, simply
  1.1032 +    // pause it beforehand if it's not already.
  1.1033 +    if (gThreadClient.state != "paused") {
  1.1034 +      gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume()));
  1.1035 +    } else {
  1.1036 +      enableOthers();
  1.1037 +    }
  1.1038 +  },
  1.1039 +
  1.1040 +  /**
  1.1041 +   * Function invoked on the "disableOthers" menuitem command.
  1.1042 +   *
  1.1043 +   * @param object aLocation
  1.1044 +   *        @see DebuggerController.Breakpoints.addBreakpoint
  1.1045 +   */
  1.1046 +  _onDisableOthers: function(aLocation) {
  1.1047 +    let other = this.getOtherBreakpoints(aLocation);
  1.1048 +    other.forEach(e => this._onDisableSelf(e.attachment));
  1.1049 +  },
  1.1050 +
  1.1051 +  /**
  1.1052 +   * Function invoked on the "deleteOthers" menuitem command.
  1.1053 +   *
  1.1054 +   * @param object aLocation
  1.1055 +   *        @see DebuggerController.Breakpoints.addBreakpoint
  1.1056 +   */
  1.1057 +  _onDeleteOthers: function(aLocation) {
  1.1058 +    let other = this.getOtherBreakpoints(aLocation);
  1.1059 +    other.forEach(e => this._onDeleteSelf(e.attachment));
  1.1060 +  },
  1.1061 +
  1.1062 +  /**
  1.1063 +   * Function invoked on the "enableAll" menuitem command.
  1.1064 +   */
  1.1065 +  _onEnableAll: function() {
  1.1066 +    this._onEnableOthers(undefined);
  1.1067 +  },
  1.1068 +
  1.1069 +  /**
  1.1070 +   * Function invoked on the "disableAll" menuitem command.
  1.1071 +   */
  1.1072 +  _onDisableAll: function() {
  1.1073 +    this._onDisableOthers(undefined);
  1.1074 +  },
  1.1075 +
  1.1076 +  /**
  1.1077 +   * Function invoked on the "deleteAll" menuitem command.
  1.1078 +   */
  1.1079 +  _onDeleteAll: function() {
  1.1080 +    this._onDeleteOthers(undefined);
  1.1081 +  },
  1.1082 +
  1.1083 +  _commandset: null,
  1.1084 +  _popupset: null,
  1.1085 +  _cmPopup: null,
  1.1086 +  _cbPanel: null,
  1.1087 +  _cbTextbox: null,
  1.1088 +  _selectedBreakpointItem: null,
  1.1089 +  _conditionalPopupVisible: false
  1.1090 +});
  1.1091 +
  1.1092 +/**
  1.1093 + * Functions handling the traces UI.
  1.1094 + */
  1.1095 +function TracerView() {
  1.1096 +  this._selectedItem = null;
  1.1097 +  this._matchingItems = null;
  1.1098 +  this.widget = null;
  1.1099 +
  1.1100 +  this._highlightItem = this._highlightItem.bind(this);
  1.1101 +  this._isNotSelectedItem = this._isNotSelectedItem.bind(this);
  1.1102 +
  1.1103 +  this._unhighlightMatchingItems =
  1.1104 +    DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this));
  1.1105 +  this._onToggleTracing =
  1.1106 +    DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this));
  1.1107 +  this._onStartTracing =
  1.1108 +    DevToolsUtils.makeInfallible(this._onStartTracing.bind(this));
  1.1109 +  this._onClear =
  1.1110 +    DevToolsUtils.makeInfallible(this._onClear.bind(this));
  1.1111 +  this._onSelect =
  1.1112 +    DevToolsUtils.makeInfallible(this._onSelect.bind(this));
  1.1113 +  this._onMouseOver =
  1.1114 +    DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
  1.1115 +  this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this));
  1.1116 +}
  1.1117 +
  1.1118 +TracerView.MAX_TRACES = 200;
  1.1119 +
  1.1120 +TracerView.prototype = Heritage.extend(WidgetMethods, {
  1.1121 +  /**
  1.1122 +   * Initialization function, called when the debugger is started.
  1.1123 +   */
  1.1124 +  initialize: function() {
  1.1125 +    dumpn("Initializing the TracerView");
  1.1126 +
  1.1127 +    this._traceButton = document.getElementById("trace");
  1.1128 +    this._tracerTab = document.getElementById("tracer-tab");
  1.1129 +
  1.1130 +    // Remove tracer related elements from the dom and tear everything down if
  1.1131 +    // the tracer isn't enabled.
  1.1132 +    if (!Prefs.tracerEnabled) {
  1.1133 +      this._traceButton.remove();
  1.1134 +      this._traceButton = null;
  1.1135 +      this._tracerTab.remove();
  1.1136 +      this._tracerTab = null;
  1.1137 +      return;
  1.1138 +    }
  1.1139 +
  1.1140 +    this.widget = new FastListWidget(document.getElementById("tracer-traces"));
  1.1141 +    this._traceButton.removeAttribute("hidden");
  1.1142 +    this._tracerTab.removeAttribute("hidden");
  1.1143 +
  1.1144 +    this._search = document.getElementById("tracer-search");
  1.1145 +    this._template = document.getElementsByClassName("trace-item-template")[0];
  1.1146 +    this._templateItem = this._template.getElementsByClassName("trace-item")[0];
  1.1147 +    this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0];
  1.1148 +    this._templateNameNode = this._template.getElementsByClassName("trace-name")[0];
  1.1149 +
  1.1150 +    this.widget.addEventListener("select", this._onSelect, false);
  1.1151 +    this.widget.addEventListener("mouseover", this._onMouseOver, false);
  1.1152 +    this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false);
  1.1153 +    this._search.addEventListener("input", this._onSearch, false);
  1.1154 +
  1.1155 +    this._startTooltip = L10N.getStr("startTracingTooltip");
  1.1156 +    this._stopTooltip = L10N.getStr("stopTracingTooltip");
  1.1157 +    this._tracingNotStartedString = L10N.getStr("tracingNotStartedText");
  1.1158 +    this._noFunctionCallsString = L10N.getStr("noFunctionCallsText");
  1.1159 +
  1.1160 +    this._traceButton.setAttribute("tooltiptext", this._startTooltip);
  1.1161 +    this.emptyText = this._tracingNotStartedString;
  1.1162 +  },
  1.1163 +
  1.1164 +  /**
  1.1165 +   * Destruction function, called when the debugger is closed.
  1.1166 +   */
  1.1167 +  destroy: function() {
  1.1168 +    dumpn("Destroying the TracerView");
  1.1169 +
  1.1170 +    if (!this.widget) {
  1.1171 +      return;
  1.1172 +    }
  1.1173 +
  1.1174 +    this.widget.removeEventListener("select", this._onSelect, false);
  1.1175 +    this.widget.removeEventListener("mouseover", this._onMouseOver, false);
  1.1176 +    this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false);
  1.1177 +    this._search.removeEventListener("input", this._onSearch, false);
  1.1178 +  },
  1.1179 +
  1.1180 +  /**
  1.1181 +   * Function invoked by the "toggleTracing" command to switch the tracer state.
  1.1182 +   */
  1.1183 +  _onToggleTracing: function() {
  1.1184 +    if (DebuggerController.Tracer.tracing) {
  1.1185 +      this._onStopTracing();
  1.1186 +    } else {
  1.1187 +      this._onStartTracing();
  1.1188 +    }
  1.1189 +  },
  1.1190 +
  1.1191 +  /**
  1.1192 +   * Function invoked either by the "startTracing" command or by
  1.1193 +   * _onToggleTracing to start execution tracing in the backend.
  1.1194 +   *
  1.1195 +   * @return object
  1.1196 +   *         A promise resolved once the tracing has successfully started.
  1.1197 +   */
  1.1198 +  _onStartTracing: function() {
  1.1199 +    this._traceButton.setAttribute("checked", true);
  1.1200 +    this._traceButton.setAttribute("tooltiptext", this._stopTooltip);
  1.1201 +
  1.1202 +    this.empty();
  1.1203 +    this.emptyText = this._noFunctionCallsString;
  1.1204 +
  1.1205 +    let deferred = promise.defer();
  1.1206 +    DebuggerController.Tracer.startTracing(deferred.resolve);
  1.1207 +    return deferred.promise;
  1.1208 +  },
  1.1209 +
  1.1210 +  /**
  1.1211 +   * Function invoked by _onToggleTracing to stop execution tracing in the
  1.1212 +   * backend.
  1.1213 +   *
  1.1214 +   * @return object
  1.1215 +   *         A promise resolved once the tracing has successfully stopped.
  1.1216 +   */
  1.1217 +  _onStopTracing: function() {
  1.1218 +    this._traceButton.removeAttribute("checked");
  1.1219 +    this._traceButton.setAttribute("tooltiptext", this._startTooltip);
  1.1220 +
  1.1221 +    this.emptyText = this._tracingNotStartedString;
  1.1222 +
  1.1223 +    let deferred = promise.defer();
  1.1224 +    DebuggerController.Tracer.stopTracing(deferred.resolve);
  1.1225 +    return deferred.promise;
  1.1226 +  },
  1.1227 +
  1.1228 +  /**
  1.1229 +   * Function invoked by the "clearTraces" command to empty the traces pane.
  1.1230 +   */
  1.1231 +  _onClear: function() {
  1.1232 +    this.empty();
  1.1233 +  },
  1.1234 +
  1.1235 +  /**
  1.1236 +   * Populate the given parent scope with the variable with the provided name
  1.1237 +   * and value.
  1.1238 +   *
  1.1239 +   * @param String aName
  1.1240 +   *        The name of the variable.
  1.1241 +   * @param Object aParent
  1.1242 +   *        The parent scope.
  1.1243 +   * @param Object aValue
  1.1244 +   *        The value of the variable.
  1.1245 +   */
  1.1246 +  _populateVariable: function(aName, aParent, aValue) {
  1.1247 +    let item = aParent.addItem(aName, { value: aValue });
  1.1248 +    if (aValue) {
  1.1249 +      let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue);
  1.1250 +      DebuggerView.Variables.controller.populate(item, wrappedValue);
  1.1251 +      item.expand();
  1.1252 +      item.twisty = false;
  1.1253 +    }
  1.1254 +  },
  1.1255 +
  1.1256 +  /**
  1.1257 +   * Handler for the widget's "select" event. Displays parameters, exception, or
  1.1258 +   * return value depending on whether the selected trace is a call, throw, or
  1.1259 +   * return respectively.
  1.1260 +   *
  1.1261 +   * @param Object traceItem
  1.1262 +   *        The selected trace item.
  1.1263 +   */
  1.1264 +  _onSelect: function _onSelect({ detail: traceItem }) {
  1.1265 +    if (!traceItem) {
  1.1266 +      return;
  1.1267 +    }
  1.1268 +
  1.1269 +    const data = traceItem.attachment.trace;
  1.1270 +    const { location: { url, line } } = data;
  1.1271 +    DebuggerView.setEditorLocation(url, line, { noDebug: true });
  1.1272 +
  1.1273 +    DebuggerView.Variables.empty();
  1.1274 +    const scope = DebuggerView.Variables.addScope();
  1.1275 +
  1.1276 +    if (data.type == "call") {
  1.1277 +      const params = DevToolsUtils.zip(data.parameterNames, data.arguments);
  1.1278 +      for (let [name, val] of params) {
  1.1279 +        if (val === undefined) {
  1.1280 +          scope.addItem(name, { value: "<value not available>" });
  1.1281 +        } else {
  1.1282 +          this._populateVariable(name, scope, val);
  1.1283 +        }
  1.1284 +      }
  1.1285 +    } else {
  1.1286 +      const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">";
  1.1287 +      this._populateVariable(varName, scope, data.returnVal);
  1.1288 +    }
  1.1289 +
  1.1290 +    scope.expand();
  1.1291 +    DebuggerView.showInstrumentsPane();
  1.1292 +  },
  1.1293 +
  1.1294 +  /**
  1.1295 +   * Add the hover frame enter/exit highlighting to a given item.
  1.1296 +   */
  1.1297 +  _highlightItem: function(aItem) {
  1.1298 +    if (!aItem || !aItem.target) {
  1.1299 +      return;
  1.1300 +    }
  1.1301 +    const trace = aItem.target.querySelector(".trace-item");
  1.1302 +    trace.classList.add("selected-matching");
  1.1303 +  },
  1.1304 +
  1.1305 +  /**
  1.1306 +   * Remove the hover frame enter/exit highlighting to a given item.
  1.1307 +   */
  1.1308 +  _unhighlightItem: function(aItem) {
  1.1309 +    if (!aItem || !aItem.target) {
  1.1310 +      return;
  1.1311 +    }
  1.1312 +    const match = aItem.target.querySelector(".selected-matching");
  1.1313 +    if (match) {
  1.1314 +      match.classList.remove("selected-matching");
  1.1315 +    }
  1.1316 +  },
  1.1317 +
  1.1318 +  /**
  1.1319 +   * Remove the frame enter/exit pair highlighting we do when hovering.
  1.1320 +   */
  1.1321 +  _unhighlightMatchingItems: function() {
  1.1322 +    if (this._matchingItems) {
  1.1323 +      this._matchingItems.forEach(this._unhighlightItem);
  1.1324 +      this._matchingItems = null;
  1.1325 +    }
  1.1326 +  },
  1.1327 +
  1.1328 +  /**
  1.1329 +   * Returns true if the given item is not the selected item.
  1.1330 +   */
  1.1331 +  _isNotSelectedItem: function(aItem) {
  1.1332 +    return aItem !== this.selectedItem;
  1.1333 +  },
  1.1334 +
  1.1335 +  /**
  1.1336 +   * Highlight the frame enter/exit pair of items for the given item.
  1.1337 +   */
  1.1338 +  _highlightMatchingItems: function(aItem) {
  1.1339 +    const frameId = aItem.attachment.trace.frameId;
  1.1340 +    const predicate = e => e.attachment.trace.frameId == frameId;
  1.1341 +
  1.1342 +    this._unhighlightMatchingItems();
  1.1343 +    this._matchingItems = this.items.filter(predicate);
  1.1344 +    this._matchingItems
  1.1345 +      .filter(this._isNotSelectedItem)
  1.1346 +      .forEach(this._highlightItem);
  1.1347 +  },
  1.1348 +
  1.1349 +  /**
  1.1350 +   * Listener for the mouseover event.
  1.1351 +   */
  1.1352 +  _onMouseOver: function({ target }) {
  1.1353 +    const traceItem = this.getItemForElement(target);
  1.1354 +    if (traceItem) {
  1.1355 +      this._highlightMatchingItems(traceItem);
  1.1356 +    }
  1.1357 +  },
  1.1358 +
  1.1359 +  /**
  1.1360 +   * Listener for typing in the search box.
  1.1361 +   */
  1.1362 +  _onSearch: function() {
  1.1363 +    const query = this._search.value.trim().toLowerCase();
  1.1364 +    const predicate = name => name.toLowerCase().contains(query);
  1.1365 +    this.filterContents(item => predicate(item.attachment.trace.name));
  1.1366 +  },
  1.1367 +
  1.1368 +  /**
  1.1369 +   * Select the traces tab in the sidebar.
  1.1370 +   */
  1.1371 +  selectTab: function() {
  1.1372 +    const tabs = this._tracerTab.parentElement;
  1.1373 +    tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab);
  1.1374 +  },
  1.1375 +
  1.1376 +  /**
  1.1377 +   * Commit all staged items to the widget. Overridden so that we can call
  1.1378 +   * |FastListWidget.prototype.flush|.
  1.1379 +   */
  1.1380 +  commit: function() {
  1.1381 +    WidgetMethods.commit.call(this);
  1.1382 +    // TODO: Accessing non-standard widget properties. Figure out what's the
  1.1383 +    // best way to expose such things. Bug 895514.
  1.1384 +    this.widget.flush();
  1.1385 +  },
  1.1386 +
  1.1387 +  /**
  1.1388 +   * Adds the trace record provided as an argument to the view.
  1.1389 +   *
  1.1390 +   * @param object aTrace
  1.1391 +   *        The trace record coming from the tracer actor.
  1.1392 +   */
  1.1393 +  addTrace: function(aTrace) {
  1.1394 +    // Create the element node for the trace item.
  1.1395 +    let view = this._createView(aTrace);
  1.1396 +
  1.1397 +    // Append a source item to this container.
  1.1398 +    this.push([view], {
  1.1399 +      staged: true,
  1.1400 +      attachment: {
  1.1401 +        trace: aTrace
  1.1402 +      }
  1.1403 +    });
  1.1404 +  },
  1.1405 +
  1.1406 +  /**
  1.1407 +   * Customization function for creating an item's UI.
  1.1408 +   *
  1.1409 +   * @return nsIDOMNode
  1.1410 +   *         The network request view.
  1.1411 +   */
  1.1412 +  _createView: function(aTrace) {
  1.1413 +    let { type, name, location, depth, frameId } = aTrace;
  1.1414 +    let { parameterNames, returnVal, arguments: args } = aTrace;
  1.1415 +    let fragment = document.createDocumentFragment();
  1.1416 +
  1.1417 +    this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
  1.1418 +    this._templateItem.style.MozPaddingStart = depth + "em";
  1.1419 +
  1.1420 +    const TYPES = ["call", "yield", "return", "throw"];
  1.1421 +    for (let t of TYPES) {
  1.1422 +      this._templateTypeIcon.classList.toggle("trace-" + t, t == type);
  1.1423 +    }
  1.1424 +    this._templateTypeIcon.setAttribute("value", {
  1.1425 +      call: "\u2192",
  1.1426 +      yield: "Y",
  1.1427 +      return: "\u2190",
  1.1428 +      throw: "E",
  1.1429 +      terminated: "TERMINATED"
  1.1430 +    }[type]);
  1.1431 +
  1.1432 +    this._templateNameNode.setAttribute("value", name);
  1.1433 +
  1.1434 +    // All extra syntax and parameter nodes added.
  1.1435 +    const addedNodes = [];
  1.1436 +
  1.1437 +    if (parameterNames) {
  1.1438 +      const syntax = (p) => {
  1.1439 +        const el = document.createElement("label");
  1.1440 +        el.setAttribute("value", p);
  1.1441 +        el.classList.add("trace-syntax");
  1.1442 +        el.classList.add("plain");
  1.1443 +        addedNodes.push(el);
  1.1444 +        return el;
  1.1445 +      };
  1.1446 +
  1.1447 +      this._templateItem.appendChild(syntax("("));
  1.1448 +
  1.1449 +      for (let i = 0, n = parameterNames.length; i < n; i++) {
  1.1450 +        let param = document.createElement("label");
  1.1451 +        param.setAttribute("value", parameterNames[i]);
  1.1452 +        param.classList.add("trace-param");
  1.1453 +        param.classList.add("plain");
  1.1454 +        addedNodes.push(param);
  1.1455 +        this._templateItem.appendChild(param);
  1.1456 +
  1.1457 +        if (i + 1 !== n) {
  1.1458 +          this._templateItem.appendChild(syntax(", "));
  1.1459 +        }
  1.1460 +      }
  1.1461 +
  1.1462 +      this._templateItem.appendChild(syntax(")"));
  1.1463 +    }
  1.1464 +
  1.1465 +    // Flatten the DOM by removing one redundant box (the template container).
  1.1466 +    for (let node of this._template.childNodes) {
  1.1467 +      fragment.appendChild(node.cloneNode(true));
  1.1468 +    }
  1.1469 +
  1.1470 +    // Remove any added nodes from the template.
  1.1471 +    for (let node of addedNodes) {
  1.1472 +      this._templateItem.removeChild(node);
  1.1473 +    }
  1.1474 +
  1.1475 +    return fragment;
  1.1476 +  }
  1.1477 +});
  1.1478 +
  1.1479 +/**
  1.1480 + * Utility functions for handling sources.
  1.1481 + */
  1.1482 +let SourceUtils = {
  1.1483 +  _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
  1.1484 +  _groupsCache: new Map(),
  1.1485 +  _minifiedCache: new WeakMap(),
  1.1486 +
  1.1487 +  /**
  1.1488 +   * Returns true if the specified url and/or content type are specific to
  1.1489 +   * javascript files.
  1.1490 +   *
  1.1491 +   * @return boolean
  1.1492 +   *         True if the source is likely javascript.
  1.1493 +   */
  1.1494 +  isJavaScript: function(aUrl, aContentType = "") {
  1.1495 +    return /\.jsm?$/.test(this.trimUrlQuery(aUrl)) ||
  1.1496 +           aContentType.contains("javascript");
  1.1497 +  },
  1.1498 +
  1.1499 +  /**
  1.1500 +   * Determines if the source text is minified by using
  1.1501 +   * the percentage indented of a subset of lines
  1.1502 +   *
  1.1503 +   * @param string aText
  1.1504 +   *        The source text.
  1.1505 +   * @return boolean
  1.1506 +   *        True if source text is minified.
  1.1507 +   */
  1.1508 +  isMinified: function(sourceClient, aText){
  1.1509 +    if (this._minifiedCache.has(sourceClient)) {
  1.1510 +      return this._minifiedCache.get(sourceClient);
  1.1511 +    }
  1.1512 +
  1.1513 +    let isMinified;
  1.1514 +    let lineEndIndex = 0;
  1.1515 +    let lineStartIndex = 0;
  1.1516 +    let lines = 0;
  1.1517 +    let indentCount = 0;
  1.1518 +    let overCharLimit = false;
  1.1519 +
  1.1520 +    // Strip comments.
  1.1521 +    aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
  1.1522 +
  1.1523 +    while (lines++ < SAMPLE_SIZE) {
  1.1524 +      lineEndIndex = aText.indexOf("\n", lineStartIndex);
  1.1525 +      if (lineEndIndex == -1) {
  1.1526 +         break;
  1.1527 +      }
  1.1528 +      if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) {
  1.1529 +        indentCount++;
  1.1530 +      }
  1.1531 +      // For files with no indents but are not minified.
  1.1532 +      if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) {
  1.1533 +        overCharLimit = true;
  1.1534 +        break;
  1.1535 +      }
  1.1536 +      lineStartIndex = lineEndIndex + 1;
  1.1537 +    }
  1.1538 +    isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD ||
  1.1539 +                 overCharLimit;
  1.1540 +
  1.1541 +    this._minifiedCache.set(sourceClient, isMinified);
  1.1542 +    return isMinified;
  1.1543 +  },
  1.1544 +
  1.1545 +  /**
  1.1546 +   * Clears the labels, groups and minify cache, populated by methods like
  1.1547 +   * SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
  1.1548 +   * This should be done every time the content location changes.
  1.1549 +   */
  1.1550 +  clearCache: function() {
  1.1551 +    this._labelsCache.clear();
  1.1552 +    this._groupsCache.clear();
  1.1553 +    this._minifiedCache.clear();
  1.1554 +  },
  1.1555 +
  1.1556 +  /**
  1.1557 +   * Gets a unique, simplified label from a source url.
  1.1558 +   *
  1.1559 +   * @param string aUrl
  1.1560 +   *        The source url.
  1.1561 +   * @return string
  1.1562 +   *         The simplified label.
  1.1563 +   */
  1.1564 +  getSourceLabel: function(aUrl) {
  1.1565 +    let cachedLabel = this._labelsCache.get(aUrl);
  1.1566 +    if (cachedLabel) {
  1.1567 +      return cachedLabel;
  1.1568 +    }
  1.1569 +
  1.1570 +    let sourceLabel = null;
  1.1571 +
  1.1572 +    for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
  1.1573 +      if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
  1.1574 +        sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length);
  1.1575 +      }
  1.1576 +    }
  1.1577 +
  1.1578 +    if (!sourceLabel) {
  1.1579 +      sourceLabel = this.trimUrl(aUrl);
  1.1580 +    }
  1.1581 +    let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
  1.1582 +    this._labelsCache.set(aUrl, unicodeLabel);
  1.1583 +    return unicodeLabel;
  1.1584 +  },
  1.1585 +
  1.1586 +  /**
  1.1587 +   * Gets as much information as possible about the hostname and directory paths
  1.1588 +   * of an url to create a short url group identifier.
  1.1589 +   *
  1.1590 +   * @param string aUrl
  1.1591 +   *        The source url.
  1.1592 +   * @return string
  1.1593 +   *         The simplified group.
  1.1594 +   */
  1.1595 +  getSourceGroup: function(aUrl) {
  1.1596 +    let cachedGroup = this._groupsCache.get(aUrl);
  1.1597 +    if (cachedGroup) {
  1.1598 +      return cachedGroup;
  1.1599 +    }
  1.1600 +
  1.1601 +    try {
  1.1602 +      // Use an nsIURL to parse all the url path parts.
  1.1603 +      var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
  1.1604 +    } catch (e) {
  1.1605 +      // This doesn't look like a url, or nsIURL can't handle it.
  1.1606 +      return "";
  1.1607 +    }
  1.1608 +
  1.1609 +    let groupLabel = uri.prePath;
  1.1610 +
  1.1611 +    for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
  1.1612 +      if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
  1.1613 +        groupLabel = name;
  1.1614 +      }
  1.1615 +    }
  1.1616 +
  1.1617 +    let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
  1.1618 +    this._groupsCache.set(aUrl, unicodeLabel)
  1.1619 +    return unicodeLabel;
  1.1620 +  },
  1.1621 +
  1.1622 +  /**
  1.1623 +   * Trims the url by shortening it if it exceeds a certain length, adding an
  1.1624 +   * ellipsis at the end.
  1.1625 +   *
  1.1626 +   * @param string aUrl
  1.1627 +   *        The source url.
  1.1628 +   * @param number aLength [optional]
  1.1629 +   *        The expected source url length.
  1.1630 +   * @param number aSection [optional]
  1.1631 +   *        The section to trim. Supported values: "start", "center", "end"
  1.1632 +   * @return string
  1.1633 +   *         The shortened url.
  1.1634 +   */
  1.1635 +  trimUrlLength: function(aUrl, aLength, aSection) {
  1.1636 +    aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH;
  1.1637 +    aSection = aSection || "end";
  1.1638 +
  1.1639 +    if (aUrl.length > aLength) {
  1.1640 +      switch (aSection) {
  1.1641 +        case "start":
  1.1642 +          return L10N.ellipsis + aUrl.slice(-aLength);
  1.1643 +          break;
  1.1644 +        case "center":
  1.1645 +          return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1);
  1.1646 +          break;
  1.1647 +        case "end":
  1.1648 +          return aUrl.substr(0, aLength) + L10N.ellipsis;
  1.1649 +          break;
  1.1650 +      }
  1.1651 +    }
  1.1652 +    return aUrl;
  1.1653 +  },
  1.1654 +
  1.1655 +  /**
  1.1656 +   * Trims the query part or reference identifier of a url string, if necessary.
  1.1657 +   *
  1.1658 +   * @param string aUrl
  1.1659 +   *        The source url.
  1.1660 +   * @return string
  1.1661 +   *         The shortened url.
  1.1662 +   */
  1.1663 +  trimUrlQuery: function(aUrl) {
  1.1664 +    let length = aUrl.length;
  1.1665 +    let q1 = aUrl.indexOf('?');
  1.1666 +    let q2 = aUrl.indexOf('&');
  1.1667 +    let q3 = aUrl.indexOf('#');
  1.1668 +    let q = Math.min(q1 != -1 ? q1 : length,
  1.1669 +                     q2 != -1 ? q2 : length,
  1.1670 +                     q3 != -1 ? q3 : length);
  1.1671 +
  1.1672 +    return aUrl.slice(0, q);
  1.1673 +  },
  1.1674 +
  1.1675 +  /**
  1.1676 +   * Trims as much as possible from a url, while keeping the label unique
  1.1677 +   * in the sources container.
  1.1678 +   *
  1.1679 +   * @param string | nsIURL aUrl
  1.1680 +   *        The source url.
  1.1681 +   * @param string aLabel [optional]
  1.1682 +   *        The resulting label at each step.
  1.1683 +   * @param number aSeq [optional]
  1.1684 +   *        The current iteration step.
  1.1685 +   * @return string
  1.1686 +   *         The resulting label at the final step.
  1.1687 +   */
  1.1688 +  trimUrl: function(aUrl, aLabel, aSeq) {
  1.1689 +    if (!(aUrl instanceof Ci.nsIURL)) {
  1.1690 +      try {
  1.1691 +        // Use an nsIURL to parse all the url path parts.
  1.1692 +        aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
  1.1693 +      } catch (e) {
  1.1694 +        // This doesn't look like a url, or nsIURL can't handle it.
  1.1695 +        return aUrl;
  1.1696 +      }
  1.1697 +    }
  1.1698 +    if (!aSeq) {
  1.1699 +      let name = aUrl.fileName;
  1.1700 +      if (name) {
  1.1701 +        // This is a regular file url, get only the file name (contains the
  1.1702 +        // base name and extension if available).
  1.1703 +
  1.1704 +        // If this url contains an invalid query, unfortunately nsIURL thinks
  1.1705 +        // it's part of the file extension. It must be removed.
  1.1706 +        aLabel = aUrl.fileName.replace(/\&.*/, "");
  1.1707 +      } else {
  1.1708 +        // This is not a file url, hence there is no base name, nor extension.
  1.1709 +        // Proceed using other available information.
  1.1710 +        aLabel = "";
  1.1711 +      }
  1.1712 +      aSeq = 1;
  1.1713 +    }
  1.1714 +
  1.1715 +    // If we have a label and it doesn't only contain a query...
  1.1716 +    if (aLabel && aLabel.indexOf("?") != 0) {
  1.1717 +      // A page may contain multiple requests to the same url but with different
  1.1718 +      // queries. It is *not* redundant to show each one.
  1.1719 +      if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) {
  1.1720 +        return aLabel;
  1.1721 +      }
  1.1722 +    }
  1.1723 +
  1.1724 +    // Append the url query.
  1.1725 +    if (aSeq == 1) {
  1.1726 +      let query = aUrl.query;
  1.1727 +      if (query) {
  1.1728 +        return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1);
  1.1729 +      }
  1.1730 +      aSeq++;
  1.1731 +    }
  1.1732 +    // Append the url reference.
  1.1733 +    if (aSeq == 2) {
  1.1734 +      let ref = aUrl.ref;
  1.1735 +      if (ref) {
  1.1736 +        return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
  1.1737 +      }
  1.1738 +      aSeq++;
  1.1739 +    }
  1.1740 +    // Prepend the url directory.
  1.1741 +    if (aSeq == 3) {
  1.1742 +      let dir = aUrl.directory;
  1.1743 +      if (dir) {
  1.1744 +        return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
  1.1745 +      }
  1.1746 +      aSeq++;
  1.1747 +    }
  1.1748 +    // Prepend the hostname and port number.
  1.1749 +    if (aSeq == 4) {
  1.1750 +      let host = aUrl.hostPort;
  1.1751 +      if (host) {
  1.1752 +        return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
  1.1753 +      }
  1.1754 +      aSeq++;
  1.1755 +    }
  1.1756 +    // Use the whole url spec but ignoring the reference.
  1.1757 +    if (aSeq == 5) {
  1.1758 +      return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
  1.1759 +    }
  1.1760 +    // Give up.
  1.1761 +    return aUrl.spec;
  1.1762 +  }
  1.1763 +};
  1.1764 +
  1.1765 +/**
  1.1766 + * Functions handling the variables bubble UI.
  1.1767 + */
  1.1768 +function VariableBubbleView() {
  1.1769 +  dumpn("VariableBubbleView was instantiated");
  1.1770 +
  1.1771 +  this._onMouseMove = this._onMouseMove.bind(this);
  1.1772 +  this._onMouseLeave = this._onMouseLeave.bind(this);
  1.1773 +  this._onPopupHiding = this._onPopupHiding.bind(this);
  1.1774 +}
  1.1775 +
  1.1776 +VariableBubbleView.prototype = {
  1.1777 +  /**
  1.1778 +   * Initialization function, called when the debugger is started.
  1.1779 +   */
  1.1780 +  initialize: function() {
  1.1781 +    dumpn("Initializing the VariableBubbleView");
  1.1782 +
  1.1783 +    this._editorContainer = document.getElementById("editor");
  1.1784 +    this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
  1.1785 +    this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false);
  1.1786 +
  1.1787 +    this._tooltip = new Tooltip(document, {
  1.1788 +      closeOnEvents: [{
  1.1789 +        emitter: DebuggerController._toolbox,
  1.1790 +        event: "select"
  1.1791 +      }, {
  1.1792 +        emitter: this._editorContainer,
  1.1793 +        event: "scroll",
  1.1794 +        useCapture: true
  1.1795 +      }]
  1.1796 +    });
  1.1797 +    this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION;
  1.1798 +    this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY;
  1.1799 +    this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
  1.1800 +  },
  1.1801 +
  1.1802 +  /**
  1.1803 +   * Destruction function, called when the debugger is closed.
  1.1804 +   */
  1.1805 +  destroy: function() {
  1.1806 +    dumpn("Destroying the VariableBubbleView");
  1.1807 +
  1.1808 +    this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding);
  1.1809 +    this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false);
  1.1810 +    this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false);
  1.1811 +  },
  1.1812 +
  1.1813 +  /**
  1.1814 +   * Specifies whether literals can be (redundantly) inspected in a popup.
  1.1815 +   * This behavior is deprecated, but still tested in a few places.
  1.1816 +   */
  1.1817 +  _ignoreLiterals: true,
  1.1818 +
  1.1819 +  /**
  1.1820 +   * Searches for an identifier underneath the specified position in the
  1.1821 +   * source editor, and if found, opens a VariablesView inspection popup.
  1.1822 +   *
  1.1823 +   * @param number x, y
  1.1824 +   *        The left/top coordinates where to look for an identifier.
  1.1825 +   */
  1.1826 +  _findIdentifier: function(x, y) {
  1.1827 +    let editor = DebuggerView.editor;
  1.1828 +
  1.1829 +    // Calculate the editor's line and column at the current x and y coords.
  1.1830 +    let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
  1.1831 +    let hoveredOffset = editor.getOffset(hoveredPos);
  1.1832 +    let hoveredLine = hoveredPos.line;
  1.1833 +    let hoveredColumn = hoveredPos.ch;
  1.1834 +
  1.1835 +    // A source contains multiple scripts. Find the start index of the script
  1.1836 +    // containing the specified offset relative to its parent source.
  1.1837 +    let contents = editor.getText();
  1.1838 +    let location = DebuggerView.Sources.selectedValue;
  1.1839 +    let parsedSource = DebuggerController.Parser.get(contents, location);
  1.1840 +    let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);
  1.1841 +
  1.1842 +    // If the script length is negative, we're not hovering JS source code.
  1.1843 +    if (scriptInfo.length == -1) {
  1.1844 +      return;
  1.1845 +    }
  1.1846 +
  1.1847 +    // Using the script offset, determine the actual line and column inside the
  1.1848 +    // script, to use when finding identifiers.
  1.1849 +    let scriptStart = editor.getPosition(scriptInfo.start);
  1.1850 +    let scriptLineOffset = scriptStart.line;
  1.1851 +    let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);
  1.1852 +
  1.1853 +    let scriptLine = hoveredLine - scriptLineOffset;
  1.1854 +    let scriptColumn = hoveredColumn - scriptColumnOffset;
  1.1855 +    let identifierInfo = parsedSource.getIdentifierAt({
  1.1856 +      line: scriptLine + 1,
  1.1857 +      column: scriptColumn,
  1.1858 +      scriptIndex: scriptInfo.index,
  1.1859 +      ignoreLiterals: this._ignoreLiterals
  1.1860 +    });
  1.1861 +
  1.1862 +    // If the info is null, we're not hovering any identifier.
  1.1863 +    if (!identifierInfo) {
  1.1864 +      return;
  1.1865 +    }
  1.1866 +
  1.1867 +    // Transform the line and column relative to the parsed script back
  1.1868 +    // to the context of the parent source.
  1.1869 +    let { start: identifierStart, end: identifierEnd } = identifierInfo.location;
  1.1870 +    let identifierCoords = {
  1.1871 +      line: identifierStart.line + scriptLineOffset,
  1.1872 +      column: identifierStart.column + scriptColumnOffset,
  1.1873 +      length: identifierEnd.column - identifierStart.column
  1.1874 +    };
  1.1875 +
  1.1876 +    // Evaluate the identifier in the current stack frame and show the
  1.1877 +    // results in a VariablesView inspection popup.
  1.1878 +    DebuggerController.StackFrames.evaluate(identifierInfo.evalString)
  1.1879 +      .then(frameFinished => {
  1.1880 +        if ("return" in frameFinished) {
  1.1881 +          this.showContents({
  1.1882 +            coords: identifierCoords,
  1.1883 +            evalPrefix: identifierInfo.evalString,
  1.1884 +            objectActor: frameFinished.return
  1.1885 +          });
  1.1886 +        } else {
  1.1887 +          let msg = "Evaluation has thrown for: " + identifierInfo.evalString;
  1.1888 +          console.warn(msg);
  1.1889 +          dumpn(msg);
  1.1890 +        }
  1.1891 +      })
  1.1892 +      .then(null, err => {
  1.1893 +        let msg = "Couldn't evaluate: " + err.message;
  1.1894 +        console.error(msg);
  1.1895 +        dumpn(msg);
  1.1896 +      });
  1.1897 +  },
  1.1898 +
  1.1899 +  /**
  1.1900 +   * Shows an inspection popup for a specified object actor grip.
  1.1901 +   *
  1.1902 +   * @param string object
  1.1903 +   *        An object containing the following properties:
  1.1904 +   *          - coords: the inspected identifier coordinates in the editor,
  1.1905 +   *                    containing the { line, column, length } properties.
  1.1906 +   *          - evalPrefix: a prefix for the variables view evaluation macros.
  1.1907 +   *          - objectActor: the value grip for the object actor.
  1.1908 +   */
  1.1909 +  showContents: function({ coords, evalPrefix, objectActor }) {
  1.1910 +    let editor = DebuggerView.editor;
  1.1911 +    let { line, column, length } = coords;
  1.1912 +
  1.1913 +    // Highlight the function found at the mouse position.
  1.1914 +    this._markedText = editor.markText(
  1.1915 +      { line: line - 1, ch: column },
  1.1916 +      { line: line - 1, ch: column + length });
  1.1917 +
  1.1918 +    // If the grip represents a primitive value, use a more lightweight
  1.1919 +    // machinery to display it.
  1.1920 +    if (VariablesView.isPrimitive({ value: objectActor })) {
  1.1921 +      let className = VariablesView.getClass(objectActor);
  1.1922 +      let textContent = VariablesView.getString(objectActor);
  1.1923 +      this._tooltip.setTextContent({
  1.1924 +        messages: [textContent],
  1.1925 +        messagesClass: className,
  1.1926 +        containerClass: "plain"
  1.1927 +      }, [{
  1.1928 +        label: L10N.getStr('addWatchExpressionButton'),
  1.1929 +        className: "dbg-expression-button",
  1.1930 +        command: () => {
  1.1931 +          DebuggerView.VariableBubble.hideContents();
  1.1932 +          DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
  1.1933 +        }
  1.1934 +      }]);
  1.1935 +    } else {
  1.1936 +      this._tooltip.setVariableContent(objectActor, {
  1.1937 +        searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"),
  1.1938 +        searchEnabled: Prefs.variablesSearchboxVisible,
  1.1939 +        eval: (variable, value) => {
  1.1940 +          let string = variable.evaluationMacro(variable, value);
  1.1941 +          DebuggerController.StackFrames.evaluate(string);
  1.1942 +          DebuggerView.VariableBubble.hideContents();
  1.1943 +        }
  1.1944 +      }, {
  1.1945 +        getEnvironmentClient: aObject => gThreadClient.environment(aObject),
  1.1946 +        getObjectClient: aObject => gThreadClient.pauseGrip(aObject),
  1.1947 +        simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix),
  1.1948 +        getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix),
  1.1949 +        overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix)
  1.1950 +      }, {
  1.1951 +        fetched: (aEvent, aType) => {
  1.1952 +          if (aType == "properties") {
  1.1953 +            window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES);
  1.1954 +          }
  1.1955 +        }
  1.1956 +      }, [{
  1.1957 +        label: L10N.getStr("addWatchExpressionButton"),
  1.1958 +        className: "dbg-expression-button",
  1.1959 +        command: () => {
  1.1960 +          DebuggerView.VariableBubble.hideContents();
  1.1961 +          DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
  1.1962 +        }
  1.1963 +      }], DebuggerController._toolbox);
  1.1964 +    }
  1.1965 +
  1.1966 +    this._tooltip.show(this._markedText.anchor);
  1.1967 +  },
  1.1968 +
  1.1969 +  /**
  1.1970 +   * Hides the inspection popup.
  1.1971 +   */
  1.1972 +  hideContents: function() {
  1.1973 +    clearNamedTimeout("editor-mouse-move");
  1.1974 +    this._tooltip.hide();
  1.1975 +  },
  1.1976 +
  1.1977 +  /**
  1.1978 +   * Checks whether the inspection popup is shown.
  1.1979 +   *
  1.1980 +   * @return boolean
  1.1981 +   *         True if the panel is shown or showing, false otherwise.
  1.1982 +   */
  1.1983 +  contentsShown: function() {
  1.1984 +    return this._tooltip.isShown();
  1.1985 +  },
  1.1986 +
  1.1987 +  /**
  1.1988 +   * Functions for getting customized variables view evaluation macros.
  1.1989 +   *
  1.1990 +   * @param string aPrefix
  1.1991 +   *        See the corresponding VariablesView.* functions.
  1.1992 +   */
  1.1993 +  _getSimpleValueEvalMacro: function(aPrefix) {
  1.1994 +    return (item, string) =>
  1.1995 +      VariablesView.simpleValueEvalMacro(item, string, aPrefix);
  1.1996 +  },
  1.1997 +  _getGetterOrSetterEvalMacro: function(aPrefix) {
  1.1998 +    return (item, string) =>
  1.1999 +      VariablesView.getterOrSetterEvalMacro(item, string, aPrefix);
  1.2000 +  },
  1.2001 +  _getOverrideValueEvalMacro: function(aPrefix) {
  1.2002 +    return (item, string) =>
  1.2003 +      VariablesView.overrideValueEvalMacro(item, string, aPrefix);
  1.2004 +  },
  1.2005 +
  1.2006 +  /**
  1.2007 +   * The mousemove listener for the source editor.
  1.2008 +   */
  1.2009 +  _onMouseMove: function({ clientX: x, clientY: y, buttons: btns }) {
  1.2010 +    // Prevent the variable inspection popup from showing when the thread client
  1.2011 +    // is not paused, or while a popup is already visible, or when the user tries
  1.2012 +    // to select text in the editor.
  1.2013 +    if (gThreadClient && gThreadClient.state != "paused"
  1.2014 +        || !this._tooltip.isHidden()
  1.2015 +        || (DebuggerView.editor.somethingSelected()
  1.2016 +         && btns > 0)) {
  1.2017 +      clearNamedTimeout("editor-mouse-move");
  1.2018 +      return;
  1.2019 +    }
  1.2020 +    // Allow events to settle down first. If the mouse hovers over
  1.2021 +    // a certain point in the editor long enough, try showing a variable bubble.
  1.2022 +    setNamedTimeout("editor-mouse-move",
  1.2023 +      EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(x, y));
  1.2024 +  },
  1.2025 +
  1.2026 +  /**
  1.2027 +   * The mouseleave listener for the source editor container node.
  1.2028 +   */
  1.2029 +  _onMouseLeave: function() {
  1.2030 +    clearNamedTimeout("editor-mouse-move");
  1.2031 +  },
  1.2032 +
  1.2033 +  /**
  1.2034 +   * Listener handling the popup hiding event.
  1.2035 +   */
  1.2036 +  _onPopupHiding: function({ target }) {
  1.2037 +    if (this._tooltip.panel != target) {
  1.2038 +      return;
  1.2039 +    }
  1.2040 +    if (this._markedText) {
  1.2041 +      this._markedText.clear();
  1.2042 +      this._markedText = null;
  1.2043 +    }
  1.2044 +    if (!this._tooltip.isEmpty()) {
  1.2045 +      this._tooltip.empty();
  1.2046 +    }
  1.2047 +  },
  1.2048 +
  1.2049 +  _editorContainer: null,
  1.2050 +  _markedText: null,
  1.2051 +  _tooltip: null
  1.2052 +};
  1.2053 +
  1.2054 +/**
  1.2055 + * Functions handling the watch expressions UI.
  1.2056 + */
  1.2057 +function WatchExpressionsView() {
  1.2058 +  dumpn("WatchExpressionsView was instantiated");
  1.2059 +
  1.2060 +  this.switchExpression = this.switchExpression.bind(this);
  1.2061 +  this.deleteExpression = this.deleteExpression.bind(this);
  1.2062 +  this._createItemView = this._createItemView.bind(this);
  1.2063 +  this._onClick = this._onClick.bind(this);
  1.2064 +  this._onClose = this._onClose.bind(this);
  1.2065 +  this._onBlur = this._onBlur.bind(this);
  1.2066 +  this._onKeyPress = this._onKeyPress.bind(this);
  1.2067 +}
  1.2068 +
  1.2069 +WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
  1.2070 +  /**
  1.2071 +   * Initialization function, called when the debugger is started.
  1.2072 +   */
  1.2073 +  initialize: function() {
  1.2074 +    dumpn("Initializing the WatchExpressionsView");
  1.2075 +
  1.2076 +    this.widget = new SimpleListWidget(document.getElementById("expressions"));
  1.2077 +    this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu");
  1.2078 +    this.widget.addEventListener("click", this._onClick, false);
  1.2079 +
  1.2080 +    this.headerText = L10N.getStr("addWatchExpressionText");
  1.2081 +  },
  1.2082 +
  1.2083 +  /**
  1.2084 +   * Destruction function, called when the debugger is closed.
  1.2085 +   */
  1.2086 +  destroy: function() {
  1.2087 +    dumpn("Destroying the WatchExpressionsView");
  1.2088 +
  1.2089 +    this.widget.removeEventListener("click", this._onClick, false);
  1.2090 +  },
  1.2091 +
  1.2092 +  /**
  1.2093 +   * Adds a watch expression in this container.
  1.2094 +   *
  1.2095 +   * @param string aExpression [optional]
  1.2096 +   *        An optional initial watch expression text.
  1.2097 +   * @param boolean aSkipUserInput [optional]
  1.2098 +   *        Pass true to avoid waiting for additional user input
  1.2099 +   *        on the watch expression.
  1.2100 +   */
  1.2101 +  addExpression: function(aExpression = "", aSkipUserInput = false) {
  1.2102 +    // Watch expressions are UI elements which benefit from visible panes.
  1.2103 +    DebuggerView.showInstrumentsPane();
  1.2104 +
  1.2105 +    // Create the element node for the watch expression item.
  1.2106 +    let itemView = this._createItemView(aExpression);
  1.2107 +
  1.2108 +    // Append a watch expression item to this container.
  1.2109 +    let expressionItem = this.push([itemView.container], {
  1.2110 +      index: 0, /* specifies on which position should the item be appended */
  1.2111 +      attachment: {
  1.2112 +        view: itemView,
  1.2113 +        initialExpression: aExpression,
  1.2114 +        currentExpression: "",
  1.2115 +      }
  1.2116 +    });
  1.2117 +
  1.2118 +    // Automatically focus the new watch expression input
  1.2119 +    // if additional user input is desired.
  1.2120 +    if (!aSkipUserInput) {
  1.2121 +      expressionItem.attachment.view.inputNode.select();
  1.2122 +      expressionItem.attachment.view.inputNode.focus();
  1.2123 +      DebuggerView.Variables.parentNode.scrollTop = 0;
  1.2124 +    }
  1.2125 +    // Otherwise, add and evaluate the new watch expression immediately.
  1.2126 +    else {
  1.2127 +      this.toggleContents(false);
  1.2128 +      this._onBlur({ target: expressionItem.attachment.view.inputNode });
  1.2129 +    }
  1.2130 +  },
  1.2131 +
  1.2132 +  /**
  1.2133 +   * Changes the watch expression corresponding to the specified variable item.
  1.2134 +   * This function is called whenever a watch expression's code is edited in
  1.2135 +   * the variables view container.
  1.2136 +   *
  1.2137 +   * @param Variable aVar
  1.2138 +   *        The variable representing the watch expression evaluation.
  1.2139 +   * @param string aExpression
  1.2140 +   *        The new watch expression text.
  1.2141 +   */
  1.2142 +  switchExpression: function(aVar, aExpression) {
  1.2143 +    let expressionItem =
  1.2144 +      [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0];
  1.2145 +
  1.2146 +    // Remove the watch expression if it's going to be empty or a duplicate.
  1.2147 +    if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) {
  1.2148 +      this.deleteExpression(aVar);
  1.2149 +      return;
  1.2150 +    }
  1.2151 +
  1.2152 +    // Save the watch expression code string.
  1.2153 +    expressionItem.attachment.currentExpression = aExpression;
  1.2154 +    expressionItem.attachment.view.inputNode.value = aExpression;
  1.2155 +
  1.2156 +    // Synchronize with the controller's watch expressions store.
  1.2157 +    DebuggerController.StackFrames.syncWatchExpressions();
  1.2158 +  },
  1.2159 +
  1.2160 +  /**
  1.2161 +   * Removes the watch expression corresponding to the specified variable item.
  1.2162 +   * This function is called whenever a watch expression's value is edited in
  1.2163 +   * the variables view container.
  1.2164 +   *
  1.2165 +   * @param Variable aVar
  1.2166 +   *        The variable representing the watch expression evaluation.
  1.2167 +   */
  1.2168 +  deleteExpression: function(aVar) {
  1.2169 +    let expressionItem =
  1.2170 +      [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0];
  1.2171 +
  1.2172 +    // Remove the watch expression.
  1.2173 +    this.remove(expressionItem);
  1.2174 +
  1.2175 +    // Synchronize with the controller's watch expressions store.
  1.2176 +    DebuggerController.StackFrames.syncWatchExpressions();
  1.2177 +  },
  1.2178 +
  1.2179 +  /**
  1.2180 +   * Gets the watch expression code string for an item in this container.
  1.2181 +   *
  1.2182 +   * @param number aIndex
  1.2183 +   *        The index used to identify the watch expression.
  1.2184 +   * @return string
  1.2185 +   *         The watch expression code string.
  1.2186 +   */
  1.2187 +  getString: function(aIndex) {
  1.2188 +    return this.getItemAtIndex(aIndex).attachment.currentExpression;
  1.2189 +  },
  1.2190 +
  1.2191 +  /**
  1.2192 +   * Gets the watch expressions code strings for all items in this container.
  1.2193 +   *
  1.2194 +   * @return array
  1.2195 +   *         The watch expressions code strings.
  1.2196 +   */
  1.2197 +  getAllStrings: function() {
  1.2198 +    return this.items.map(e => e.attachment.currentExpression);
  1.2199 +  },
  1.2200 +
  1.2201 +  /**
  1.2202 +   * Customization function for creating an item's UI.
  1.2203 +   *
  1.2204 +   * @param string aExpression
  1.2205 +   *        The watch expression string.
  1.2206 +   */
  1.2207 +  _createItemView: function(aExpression) {
  1.2208 +    let container = document.createElement("hbox");
  1.2209 +    container.className = "list-widget-item dbg-expression";
  1.2210 +
  1.2211 +    let arrowNode = document.createElement("hbox");
  1.2212 +    arrowNode.className = "dbg-expression-arrow";
  1.2213 +
  1.2214 +    let inputNode = document.createElement("textbox");
  1.2215 +    inputNode.className = "plain dbg-expression-input devtools-monospace";
  1.2216 +    inputNode.setAttribute("value", aExpression);
  1.2217 +    inputNode.setAttribute("flex", "1");
  1.2218 +
  1.2219 +    let closeNode = document.createElement("toolbarbutton");
  1.2220 +    closeNode.className = "plain variables-view-delete";
  1.2221 +
  1.2222 +    closeNode.addEventListener("click", this._onClose, false);
  1.2223 +    inputNode.addEventListener("blur", this._onBlur, false);
  1.2224 +    inputNode.addEventListener("keypress", this._onKeyPress, false);
  1.2225 +
  1.2226 +    container.appendChild(arrowNode);
  1.2227 +    container.appendChild(inputNode);
  1.2228 +    container.appendChild(closeNode);
  1.2229 +
  1.2230 +    return {
  1.2231 +      container: container,
  1.2232 +      arrowNode: arrowNode,
  1.2233 +      inputNode: inputNode,
  1.2234 +      closeNode: closeNode
  1.2235 +    };
  1.2236 +  },
  1.2237 +
  1.2238 +  /**
  1.2239 +   * Called when the add watch expression key sequence was pressed.
  1.2240 +   */
  1.2241 +  _onCmdAddExpression: function(aText) {
  1.2242 +    // Only add a new expression if there's no pending input.
  1.2243 +    if (this.getAllStrings().indexOf("") == -1) {
  1.2244 +      this.addExpression(aText || DebuggerView.editor.getSelection());
  1.2245 +    }
  1.2246 +  },
  1.2247 +
  1.2248 +  /**
  1.2249 +   * Called when the remove all watch expressions key sequence was pressed.
  1.2250 +   */
  1.2251 +  _onCmdRemoveAllExpressions: function() {
  1.2252 +    // Empty the view of all the watch expressions and clear the cache.
  1.2253 +    this.empty();
  1.2254 +
  1.2255 +    // Synchronize with the controller's watch expressions store.
  1.2256 +    DebuggerController.StackFrames.syncWatchExpressions();
  1.2257 +  },
  1.2258 +
  1.2259 +  /**
  1.2260 +   * The click listener for this container.
  1.2261 +   */
  1.2262 +  _onClick: function(e) {
  1.2263 +    if (e.button != 0) {
  1.2264 +      // Only allow left-click to trigger this event.
  1.2265 +      return;
  1.2266 +    }
  1.2267 +    let expressionItem = this.getItemForElement(e.target);
  1.2268 +    if (!expressionItem) {
  1.2269 +      // The container is empty or we didn't click on an actual item.
  1.2270 +      this.addExpression();
  1.2271 +    }
  1.2272 +  },
  1.2273 +
  1.2274 +  /**
  1.2275 +   * The click listener for a watch expression's close button.
  1.2276 +   */
  1.2277 +  _onClose: function(e) {
  1.2278 +    // Remove the watch expression.
  1.2279 +    this.remove(this.getItemForElement(e.target));
  1.2280 +
  1.2281 +    // Synchronize with the controller's watch expressions store.
  1.2282 +    DebuggerController.StackFrames.syncWatchExpressions();
  1.2283 +
  1.2284 +    // Prevent clicking the expression element itself.
  1.2285 +    e.preventDefault();
  1.2286 +    e.stopPropagation();
  1.2287 +  },
  1.2288 +
  1.2289 +  /**
  1.2290 +   * The blur listener for a watch expression's textbox.
  1.2291 +   */
  1.2292 +  _onBlur: function({ target: textbox }) {
  1.2293 +    let expressionItem = this.getItemForElement(textbox);
  1.2294 +    let oldExpression = expressionItem.attachment.currentExpression;
  1.2295 +    let newExpression = textbox.value.trim();
  1.2296 +
  1.2297 +    // Remove the watch expression if it's empty.
  1.2298 +    if (!newExpression) {
  1.2299 +      this.remove(expressionItem);
  1.2300 +    }
  1.2301 +    // Remove the watch expression if it's a duplicate.
  1.2302 +    else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) {
  1.2303 +      this.remove(expressionItem);
  1.2304 +    }
  1.2305 +    // Expression is eligible.
  1.2306 +    else {
  1.2307 +      expressionItem.attachment.currentExpression = newExpression;
  1.2308 +    }
  1.2309 +
  1.2310 +    // Synchronize with the controller's watch expressions store.
  1.2311 +    DebuggerController.StackFrames.syncWatchExpressions();
  1.2312 +  },
  1.2313 +
  1.2314 +  /**
  1.2315 +   * The keypress listener for a watch expression's textbox.
  1.2316 +   */
  1.2317 +  _onKeyPress: function(e) {
  1.2318 +    switch(e.keyCode) {
  1.2319 +      case e.DOM_VK_RETURN:
  1.2320 +      case e.DOM_VK_ESCAPE:
  1.2321 +        e.stopPropagation();
  1.2322 +        DebuggerView.editor.focus();
  1.2323 +        return;
  1.2324 +    }
  1.2325 +  }
  1.2326 +});
  1.2327 +
  1.2328 +/**
  1.2329 + * Functions handling the event listeners UI.
  1.2330 + */
  1.2331 +function EventListenersView() {
  1.2332 +  dumpn("EventListenersView was instantiated");
  1.2333 +
  1.2334 +  this._onCheck = this._onCheck.bind(this);
  1.2335 +  this._onClick = this._onClick.bind(this);
  1.2336 +}
  1.2337 +
  1.2338 +EventListenersView.prototype = Heritage.extend(WidgetMethods, {
  1.2339 +  /**
  1.2340 +   * Initialization function, called when the debugger is started.
  1.2341 +   */
  1.2342 +  initialize: function() {
  1.2343 +    dumpn("Initializing the EventListenersView");
  1.2344 +
  1.2345 +    this.widget = new SideMenuWidget(document.getElementById("event-listeners"), {
  1.2346 +      showItemCheckboxes: true,
  1.2347 +      showGroupCheckboxes: true
  1.2348 +    });
  1.2349 +
  1.2350 +    this.emptyText = L10N.getStr("noEventListenersText");
  1.2351 +    this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip");
  1.2352 +    this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " ";
  1.2353 +    this._inSourceString = " " + L10N.getStr("eventInSource") + " ";
  1.2354 +    this._inNativeCodeString = L10N.getStr("eventNative");
  1.2355 +
  1.2356 +    this.widget.addEventListener("check", this._onCheck, false);
  1.2357 +    this.widget.addEventListener("click", this._onClick, false);
  1.2358 +  },
  1.2359 +
  1.2360 +  /**
  1.2361 +   * Destruction function, called when the debugger is closed.
  1.2362 +   */
  1.2363 +  destroy: function() {
  1.2364 +    dumpn("Destroying the EventListenersView");
  1.2365 +
  1.2366 +    this.widget.removeEventListener("check", this._onCheck, false);
  1.2367 +    this.widget.removeEventListener("click", this._onClick, false);
  1.2368 +  },
  1.2369 +
  1.2370 +  /**
  1.2371 +   * Adds an event to this event listeners container.
  1.2372 +   *
  1.2373 +   * @param object aListener
  1.2374 +   *        The listener object coming from the active thread.
  1.2375 +   * @param object aOptions [optional]
  1.2376 +   *        Additional options for adding the source. Supported options:
  1.2377 +   *        - staged: true to stage the item to be appended later
  1.2378 +   */
  1.2379 +  addListener: function(aListener, aOptions = {}) {
  1.2380 +    let { node: { selector }, function: { url }, type } = aListener;
  1.2381 +    if (!type) return;
  1.2382 +
  1.2383 +    // Some listener objects may be added from plugins, thus getting
  1.2384 +    // translated to native code.
  1.2385 +    if (!url) {
  1.2386 +      url = this._inNativeCodeString;
  1.2387 +    }
  1.2388 +
  1.2389 +    // If an event item for this listener's url and type was already added,
  1.2390 +    // avoid polluting the view and simply increase the "targets" count.
  1.2391 +    let eventItem = this.getItemForPredicate(aItem =>
  1.2392 +      aItem.attachment.url == url &&
  1.2393 +      aItem.attachment.type == type);
  1.2394 +    if (eventItem) {
  1.2395 +      let { selectors, view: { targets } } = eventItem.attachment;
  1.2396 +      if (selectors.indexOf(selector) == -1) {
  1.2397 +        selectors.push(selector);
  1.2398 +        targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length));
  1.2399 +      }
  1.2400 +      return;
  1.2401 +    }
  1.2402 +
  1.2403 +    // There's no easy way of grouping event types into higher-level groups,
  1.2404 +    // so we need to do this by hand.
  1.2405 +    let is = (...args) => args.indexOf(type) != -1;
  1.2406 +    let has = str => type.contains(str);
  1.2407 +    let starts = str => type.startsWith(str);
  1.2408 +    let group;
  1.2409 +
  1.2410 +    if (starts("animation")) {
  1.2411 +      group = L10N.getStr("animationEvents");
  1.2412 +    } else if (starts("audio")) {
  1.2413 +      group = L10N.getStr("audioEvents");
  1.2414 +    } else if (is("levelchange")) {
  1.2415 +      group = L10N.getStr("batteryEvents");
  1.2416 +    } else if (is("cut", "copy", "paste")) {
  1.2417 +      group = L10N.getStr("clipboardEvents");
  1.2418 +    } else if (starts("composition")) {
  1.2419 +      group = L10N.getStr("compositionEvents");
  1.2420 +    } else if (starts("device")) {
  1.2421 +      group = L10N.getStr("deviceEvents");
  1.2422 +    } else if (is("fullscreenchange", "fullscreenerror", "orientationchange",
  1.2423 +      "overflow", "resize", "scroll", "underflow", "zoom")) {
  1.2424 +      group = L10N.getStr("displayEvents");
  1.2425 +    } else if (starts("drag") || starts("drop")) {
  1.2426 +      group = L10N.getStr("Drag and dropEvents");
  1.2427 +    } else if (starts("gamepad")) {
  1.2428 +      group = L10N.getStr("gamepadEvents");
  1.2429 +    } else if (is("canplay", "canplaythrough", "durationchange", "emptied",
  1.2430 +      "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing",
  1.2431 +      "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate",
  1.2432 +      "volumechange", "waiting")) {
  1.2433 +      group = L10N.getStr("mediaEvents");
  1.2434 +    } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) {
  1.2435 +      group = L10N.getStr("indexedDBEvents");
  1.2436 +    } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid",
  1.2437 +      "reset", "select", "submit")) {
  1.2438 +      group = L10N.getStr("interactionEvents");
  1.2439 +    } else if (starts("key") || is("input")) {
  1.2440 +      group = L10N.getStr("keyboardEvents");
  1.2441 +    } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) {
  1.2442 +      group = L10N.getStr("mouseEvents");
  1.2443 +    } else if (starts("DOM")) {
  1.2444 +      group = L10N.getStr("mutationEvents");
  1.2445 +    } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart",
  1.2446 +      "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress",
  1.2447 +      "visibilitychange")) {
  1.2448 +      group = L10N.getStr("navigationEvents");
  1.2449 +    } else if (is("pointerlockchange", "pointerlockerror")) {
  1.2450 +      group = L10N.getStr("Pointer lockEvents");
  1.2451 +    } else if (is("compassneedscalibration", "userproximity")) {
  1.2452 +      group = L10N.getStr("sensorEvents");
  1.2453 +    } else if (starts("storage")) {
  1.2454 +      group = L10N.getStr("storageEvents");
  1.2455 +    } else if (is("beginEvent", "endEvent", "repeatEvent")) {
  1.2456 +      group = L10N.getStr("timeEvents");
  1.2457 +    } else if (starts("touch")) {
  1.2458 +      group = L10N.getStr("touchEvents");
  1.2459 +    } else {
  1.2460 +      group = L10N.getStr("otherEvents");
  1.2461 +    }
  1.2462 +
  1.2463 +    // Create the element node for the event listener item.
  1.2464 +    let itemView = this._createItemView(type, selector, url);
  1.2465 +
  1.2466 +    // Event breakpoints survive target navigations. Make sure the newly
  1.2467 +    // inserted event item is correctly checked.
  1.2468 +    let checkboxState =
  1.2469 +      DebuggerController.Breakpoints.DOM.activeEventNames.indexOf(type) != -1;
  1.2470 +
  1.2471 +    // Append an event listener item to this container.
  1.2472 +    this.push([itemView.container], {
  1.2473 +      staged: aOptions.staged, /* stage the item to be appended later? */
  1.2474 +      attachment: {
  1.2475 +        url: url,
  1.2476 +        type: type,
  1.2477 +        view: itemView,
  1.2478 +        selectors: [selector],
  1.2479 +        group: group,
  1.2480 +        checkboxState: checkboxState,
  1.2481 +        checkboxTooltip: this._eventCheckboxTooltip
  1.2482 +      }
  1.2483 +    });
  1.2484 +  },
  1.2485 +
  1.2486 +  /**
  1.2487 +   * Gets all the event types known to this container.
  1.2488 +   *
  1.2489 +   * @return array
  1.2490 +   *         List of event types, for example ["load", "click"...]
  1.2491 +   */
  1.2492 +  getAllEvents: function() {
  1.2493 +    return this.attachments.map(e => e.type);
  1.2494 +  },
  1.2495 +
  1.2496 +  /**
  1.2497 +   * Gets the checked event types in this container.
  1.2498 +   *
  1.2499 +   * @return array
  1.2500 +   *         List of event types, for example ["load", "click"...]
  1.2501 +   */
  1.2502 +  getCheckedEvents: function() {
  1.2503 +    return this.attachments.filter(e => e.checkboxState).map(e => e.type);
  1.2504 +  },
  1.2505 +
  1.2506 +  /**
  1.2507 +   * Customization function for creating an item's UI.
  1.2508 +   *
  1.2509 +   * @param string aType
  1.2510 +   *        The event type, for example "click".
  1.2511 +   * @param string aSelector
  1.2512 +   *        The target element's selector.
  1.2513 +   * @param string url
  1.2514 +   *        The source url in which the event listener is located.
  1.2515 +   * @return object
  1.2516 +   *         An object containing the event listener view nodes.
  1.2517 +   */
  1.2518 +  _createItemView: function(aType, aSelector, aUrl) {
  1.2519 +    let container = document.createElement("hbox");
  1.2520 +    container.className = "dbg-event-listener";
  1.2521 +
  1.2522 +    let eventType = document.createElement("label");
  1.2523 +    eventType.className = "plain dbg-event-listener-type";
  1.2524 +    eventType.setAttribute("value", aType);
  1.2525 +    container.appendChild(eventType);
  1.2526 +
  1.2527 +    let typeSeparator = document.createElement("label");
  1.2528 +    typeSeparator.className = "plain dbg-event-listener-separator";
  1.2529 +    typeSeparator.setAttribute("value", this._onSelectorString);
  1.2530 +    container.appendChild(typeSeparator);
  1.2531 +
  1.2532 +    let eventTargets = document.createElement("label");
  1.2533 +    eventTargets.className = "plain dbg-event-listener-targets";
  1.2534 +    eventTargets.setAttribute("value", aSelector);
  1.2535 +    container.appendChild(eventTargets);
  1.2536 +
  1.2537 +    let selectorSeparator = document.createElement("label");
  1.2538 +    selectorSeparator.className = "plain dbg-event-listener-separator";
  1.2539 +    selectorSeparator.setAttribute("value", this._inSourceString);
  1.2540 +    container.appendChild(selectorSeparator);
  1.2541 +
  1.2542 +    let eventLocation = document.createElement("label");
  1.2543 +    eventLocation.className = "plain dbg-event-listener-location";
  1.2544 +    eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
  1.2545 +    eventLocation.setAttribute("flex", "1");
  1.2546 +    eventLocation.setAttribute("crop", "center");
  1.2547 +    container.appendChild(eventLocation);
  1.2548 +
  1.2549 +    return {
  1.2550 +      container: container,
  1.2551 +      type: eventType,
  1.2552 +      targets: eventTargets,
  1.2553 +      location: eventLocation
  1.2554 +    };
  1.2555 +  },
  1.2556 +
  1.2557 +  /**
  1.2558 +   * The check listener for the event listeners container.
  1.2559 +   */
  1.2560 +  _onCheck: function({ detail: { description, checked }, target }) {
  1.2561 +    if (description == "item") {
  1.2562 +      this.getItemForElement(target).attachment.checkboxState = checked;
  1.2563 +      DebuggerController.Breakpoints.DOM.scheduleEventBreakpointsUpdate();
  1.2564 +      return;
  1.2565 +    }
  1.2566 +
  1.2567 +    // Check all the event items in this group.
  1.2568 +    this.items
  1.2569 +      .filter(e => e.attachment.group == description)
  1.2570 +      .forEach(e => this.callMethod("checkItem", e.target, checked));
  1.2571 +  },
  1.2572 +
  1.2573 +  /**
  1.2574 +   * The select listener for the event listeners container.
  1.2575 +   */
  1.2576 +  _onClick: function({ target }) {
  1.2577 +    // Changing the checkbox state is handled by the _onCheck event. Avoid
  1.2578 +    // handling that again in this click event, so pass in "noSiblings"
  1.2579 +    // when retrieving the target's item, to ignore the checkbox.
  1.2580 +    let eventItem = this.getItemForElement(target, { noSiblings: true });
  1.2581 +    if (eventItem) {
  1.2582 +      let newState = eventItem.attachment.checkboxState ^= 1;
  1.2583 +      this.callMethod("checkItem", eventItem.target, newState);
  1.2584 +    }
  1.2585 +  },
  1.2586 +
  1.2587 +  _eventCheckboxTooltip: "",
  1.2588 +  _onSelectorString: "",
  1.2589 +  _inSourceString: "",
  1.2590 +  _inNativeCodeString: ""
  1.2591 +});
  1.2592 +
  1.2593 +/**
  1.2594 + * Functions handling the global search UI.
  1.2595 + */
  1.2596 +function GlobalSearchView() {
  1.2597 +  dumpn("GlobalSearchView was instantiated");
  1.2598 +
  1.2599 +  this._onHeaderClick = this._onHeaderClick.bind(this);
  1.2600 +  this._onLineClick = this._onLineClick.bind(this);
  1.2601 +  this._onMatchClick = this._onMatchClick.bind(this);
  1.2602 +}
  1.2603 +
  1.2604 +GlobalSearchView.prototype = Heritage.extend(WidgetMethods, {
  1.2605 +  /**
  1.2606 +   * Initialization function, called when the debugger is started.
  1.2607 +   */
  1.2608 +  initialize: function() {
  1.2609 +    dumpn("Initializing the GlobalSearchView");
  1.2610 +
  1.2611 +    this.widget = new SimpleListWidget(document.getElementById("globalsearch"));
  1.2612 +    this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");
  1.2613 +
  1.2614 +    this.emptyText = L10N.getStr("noMatchingStringsText");
  1.2615 +  },
  1.2616 +
  1.2617 +  /**
  1.2618 +   * Destruction function, called when the debugger is closed.
  1.2619 +   */
  1.2620 +  destroy: function() {
  1.2621 +    dumpn("Destroying the GlobalSearchView");
  1.2622 +  },
  1.2623 +
  1.2624 +  /**
  1.2625 +   * Sets the results container hidden or visible. It's hidden by default.
  1.2626 +   * @param boolean aFlag
  1.2627 +   */
  1.2628 +  set hidden(aFlag) {
  1.2629 +    this.widget.setAttribute("hidden", aFlag);
  1.2630 +    this._splitter.setAttribute("hidden", aFlag);
  1.2631 +  },
  1.2632 +
  1.2633 +  /**
  1.2634 +   * Gets the visibility state of the global search container.
  1.2635 +   * @return boolean
  1.2636 +   */
  1.2637 +  get hidden()
  1.2638 +    this.widget.getAttribute("hidden") == "true" ||
  1.2639 +    this._splitter.getAttribute("hidden") == "true",
  1.2640 +
  1.2641 +  /**
  1.2642 +   * Hides and removes all items from this search container.
  1.2643 +   */
  1.2644 +  clearView: function() {
  1.2645 +    this.hidden = true;
  1.2646 +    this.empty();
  1.2647 +  },
  1.2648 +
  1.2649 +  /**
  1.2650 +   * Selects the next found item in this container.
  1.2651 +   * Does not change the currently focused node.
  1.2652 +   */
  1.2653 +  selectNext: function() {
  1.2654 +    let totalLineResults = LineResults.size();
  1.2655 +    if (!totalLineResults) {
  1.2656 +      return;
  1.2657 +    }
  1.2658 +    if (++this._currentlyFocusedMatch >= totalLineResults) {
  1.2659 +      this._currentlyFocusedMatch = 0;
  1.2660 +    }
  1.2661 +    this._onMatchClick({
  1.2662 +      target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
  1.2663 +    });
  1.2664 +  },
  1.2665 +
  1.2666 +  /**
  1.2667 +   * Selects the previously found item in this container.
  1.2668 +   * Does not change the currently focused node.
  1.2669 +   */
  1.2670 +  selectPrev: function() {
  1.2671 +    let totalLineResults = LineResults.size();
  1.2672 +    if (!totalLineResults) {
  1.2673 +      return;
  1.2674 +    }
  1.2675 +    if (--this._currentlyFocusedMatch < 0) {
  1.2676 +      this._currentlyFocusedMatch = totalLineResults - 1;
  1.2677 +    }
  1.2678 +    this._onMatchClick({
  1.2679 +      target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
  1.2680 +    });
  1.2681 +  },
  1.2682 +
  1.2683 +  /**
  1.2684 +   * Schedules searching for a string in all of the sources.
  1.2685 +   *
  1.2686 +   * @param string aToken
  1.2687 +   *        The string to search for.
  1.2688 +   * @param number aWait
  1.2689 +   *        The amount of milliseconds to wait until draining.
  1.2690 +   */
  1.2691 +  scheduleSearch: function(aToken, aWait) {
  1.2692 +    // The amount of time to wait for the requests to settle.
  1.2693 +    let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY;
  1.2694 +    let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
  1.2695 +
  1.2696 +    // Allow requests to settle down first.
  1.2697 +    setNamedTimeout("global-search", delay, () => {
  1.2698 +      // Start fetching as many sources as possible, then perform the search.
  1.2699 +      let urls = DebuggerView.Sources.values;
  1.2700 +      let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls);
  1.2701 +      sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
  1.2702 +    });
  1.2703 +  },
  1.2704 +
  1.2705 +  /**
  1.2706 +   * Finds string matches in all the sources stored in the controller's cache,
  1.2707 +   * and groups them by url and line number.
  1.2708 +   *
  1.2709 +   * @param string aToken
  1.2710 +   *        The string to search for.
  1.2711 +   * @param array aSources
  1.2712 +   *        An array of [url, text] tuples for each source.
  1.2713 +   */
  1.2714 +  _doSearch: function(aToken, aSources) {
  1.2715 +    // Don't continue filtering if the searched token is an empty string.
  1.2716 +    if (!aToken) {
  1.2717 +      this.clearView();
  1.2718 +      return;
  1.2719 +    }
  1.2720 +
  1.2721 +    // Search is not case sensitive, prepare the actual searched token.
  1.2722 +    let lowerCaseToken = aToken.toLowerCase();
  1.2723 +    let tokenLength = aToken.length;
  1.2724 +
  1.2725 +    // Create a Map containing search details for each source.
  1.2726 +    let globalResults = new GlobalResults();
  1.2727 +
  1.2728 +    // Search for the specified token in each source's text.
  1.2729 +    for (let [url, text] of aSources) {
  1.2730 +      // Verify that the search token is found anywhere in the source.
  1.2731 +      if (!text.toLowerCase().contains(lowerCaseToken)) {
  1.2732 +        continue;
  1.2733 +      }
  1.2734 +      // ...and if so, create a Map containing search details for each line.
  1.2735 +      let sourceResults = new SourceResults(url, globalResults);
  1.2736 +
  1.2737 +      // Search for the specified token in each line's text.
  1.2738 +      text.split("\n").forEach((aString, aLine) => {
  1.2739 +        // Search is not case sensitive, prepare the actual searched line.
  1.2740 +        let lowerCaseLine = aString.toLowerCase();
  1.2741 +
  1.2742 +        // Verify that the search token is found anywhere in this line.
  1.2743 +        if (!lowerCaseLine.contains(lowerCaseToken)) {
  1.2744 +          return;
  1.2745 +        }
  1.2746 +        // ...and if so, create a Map containing search details for each word.
  1.2747 +        let lineResults = new LineResults(aLine, sourceResults);
  1.2748 +
  1.2749 +        // Search for the specified token this line's text.
  1.2750 +        lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => {
  1.2751 +          let prevLength = aPrev.length;
  1.2752 +          let currLength = aCurr.length;
  1.2753 +
  1.2754 +          // Everything before the token is unmatched.
  1.2755 +          let unmatched = aString.substr(prevLength, currLength);
  1.2756 +          lineResults.add(unmatched);
  1.2757 +
  1.2758 +          // The lowered-case line was split by the lowered-case token. So,
  1.2759 +          // get the actual matched text from the original line's text.
  1.2760 +          if (aIndex != aArray.length - 1) {
  1.2761 +            let matched = aString.substr(prevLength + currLength, tokenLength);
  1.2762 +            let range = { start: prevLength + currLength, length: matched.length };
  1.2763 +            lineResults.add(matched, range, true);
  1.2764 +          }
  1.2765 +
  1.2766 +          // Continue with the next sub-region in this line's text.
  1.2767 +          return aPrev + aToken + aCurr;
  1.2768 +        }, "");
  1.2769 +
  1.2770 +        if (lineResults.matchCount) {
  1.2771 +          sourceResults.add(lineResults);
  1.2772 +        }
  1.2773 +      });
  1.2774 +
  1.2775 +      if (sourceResults.matchCount) {
  1.2776 +        globalResults.add(sourceResults);
  1.2777 +      }
  1.2778 +    }
  1.2779 +
  1.2780 +    // Rebuild the results, then signal if there are any matches.
  1.2781 +    if (globalResults.matchCount) {
  1.2782 +      this.hidden = false;
  1.2783 +      this._currentlyFocusedMatch = -1;
  1.2784 +      this._createGlobalResultsUI(globalResults);
  1.2785 +      window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND);
  1.2786 +    } else {
  1.2787 +      window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND);
  1.2788 +    }
  1.2789 +  },
  1.2790 +
  1.2791 +  /**
  1.2792 +   * Creates global search results entries and adds them to this container.
  1.2793 +   *
  1.2794 +   * @param GlobalResults aGlobalResults
  1.2795 +   *        An object containing all source results, grouped by source location.
  1.2796 +   */
  1.2797 +  _createGlobalResultsUI: function(aGlobalResults) {
  1.2798 +    let i = 0;
  1.2799 +
  1.2800 +    for (let sourceResults of aGlobalResults) {
  1.2801 +      if (i++ == 0) {
  1.2802 +        this._createSourceResultsUI(sourceResults);
  1.2803 +      } else {
  1.2804 +        // Dispatch subsequent document manipulation operations, to avoid
  1.2805 +        // blocking the main thread when a large number of search results
  1.2806 +        // is found, thus giving the impression of faster searching.
  1.2807 +        Services.tm.currentThread.dispatch({ run:
  1.2808 +          this._createSourceResultsUI.bind(this, sourceResults)
  1.2809 +        }, 0);
  1.2810 +      }
  1.2811 +    }
  1.2812 +  },
  1.2813 +
  1.2814 +  /**
  1.2815 +   * Creates source search results entries and adds them to this container.
  1.2816 +   *
  1.2817 +   * @param SourceResults aSourceResults
  1.2818 +   *        An object containing all the matched lines for a specific source.
  1.2819 +   */
  1.2820 +  _createSourceResultsUI: function(aSourceResults) {
  1.2821 +    // Create the element node for the source results item.
  1.2822 +    let container = document.createElement("hbox");
  1.2823 +    aSourceResults.createView(container, {
  1.2824 +      onHeaderClick: this._onHeaderClick,
  1.2825 +      onLineClick: this._onLineClick,
  1.2826 +      onMatchClick: this._onMatchClick
  1.2827 +    });
  1.2828 +
  1.2829 +    // Append a source results item to this container.
  1.2830 +    let item = this.push([container], {
  1.2831 +      index: -1, /* specifies on which position should the item be appended */
  1.2832 +      attachment: {
  1.2833 +        sourceResults: aSourceResults
  1.2834 +      }
  1.2835 +    });
  1.2836 +  },
  1.2837 +
  1.2838 +  /**
  1.2839 +   * The click listener for a results header.
  1.2840 +   */
  1.2841 +  _onHeaderClick: function(e) {
  1.2842 +    let sourceResultsItem = SourceResults.getItemForElement(e.target);
  1.2843 +    sourceResultsItem.instance.toggle(e);
  1.2844 +  },
  1.2845 +
  1.2846 +  /**
  1.2847 +   * The click listener for a results line.
  1.2848 +   */
  1.2849 +  _onLineClick: function(e) {
  1.2850 +    let lineResultsItem = LineResults.getItemForElement(e.target);
  1.2851 +    this._onMatchClick({ target: lineResultsItem.firstMatch });
  1.2852 +  },
  1.2853 +
  1.2854 +  /**
  1.2855 +   * The click listener for a result match.
  1.2856 +   */
  1.2857 +  _onMatchClick: function(e) {
  1.2858 +    if (e instanceof Event) {
  1.2859 +      e.preventDefault();
  1.2860 +      e.stopPropagation();
  1.2861 +    }
  1.2862 +
  1.2863 +    let target = e.target;
  1.2864 +    let sourceResultsItem = SourceResults.getItemForElement(target);
  1.2865 +    let lineResultsItem = LineResults.getItemForElement(target);
  1.2866 +
  1.2867 +    sourceResultsItem.instance.expand();
  1.2868 +    this._currentlyFocusedMatch = LineResults.indexOfElement(target);
  1.2869 +    this._scrollMatchIntoViewIfNeeded(target);
  1.2870 +    this._bounceMatch(target);
  1.2871 +
  1.2872 +    let url = sourceResultsItem.instance.url;
  1.2873 +    let line = lineResultsItem.instance.line;
  1.2874 +
  1.2875 +    DebuggerView.setEditorLocation(url, line + 1, { noDebug: true });
  1.2876 +
  1.2877 +    let range = lineResultsItem.lineData.range;
  1.2878 +    let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 });
  1.2879 +    let [ anchor, head ] = DebuggerView.editor.getPosition(
  1.2880 +      cursor + range.start,
  1.2881 +      cursor + range.start + range.length
  1.2882 +    );
  1.2883 +
  1.2884 +    DebuggerView.editor.setSelection(anchor, head);
  1.2885 +  },
  1.2886 +
  1.2887 +  /**
  1.2888 +   * Scrolls a match into view if not already visible.
  1.2889 +   *
  1.2890 +   * @param nsIDOMNode aMatch
  1.2891 +   *        The match to scroll into view.
  1.2892 +   */
  1.2893 +  _scrollMatchIntoViewIfNeeded: function(aMatch) {
  1.2894 +    this.widget.ensureElementIsVisible(aMatch);
  1.2895 +  },
  1.2896 +
  1.2897 +  /**
  1.2898 +   * Starts a bounce animation for a match.
  1.2899 +   *
  1.2900 +   * @param nsIDOMNode aMatch
  1.2901 +   *        The match to start a bounce animation for.
  1.2902 +   */
  1.2903 +  _bounceMatch: function(aMatch) {
  1.2904 +    Services.tm.currentThread.dispatch({ run: () => {
  1.2905 +      aMatch.addEventListener("transitionend", function onEvent() {
  1.2906 +        aMatch.removeEventListener("transitionend", onEvent);
  1.2907 +        aMatch.removeAttribute("focused");
  1.2908 +      });
  1.2909 +      aMatch.setAttribute("focused", "");
  1.2910 +    }}, 0);
  1.2911 +    aMatch.setAttribute("focusing", "");
  1.2912 +  },
  1.2913 +
  1.2914 +  _splitter: null,
  1.2915 +  _currentlyFocusedMatch: -1,
  1.2916 +  _forceExpandResults: false
  1.2917 +});
  1.2918 +
  1.2919 +/**
  1.2920 + * An object containing all source results, grouped by source location.
  1.2921 + * Iterable via "for (let [location, sourceResults] of globalResults) { }".
  1.2922 + */
  1.2923 +function GlobalResults() {
  1.2924 +  this._store = [];
  1.2925 +  SourceResults._itemsByElement = new Map();
  1.2926 +  LineResults._itemsByElement = new Map();
  1.2927 +}
  1.2928 +
  1.2929 +GlobalResults.prototype = {
  1.2930 +  /**
  1.2931 +   * Adds source results to this store.
  1.2932 +   *
  1.2933 +   * @param SourceResults aSourceResults
  1.2934 +   *        An object containing search results for a specific source.
  1.2935 +   */
  1.2936 +  add: function(aSourceResults) {
  1.2937 +    this._store.push(aSourceResults);
  1.2938 +  },
  1.2939 +
  1.2940 +  /**
  1.2941 +   * Gets the number of source results in this store.
  1.2942 +   */
  1.2943 +  get matchCount() this._store.length
  1.2944 +};
  1.2945 +
  1.2946 +/**
  1.2947 + * An object containing all the matched lines for a specific source.
  1.2948 + * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }".
  1.2949 + *
  1.2950 + * @param string aUrl
  1.2951 + *        The target source url.
  1.2952 + * @param GlobalResults aGlobalResults
  1.2953 + *        An object containing all source results, grouped by source location.
  1.2954 + */
  1.2955 +function SourceResults(aUrl, aGlobalResults) {
  1.2956 +  this.url = aUrl;
  1.2957 +  this._globalResults = aGlobalResults;
  1.2958 +  this._store = [];
  1.2959 +}
  1.2960 +
  1.2961 +SourceResults.prototype = {
  1.2962 +  /**
  1.2963 +   * Adds line results to this store.
  1.2964 +   *
  1.2965 +   * @param LineResults aLineResults
  1.2966 +   *        An object containing search results for a specific line.
  1.2967 +   */
  1.2968 +  add: function(aLineResults) {
  1.2969 +    this._store.push(aLineResults);
  1.2970 +  },
  1.2971 +
  1.2972 +  /**
  1.2973 +   * Gets the number of line results in this store.
  1.2974 +   */
  1.2975 +  get matchCount() this._store.length,
  1.2976 +
  1.2977 +  /**
  1.2978 +   * Expands the element, showing all the added details.
  1.2979 +   */
  1.2980 +  expand: function() {
  1.2981 +    this._resultsContainer.removeAttribute("hidden");
  1.2982 +    this._arrow.setAttribute("open", "");
  1.2983 +  },
  1.2984 +
  1.2985 +  /**
  1.2986 +   * Collapses the element, hiding all the added details.
  1.2987 +   */
  1.2988 +  collapse: function() {
  1.2989 +    this._resultsContainer.setAttribute("hidden", "true");
  1.2990 +    this._arrow.removeAttribute("open");
  1.2991 +  },
  1.2992 +
  1.2993 +  /**
  1.2994 +   * Toggles between the element collapse/expand state.
  1.2995 +   */
  1.2996 +  toggle: function(e) {
  1.2997 +    this.expanded ^= 1;
  1.2998 +  },
  1.2999 +
  1.3000 +  /**
  1.3001 +   * Gets this element's expanded state.
  1.3002 +   * @return boolean
  1.3003 +   */
  1.3004 +  get expanded()
  1.3005 +    this._resultsContainer.getAttribute("hidden") != "true" &&
  1.3006 +    this._arrow.hasAttribute("open"),
  1.3007 +
  1.3008 +  /**
  1.3009 +   * Sets this element's expanded state.
  1.3010 +   * @param boolean aFlag
  1.3011 +   */
  1.3012 +  set expanded(aFlag) this[aFlag ? "expand" : "collapse"](),
  1.3013 +
  1.3014 +  /**
  1.3015 +   * Gets the element associated with this item.
  1.3016 +   * @return nsIDOMNode
  1.3017 +   */
  1.3018 +  get target() this._target,
  1.3019 +
  1.3020 +  /**
  1.3021 +   * Customization function for creating this item's UI.
  1.3022 +   *
  1.3023 +   * @param nsIDOMNode aElementNode
  1.3024 +   *        The element associated with the displayed item.
  1.3025 +   * @param object aCallbacks
  1.3026 +   *        An object containing all the necessary callback functions:
  1.3027 +   *          - onHeaderClick
  1.3028 +   *          - onMatchClick
  1.3029 +   */
  1.3030 +  createView: function(aElementNode, aCallbacks) {
  1.3031 +    this._target = aElementNode;
  1.3032 +
  1.3033 +    let arrow = this._arrow = document.createElement("box");
  1.3034 +    arrow.className = "arrow";
  1.3035 +
  1.3036 +    let locationNode = document.createElement("label");
  1.3037 +    locationNode.className = "plain dbg-results-header-location";
  1.3038 +    locationNode.setAttribute("value", this.url);
  1.3039 +
  1.3040 +    let matchCountNode = document.createElement("label");
  1.3041 +    matchCountNode.className = "plain dbg-results-header-match-count";
  1.3042 +    matchCountNode.setAttribute("value", "(" + this.matchCount + ")");
  1.3043 +
  1.3044 +    let resultsHeader = this._resultsHeader = document.createElement("hbox");
  1.3045 +    resultsHeader.className = "dbg-results-header";
  1.3046 +    resultsHeader.setAttribute("align", "center")
  1.3047 +    resultsHeader.appendChild(arrow);
  1.3048 +    resultsHeader.appendChild(locationNode);
  1.3049 +    resultsHeader.appendChild(matchCountNode);
  1.3050 +    resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false);
  1.3051 +
  1.3052 +    let resultsContainer = this._resultsContainer = document.createElement("vbox");
  1.3053 +    resultsContainer.className = "dbg-results-container";
  1.3054 +    resultsContainer.setAttribute("hidden", "true");
  1.3055 +
  1.3056 +    // Create lines search results entries and add them to this container.
  1.3057 +    // Afterwards, if the number of matches is reasonable, expand this
  1.3058 +    // container automatically.
  1.3059 +    for (let lineResults of this._store) {
  1.3060 +      lineResults.createView(resultsContainer, aCallbacks);
  1.3061 +    }
  1.3062 +    if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) {
  1.3063 +      this.expand();
  1.3064 +    }
  1.3065 +
  1.3066 +    let resultsBox = document.createElement("vbox");
  1.3067 +    resultsBox.setAttribute("flex", "1");
  1.3068 +    resultsBox.appendChild(resultsHeader);
  1.3069 +    resultsBox.appendChild(resultsContainer);
  1.3070 +
  1.3071 +    aElementNode.id = "source-results-" + this.url;
  1.3072 +    aElementNode.className = "dbg-source-results";
  1.3073 +    aElementNode.appendChild(resultsBox);
  1.3074 +
  1.3075 +    SourceResults._itemsByElement.set(aElementNode, { instance: this });
  1.3076 +  },
  1.3077 +
  1.3078 +  url: "",
  1.3079 +  _globalResults: null,
  1.3080 +  _store: null,
  1.3081 +  _target: null,
  1.3082 +  _arrow: null,
  1.3083 +  _resultsHeader: null,
  1.3084 +  _resultsContainer: null
  1.3085 +};
  1.3086 +
  1.3087 +/**
  1.3088 + * An object containing all the matches for a specific line.
  1.3089 + * Iterable via "for (let chunk of lineResults) { }".
  1.3090 + *
  1.3091 + * @param number aLine
  1.3092 + *        The target line in the source.
  1.3093 + * @param SourceResults aSourceResults
  1.3094 + *        An object containing all the matched lines for a specific source.
  1.3095 + */
  1.3096 +function LineResults(aLine, aSourceResults) {
  1.3097 +  this.line = aLine;
  1.3098 +  this._sourceResults = aSourceResults;
  1.3099 +  this._store = [];
  1.3100 +  this._matchCount = 0;
  1.3101 +}
  1.3102 +
  1.3103 +LineResults.prototype = {
  1.3104 +  /**
  1.3105 +   * Adds string details to this store.
  1.3106 +   *
  1.3107 +   * @param string aString
  1.3108 +   *        The text contents chunk in the line.
  1.3109 +   * @param object aRange
  1.3110 +   *        An object containing the { start, length } of the chunk.
  1.3111 +   * @param boolean aMatchFlag
  1.3112 +   *        True if the chunk is a matched string, false if just text content.
  1.3113 +   */
  1.3114 +  add: function(aString, aRange, aMatchFlag) {
  1.3115 +    this._store.push({ string: aString, range: aRange, match: !!aMatchFlag });
  1.3116 +    this._matchCount += aMatchFlag ? 1 : 0;
  1.3117 +  },
  1.3118 +
  1.3119 +  /**
  1.3120 +   * Gets the number of word results in this store.
  1.3121 +   */
  1.3122 +  get matchCount() this._matchCount,
  1.3123 +
  1.3124 +  /**
  1.3125 +   * Gets the element associated with this item.
  1.3126 +   * @return nsIDOMNode
  1.3127 +   */
  1.3128 +  get target() this._target,
  1.3129 +
  1.3130 +  /**
  1.3131 +   * Customization function for creating this item's UI.
  1.3132 +   *
  1.3133 +   * @param nsIDOMNode aElementNode
  1.3134 +   *        The element associated with the displayed item.
  1.3135 +   * @param object aCallbacks
  1.3136 +   *        An object containing all the necessary callback functions:
  1.3137 +   *          - onMatchClick
  1.3138 +   *          - onLineClick
  1.3139 +   */
  1.3140 +  createView: function(aElementNode, aCallbacks) {
  1.3141 +    this._target = aElementNode;
  1.3142 +
  1.3143 +    let lineNumberNode = document.createElement("label");
  1.3144 +    lineNumberNode.className = "plain dbg-results-line-number";
  1.3145 +    lineNumberNode.classList.add("devtools-monospace");
  1.3146 +    lineNumberNode.setAttribute("value", this.line + 1);
  1.3147 +
  1.3148 +    let lineContentsNode = document.createElement("hbox");
  1.3149 +    lineContentsNode.className = "dbg-results-line-contents";
  1.3150 +    lineContentsNode.classList.add("devtools-monospace");
  1.3151 +    lineContentsNode.setAttribute("flex", "1");
  1.3152 +
  1.3153 +    let lineString = "";
  1.3154 +    let lineLength = 0;
  1.3155 +    let firstMatch = null;
  1.3156 +
  1.3157 +    for (let lineChunk of this._store) {
  1.3158 +      let { string, range, match } = lineChunk;
  1.3159 +      lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength);
  1.3160 +      lineLength += string.length;
  1.3161 +
  1.3162 +      let lineChunkNode = document.createElement("label");
  1.3163 +      lineChunkNode.className = "plain dbg-results-line-contents-string";
  1.3164 +      lineChunkNode.setAttribute("value", lineString);
  1.3165 +      lineChunkNode.setAttribute("match", match);
  1.3166 +      lineContentsNode.appendChild(lineChunkNode);
  1.3167 +
  1.3168 +      if (match) {
  1.3169 +        this._entangleMatch(lineChunkNode, lineChunk);
  1.3170 +        lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false);
  1.3171 +        firstMatch = firstMatch || lineChunkNode;
  1.3172 +      }
  1.3173 +      if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
  1.3174 +        lineContentsNode.appendChild(this._ellipsis.cloneNode(true));
  1.3175 +        break;
  1.3176 +      }
  1.3177 +    }
  1.3178 +
  1.3179 +    this._entangleLine(lineContentsNode, firstMatch);
  1.3180 +    lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false);
  1.3181 +
  1.3182 +    let searchResult = document.createElement("hbox");
  1.3183 +    searchResult.className = "dbg-search-result";
  1.3184 +    searchResult.appendChild(lineNumberNode);
  1.3185 +    searchResult.appendChild(lineContentsNode);
  1.3186 +
  1.3187 +    aElementNode.appendChild(searchResult);
  1.3188 +  },
  1.3189 +
  1.3190 +  /**
  1.3191 +   * Handles a match while creating the view.
  1.3192 +   * @param nsIDOMNode aNode
  1.3193 +   * @param object aMatchChunk
  1.3194 +   */
  1.3195 +  _entangleMatch: function(aNode, aMatchChunk) {
  1.3196 +    LineResults._itemsByElement.set(aNode, {
  1.3197 +      instance: this,
  1.3198 +      lineData: aMatchChunk
  1.3199 +    });
  1.3200 +  },
  1.3201 +
  1.3202 +  /**
  1.3203 +   * Handles a line while creating the view.
  1.3204 +   * @param nsIDOMNode aNode
  1.3205 +   * @param nsIDOMNode aFirstMatch
  1.3206 +   */
  1.3207 +  _entangleLine: function(aNode, aFirstMatch) {
  1.3208 +    LineResults._itemsByElement.set(aNode, {
  1.3209 +      instance: this,
  1.3210 +      firstMatch: aFirstMatch,
  1.3211 +      ignored: true
  1.3212 +    });
  1.3213 +  },
  1.3214 +
  1.3215 +  /**
  1.3216 +   * An nsIDOMNode label with an ellipsis value.
  1.3217 +   */
  1.3218 +  _ellipsis: (function() {
  1.3219 +    let label = document.createElement("label");
  1.3220 +    label.className = "plain dbg-results-line-contents-string";
  1.3221 +    label.setAttribute("value", L10N.ellipsis);
  1.3222 +    return label;
  1.3223 +  })(),
  1.3224 +
  1.3225 +  line: 0,
  1.3226 +  _sourceResults: null,
  1.3227 +  _store: null,
  1.3228 +  _target: null
  1.3229 +};
  1.3230 +
  1.3231 +/**
  1.3232 + * A generator-iterator over the global, source or line results.
  1.3233 + */
  1.3234 +GlobalResults.prototype["@@iterator"] =
  1.3235 +SourceResults.prototype["@@iterator"] =
  1.3236 +LineResults.prototype["@@iterator"] = function*() {
  1.3237 +  yield* this._store;
  1.3238 +};
  1.3239 +
  1.3240 +/**
  1.3241 + * Gets the item associated with the specified element.
  1.3242 + *
  1.3243 + * @param nsIDOMNode aElement
  1.3244 + *        The element used to identify the item.
  1.3245 + * @return object
  1.3246 + *         The matched item, or null if nothing is found.
  1.3247 + */
  1.3248 +SourceResults.getItemForElement =
  1.3249 +LineResults.getItemForElement = function(aElement) {
  1.3250 +  return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true });
  1.3251 +};
  1.3252 +
  1.3253 +/**
  1.3254 + * Gets the element associated with a particular item at a specified index.
  1.3255 + *
  1.3256 + * @param number aIndex
  1.3257 + *        The index used to identify the item.
  1.3258 + * @return nsIDOMNode
  1.3259 + *         The matched element, or null if nothing is found.
  1.3260 + */
  1.3261 +SourceResults.getElementAtIndex =
  1.3262 +LineResults.getElementAtIndex = function(aIndex) {
  1.3263 +  for (let [element, item] of this._itemsByElement) {
  1.3264 +    if (!item.ignored && !aIndex--) {
  1.3265 +      return element;
  1.3266 +    }
  1.3267 +  }
  1.3268 +  return null;
  1.3269 +};
  1.3270 +
  1.3271 +/**
  1.3272 + * Gets the index of an item associated with the specified element.
  1.3273 + *
  1.3274 + * @param nsIDOMNode aElement
  1.3275 + *        The element to get the index for.
  1.3276 + * @return number
  1.3277 + *         The index of the matched element, or -1 if nothing is found.
  1.3278 + */
  1.3279 +SourceResults.indexOfElement =
  1.3280 +LineResults.indexOfElement = function(aElement) {
  1.3281 +  let count = 0;
  1.3282 +  for (let [element, item] of this._itemsByElement) {
  1.3283 +    if (element == aElement) {
  1.3284 +      return count;
  1.3285 +    }
  1.3286 +    if (!item.ignored) {
  1.3287 +      count++;
  1.3288 +    }
  1.3289 +  }
  1.3290 +  return -1;
  1.3291 +};
  1.3292 +
  1.3293 +/**
  1.3294 + * Gets the number of cached items associated with a specified element.
  1.3295 + *
  1.3296 + * @return number
  1.3297 + *         The number of key/value pairs in the corresponding map.
  1.3298 + */
  1.3299 +SourceResults.size =
  1.3300 +LineResults.size = function() {
  1.3301 +  let count = 0;
  1.3302 +  for (let [, item] of this._itemsByElement) {
  1.3303 +    if (!item.ignored) {
  1.3304 +      count++;
  1.3305 +    }
  1.3306 +  }
  1.3307 +  return count;
  1.3308 +};
  1.3309 +
  1.3310 +/**
  1.3311 + * Preliminary setup for the DebuggerView object.
  1.3312 + */
  1.3313 +DebuggerView.Sources = new SourcesView();
  1.3314 +DebuggerView.VariableBubble = new VariableBubbleView();
  1.3315 +DebuggerView.Tracer = new TracerView();
  1.3316 +DebuggerView.WatchExpressions = new WatchExpressionsView();
  1.3317 +DebuggerView.EventListeners = new EventListenersView();
  1.3318 +DebuggerView.GlobalSearch = new GlobalSearchView();

mercurial