/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");

// Used to detect minification for automatic pretty printing
const SAMPLE_SIZE = 50; // no of lines
const INDENT_COUNT_THRESHOLD = 5; // percentage
const CHARACTER_LIMIT = 250; // line character limit

// Maps known URLs to friendly source group names
const KNOWN_SOURCE_GROUPS = {
  "Add-on SDK": "resource://gre/modules/commonjs/",
};

/**
 * Functions handling the sources UI.
 */
function SourcesView() {
  dumpn("SourcesView was instantiated");

  this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
  this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this);
  this.toggleBreakpoints = this.toggleBreakpoints.bind(this);

  this._onEditorLoad = this._onEditorLoad.bind(this);
  this._onEditorUnload = this._onEditorUnload.bind(this);
  this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this);
  this._onSourceSelect = this._onSourceSelect.bind(this);
  this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this);
  this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
  this._onBreakpointClick = this._onBreakpointClick.bind(this);
  this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this);
  this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this);
  this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
  this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
  this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);

  this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this);
}

SourcesView.prototype = Heritage.extend(WidgetMethods, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function() {
    dumpn("Initializing the SourcesView");

    this.widget = new SideMenuWidget(document.getElementById("sources"), {
      showArrows: true
    });

    // Sort known source groups towards the end of the list
    this.widget.groupSortPredicate = function(a, b) {
      if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
        return a.localeCompare(b);
      }

      return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
    };

    this.emptyText = L10N.getStr("noSourcesText");
    this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");

    this._commandset = document.getElementById("debuggerCommands");
    this._popupset = document.getElementById("debuggerPopupset");
    this._cmPopup = document.getElementById("sourceEditorContextMenu");
    this._cbPanel = document.getElementById("conditional-breakpoint-panel");
    this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox");
    this._blackBoxButton = document.getElementById("black-box");
    this._stopBlackBoxButton = document.getElementById("black-boxed-message-button");
    this._prettyPrintButton = document.getElementById("pretty-print");
    this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints");

    if (Prefs.prettyPrintEnabled) {
      this._prettyPrintButton.removeAttribute("hidden");
    }

    window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false);
    window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false);
    this.widget.addEventListener("select", this._onSourceSelect, false);
    this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false);
    this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false);
    this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false);
    this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false);
    this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false);

    this.autoFocusOnSelection = false;

    // Sort the contents by the displayed label.
    this.sortContents((aFirst, aSecond) => {
      return +(aFirst.attachment.label.toLowerCase() >
               aSecond.attachment.label.toLowerCase());
    });
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function() {
    dumpn("Destroying the SourcesView");

    window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false);
    window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false);
    this.widget.removeEventListener("select", this._onSourceSelect, false);
    this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false);
    this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
    this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false);
    this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
    this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);
  },

  /**
   * Sets the preferred location to be selected in this sources container.
   * @param string aUrl
   */
  set preferredSource(aUrl) {
    this._preferredValue = aUrl;

    // Selects the element with the specified value in this sources container,
    // if already inserted.
    if (this.containsValue(aUrl)) {
      this.selectedValue = aUrl;
    }
  },

  /**
   * Adds a source to this sources container.
   *
   * @param object aSource
   *        The source object coming from the active thread.
   * @param object aOptions [optional]
   *        Additional options for adding the source. Supported options:
   *        - staged: true to stage the item to be appended later
   */
  addSource: function(aSource, aOptions = {}) {
    let fullUrl = aSource.url;
    let url = fullUrl.split(" -> ").pop();
    let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url);
    let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url);
    let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl));

    let contents = document.createElement("label");
    contents.className = "plain dbg-source-item";
    contents.setAttribute("value", label);
    contents.setAttribute("crop", "start");
    contents.setAttribute("flex", "1");
    contents.setAttribute("tooltiptext", unicodeUrl);

    // Append a source item to this container.
    this.push([contents, fullUrl], {
      staged: aOptions.staged, /* stage the item to be appended later? */
      attachment: {
        label: label,
        group: group,
        checkboxState: !aSource.isBlackBoxed,
        checkboxTooltip: this._blackBoxCheckboxTooltip,
        source: aSource
      }
    });
  },

  /**
   * Adds a breakpoint to this sources container.
   *
   * @param object aBreakpointData
   *        Information about the breakpoint to be shown.
   *        This object must have the following properties:
   *          - location: the breakpoint's source location and line number
   *          - disabled: the breakpoint's disabled state, boolean
   *          - text: the breakpoint's line text to be displayed
   * @param object aOptions [optional]
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  addBreakpoint: function(aBreakpointData, aOptions = {}) {
    let { location, disabled } = aBreakpointData;

    // Make sure we're not duplicating anything. If a breakpoint at the
    // specified source url and line already exists, just toggle it.
    if (this.getBreakpoint(location)) {
      this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location);
      return;
    }

    // Get the source item to which the breakpoint should be attached.
    let sourceItem = this.getItemByValue(location.url);

    // Create the element node and menu popup for the breakpoint item.
    let breakpointArgs = Heritage.extend(aBreakpointData, aOptions);
    let breakpointView = this._createBreakpointView.call(this, breakpointArgs);
    let contextMenu = this._createContextMenu.call(this, breakpointArgs);

    // Append a breakpoint child item to the corresponding source item.
    sourceItem.append(breakpointView.container, {
      attachment: Heritage.extend(breakpointArgs, {
        url: location.url,
        line: location.line,
        view: breakpointView,
        popup: contextMenu
      }),
      attributes: [
        ["contextmenu", contextMenu.menupopupId]
      ],
      // Make sure that when the breakpoint item is removed, the corresponding
      // menupopup and commandset are also destroyed.
      finalize: this._onBreakpointRemoved
    });

    // Highlight the newly appended breakpoint child item if necessary.
    if (aOptions.openPopup || !aOptions.noEditorUpdate) {
      this.highlightBreakpoint(location, aOptions);
    }
  },

  /**
   * Removes a breakpoint from this sources container.
   * It does not also remove the breakpoint from the controller. Be careful.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  removeBreakpoint: function(aLocation) {
    // When a parent source item is removed, all the child breakpoint items are
    // also automagically removed.
    let sourceItem = this.getItemByValue(aLocation.url);
    if (!sourceItem) {
      return;
    }
    let breakpointItem = this.getBreakpoint(aLocation);
    if (!breakpointItem) {
      return;
    }

    // Clear the breakpoint view.
    sourceItem.remove(breakpointItem);
  },

  /**
   * Returns the breakpoint at the specified source url and line.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @return object
   *         The corresponding breakpoint item if found, null otherwise.
   */
  getBreakpoint: function(aLocation) {
    return this.getItemForPredicate(aItem =>
      aItem.attachment.url == aLocation.url &&
      aItem.attachment.line == aLocation.line);
  },

  /**
   * Returns all breakpoints for all sources.
   *
   * @return array
   *         The breakpoints for all sources if any, an empty array otherwise.
   */
  getAllBreakpoints: function(aStore = []) {
    return this.getOtherBreakpoints(undefined, aStore);
  },

  /**
   * Returns all breakpoints which are not at the specified source url and line.
   *
   * @param object aLocation [optional]
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @param array aStore [optional]
   *        A list in which to store the corresponding breakpoints.
   * @return array
   *         The corresponding breakpoints if found, an empty array otherwise.
   */
  getOtherBreakpoints: function(aLocation = {}, aStore = []) {
    for (let source of this) {
      for (let breakpointItem of source) {
        let { url, line } = breakpointItem.attachment;
        if (url != aLocation.url || line != aLocation.line) {
          aStore.push(breakpointItem);
        }
      }
    }
    return aStore;
  },

  /**
   * Enables a breakpoint.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @param object aOptions [optional]
   *        Additional options or flags supported by this operation:
   *          - silent: pass true to not update the checkbox checked state;
   *                    this is usually necessary when the checked state will
   *                    be updated automatically (e.g: on a checkbox click).
   * @return object
   *         A promise that is resolved after the breakpoint is enabled, or
   *         rejected if no breakpoint was found at the specified location.
   */
  enableBreakpoint: function(aLocation, aOptions = {}) {
    let breakpointItem = this.getBreakpoint(aLocation);
    if (!breakpointItem) {
      return promise.reject(new Error("No breakpoint found."));
    }

    // Breakpoint will now be enabled.
    let attachment = breakpointItem.attachment;
    attachment.disabled = false;

    // Update the corresponding menu items to reflect the enabled state.
    let prefix = "bp-cMenu-"; // "breakpoints context menu"
    let identifier = DebuggerController.Breakpoints.getIdentifier(attachment);
    let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
    let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
    document.getElementById(enableSelfId).setAttribute("hidden", "true");
    document.getElementById(disableSelfId).removeAttribute("hidden");

    // Update the breakpoint toggle button checked state.
    this._toggleBreakpointsButton.removeAttribute("checked");

    // Update the checkbox state if necessary.
    if (!aOptions.silent) {
      attachment.view.checkbox.setAttribute("checked", "true");
    }

    return DebuggerController.Breakpoints.addBreakpoint(aLocation, {
      // No need to update the pane, since this method is invoked because
      // a breakpoint's view was interacted with.
      noPaneUpdate: true
    });
  },

  /**
   * Disables a breakpoint.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @param object aOptions [optional]
   *        Additional options or flags supported by this operation:
   *          - silent: pass true to not update the checkbox checked state;
   *                    this is usually necessary when the checked state will
   *                    be updated automatically (e.g: on a checkbox click).
   * @return object
   *         A promise that is resolved after the breakpoint is disabled, or
   *         rejected if no breakpoint was found at the specified location.
   */
  disableBreakpoint: function(aLocation, aOptions = {}) {
    let breakpointItem = this.getBreakpoint(aLocation);
    if (!breakpointItem) {
      return promise.reject(new Error("No breakpoint found."));
    }

    // Breakpoint will now be disabled.
    let attachment = breakpointItem.attachment;
    attachment.disabled = true;

    // Update the corresponding menu items to reflect the disabled state.
    let prefix = "bp-cMenu-"; // "breakpoints context menu"
    let identifier = DebuggerController.Breakpoints.getIdentifier(attachment);
    let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
    let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
    document.getElementById(enableSelfId).removeAttribute("hidden");
    document.getElementById(disableSelfId).setAttribute("hidden", "true");

    // Update the checkbox state if necessary.
    if (!aOptions.silent) {
      attachment.view.checkbox.removeAttribute("checked");
    }

    return DebuggerController.Breakpoints.removeBreakpoint(aLocation, {
      // No need to update this pane, since this method is invoked because
      // a breakpoint's view was interacted with.
      noPaneUpdate: true,
      // Mark this breakpoint as being "disabled", not completely removed.
      // This makes sure it will not be forgotten across target navigations.
      rememberDisabled: true
    });
  },

  /**
   * Highlights a breakpoint in this sources container.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @param object aOptions [optional]
   *        An object containing some of the following boolean properties:
   *          - openPopup: tells if the expression popup should be shown.
   *          - noEditorUpdate: tells if you want to skip editor updates.
   */
  highlightBreakpoint: function(aLocation, aOptions = {}) {
    let breakpointItem = this.getBreakpoint(aLocation);
    if (!breakpointItem) {
      return;
    }

    // Breakpoint will now be selected.
    this._selectBreakpoint(breakpointItem);

    // Update the editor location if necessary.
    if (!aOptions.noEditorUpdate) {
      DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true });
    }

    // If the breakpoint requires a new conditional expression, display
    // the panel to input the corresponding expression.
    if (aOptions.openPopup) {
      this._openConditionalPopup();
    } else {
      this._hideConditionalPopup();
    }
  },

  /**
   * Unhighlights the current breakpoint in this sources container.
   */
  unhighlightBreakpoint: function() {
    this._unselectBreakpoint();
    this._hideConditionalPopup();
  },

  /**
   * Update the checked/unchecked and enabled/disabled states of the buttons in
   * the sources toolbar based on the currently selected source's state.
   */
  updateToolbarButtonsState: function() {
    const { source } = this.selectedItem.attachment;
    const sourceClient = gThreadClient.source(source);

    if (sourceClient.isBlackBoxed) {
      this._prettyPrintButton.setAttribute("disabled", true);
      this._blackBoxButton.setAttribute("checked", true);
    } else {
      this._prettyPrintButton.removeAttribute("disabled");
      this._blackBoxButton.removeAttribute("checked");
    }

    if (sourceClient.isPrettyPrinted) {
      this._prettyPrintButton.setAttribute("checked", true);
    } else {
      this._prettyPrintButton.removeAttribute("checked");
    }
  },

  /**
   * Toggle the pretty printing of the selected source.
   */
  togglePrettyPrint: function() {
    if (this._prettyPrintButton.hasAttribute("disabled")) {
      return;
    }

    const resetEditor = ([{ url }]) => {
      // Only set the text when the source is still selected.
      if (url == this.selectedValue) {
        DebuggerView.setEditorLocation(url, 0, { force: true });
      }
    };

    const printError = ([{ url }, error]) => {
      DevToolsUtils.reportException("togglePrettyPrint", error);
    };

    DebuggerView.showProgressBar();
    const { source } = this.selectedItem.attachment;
    const sourceClient = gThreadClient.source(source);
    const shouldPrettyPrint = !sourceClient.isPrettyPrinted;

    if (shouldPrettyPrint) {
      this._prettyPrintButton.setAttribute("checked", true);
    } else {
      this._prettyPrintButton.removeAttribute("checked");
    }

    DebuggerController.SourceScripts.togglePrettyPrint(source)
      .then(resetEditor, printError)
      .then(DebuggerView.showEditor)
      .then(this.updateToolbarButtonsState);
  },

  /**
   * Toggle the black boxed state of the selected source.
   */
  toggleBlackBoxing: function() {
    const { source } = this.selectedItem.attachment;
    const sourceClient = gThreadClient.source(source);
    const shouldBlackBox = !sourceClient.isBlackBoxed;

    // Be optimistic that the (un-)black boxing will succeed, so enable/disable
    // the pretty print button and check/uncheck the black box button
    // immediately. Then, once we actually get the results from the server, make
    // sure that it is in the correct state again by calling
    // `updateToolbarButtonsState`.

    if (shouldBlackBox) {
      this._prettyPrintButton.setAttribute("disabled", true);
      this._blackBoxButton.setAttribute("checked", true);
    } else {
      this._prettyPrintButton.removeAttribute("disabled");
      this._blackBoxButton.removeAttribute("checked");
    }

    DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox)
      .then(this.updateToolbarButtonsState,
            this.updateToolbarButtonsState);
  },

  /**
   * Toggles all breakpoints enabled/disabled.
   */
  toggleBreakpoints: function() {
    let breakpoints = this.getAllBreakpoints();
    let hasBreakpoints = breakpoints.length > 0;
    let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled);

    if (hasBreakpoints && hasEnabledBreakpoints) {
      this._toggleBreakpointsButton.setAttribute("checked", true);
      this._onDisableAll();
    } else {
      this._toggleBreakpointsButton.removeAttribute("checked");
      this._onEnableAll();
    }
  },

  /**
   * Marks a breakpoint as selected in this sources container.
   *
   * @param object aItem
   *        The breakpoint item to select.
   */
  _selectBreakpoint: function(aItem) {
    if (this._selectedBreakpointItem == aItem) {
      return;
    }
    this._unselectBreakpoint();
    this._selectedBreakpointItem = aItem;
    this._selectedBreakpointItem.target.classList.add("selected");

    // Ensure the currently selected breakpoint is visible.
    this.widget.ensureElementIsVisible(aItem.target);
  },

  /**
   * Marks the current breakpoint as unselected in this sources container.
   */
  _unselectBreakpoint: function() {
    if (!this._selectedBreakpointItem) {
      return;
    }
    this._selectedBreakpointItem.target.classList.remove("selected");
    this._selectedBreakpointItem = null;
  },

  /**
   * Opens a conditional breakpoint's expression input popup.
   */
  _openConditionalPopup: function() {
    let breakpointItem = this._selectedBreakpointItem;
    let attachment = breakpointItem.attachment;
    // Check if this is an enabled conditional breakpoint, and if so,
    // retrieve the current conditional epression.
    let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
    if (breakpointPromise) {
      breakpointPromise.then(aBreakpointClient => {
        let isConditionalBreakpoint = aBreakpointClient.hasCondition();
        let condition = aBreakpointClient.getCondition();
        doOpen.call(this, isConditionalBreakpoint ? condition : "")
      });
    } else {
      doOpen.call(this, "")
    }

    function doOpen(aConditionalExpression) {
      // Update the conditional expression textbox. If no expression was
      // previously set, revert to using an empty string by default.
      this._cbTextbox.value = aConditionalExpression;

      // Show the conditional expression panel. The popup arrow should be pointing
      // at the line number node in the breakpoint item view.
      this._cbPanel.hidden = false;
      this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber,
        BREAKPOINT_CONDITIONAL_POPUP_POSITION,
        BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
        BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
    }
  },

  /**
   * Hides a conditional breakpoint's expression input popup.
   */
  _hideConditionalPopup: function() {
    this._cbPanel.hidden = true;

    // Sometimes this._cbPanel doesn't have hidePopup method which doesn't
    // break anything but simply outputs an exception to the console.
    if (this._cbPanel.hidePopup) {
      this._cbPanel.hidePopup();
    }
  },

  /**
   * Customization function for creating a breakpoint item's UI.
   *
   * @param object aOptions
   *        A couple of options or flags supported by this operation:
   *          - location: the breakpoint's source location and line number
   *          - disabled: the breakpoint's disabled state, boolean
   *          - text: the breakpoint's line text to be displayed
   * @return object
   *         An object containing the breakpoint container, checkbox,
   *         line number and line text nodes.
   */
  _createBreakpointView: function(aOptions) {
    let { location, disabled, text } = aOptions;
    let identifier = DebuggerController.Breakpoints.getIdentifier(location);

    let checkbox = document.createElement("checkbox");
    checkbox.setAttribute("checked", !disabled);
    checkbox.className = "dbg-breakpoint-checkbox";

    let lineNumberNode = document.createElement("label");
    lineNumberNode.className = "plain dbg-breakpoint-line";
    lineNumberNode.setAttribute("value", location.line);

    let lineTextNode = document.createElement("label");
    lineTextNode.className = "plain dbg-breakpoint-text";
    lineTextNode.setAttribute("value", text);
    lineTextNode.setAttribute("crop", "end");
    lineTextNode.setAttribute("flex", "1");

    let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH);
    lineTextNode.setAttribute("tooltiptext", tooltip);

    let container = document.createElement("hbox");
    container.id = "breakpoint-" + identifier;
    container.className = "dbg-breakpoint side-menu-widget-item-other";
    container.classList.add("devtools-monospace");
    container.setAttribute("align", "center");
    container.setAttribute("flex", "1");

    container.addEventListener("click", this._onBreakpointClick, false);
    checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false);

    container.appendChild(checkbox);
    container.appendChild(lineNumberNode);
    container.appendChild(lineTextNode);

    return {
      container: container,
      checkbox: checkbox,
      lineNumber: lineNumberNode,
      lineText: lineTextNode
    };
  },

  /**
   * Creates a context menu for a breakpoint element.
   *
   * @param object aOptions
   *        A couple of options or flags supported by this operation:
   *          - location: the breakpoint's source location and line number
   *          - disabled: the breakpoint's disabled state, boolean
   * @return object
   *         An object containing the breakpoint commandset and menu popup ids.
   */
  _createContextMenu: function(aOptions) {
    let { location, disabled } = aOptions;
    let identifier = DebuggerController.Breakpoints.getIdentifier(location);

    let commandset = document.createElement("commandset");
    let menupopup = document.createElement("menupopup");
    commandset.id = "bp-cSet-" + identifier;
    menupopup.id = "bp-mPop-" + identifier;

    createMenuItem.call(this, "enableSelf", !disabled);
    createMenuItem.call(this, "disableSelf", disabled);
    createMenuItem.call(this, "deleteSelf");
    createMenuSeparator();
    createMenuItem.call(this, "setConditional");
    createMenuSeparator();
    createMenuItem.call(this, "enableOthers");
    createMenuItem.call(this, "disableOthers");
    createMenuItem.call(this, "deleteOthers");
    createMenuSeparator();
    createMenuItem.call(this, "enableAll");
    createMenuItem.call(this, "disableAll");
    createMenuSeparator();
    createMenuItem.call(this, "deleteAll");

    this._popupset.appendChild(menupopup);
    this._commandset.appendChild(commandset);

    return {
      commandsetId: commandset.id,
      menupopupId: menupopup.id
    };

    /**
     * Creates a menu item specified by a name with the appropriate attributes
     * (label and handler).
     *
     * @param string aName
     *        A global identifier for the menu item.
     * @param boolean aHiddenFlag
     *        True if this menuitem should be hidden.
     */
    function createMenuItem(aName, aHiddenFlag) {
      let menuitem = document.createElement("menuitem");
      let command = document.createElement("command");

      let prefix = "bp-cMenu-"; // "breakpoints context menu"
      let commandId = prefix + aName + "-" + identifier + "-command";
      let menuitemId = prefix + aName + "-" + identifier + "-menuitem";

      let label = L10N.getStr("breakpointMenuItem." + aName);
      let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1);

      command.id = commandId;
      command.setAttribute("label", label);
      command.addEventListener("command", () => this[func](location), false);

      menuitem.id = menuitemId;
      menuitem.setAttribute("command", commandId);
      aHiddenFlag && menuitem.setAttribute("hidden", "true");

      commandset.appendChild(command);
      menupopup.appendChild(menuitem);
    }

    /**
     * Creates a simple menu separator element and appends it to the current
     * menupopup hierarchy.
     */
    function createMenuSeparator() {
      let menuseparator = document.createElement("menuseparator");
      menupopup.appendChild(menuseparator);
    }
  },

  /**
   * Function called each time a breakpoint item is removed.
   *
   * @param object aItem
   *        The corresponding item.
   */
  _onBreakpointRemoved: function(aItem) {
    dumpn("Finalizing breakpoint item: " + aItem);

    // Destroy the context menu for the breakpoint.
    let contextMenu = aItem.attachment.popup;
    document.getElementById(contextMenu.commandsetId).remove();
    document.getElementById(contextMenu.menupopupId).remove();

    // Clear the breakpoint selection.
    if (this._selectedBreakpointItem == aItem) {
      this._selectedBreakpointItem = null;
    }
  },

  /**
   * The load listener for the source editor.
   */
  _onEditorLoad: function(aName, aEditor) {
    aEditor.on("cursorActivity", this._onEditorCursorActivity);
  },

  /**
   * The unload listener for the source editor.
   */
  _onEditorUnload: function(aName, aEditor) {
    aEditor.off("cursorActivity", this._onEditorCursorActivity);
  },

  /**
   * The selection listener for the source editor.
   */
  _onEditorCursorActivity: function(e) {
    let editor = DebuggerView.editor;
    let start  = editor.getCursor("start").line + 1;
    let end    = editor.getCursor().line + 1;
    let url    = this.selectedValue;

    let location = { url: url, line: start };

    if (this.getBreakpoint(location) && start == end) {
      this.highlightBreakpoint(location, { noEditorUpdate: true });
    } else {
      this.unhighlightBreakpoint();
    }
  },

  /**
   * The select listener for the sources container.
   */
  _onSourceSelect: function({ detail: sourceItem }) {
    if (!sourceItem) {
      return;
    }
    const { source } = sourceItem.attachment;
    const sourceClient = gThreadClient.source(source);

    // The container is not empty and an actual item was selected.
    DebuggerView.setEditorLocation(sourceItem.value);

    if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) {
      DebuggerController.SourceScripts.getText(source).then(([, aText]) => {
        if (SourceUtils.isMinified(sourceClient, aText)) {
          this.togglePrettyPrint();
        }
      }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e));
    }

    // Set window title. No need to split the url by " -> " here, because it was
    // already sanitized when the source was added.
    document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value);

    DebuggerView.maybeShowBlackBoxMessage();
    this.updateToolbarButtonsState();
  },

  /**
   * The click listener for the "stop black boxing" button.
   */
  _onStopBlackBoxing: function() {
    const { source } = this.selectedItem.attachment;

    DebuggerController.SourceScripts.setBlackBoxing(source, false)
      .then(this.updateToolbarButtonsState,
            this.updateToolbarButtonsState);
  },

  /**
   * The click listener for a breakpoint container.
   */
  _onBreakpointClick: function(e) {
    let sourceItem = this.getItemForElement(e.target);
    let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
    let attachment = breakpointItem.attachment;

    // Check if this is an enabled conditional breakpoint.
    let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
    if (breakpointPromise) {
      breakpointPromise.then(aBreakpointClient => {
        doHighlight.call(this, aBreakpointClient.hasCondition());
      });
    } else {
      doHighlight.call(this, false);
    }

    function doHighlight(aConditionalBreakpointFlag) {
      // Highlight the breakpoint in this pane and in the editor.
      this.highlightBreakpoint(attachment, {
        // Don't show the conditional expression popup if this is not a
        // conditional breakpoint, or the right mouse button was pressed (to
        // avoid clashing the popup with the context menu).
        openPopup: aConditionalBreakpointFlag && e.button == 0
      });
    }
  },

  /**
   * The click listener for a breakpoint checkbox.
   */
  _onBreakpointCheckboxClick: function(e) {
    let sourceItem = this.getItemForElement(e.target);
    let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
    let attachment = breakpointItem.attachment;

    // Toggle the breakpoint enabled or disabled.
    this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, {
      // Do this silently (don't update the checkbox checked state), since
      // this listener is triggered because a checkbox was already clicked.
      silent: true
    });

    // Don't update the editor location (avoid propagating into _onBreakpointClick).
    e.preventDefault();
    e.stopPropagation();
  },

  /**
   * The popup showing listener for the breakpoints conditional expression panel.
   */
  _onConditionalPopupShowing: function() {
    this._conditionalPopupVisible = true; // Used in tests.
    window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
  },

  /**
   * The popup shown listener for the breakpoints conditional expression panel.
   */
  _onConditionalPopupShown: function() {
    this._cbTextbox.focus();
    this._cbTextbox.select();
  },

  /**
   * The popup hiding listener for the breakpoints conditional expression panel.
   */
  _onConditionalPopupHiding: Task.async(function*() {
    this._conditionalPopupVisible = false; // Used in tests.
    let breakpointItem = this._selectedBreakpointItem;
    let attachment = breakpointItem.attachment;

    // Check if this is an enabled conditional breakpoint, and if so,
    // save the current conditional epression.
    let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
    if (breakpointPromise) {
      let breakpointClient = yield breakpointPromise;
      yield DebuggerController.Breakpoints.updateCondition(
        breakpointClient.location,
        this._cbTextbox.value
      );
    }

    window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
  }),

  /**
   * The keypress listener for the breakpoints conditional expression textbox.
   */
  _onConditionalTextboxKeyPress: function(e) {
    if (e.keyCode == e.DOM_VK_RETURN) {
      this._hideConditionalPopup();
    }
  },

  /**
   * Called when the add breakpoint key sequence was pressed.
   */
  _onCmdAddBreakpoint: function(e) {
    let url = DebuggerView.Sources.selectedValue;
    let line = DebuggerView.editor.getCursor().line + 1;
    let location = { url: url, line: line };
    let breakpointItem = this.getBreakpoint(location);

    // If a breakpoint already existed, remove it now.
    if (breakpointItem) {
      DebuggerController.Breakpoints.removeBreakpoint(location);
    }
    // No breakpoint existed at the required location, add one now.
    else {
      DebuggerController.Breakpoints.addBreakpoint(location);
    }
  },

  /**
   * Called when the add conditional breakpoint key sequence was pressed.
   */
  _onCmdAddConditionalBreakpoint: function() {
    let url =  DebuggerView.Sources.selectedValue;
    let line = DebuggerView.editor.getCursor().line + 1;
    let location = { url: url, line: line };
    let breakpointItem = this.getBreakpoint(location);

    // If a breakpoint already existed or wasn't a conditional, morph it now.
    if (breakpointItem) {
      this.highlightBreakpoint(location, { openPopup: true });
    }
    // No breakpoint existed at the required location, add one now.
    else {
      DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true });
    }
  },

  /**
   * Function invoked on the "setConditional" menuitem command.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _onSetConditional: function(aLocation) {
    // Highlight the breakpoint and show a conditional expression popup.
    this.highlightBreakpoint(aLocation, { openPopup: true });
  },

  /**
   * Function invoked on the "enableSelf" menuitem command.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _onEnableSelf: function(aLocation) {
    // Enable the breakpoint, in this container and the controller store.
    this.enableBreakpoint(aLocation);
  },

  /**
   * Function invoked on the "disableSelf" menuitem command.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _onDisableSelf: function(aLocation) {
    // Disable the breakpoint, in this container and the controller store.
    this.disableBreakpoint(aLocation);
  },

  /**
   * Function invoked on the "deleteSelf" menuitem command.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _onDeleteSelf: function(aLocation) {
    // Remove the breakpoint, from this container and the controller store.
    this.removeBreakpoint(aLocation);
    DebuggerController.Breakpoints.removeBreakpoint(aLocation);
  },

  /**
   * Function invoked on the "enableOthers" menuitem command.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _onEnableOthers: function(aLocation) {
    let enableOthers = aCallback => {
      let other = this.getOtherBreakpoints(aLocation);
      let outstanding = other.map(e => this.enableBreakpoint(e.attachment));
      promise.all(outstanding).then(aCallback);
    }

    // Breakpoints can only be set while the debuggee is paused. To avoid
    // an avalanche of pause/resume interrupts of the main thread, simply
    // pause it beforehand if it's not already.
    if (gThreadClient.state != "paused") {
      gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume()));
    } else {
      enableOthers();
    }
  },

  /**
   * Function invoked on the "disableOthers" menuitem command.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _onDisableOthers: function(aLocation) {
    let other = this.getOtherBreakpoints(aLocation);
    other.forEach(e => this._onDisableSelf(e.attachment));
  },

  /**
   * Function invoked on the "deleteOthers" menuitem command.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _onDeleteOthers: function(aLocation) {
    let other = this.getOtherBreakpoints(aLocation);
    other.forEach(e => this._onDeleteSelf(e.attachment));
  },

  /**
   * Function invoked on the "enableAll" menuitem command.
   */
  _onEnableAll: function() {
    this._onEnableOthers(undefined);
  },

  /**
   * Function invoked on the "disableAll" menuitem command.
   */
  _onDisableAll: function() {
    this._onDisableOthers(undefined);
  },

  /**
   * Function invoked on the "deleteAll" menuitem command.
   */
  _onDeleteAll: function() {
    this._onDeleteOthers(undefined);
  },

  _commandset: null,
  _popupset: null,
  _cmPopup: null,
  _cbPanel: null,
  _cbTextbox: null,
  _selectedBreakpointItem: null,
  _conditionalPopupVisible: false
});

/**
 * Functions handling the traces UI.
 */
function TracerView() {
  this._selectedItem = null;
  this._matchingItems = null;
  this.widget = null;

  this._highlightItem = this._highlightItem.bind(this);
  this._isNotSelectedItem = this._isNotSelectedItem.bind(this);

  this._unhighlightMatchingItems =
    DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this));
  this._onToggleTracing =
    DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this));
  this._onStartTracing =
    DevToolsUtils.makeInfallible(this._onStartTracing.bind(this));
  this._onClear =
    DevToolsUtils.makeInfallible(this._onClear.bind(this));
  this._onSelect =
    DevToolsUtils.makeInfallible(this._onSelect.bind(this));
  this._onMouseOver =
    DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
  this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this));
}

TracerView.MAX_TRACES = 200;

TracerView.prototype = Heritage.extend(WidgetMethods, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function() {
    dumpn("Initializing the TracerView");

    this._traceButton = document.getElementById("trace");
    this._tracerTab = document.getElementById("tracer-tab");

    // Remove tracer related elements from the dom and tear everything down if
    // the tracer isn't enabled.
    if (!Prefs.tracerEnabled) {
      this._traceButton.remove();
      this._traceButton = null;
      this._tracerTab.remove();
      this._tracerTab = null;
      return;
    }

    this.widget = new FastListWidget(document.getElementById("tracer-traces"));
    this._traceButton.removeAttribute("hidden");
    this._tracerTab.removeAttribute("hidden");

    this._search = document.getElementById("tracer-search");
    this._template = document.getElementsByClassName("trace-item-template")[0];
    this._templateItem = this._template.getElementsByClassName("trace-item")[0];
    this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0];
    this._templateNameNode = this._template.getElementsByClassName("trace-name")[0];

    this.widget.addEventListener("select", this._onSelect, false);
    this.widget.addEventListener("mouseover", this._onMouseOver, false);
    this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false);
    this._search.addEventListener("input", this._onSearch, false);

    this._startTooltip = L10N.getStr("startTracingTooltip");
    this._stopTooltip = L10N.getStr("stopTracingTooltip");
    this._tracingNotStartedString = L10N.getStr("tracingNotStartedText");
    this._noFunctionCallsString = L10N.getStr("noFunctionCallsText");

    this._traceButton.setAttribute("tooltiptext", this._startTooltip);
    this.emptyText = this._tracingNotStartedString;
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function() {
    dumpn("Destroying the TracerView");

    if (!this.widget) {
      return;
    }

    this.widget.removeEventListener("select", this._onSelect, false);
    this.widget.removeEventListener("mouseover", this._onMouseOver, false);
    this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false);
    this._search.removeEventListener("input", this._onSearch, false);
  },

  /**
   * Function invoked by the "toggleTracing" command to switch the tracer state.
   */
  _onToggleTracing: function() {
    if (DebuggerController.Tracer.tracing) {
      this._onStopTracing();
    } else {
      this._onStartTracing();
    }
  },

  /**
   * Function invoked either by the "startTracing" command or by
   * _onToggleTracing to start execution tracing in the backend.
   *
   * @return object
   *         A promise resolved once the tracing has successfully started.
   */
  _onStartTracing: function() {
    this._traceButton.setAttribute("checked", true);
    this._traceButton.setAttribute("tooltiptext", this._stopTooltip);

    this.empty();
    this.emptyText = this._noFunctionCallsString;

    let deferred = promise.defer();
    DebuggerController.Tracer.startTracing(deferred.resolve);
    return deferred.promise;
  },

  /**
   * Function invoked by _onToggleTracing to stop execution tracing in the
   * backend.
   *
   * @return object
   *         A promise resolved once the tracing has successfully stopped.
   */
  _onStopTracing: function() {
    this._traceButton.removeAttribute("checked");
    this._traceButton.setAttribute("tooltiptext", this._startTooltip);

    this.emptyText = this._tracingNotStartedString;

    let deferred = promise.defer();
    DebuggerController.Tracer.stopTracing(deferred.resolve);
    return deferred.promise;
  },

  /**
   * Function invoked by the "clearTraces" command to empty the traces pane.
   */
  _onClear: function() {
    this.empty();
  },

  /**
   * Populate the given parent scope with the variable with the provided name
   * and value.
   *
   * @param String aName
   *        The name of the variable.
   * @param Object aParent
   *        The parent scope.
   * @param Object aValue
   *        The value of the variable.
   */
  _populateVariable: function(aName, aParent, aValue) {
    let item = aParent.addItem(aName, { value: aValue });
    if (aValue) {
      let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue);
      DebuggerView.Variables.controller.populate(item, wrappedValue);
      item.expand();
      item.twisty = false;
    }
  },

  /**
   * Handler for the widget's "select" event. Displays parameters, exception, or
   * return value depending on whether the selected trace is a call, throw, or
   * return respectively.
   *
   * @param Object traceItem
   *        The selected trace item.
   */
  _onSelect: function _onSelect({ detail: traceItem }) {
    if (!traceItem) {
      return;
    }

    const data = traceItem.attachment.trace;
    const { location: { url, line } } = data;
    DebuggerView.setEditorLocation(url, line, { noDebug: true });

    DebuggerView.Variables.empty();
    const scope = DebuggerView.Variables.addScope();

    if (data.type == "call") {
      const params = DevToolsUtils.zip(data.parameterNames, data.arguments);
      for (let [name, val] of params) {
        if (val === undefined) {
          scope.addItem(name, { value: "<value not available>" });
        } else {
          this._populateVariable(name, scope, val);
        }
      }
    } else {
      const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">";
      this._populateVariable(varName, scope, data.returnVal);
    }

    scope.expand();
    DebuggerView.showInstrumentsPane();
  },

  /**
   * Add the hover frame enter/exit highlighting to a given item.
   */
  _highlightItem: function(aItem) {
    if (!aItem || !aItem.target) {
      return;
    }
    const trace = aItem.target.querySelector(".trace-item");
    trace.classList.add("selected-matching");
  },

  /**
   * Remove the hover frame enter/exit highlighting to a given item.
   */
  _unhighlightItem: function(aItem) {
    if (!aItem || !aItem.target) {
      return;
    }
    const match = aItem.target.querySelector(".selected-matching");
    if (match) {
      match.classList.remove("selected-matching");
    }
  },

  /**
   * Remove the frame enter/exit pair highlighting we do when hovering.
   */
  _unhighlightMatchingItems: function() {
    if (this._matchingItems) {
      this._matchingItems.forEach(this._unhighlightItem);
      this._matchingItems = null;
    }
  },

  /**
   * Returns true if the given item is not the selected item.
   */
  _isNotSelectedItem: function(aItem) {
    return aItem !== this.selectedItem;
  },

  /**
   * Highlight the frame enter/exit pair of items for the given item.
   */
  _highlightMatchingItems: function(aItem) {
    const frameId = aItem.attachment.trace.frameId;
    const predicate = e => e.attachment.trace.frameId == frameId;

    this._unhighlightMatchingItems();
    this._matchingItems = this.items.filter(predicate);
    this._matchingItems
      .filter(this._isNotSelectedItem)
      .forEach(this._highlightItem);
  },

  /**
   * Listener for the mouseover event.
   */
  _onMouseOver: function({ target }) {
    const traceItem = this.getItemForElement(target);
    if (traceItem) {
      this._highlightMatchingItems(traceItem);
    }
  },

  /**
   * Listener for typing in the search box.
   */
  _onSearch: function() {
    const query = this._search.value.trim().toLowerCase();
    const predicate = name => name.toLowerCase().contains(query);
    this.filterContents(item => predicate(item.attachment.trace.name));
  },

  /**
   * Select the traces tab in the sidebar.
   */
  selectTab: function() {
    const tabs = this._tracerTab.parentElement;
    tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab);
  },

  /**
   * Commit all staged items to the widget. Overridden so that we can call
   * |FastListWidget.prototype.flush|.
   */
  commit: function() {
    WidgetMethods.commit.call(this);
    // TODO: Accessing non-standard widget properties. Figure out what's the
    // best way to expose such things. Bug 895514.
    this.widget.flush();
  },

  /**
   * Adds the trace record provided as an argument to the view.
   *
   * @param object aTrace
   *        The trace record coming from the tracer actor.
   */
  addTrace: function(aTrace) {
    // Create the element node for the trace item.
    let view = this._createView(aTrace);

    // Append a source item to this container.
    this.push([view], {
      staged: true,
      attachment: {
        trace: aTrace
      }
    });
  },

  /**
   * Customization function for creating an item's UI.
   *
   * @return nsIDOMNode
   *         The network request view.
   */
  _createView: function(aTrace) {
    let { type, name, location, depth, frameId } = aTrace;
    let { parameterNames, returnVal, arguments: args } = aTrace;
    let fragment = document.createDocumentFragment();

    this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
    this._templateItem.style.MozPaddingStart = depth + "em";

    const TYPES = ["call", "yield", "return", "throw"];
    for (let t of TYPES) {
      this._templateTypeIcon.classList.toggle("trace-" + t, t == type);
    }
    this._templateTypeIcon.setAttribute("value", {
      call: "\u2192",
      yield: "Y",
      return: "\u2190",
      throw: "E",
      terminated: "TERMINATED"
    }[type]);

    this._templateNameNode.setAttribute("value", name);

    // All extra syntax and parameter nodes added.
    const addedNodes = [];

    if (parameterNames) {
      const syntax = (p) => {
        const el = document.createElement("label");
        el.setAttribute("value", p);
        el.classList.add("trace-syntax");
        el.classList.add("plain");
        addedNodes.push(el);
        return el;
      };

      this._templateItem.appendChild(syntax("("));

      for (let i = 0, n = parameterNames.length; i < n; i++) {
        let param = document.createElement("label");
        param.setAttribute("value", parameterNames[i]);
        param.classList.add("trace-param");
        param.classList.add("plain");
        addedNodes.push(param);
        this._templateItem.appendChild(param);

        if (i + 1 !== n) {
          this._templateItem.appendChild(syntax(", "));
        }
      }

      this._templateItem.appendChild(syntax(")"));
    }

    // Flatten the DOM by removing one redundant box (the template container).
    for (let node of this._template.childNodes) {
      fragment.appendChild(node.cloneNode(true));
    }

    // Remove any added nodes from the template.
    for (let node of addedNodes) {
      this._templateItem.removeChild(node);
    }

    return fragment;
  }
});

/**
 * Utility functions for handling sources.
 */
let SourceUtils = {
  _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
  _groupsCache: new Map(),
  _minifiedCache: new WeakMap(),

  /**
   * Returns true if the specified url and/or content type are specific to
   * javascript files.
   *
   * @return boolean
   *         True if the source is likely javascript.
   */
  isJavaScript: function(aUrl, aContentType = "") {
    return /\.jsm?$/.test(this.trimUrlQuery(aUrl)) ||
           aContentType.contains("javascript");
  },

  /**
   * Determines if the source text is minified by using
   * the percentage indented of a subset of lines
   *
   * @param string aText
   *        The source text.
   * @return boolean
   *        True if source text is minified.
   */
  isMinified: function(sourceClient, aText){
    if (this._minifiedCache.has(sourceClient)) {
      return this._minifiedCache.get(sourceClient);
    }

    let isMinified;
    let lineEndIndex = 0;
    let lineStartIndex = 0;
    let lines = 0;
    let indentCount = 0;
    let overCharLimit = false;

    // Strip comments.
    aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");

    while (lines++ < SAMPLE_SIZE) {
      lineEndIndex = aText.indexOf("\n", lineStartIndex);
      if (lineEndIndex == -1) {
         break;
      }
      if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) {
        indentCount++;
      }
      // For files with no indents but are not minified.
      if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) {
        overCharLimit = true;
        break;
      }
      lineStartIndex = lineEndIndex + 1;
    }
    isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD ||
                 overCharLimit;

    this._minifiedCache.set(sourceClient, isMinified);
    return isMinified;
  },

  /**
   * Clears the labels, groups and minify cache, populated by methods like
   * SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
   * This should be done every time the content location changes.
   */
  clearCache: function() {
    this._labelsCache.clear();
    this._groupsCache.clear();
    this._minifiedCache.clear();
  },

  /**
   * Gets a unique, simplified label from a source url.
   *
   * @param string aUrl
   *        The source url.
   * @return string
   *         The simplified label.
   */
  getSourceLabel: function(aUrl) {
    let cachedLabel = this._labelsCache.get(aUrl);
    if (cachedLabel) {
      return cachedLabel;
    }

    let sourceLabel = null;

    for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
      if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
        sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length);
      }
    }

    if (!sourceLabel) {
      sourceLabel = this.trimUrl(aUrl);
    }
    let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
    this._labelsCache.set(aUrl, unicodeLabel);
    return unicodeLabel;
  },

  /**
   * Gets as much information as possible about the hostname and directory paths
   * of an url to create a short url group identifier.
   *
   * @param string aUrl
   *        The source url.
   * @return string
   *         The simplified group.
   */
  getSourceGroup: function(aUrl) {
    let cachedGroup = this._groupsCache.get(aUrl);
    if (cachedGroup) {
      return cachedGroup;
    }

    try {
      // Use an nsIURL to parse all the url path parts.
      var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
    } catch (e) {
      // This doesn't look like a url, or nsIURL can't handle it.
      return "";
    }

    let groupLabel = uri.prePath;

    for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
      if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
        groupLabel = name;
      }
    }

    let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
    this._groupsCache.set(aUrl, unicodeLabel)
    return unicodeLabel;
  },

  /**
   * Trims the url by shortening it if it exceeds a certain length, adding an
   * ellipsis at the end.
   *
   * @param string aUrl
   *        The source url.
   * @param number aLength [optional]
   *        The expected source url length.
   * @param number aSection [optional]
   *        The section to trim. Supported values: "start", "center", "end"
   * @return string
   *         The shortened url.
   */
  trimUrlLength: function(aUrl, aLength, aSection) {
    aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH;
    aSection = aSection || "end";

    if (aUrl.length > aLength) {
      switch (aSection) {
        case "start":
          return L10N.ellipsis + aUrl.slice(-aLength);
          break;
        case "center":
          return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1);
          break;
        case "end":
          return aUrl.substr(0, aLength) + L10N.ellipsis;
          break;
      }
    }
    return aUrl;
  },

  /**
   * Trims the query part or reference identifier of a url string, if necessary.
   *
   * @param string aUrl
   *        The source url.
   * @return string
   *         The shortened url.
   */
  trimUrlQuery: function(aUrl) {
    let length = aUrl.length;
    let q1 = aUrl.indexOf('?');
    let q2 = aUrl.indexOf('&');
    let q3 = aUrl.indexOf('#');
    let q = Math.min(q1 != -1 ? q1 : length,
                     q2 != -1 ? q2 : length,
                     q3 != -1 ? q3 : length);

    return aUrl.slice(0, q);
  },

  /**
   * Trims as much as possible from a url, while keeping the label unique
   * in the sources container.
   *
   * @param string | nsIURL aUrl
   *        The source url.
   * @param string aLabel [optional]
   *        The resulting label at each step.
   * @param number aSeq [optional]
   *        The current iteration step.
   * @return string
   *         The resulting label at the final step.
   */
  trimUrl: function(aUrl, aLabel, aSeq) {
    if (!(aUrl instanceof Ci.nsIURL)) {
      try {
        // Use an nsIURL to parse all the url path parts.
        aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
      } catch (e) {
        // This doesn't look like a url, or nsIURL can't handle it.
        return aUrl;
      }
    }
    if (!aSeq) {
      let name = aUrl.fileName;
      if (name) {
        // This is a regular file url, get only the file name (contains the
        // base name and extension if available).

        // If this url contains an invalid query, unfortunately nsIURL thinks
        // it's part of the file extension. It must be removed.
        aLabel = aUrl.fileName.replace(/\&.*/, "");
      } else {
        // This is not a file url, hence there is no base name, nor extension.
        // Proceed using other available information.
        aLabel = "";
      }
      aSeq = 1;
    }

    // If we have a label and it doesn't only contain a query...
    if (aLabel && aLabel.indexOf("?") != 0) {
      // A page may contain multiple requests to the same url but with different
      // queries. It is *not* redundant to show each one.
      if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) {
        return aLabel;
      }
    }

    // Append the url query.
    if (aSeq == 1) {
      let query = aUrl.query;
      if (query) {
        return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1);
      }
      aSeq++;
    }
    // Append the url reference.
    if (aSeq == 2) {
      let ref = aUrl.ref;
      if (ref) {
        return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
      }
      aSeq++;
    }
    // Prepend the url directory.
    if (aSeq == 3) {
      let dir = aUrl.directory;
      if (dir) {
        return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
      }
      aSeq++;
    }
    // Prepend the hostname and port number.
    if (aSeq == 4) {
      let host = aUrl.hostPort;
      if (host) {
        return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
      }
      aSeq++;
    }
    // Use the whole url spec but ignoring the reference.
    if (aSeq == 5) {
      return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
    }
    // Give up.
    return aUrl.spec;
  }
};

/**
 * Functions handling the variables bubble UI.
 */
function VariableBubbleView() {
  dumpn("VariableBubbleView was instantiated");

  this._onMouseMove = this._onMouseMove.bind(this);
  this._onMouseLeave = this._onMouseLeave.bind(this);
  this._onPopupHiding = this._onPopupHiding.bind(this);
}

VariableBubbleView.prototype = {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function() {
    dumpn("Initializing the VariableBubbleView");

    this._editorContainer = document.getElementById("editor");
    this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
    this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false);

    this._tooltip = new Tooltip(document, {
      closeOnEvents: [{
        emitter: DebuggerController._toolbox,
        event: "select"
      }, {
        emitter: this._editorContainer,
        event: "scroll",
        useCapture: true
      }]
    });
    this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION;
    this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY;
    this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function() {
    dumpn("Destroying the VariableBubbleView");

    this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding);
    this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false);
    this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false);
  },

  /**
   * Specifies whether literals can be (redundantly) inspected in a popup.
   * This behavior is deprecated, but still tested in a few places.
   */
  _ignoreLiterals: true,

  /**
   * Searches for an identifier underneath the specified position in the
   * source editor, and if found, opens a VariablesView inspection popup.
   *
   * @param number x, y
   *        The left/top coordinates where to look for an identifier.
   */
  _findIdentifier: function(x, y) {
    let editor = DebuggerView.editor;

    // Calculate the editor's line and column at the current x and y coords.
    let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
    let hoveredOffset = editor.getOffset(hoveredPos);
    let hoveredLine = hoveredPos.line;
    let hoveredColumn = hoveredPos.ch;

    // A source contains multiple scripts. Find the start index of the script
    // containing the specified offset relative to its parent source.
    let contents = editor.getText();
    let location = DebuggerView.Sources.selectedValue;
    let parsedSource = DebuggerController.Parser.get(contents, location);
    let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);

    // If the script length is negative, we're not hovering JS source code.
    if (scriptInfo.length == -1) {
      return;
    }

    // Using the script offset, determine the actual line and column inside the
    // script, to use when finding identifiers.
    let scriptStart = editor.getPosition(scriptInfo.start);
    let scriptLineOffset = scriptStart.line;
    let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);

    let scriptLine = hoveredLine - scriptLineOffset;
    let scriptColumn = hoveredColumn - scriptColumnOffset;
    let identifierInfo = parsedSource.getIdentifierAt({
      line: scriptLine + 1,
      column: scriptColumn,
      scriptIndex: scriptInfo.index,
      ignoreLiterals: this._ignoreLiterals
    });

    // If the info is null, we're not hovering any identifier.
    if (!identifierInfo) {
      return;
    }

    // Transform the line and column relative to the parsed script back
    // to the context of the parent source.
    let { start: identifierStart, end: identifierEnd } = identifierInfo.location;
    let identifierCoords = {
      line: identifierStart.line + scriptLineOffset,
      column: identifierStart.column + scriptColumnOffset,
      length: identifierEnd.column - identifierStart.column
    };

    // Evaluate the identifier in the current stack frame and show the
    // results in a VariablesView inspection popup.
    DebuggerController.StackFrames.evaluate(identifierInfo.evalString)
      .then(frameFinished => {
        if ("return" in frameFinished) {
          this.showContents({
            coords: identifierCoords,
            evalPrefix: identifierInfo.evalString,
            objectActor: frameFinished.return
          });
        } else {
          let msg = "Evaluation has thrown for: " + identifierInfo.evalString;
          console.warn(msg);
          dumpn(msg);
        }
      })
      .then(null, err => {
        let msg = "Couldn't evaluate: " + err.message;
        console.error(msg);
        dumpn(msg);
      });
  },

  /**
   * Shows an inspection popup for a specified object actor grip.
   *
   * @param string object
   *        An object containing the following properties:
   *          - coords: the inspected identifier coordinates in the editor,
   *                    containing the { line, column, length } properties.
   *          - evalPrefix: a prefix for the variables view evaluation macros.
   *          - objectActor: the value grip for the object actor.
   */
  showContents: function({ coords, evalPrefix, objectActor }) {
    let editor = DebuggerView.editor;
    let { line, column, length } = coords;

    // Highlight the function found at the mouse position.
    this._markedText = editor.markText(
      { line: line - 1, ch: column },
      { line: line - 1, ch: column + length });

    // If the grip represents a primitive value, use a more lightweight
    // machinery to display it.
    if (VariablesView.isPrimitive({ value: objectActor })) {
      let className = VariablesView.getClass(objectActor);
      let textContent = VariablesView.getString(objectActor);
      this._tooltip.setTextContent({
        messages: [textContent],
        messagesClass: className,
        containerClass: "plain"
      }, [{
        label: L10N.getStr('addWatchExpressionButton'),
        className: "dbg-expression-button",
        command: () => {
          DebuggerView.VariableBubble.hideContents();
          DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
        }
      }]);
    } else {
      this._tooltip.setVariableContent(objectActor, {
        searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"),
        searchEnabled: Prefs.variablesSearchboxVisible,
        eval: (variable, value) => {
          let string = variable.evaluationMacro(variable, value);
          DebuggerController.StackFrames.evaluate(string);
          DebuggerView.VariableBubble.hideContents();
        }
      }, {
        getEnvironmentClient: aObject => gThreadClient.environment(aObject),
        getObjectClient: aObject => gThreadClient.pauseGrip(aObject),
        simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix),
        getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix),
        overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix)
      }, {
        fetched: (aEvent, aType) => {
          if (aType == "properties") {
            window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES);
          }
        }
      }, [{
        label: L10N.getStr("addWatchExpressionButton"),
        className: "dbg-expression-button",
        command: () => {
          DebuggerView.VariableBubble.hideContents();
          DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
        }
      }], DebuggerController._toolbox);
    }

    this._tooltip.show(this._markedText.anchor);
  },

  /**
   * Hides the inspection popup.
   */
  hideContents: function() {
    clearNamedTimeout("editor-mouse-move");
    this._tooltip.hide();
  },

  /**
   * Checks whether the inspection popup is shown.
   *
   * @return boolean
   *         True if the panel is shown or showing, false otherwise.
   */
  contentsShown: function() {
    return this._tooltip.isShown();
  },

  /**
   * Functions for getting customized variables view evaluation macros.
   *
   * @param string aPrefix
   *        See the corresponding VariablesView.* functions.
   */
  _getSimpleValueEvalMacro: function(aPrefix) {
    return (item, string) =>
      VariablesView.simpleValueEvalMacro(item, string, aPrefix);
  },
  _getGetterOrSetterEvalMacro: function(aPrefix) {
    return (item, string) =>
      VariablesView.getterOrSetterEvalMacro(item, string, aPrefix);
  },
  _getOverrideValueEvalMacro: function(aPrefix) {
    return (item, string) =>
      VariablesView.overrideValueEvalMacro(item, string, aPrefix);
  },

  /**
   * The mousemove listener for the source editor.
   */
  _onMouseMove: function({ clientX: x, clientY: y, buttons: btns }) {
    // Prevent the variable inspection popup from showing when the thread client
    // is not paused, or while a popup is already visible, or when the user tries
    // to select text in the editor.
    if (gThreadClient && gThreadClient.state != "paused"
        || !this._tooltip.isHidden()
        || (DebuggerView.editor.somethingSelected()
         && btns > 0)) {
      clearNamedTimeout("editor-mouse-move");
      return;
    }
    // Allow events to settle down first. If the mouse hovers over
    // a certain point in the editor long enough, try showing a variable bubble.
    setNamedTimeout("editor-mouse-move",
      EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(x, y));
  },

  /**
   * The mouseleave listener for the source editor container node.
   */
  _onMouseLeave: function() {
    clearNamedTimeout("editor-mouse-move");
  },

  /**
   * Listener handling the popup hiding event.
   */
  _onPopupHiding: function({ target }) {
    if (this._tooltip.panel != target) {
      return;
    }
    if (this._markedText) {
      this._markedText.clear();
      this._markedText = null;
    }
    if (!this._tooltip.isEmpty()) {
      this._tooltip.empty();
    }
  },

  _editorContainer: null,
  _markedText: null,
  _tooltip: null
};

/**
 * Functions handling the watch expressions UI.
 */
function WatchExpressionsView() {
  dumpn("WatchExpressionsView was instantiated");

  this.switchExpression = this.switchExpression.bind(this);
  this.deleteExpression = this.deleteExpression.bind(this);
  this._createItemView = this._createItemView.bind(this);
  this._onClick = this._onClick.bind(this);
  this._onClose = this._onClose.bind(this);
  this._onBlur = this._onBlur.bind(this);
  this._onKeyPress = this._onKeyPress.bind(this);
}

WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function() {
    dumpn("Initializing the WatchExpressionsView");

    this.widget = new SimpleListWidget(document.getElementById("expressions"));
    this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu");
    this.widget.addEventListener("click", this._onClick, false);

    this.headerText = L10N.getStr("addWatchExpressionText");
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function() {
    dumpn("Destroying the WatchExpressionsView");

    this.widget.removeEventListener("click", this._onClick, false);
  },

  /**
   * Adds a watch expression in this container.
   *
   * @param string aExpression [optional]
   *        An optional initial watch expression text.
   * @param boolean aSkipUserInput [optional]
   *        Pass true to avoid waiting for additional user input
   *        on the watch expression.
   */
  addExpression: function(aExpression = "", aSkipUserInput = false) {
    // Watch expressions are UI elements which benefit from visible panes.
    DebuggerView.showInstrumentsPane();

    // Create the element node for the watch expression item.
    let itemView = this._createItemView(aExpression);

    // Append a watch expression item to this container.
    let expressionItem = this.push([itemView.container], {
      index: 0, /* specifies on which position should the item be appended */
      attachment: {
        view: itemView,
        initialExpression: aExpression,
        currentExpression: "",
      }
    });

    // Automatically focus the new watch expression input
    // if additional user input is desired.
    if (!aSkipUserInput) {
      expressionItem.attachment.view.inputNode.select();
      expressionItem.attachment.view.inputNode.focus();
      DebuggerView.Variables.parentNode.scrollTop = 0;
    }
    // Otherwise, add and evaluate the new watch expression immediately.
    else {
      this.toggleContents(false);
      this._onBlur({ target: expressionItem.attachment.view.inputNode });
    }
  },

  /**
   * Changes the watch expression corresponding to the specified variable item.
   * This function is called whenever a watch expression's code is edited in
   * the variables view container.
   *
   * @param Variable aVar
   *        The variable representing the watch expression evaluation.
   * @param string aExpression
   *        The new watch expression text.
   */
  switchExpression: function(aVar, aExpression) {
    let expressionItem =
      [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0];

    // Remove the watch expression if it's going to be empty or a duplicate.
    if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) {
      this.deleteExpression(aVar);
      return;
    }

    // Save the watch expression code string.
    expressionItem.attachment.currentExpression = aExpression;
    expressionItem.attachment.view.inputNode.value = aExpression;

    // Synchronize with the controller's watch expressions store.
    DebuggerController.StackFrames.syncWatchExpressions();
  },

  /**
   * Removes the watch expression corresponding to the specified variable item.
   * This function is called whenever a watch expression's value is edited in
   * the variables view container.
   *
   * @param Variable aVar
   *        The variable representing the watch expression evaluation.
   */
  deleteExpression: function(aVar) {
    let expressionItem =
      [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0];

    // Remove the watch expression.
    this.remove(expressionItem);

    // Synchronize with the controller's watch expressions store.
    DebuggerController.StackFrames.syncWatchExpressions();
  },

  /**
   * Gets the watch expression code string for an item in this container.
   *
   * @param number aIndex
   *        The index used to identify the watch expression.
   * @return string
   *         The watch expression code string.
   */
  getString: function(aIndex) {
    return this.getItemAtIndex(aIndex).attachment.currentExpression;
  },

  /**
   * Gets the watch expressions code strings for all items in this container.
   *
   * @return array
   *         The watch expressions code strings.
   */
  getAllStrings: function() {
    return this.items.map(e => e.attachment.currentExpression);
  },

  /**
   * Customization function for creating an item's UI.
   *
   * @param string aExpression
   *        The watch expression string.
   */
  _createItemView: function(aExpression) {
    let container = document.createElement("hbox");
    container.className = "list-widget-item dbg-expression";

    let arrowNode = document.createElement("hbox");
    arrowNode.className = "dbg-expression-arrow";

    let inputNode = document.createElement("textbox");
    inputNode.className = "plain dbg-expression-input devtools-monospace";
    inputNode.setAttribute("value", aExpression);
    inputNode.setAttribute("flex", "1");

    let closeNode = document.createElement("toolbarbutton");
    closeNode.className = "plain variables-view-delete";

    closeNode.addEventListener("click", this._onClose, false);
    inputNode.addEventListener("blur", this._onBlur, false);
    inputNode.addEventListener("keypress", this._onKeyPress, false);

    container.appendChild(arrowNode);
    container.appendChild(inputNode);
    container.appendChild(closeNode);

    return {
      container: container,
      arrowNode: arrowNode,
      inputNode: inputNode,
      closeNode: closeNode
    };
  },

  /**
   * Called when the add watch expression key sequence was pressed.
   */
  _onCmdAddExpression: function(aText) {
    // Only add a new expression if there's no pending input.
    if (this.getAllStrings().indexOf("") == -1) {
      this.addExpression(aText || DebuggerView.editor.getSelection());
    }
  },

  /**
   * Called when the remove all watch expressions key sequence was pressed.
   */
  _onCmdRemoveAllExpressions: function() {
    // Empty the view of all the watch expressions and clear the cache.
    this.empty();

    // Synchronize with the controller's watch expressions store.
    DebuggerController.StackFrames.syncWatchExpressions();
  },

  /**
   * The click listener for this container.
   */
  _onClick: function(e) {
    if (e.button != 0) {
      // Only allow left-click to trigger this event.
      return;
    }
    let expressionItem = this.getItemForElement(e.target);
    if (!expressionItem) {
      // The container is empty or we didn't click on an actual item.
      this.addExpression();
    }
  },

  /**
   * The click listener for a watch expression's close button.
   */
  _onClose: function(e) {
    // Remove the watch expression.
    this.remove(this.getItemForElement(e.target));

    // Synchronize with the controller's watch expressions store.
    DebuggerController.StackFrames.syncWatchExpressions();

    // Prevent clicking the expression element itself.
    e.preventDefault();
    e.stopPropagation();
  },

  /**
   * The blur listener for a watch expression's textbox.
   */
  _onBlur: function({ target: textbox }) {
    let expressionItem = this.getItemForElement(textbox);
    let oldExpression = expressionItem.attachment.currentExpression;
    let newExpression = textbox.value.trim();

    // Remove the watch expression if it's empty.
    if (!newExpression) {
      this.remove(expressionItem);
    }
    // Remove the watch expression if it's a duplicate.
    else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) {
      this.remove(expressionItem);
    }
    // Expression is eligible.
    else {
      expressionItem.attachment.currentExpression = newExpression;
    }

    // Synchronize with the controller's watch expressions store.
    DebuggerController.StackFrames.syncWatchExpressions();
  },

  /**
   * The keypress listener for a watch expression's textbox.
   */
  _onKeyPress: function(e) {
    switch(e.keyCode) {
      case e.DOM_VK_RETURN:
      case e.DOM_VK_ESCAPE:
        e.stopPropagation();
        DebuggerView.editor.focus();
        return;
    }
  }
});

/**
 * Functions handling the event listeners UI.
 */
function EventListenersView() {
  dumpn("EventListenersView was instantiated");

  this._onCheck = this._onCheck.bind(this);
  this._onClick = this._onClick.bind(this);
}

EventListenersView.prototype = Heritage.extend(WidgetMethods, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function() {
    dumpn("Initializing the EventListenersView");

    this.widget = new SideMenuWidget(document.getElementById("event-listeners"), {
      showItemCheckboxes: true,
      showGroupCheckboxes: true
    });

    this.emptyText = L10N.getStr("noEventListenersText");
    this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip");
    this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " ";
    this._inSourceString = " " + L10N.getStr("eventInSource") + " ";
    this._inNativeCodeString = L10N.getStr("eventNative");

    this.widget.addEventListener("check", this._onCheck, false);
    this.widget.addEventListener("click", this._onClick, false);
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function() {
    dumpn("Destroying the EventListenersView");

    this.widget.removeEventListener("check", this._onCheck, false);
    this.widget.removeEventListener("click", this._onClick, false);
  },

  /**
   * Adds an event to this event listeners container.
   *
   * @param object aListener
   *        The listener object coming from the active thread.
   * @param object aOptions [optional]
   *        Additional options for adding the source. Supported options:
   *        - staged: true to stage the item to be appended later
   */
  addListener: function(aListener, aOptions = {}) {
    let { node: { selector }, function: { url }, type } = aListener;
    if (!type) return;

    // Some listener objects may be added from plugins, thus getting
    // translated to native code.
    if (!url) {
      url = this._inNativeCodeString;
    }

    // If an event item for this listener's url and type was already added,
    // avoid polluting the view and simply increase the "targets" count.
    let eventItem = this.getItemForPredicate(aItem =>
      aItem.attachment.url == url &&
      aItem.attachment.type == type);
    if (eventItem) {
      let { selectors, view: { targets } } = eventItem.attachment;
      if (selectors.indexOf(selector) == -1) {
        selectors.push(selector);
        targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length));
      }
      return;
    }

    // There's no easy way of grouping event types into higher-level groups,
    // so we need to do this by hand.
    let is = (...args) => args.indexOf(type) != -1;
    let has = str => type.contains(str);
    let starts = str => type.startsWith(str);
    let group;

    if (starts("animation")) {
      group = L10N.getStr("animationEvents");
    } else if (starts("audio")) {
      group = L10N.getStr("audioEvents");
    } else if (is("levelchange")) {
      group = L10N.getStr("batteryEvents");
    } else if (is("cut", "copy", "paste")) {
      group = L10N.getStr("clipboardEvents");
    } else if (starts("composition")) {
      group = L10N.getStr("compositionEvents");
    } else if (starts("device")) {
      group = L10N.getStr("deviceEvents");
    } else if (is("fullscreenchange", "fullscreenerror", "orientationchange",
      "overflow", "resize", "scroll", "underflow", "zoom")) {
      group = L10N.getStr("displayEvents");
    } else if (starts("drag") || starts("drop")) {
      group = L10N.getStr("Drag and dropEvents");
    } else if (starts("gamepad")) {
      group = L10N.getStr("gamepadEvents");
    } else if (is("canplay", "canplaythrough", "durationchange", "emptied",
      "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing",
      "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate",
      "volumechange", "waiting")) {
      group = L10N.getStr("mediaEvents");
    } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) {
      group = L10N.getStr("indexedDBEvents");
    } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid",
      "reset", "select", "submit")) {
      group = L10N.getStr("interactionEvents");
    } else if (starts("key") || is("input")) {
      group = L10N.getStr("keyboardEvents");
    } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) {
      group = L10N.getStr("mouseEvents");
    } else if (starts("DOM")) {
      group = L10N.getStr("mutationEvents");
    } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart",
      "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress",
      "visibilitychange")) {
      group = L10N.getStr("navigationEvents");
    } else if (is("pointerlockchange", "pointerlockerror")) {
      group = L10N.getStr("Pointer lockEvents");
    } else if (is("compassneedscalibration", "userproximity")) {
      group = L10N.getStr("sensorEvents");
    } else if (starts("storage")) {
      group = L10N.getStr("storageEvents");
    } else if (is("beginEvent", "endEvent", "repeatEvent")) {
      group = L10N.getStr("timeEvents");
    } else if (starts("touch")) {
      group = L10N.getStr("touchEvents");
    } else {
      group = L10N.getStr("otherEvents");
    }

    // Create the element node for the event listener item.
    let itemView = this._createItemView(type, selector, url);

    // Event breakpoints survive target navigations. Make sure the newly
    // inserted event item is correctly checked.
    let checkboxState =
      DebuggerController.Breakpoints.DOM.activeEventNames.indexOf(type) != -1;

    // Append an event listener item to this container.
    this.push([itemView.container], {
      staged: aOptions.staged, /* stage the item to be appended later? */
      attachment: {
        url: url,
        type: type,
        view: itemView,
        selectors: [selector],
        group: group,
        checkboxState: checkboxState,
        checkboxTooltip: this._eventCheckboxTooltip
      }
    });
  },

  /**
   * Gets all the event types known to this container.
   *
   * @return array
   *         List of event types, for example ["load", "click"...]
   */
  getAllEvents: function() {
    return this.attachments.map(e => e.type);
  },

  /**
   * Gets the checked event types in this container.
   *
   * @return array
   *         List of event types, for example ["load", "click"...]
   */
  getCheckedEvents: function() {
    return this.attachments.filter(e => e.checkboxState).map(e => e.type);
  },

  /**
   * Customization function for creating an item's UI.
   *
   * @param string aType
   *        The event type, for example "click".
   * @param string aSelector
   *        The target element's selector.
   * @param string url
   *        The source url in which the event listener is located.
   * @return object
   *         An object containing the event listener view nodes.
   */
  _createItemView: function(aType, aSelector, aUrl) {
    let container = document.createElement("hbox");
    container.className = "dbg-event-listener";

    let eventType = document.createElement("label");
    eventType.className = "plain dbg-event-listener-type";
    eventType.setAttribute("value", aType);
    container.appendChild(eventType);

    let typeSeparator = document.createElement("label");
    typeSeparator.className = "plain dbg-event-listener-separator";
    typeSeparator.setAttribute("value", this._onSelectorString);
    container.appendChild(typeSeparator);

    let eventTargets = document.createElement("label");
    eventTargets.className = "plain dbg-event-listener-targets";
    eventTargets.setAttribute("value", aSelector);
    container.appendChild(eventTargets);

    let selectorSeparator = document.createElement("label");
    selectorSeparator.className = "plain dbg-event-listener-separator";
    selectorSeparator.setAttribute("value", this._inSourceString);
    container.appendChild(selectorSeparator);

    let eventLocation = document.createElement("label");
    eventLocation.className = "plain dbg-event-listener-location";
    eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
    eventLocation.setAttribute("flex", "1");
    eventLocation.setAttribute("crop", "center");
    container.appendChild(eventLocation);

    return {
      container: container,
      type: eventType,
      targets: eventTargets,
      location: eventLocation
    };
  },

  /**
   * The check listener for the event listeners container.
   */
  _onCheck: function({ detail: { description, checked }, target }) {
    if (description == "item") {
      this.getItemForElement(target).attachment.checkboxState = checked;
      DebuggerController.Breakpoints.DOM.scheduleEventBreakpointsUpdate();
      return;
    }

    // Check all the event items in this group.
    this.items
      .filter(e => e.attachment.group == description)
      .forEach(e => this.callMethod("checkItem", e.target, checked));
  },

  /**
   * The select listener for the event listeners container.
   */
  _onClick: function({ target }) {
    // Changing the checkbox state is handled by the _onCheck event. Avoid
    // handling that again in this click event, so pass in "noSiblings"
    // when retrieving the target's item, to ignore the checkbox.
    let eventItem = this.getItemForElement(target, { noSiblings: true });
    if (eventItem) {
      let newState = eventItem.attachment.checkboxState ^= 1;
      this.callMethod("checkItem", eventItem.target, newState);
    }
  },

  _eventCheckboxTooltip: "",
  _onSelectorString: "",
  _inSourceString: "",
  _inNativeCodeString: ""
});

/**
 * Functions handling the global search UI.
 */
function GlobalSearchView() {
  dumpn("GlobalSearchView was instantiated");

  this._onHeaderClick = this._onHeaderClick.bind(this);
  this._onLineClick = this._onLineClick.bind(this);
  this._onMatchClick = this._onMatchClick.bind(this);
}

GlobalSearchView.prototype = Heritage.extend(WidgetMethods, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function() {
    dumpn("Initializing the GlobalSearchView");

    this.widget = new SimpleListWidget(document.getElementById("globalsearch"));
    this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");

    this.emptyText = L10N.getStr("noMatchingStringsText");
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function() {
    dumpn("Destroying the GlobalSearchView");
  },

  /**
   * Sets the results container hidden or visible. It's hidden by default.
   * @param boolean aFlag
   */
  set hidden(aFlag) {
    this.widget.setAttribute("hidden", aFlag);
    this._splitter.setAttribute("hidden", aFlag);
  },

  /**
   * Gets the visibility state of the global search container.
   * @return boolean
   */
  get hidden()
    this.widget.getAttribute("hidden") == "true" ||
    this._splitter.getAttribute("hidden") == "true",

  /**
   * Hides and removes all items from this search container.
   */
  clearView: function() {
    this.hidden = true;
    this.empty();
  },

  /**
   * Selects the next found item in this container.
   * Does not change the currently focused node.
   */
  selectNext: function() {
    let totalLineResults = LineResults.size();
    if (!totalLineResults) {
      return;
    }
    if (++this._currentlyFocusedMatch >= totalLineResults) {
      this._currentlyFocusedMatch = 0;
    }
    this._onMatchClick({
      target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
    });
  },

  /**
   * Selects the previously found item in this container.
   * Does not change the currently focused node.
   */
  selectPrev: function() {
    let totalLineResults = LineResults.size();
    if (!totalLineResults) {
      return;
    }
    if (--this._currentlyFocusedMatch < 0) {
      this._currentlyFocusedMatch = totalLineResults - 1;
    }
    this._onMatchClick({
      target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
    });
  },

  /**
   * Schedules searching for a string in all of the sources.
   *
   * @param string aToken
   *        The string to search for.
   * @param number aWait
   *        The amount of milliseconds to wait until draining.
   */
  scheduleSearch: function(aToken, aWait) {
    // The amount of time to wait for the requests to settle.
    let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY;
    let delay = aWait === undefined ? maxDelay / aToken.length : aWait;

    // Allow requests to settle down first.
    setNamedTimeout("global-search", delay, () => {
      // Start fetching as many sources as possible, then perform the search.
      let urls = DebuggerView.Sources.values;
      let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls);
      sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
    });
  },

  /**
   * Finds string matches in all the sources stored in the controller's cache,
   * and groups them by url and line number.
   *
   * @param string aToken
   *        The string to search for.
   * @param array aSources
   *        An array of [url, text] tuples for each source.
   */
  _doSearch: function(aToken, aSources) {
    // Don't continue filtering if the searched token is an empty string.
    if (!aToken) {
      this.clearView();
      return;
    }

    // Search is not case sensitive, prepare the actual searched token.
    let lowerCaseToken = aToken.toLowerCase();
    let tokenLength = aToken.length;

    // Create a Map containing search details for each source.
    let globalResults = new GlobalResults();

    // Search for the specified token in each source's text.
    for (let [url, text] of aSources) {
      // Verify that the search token is found anywhere in the source.
      if (!text.toLowerCase().contains(lowerCaseToken)) {
        continue;
      }
      // ...and if so, create a Map containing search details for each line.
      let sourceResults = new SourceResults(url, globalResults);

      // Search for the specified token in each line's text.
      text.split("\n").forEach((aString, aLine) => {
        // Search is not case sensitive, prepare the actual searched line.
        let lowerCaseLine = aString.toLowerCase();

        // Verify that the search token is found anywhere in this line.
        if (!lowerCaseLine.contains(lowerCaseToken)) {
          return;
        }
        // ...and if so, create a Map containing search details for each word.
        let lineResults = new LineResults(aLine, sourceResults);

        // Search for the specified token this line's text.
        lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => {
          let prevLength = aPrev.length;
          let currLength = aCurr.length;

          // Everything before the token is unmatched.
          let unmatched = aString.substr(prevLength, currLength);
          lineResults.add(unmatched);

          // The lowered-case line was split by the lowered-case token. So,
          // get the actual matched text from the original line's text.
          if (aIndex != aArray.length - 1) {
            let matched = aString.substr(prevLength + currLength, tokenLength);
            let range = { start: prevLength + currLength, length: matched.length };
            lineResults.add(matched, range, true);
          }

          // Continue with the next sub-region in this line's text.
          return aPrev + aToken + aCurr;
        }, "");

        if (lineResults.matchCount) {
          sourceResults.add(lineResults);
        }
      });

      if (sourceResults.matchCount) {
        globalResults.add(sourceResults);
      }
    }

    // Rebuild the results, then signal if there are any matches.
    if (globalResults.matchCount) {
      this.hidden = false;
      this._currentlyFocusedMatch = -1;
      this._createGlobalResultsUI(globalResults);
      window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND);
    } else {
      window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND);
    }
  },

  /**
   * Creates global search results entries and adds them to this container.
   *
   * @param GlobalResults aGlobalResults
   *        An object containing all source results, grouped by source location.
   */
  _createGlobalResultsUI: function(aGlobalResults) {
    let i = 0;

    for (let sourceResults of aGlobalResults) {
      if (i++ == 0) {
        this._createSourceResultsUI(sourceResults);
      } else {
        // Dispatch subsequent document manipulation operations, to avoid
        // blocking the main thread when a large number of search results
        // is found, thus giving the impression of faster searching.
        Services.tm.currentThread.dispatch({ run:
          this._createSourceResultsUI.bind(this, sourceResults)
        }, 0);
      }
    }
  },

  /**
   * Creates source search results entries and adds them to this container.
   *
   * @param SourceResults aSourceResults
   *        An object containing all the matched lines for a specific source.
   */
  _createSourceResultsUI: function(aSourceResults) {
    // Create the element node for the source results item.
    let container = document.createElement("hbox");
    aSourceResults.createView(container, {
      onHeaderClick: this._onHeaderClick,
      onLineClick: this._onLineClick,
      onMatchClick: this._onMatchClick
    });

    // Append a source results item to this container.
    let item = this.push([container], {
      index: -1, /* specifies on which position should the item be appended */
      attachment: {
        sourceResults: aSourceResults
      }
    });
  },

  /**
   * The click listener for a results header.
   */
  _onHeaderClick: function(e) {
    let sourceResultsItem = SourceResults.getItemForElement(e.target);
    sourceResultsItem.instance.toggle(e);
  },

  /**
   * The click listener for a results line.
   */
  _onLineClick: function(e) {
    let lineResultsItem = LineResults.getItemForElement(e.target);
    this._onMatchClick({ target: lineResultsItem.firstMatch });
  },

  /**
   * The click listener for a result match.
   */
  _onMatchClick: function(e) {
    if (e instanceof Event) {
      e.preventDefault();
      e.stopPropagation();
    }

    let target = e.target;
    let sourceResultsItem = SourceResults.getItemForElement(target);
    let lineResultsItem = LineResults.getItemForElement(target);

    sourceResultsItem.instance.expand();
    this._currentlyFocusedMatch = LineResults.indexOfElement(target);
    this._scrollMatchIntoViewIfNeeded(target);
    this._bounceMatch(target);

    let url = sourceResultsItem.instance.url;
    let line = lineResultsItem.instance.line;

    DebuggerView.setEditorLocation(url, line + 1, { noDebug: true });

    let range = lineResultsItem.lineData.range;
    let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 });
    let [ anchor, head ] = DebuggerView.editor.getPosition(
      cursor + range.start,
      cursor + range.start + range.length
    );

    DebuggerView.editor.setSelection(anchor, head);
  },

  /**
   * Scrolls a match into view if not already visible.
   *
   * @param nsIDOMNode aMatch
   *        The match to scroll into view.
   */
  _scrollMatchIntoViewIfNeeded: function(aMatch) {
    this.widget.ensureElementIsVisible(aMatch);
  },

  /**
   * Starts a bounce animation for a match.
   *
   * @param nsIDOMNode aMatch
   *        The match to start a bounce animation for.
   */
  _bounceMatch: function(aMatch) {
    Services.tm.currentThread.dispatch({ run: () => {
      aMatch.addEventListener("transitionend", function onEvent() {
        aMatch.removeEventListener("transitionend", onEvent);
        aMatch.removeAttribute("focused");
      });
      aMatch.setAttribute("focused", "");
    }}, 0);
    aMatch.setAttribute("focusing", "");
  },

  _splitter: null,
  _currentlyFocusedMatch: -1,
  _forceExpandResults: false
});

/**
 * An object containing all source results, grouped by source location.
 * Iterable via "for (let [location, sourceResults] of globalResults) { }".
 */
function GlobalResults() {
  this._store = [];
  SourceResults._itemsByElement = new Map();
  LineResults._itemsByElement = new Map();
}

GlobalResults.prototype = {
  /**
   * Adds source results to this store.
   *
   * @param SourceResults aSourceResults
   *        An object containing search results for a specific source.
   */
  add: function(aSourceResults) {
    this._store.push(aSourceResults);
  },

  /**
   * Gets the number of source results in this store.
   */
  get matchCount() this._store.length
};

/**
 * An object containing all the matched lines for a specific source.
 * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }".
 *
 * @param string aUrl
 *        The target source url.
 * @param GlobalResults aGlobalResults
 *        An object containing all source results, grouped by source location.
 */
function SourceResults(aUrl, aGlobalResults) {
  this.url = aUrl;
  this._globalResults = aGlobalResults;
  this._store = [];
}

SourceResults.prototype = {
  /**
   * Adds line results to this store.
   *
   * @param LineResults aLineResults
   *        An object containing search results for a specific line.
   */
  add: function(aLineResults) {
    this._store.push(aLineResults);
  },

  /**
   * Gets the number of line results in this store.
   */
  get matchCount() this._store.length,

  /**
   * Expands the element, showing all the added details.
   */
  expand: function() {
    this._resultsContainer.removeAttribute("hidden");
    this._arrow.setAttribute("open", "");
  },

  /**
   * Collapses the element, hiding all the added details.
   */
  collapse: function() {
    this._resultsContainer.setAttribute("hidden", "true");
    this._arrow.removeAttribute("open");
  },

  /**
   * Toggles between the element collapse/expand state.
   */
  toggle: function(e) {
    this.expanded ^= 1;
  },

  /**
   * Gets this element's expanded state.
   * @return boolean
   */
  get expanded()
    this._resultsContainer.getAttribute("hidden") != "true" &&
    this._arrow.hasAttribute("open"),

  /**
   * Sets this element's expanded state.
   * @param boolean aFlag
   */
  set expanded(aFlag) this[aFlag ? "expand" : "collapse"](),

  /**
   * Gets the element associated with this item.
   * @return nsIDOMNode
   */
  get target() this._target,

  /**
   * Customization function for creating this item's UI.
   *
   * @param nsIDOMNode aElementNode
   *        The element associated with the displayed item.
   * @param object aCallbacks
   *        An object containing all the necessary callback functions:
   *          - onHeaderClick
   *          - onMatchClick
   */
  createView: function(aElementNode, aCallbacks) {
    this._target = aElementNode;

    let arrow = this._arrow = document.createElement("box");
    arrow.className = "arrow";

    let locationNode = document.createElement("label");
    locationNode.className = "plain dbg-results-header-location";
    locationNode.setAttribute("value", this.url);

    let matchCountNode = document.createElement("label");
    matchCountNode.className = "plain dbg-results-header-match-count";
    matchCountNode.setAttribute("value", "(" + this.matchCount + ")");

    let resultsHeader = this._resultsHeader = document.createElement("hbox");
    resultsHeader.className = "dbg-results-header";
    resultsHeader.setAttribute("align", "center")
    resultsHeader.appendChild(arrow);
    resultsHeader.appendChild(locationNode);
    resultsHeader.appendChild(matchCountNode);
    resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false);

    let resultsContainer = this._resultsContainer = document.createElement("vbox");
    resultsContainer.className = "dbg-results-container";
    resultsContainer.setAttribute("hidden", "true");

    // Create lines search results entries and add them to this container.
    // Afterwards, if the number of matches is reasonable, expand this
    // container automatically.
    for (let lineResults of this._store) {
      lineResults.createView(resultsContainer, aCallbacks);
    }
    if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) {
      this.expand();
    }

    let resultsBox = document.createElement("vbox");
    resultsBox.setAttribute("flex", "1");
    resultsBox.appendChild(resultsHeader);
    resultsBox.appendChild(resultsContainer);

    aElementNode.id = "source-results-" + this.url;
    aElementNode.className = "dbg-source-results";
    aElementNode.appendChild(resultsBox);

    SourceResults._itemsByElement.set(aElementNode, { instance: this });
  },

  url: "",
  _globalResults: null,
  _store: null,
  _target: null,
  _arrow: null,
  _resultsHeader: null,
  _resultsContainer: null
};

/**
 * An object containing all the matches for a specific line.
 * Iterable via "for (let chunk of lineResults) { }".
 *
 * @param number aLine
 *        The target line in the source.
 * @param SourceResults aSourceResults
 *        An object containing all the matched lines for a specific source.
 */
function LineResults(aLine, aSourceResults) {
  this.line = aLine;
  this._sourceResults = aSourceResults;
  this._store = [];
  this._matchCount = 0;
}

LineResults.prototype = {
  /**
   * Adds string details to this store.
   *
   * @param string aString
   *        The text contents chunk in the line.
   * @param object aRange
   *        An object containing the { start, length } of the chunk.
   * @param boolean aMatchFlag
   *        True if the chunk is a matched string, false if just text content.
   */
  add: function(aString, aRange, aMatchFlag) {
    this._store.push({ string: aString, range: aRange, match: !!aMatchFlag });
    this._matchCount += aMatchFlag ? 1 : 0;
  },

  /**
   * Gets the number of word results in this store.
   */
  get matchCount() this._matchCount,

  /**
   * Gets the element associated with this item.
   * @return nsIDOMNode
   */
  get target() this._target,

  /**
   * Customization function for creating this item's UI.
   *
   * @param nsIDOMNode aElementNode
   *        The element associated with the displayed item.
   * @param object aCallbacks
   *        An object containing all the necessary callback functions:
   *          - onMatchClick
   *          - onLineClick
   */
  createView: function(aElementNode, aCallbacks) {
    this._target = aElementNode;

    let lineNumberNode = document.createElement("label");
    lineNumberNode.className = "plain dbg-results-line-number";
    lineNumberNode.classList.add("devtools-monospace");
    lineNumberNode.setAttribute("value", this.line + 1);

    let lineContentsNode = document.createElement("hbox");
    lineContentsNode.className = "dbg-results-line-contents";
    lineContentsNode.classList.add("devtools-monospace");
    lineContentsNode.setAttribute("flex", "1");

    let lineString = "";
    let lineLength = 0;
    let firstMatch = null;

    for (let lineChunk of this._store) {
      let { string, range, match } = lineChunk;
      lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength);
      lineLength += string.length;

      let lineChunkNode = document.createElement("label");
      lineChunkNode.className = "plain dbg-results-line-contents-string";
      lineChunkNode.setAttribute("value", lineString);
      lineChunkNode.setAttribute("match", match);
      lineContentsNode.appendChild(lineChunkNode);

      if (match) {
        this._entangleMatch(lineChunkNode, lineChunk);
        lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false);
        firstMatch = firstMatch || lineChunkNode;
      }
      if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
        lineContentsNode.appendChild(this._ellipsis.cloneNode(true));
        break;
      }
    }

    this._entangleLine(lineContentsNode, firstMatch);
    lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false);

    let searchResult = document.createElement("hbox");
    searchResult.className = "dbg-search-result";
    searchResult.appendChild(lineNumberNode);
    searchResult.appendChild(lineContentsNode);

    aElementNode.appendChild(searchResult);
  },

  /**
   * Handles a match while creating the view.
   * @param nsIDOMNode aNode
   * @param object aMatchChunk
   */
  _entangleMatch: function(aNode, aMatchChunk) {
    LineResults._itemsByElement.set(aNode, {
      instance: this,
      lineData: aMatchChunk
    });
  },

  /**
   * Handles a line while creating the view.
   * @param nsIDOMNode aNode
   * @param nsIDOMNode aFirstMatch
   */
  _entangleLine: function(aNode, aFirstMatch) {
    LineResults._itemsByElement.set(aNode, {
      instance: this,
      firstMatch: aFirstMatch,
      ignored: true
    });
  },

  /**
   * An nsIDOMNode label with an ellipsis value.
   */
  _ellipsis: (function() {
    let label = document.createElement("label");
    label.className = "plain dbg-results-line-contents-string";
    label.setAttribute("value", L10N.ellipsis);
    return label;
  })(),

  line: 0,
  _sourceResults: null,
  _store: null,
  _target: null
};

/**
 * A generator-iterator over the global, source or line results.
 */
GlobalResults.prototype["@@iterator"] =
SourceResults.prototype["@@iterator"] =
LineResults.prototype["@@iterator"] = function*() {
  yield* this._store;
};

/**
 * Gets the item associated with the specified element.
 *
 * @param nsIDOMNode aElement
 *        The element used to identify the item.
 * @return object
 *         The matched item, or null if nothing is found.
 */
SourceResults.getItemForElement =
LineResults.getItemForElement = function(aElement) {
  return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true });
};

/**
 * Gets the element associated with a particular item at a specified index.
 *
 * @param number aIndex
 *        The index used to identify the item.
 * @return nsIDOMNode
 *         The matched element, or null if nothing is found.
 */
SourceResults.getElementAtIndex =
LineResults.getElementAtIndex = function(aIndex) {
  for (let [element, item] of this._itemsByElement) {
    if (!item.ignored && !aIndex--) {
      return element;
    }
  }
  return null;
};

/**
 * Gets the index of an item associated with the specified element.
 *
 * @param nsIDOMNode aElement
 *        The element to get the index for.
 * @return number
 *         The index of the matched element, or -1 if nothing is found.
 */
SourceResults.indexOfElement =
LineResults.indexOfElement = function(aElement) {
  let count = 0;
  for (let [element, item] of this._itemsByElement) {
    if (element == aElement) {
      return count;
    }
    if (!item.ignored) {
      count++;
    }
  }
  return -1;
};

/**
 * Gets the number of cached items associated with a specified element.
 *
 * @return number
 *         The number of key/value pairs in the corresponding map.
 */
SourceResults.size =
LineResults.size = function() {
  let count = 0;
  for (let [, item] of this._itemsByElement) {
    if (!item.ignored) {
      count++;
    }
  }
  return count;
};

/**
 * Preliminary setup for the DebuggerView object.
 */
DebuggerView.Sources = new SourcesView();
DebuggerView.VariableBubble = new VariableBubbleView();
DebuggerView.Tracer = new TracerView();
DebuggerView.WatchExpressions = new WatchExpressionsView();
DebuggerView.EventListeners = new EventListenersView();
DebuggerView.GlobalSearch = new GlobalSearchView();
