/* -*- 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";

/**
 * Functions handling the stackframes UI.
 */
function StackFramesView() {
  dumpn("StackFramesView was instantiated");
  MenuContainer.call(this);
  this._onClick = this._onClick.bind(this);
  this._onScroll = this._onScroll.bind(this);
}

create({ constructor: StackFramesView, proto: MenuContainer.prototype }, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function DVSF_initialize() {
    dumpn("Initializing the StackFramesView");
    this._container = new StackList(document.getElementById("stackframes"));
    this._container.emptyText = L10N.getStr("emptyStackText");

    this._container.addEventListener("click", this._onClick, false);
    this._container.addEventListener("scroll", this._onScroll, true);
    window.addEventListener("resize", this._onScroll, true);

    this._cache = new Map();
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function DVSF_destroy() {
    dumpn("Destroying the StackFramesView");
    this._container.removeEventListener("click", this._onClick, false);
    this._container.removeEventListener("scroll", this._onScroll, true);
    window.removeEventListener("resize", this._onScroll, true);
  },

  /**
   * Adds a frame in this stackframes container.
   *
   * @param string aFrameName
   *        Name to be displayed in the list.
   * @param string aFrameDetails
   *        Details to be displayed in the list.
   * @param number aDepth
   *        The frame depth specified by the debugger.
   */
  addFrame: function DVSF_addFrame(aFrameName, aFrameDetails, aDepth) {
    // Stackframes are UI elements which benefit from visible panes.
    DebuggerView.showPanesSoon();

    // Append a stackframe item to this container.
    let stackframeItem = this.push(aFrameName, aFrameDetails, {
      forced: true,
      unsorted: true,
      relaxed: true,
      attachment: {
        depth: aDepth
      }
    });

    // Check if stackframe was already appended.
    if (!stackframeItem) {
      return;
    }

    let element = stackframeItem.target;
    element.id = "stackframe-" + aDepth;
    element.className = "dbg-stackframe list-item";
    element.labelNode.className = "dbg-stackframe-name plain";
    element.valueNode.className = "dbg-stackframe-details plain";

    this._cache.set(aDepth, stackframeItem);
  },

  /**
   * Highlights a frame in this stackframes container.
   *
   * @param number aDepth
   *        The frame depth specified by the debugger controller.
   */
  highlightFrame: function DVSF_highlightFrame(aDepth) {
    this._container.selectedItem = this._cache.get(aDepth).target;
  },

  /**
   * Specifies if the active thread has more frames that need to be loaded.
   */
  dirty: false,

  /**
   * The click listener for the stackframes container.
   */
  _onClick: function DVSF__onClick(e) {
    let item = this.getItemForElement(e.target);
    if (item) {
      // The container is not empty and we clicked on an actual item.
      DebuggerController.StackFrames.selectFrame(item.attachment.depth);
    }
  },

  /**
   * The scroll listener for the stackframes container.
   */
  _onScroll: function DVSF__onScroll() {
    // Update the stackframes container only if we have to.
    if (this.dirty) {
      let list = this._container._list;

      // If the stackframes container was scrolled past 95% of the height,
      // load more content.
      if (list.scrollTop >= (list.scrollHeight - list.clientHeight) * 0.95) {
        DebuggerController.StackFrames.addMoreFrames();
        this.dirty = false;
      }
    }
  },

  _cache: null
});

/**
 * Utility functions for handling stackframes.
 */
let StackFrameUtils = {
  /**
   * Create a textual representation for the specified stack frame
   * to display in the stack frame container.
   *
   * @param object aFrame
   *        The stack frame to label.
   */
  getFrameTitle: function SFU_getFrameTitle(aFrame) {
    if (aFrame.type == "call") {
      let c = aFrame.callee;
      return (c.name || c.userDisplayName || c.displayName || "(anonymous)");
    }
    return "(" + aFrame.type + ")";
  }
};

/**
 * Functions handling the breakpoints UI.
 */
function BreakpointsView() {
  dumpn("BreakpointsView was instantiated");
  MenuContainer.call(this);
  this._createItemView = this._createItemView.bind(this);
  this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
  this._onEditorLoad = this._onEditorLoad.bind(this);
  this._onEditorUnload = this._onEditorUnload.bind(this);
  this._onEditorSelection = this._onEditorSelection.bind(this);
  this._onEditorContextMenu = this._onEditorContextMenu.bind(this);
  this._onEditorContextMenuPopupHidden = this._onEditorContextMenuPopupHidden.bind(this);
  this._onBreakpointClick = this._onBreakpointClick.bind(this);
  this._onCheckboxClick = this._onCheckboxClick.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);
}

create({ constructor: BreakpointsView, proto: MenuContainer.prototype }, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function DVB_initialize() {
    dumpn("Initializing the BreakpointsView");
    this._container = new StackList(document.getElementById("breakpoints"));
    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-textbox");

    this._container.emptyText = L10N.getStr("emptyBreakpointsText");
    this._container.itemFactory = this._createItemView;
    this._container.uniquenessQualifier = 2;

    window.addEventListener("Debugger:EditorLoaded", this._onEditorLoad, false);
    window.addEventListener("Debugger:EditorUnloaded", this._onEditorUnload, false);
    this._container.addEventListener("click", this._onBreakpointClick, false);
    this._cmPopup.addEventListener("popuphidden", this._onEditorContextMenuPopupHidden, 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._cache = new Map();
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function DVB_destroy() {
    dumpn("Destroying the BreakpointsView");
    window.removeEventListener("Debugger:EditorLoaded", this._onEditorLoad, false);
    window.removeEventListener("Debugger:EditorUnloaded", this._onEditorUnload, false);
    this._container.removeEventListener("click", this._onBreakpointClick, false);
    this._cmPopup.removeEventListener("popuphidden", this._onEditorContextMenuPopupHidden, false);
    this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
    this._cbPanel.removeEventListener("popupshown", this._onConditionalPopupShown, false);
    this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
    this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);

    this._cbPanel.hidePopup();
  },

  /**
   * Adds a breakpoint in this breakpoints container.
   *
   * @param string aSourceLocation
   *        The breakpoint source location specified by the debugger controller.
   * @param number aLineNumber
   *        The breakpoint line number specified by the debugger controller.
   * @param string aActor
   *        A breakpoint identifier specified by the debugger controller.
   * @param string aLineInfo
   *        Line information (parent source etc.) to be displayed in the list.
   * @param string aLineText
   *        Line text to be displayed in the list.
   * @param boolean aConditionalFlag [optional]
   *        A flag specifying if this is a conditional breakpoint.
   * @param boolean aOpenPopupFlag [optional]
   *        A flag specifying if the expression popup should be shown.
   */
  addBreakpoint: function DVB_addBreakpoint(aSourceLocation, aLineNumber,
                                            aActor, aLineInfo, aLineText,
                                            aConditionalFlag, aOpenPopupFlag) {
    // Append a breakpoint item to this container.
    let breakpointItem = this.push(aLineInfo.trim(), aLineText.trim(), {
      forced: true,
      attachment: {
        enabled: true,
        sourceLocation: aSourceLocation,
        lineNumber: aLineNumber,
        isConditional: aConditionalFlag
      }
    });

    // Check if breakpoint was already appended.
    if (!breakpointItem) {
      this.enableBreakpoint(aSourceLocation, aLineNumber, { id: aActor });
      return;
    }

    let element = breakpointItem.target;
    element.id = "breakpoint-" + aActor;
    element.className = "dbg-breakpoint list-item";
    element.infoNode.className = "dbg-breakpoint-info plain";
    element.textNode.className = "dbg-breakpoint-text plain";
    element.setAttribute("contextmenu", this._createContextMenu(element));

    breakpointItem.finalize = this._onBreakpointRemoved;
    this._cache.set(this._key(aSourceLocation, aLineNumber), breakpointItem);

    // If this is a conditional breakpoint, display the panes and a panel
    // to input the corresponding conditional expression.
    if (aConditionalFlag && aOpenPopupFlag) {
      this.highlightBreakpoint(aSourceLocation, aLineNumber, { openPopup: true });
    }
  },

  /**
   * Removes a breakpoint from this breakpoints container.
   *
   * @param string aSourceLocation
   *        The breakpoint source location.
   * @param number aLineNumber
   *        The breakpoint line number.
   */
  removeBreakpoint: function DVB_removeBreakpoint(aSourceLocation, aLineNumber) {
    let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
    if (breakpointItem) {
      this.remove(breakpointItem);
      this._cache.delete(this._key(aSourceLocation, aLineNumber));
    }
  },

  /**
   * Enables a breakpoint.
   *
   * @param string aSourceLocation
   *        The breakpoint source location.
   * @param number aLineNumber
   *        The breakpoint line number.
   * @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).
   *          - callback: function to invoke once the breakpoint is disabled
   *          - id: a new id to be applied to the corresponding element node
   * @return boolean
   *         True if breakpoint existed and was enabled, false otherwise.
   */
  enableBreakpoint:
  function DVB_enableBreakpoint(aSourceLocation, aLineNumber, aOptions = {}) {
    let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
    if (breakpointItem) {
      // Set a new id to the corresponding breakpoint element if required.
      if (aOptions.id) {
        breakpointItem.target.id = "breakpoint-" + aOptions.id;
      }

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

      let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
      let breakpointLocation = { url: url, line: line };
      DebuggerController.Breakpoints.addBreakpoint(breakpointLocation, aOptions.callback, {
        noPaneUpdate: true
      });

      // Breakpoint is now enabled.
      breakpointItem.attachment.enabled = true;
      return true;
    }
    return false;
  },

  /**
   * Disables a breakpoint.
   *
   * @param string aSourceLocation
   *        The breakpoint source location.
   * @param number aLineNumber
   *        The breakpoint line number.
   * @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).
   *          - callback: function to invoke once the breakpoint is disabled
   * @return boolean
   *         True if breakpoint existed and was disabled, false otherwise.
   */
  disableBreakpoint:
  function DVB_disableBreakpoint(aSourceLocation, aLineNumber, aOptions = {}) {
    let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
    if (breakpointItem) {
      // Update the checkbox state if necessary.
      if (!aOptions.silent) {
        breakpointItem.target.checkbox.removeAttribute("checked");
      }

      let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
      let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
      DebuggerController.Breakpoints.removeBreakpoint(breakpointClient, aOptions.callback, {
        noPaneUpdate: true
      });

      // Breakpoint is now disabled.
      breakpointItem.attachment.enabled = false;
      return true;
    }
    return false;
  },

  /**
   * Highlights a breakpoint in this breakpoints container.
   *
   * @param string aSourceLocation
   *        The breakpoint source location.
   * @param number aLineNumber
   *        The breakpoint line number.
   * @param object aFlags [optional]
   *        An object containing some of the following boolean properties:
   *          - updateEditor: true if editor updates should be allowed
   *          - openPopup: true if the expression popup should be shown
   */
  highlightBreakpoint:
  function DVB_highlightBreakpoint(aSourceLocation, aLineNumber, aFlags = {}) {
    let breakpointItem = this.getBreakpoint(aSourceLocation, aLineNumber);
    if (breakpointItem) {
      // Update the editor source location and line number if necessary.
      if (aFlags.updateEditor) {
        DebuggerView.updateEditor(aSourceLocation, aLineNumber, { noDebug: true });
      }

      // If the breakpoint requires a new conditional expression, display
      // the panes and the panel to input the corresponding expression.
      if (aFlags.openPopup && breakpointItem.attachment.isConditional) {
        let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
        let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);

        // The conditional expression popup can only be shown with visible panes.
        DebuggerView.showPanesSoon(function() {
          // Verify if the breakpoint wasn't removed before the panes were shown.
          if (this.getBreakpoint(aSourceLocation, aLineNumber)) {
            this._cbTextbox.value = breakpointClient.conditionalExpression || "";
            this._cbPanel.openPopup(breakpointItem.target,
              BREAKPOINT_CONDITIONAL_POPUP_POSITION,
              BREAKPOINT_CONDITIONAL_POPUP_OFFSET);
          }
        }.bind(this));
      } else {
        this._cbPanel.hidePopup();
      }

      // Breakpoint is now highlighted.
      this._container.selectedItem = breakpointItem.target;
    }
    // Can't find a breakpoint at the requested source location and line number.
    else {
      this._container.selectedIndex = -1;
      this._cbPanel.hidePopup();
    }
  },

  /**
   * Unhighlights the current breakpoint in this breakpoints container.
   */
  unhighlightBreakpoint: function DVB_highlightBreakpoint() {
    this.highlightBreakpoint(null);
  },

  /**
   * Checks whether a breakpoint with the specified source location and
   * line number exists in this container, and returns the corresponding item
   * if true, null otherwise.
   *
   * @param string aSourceLocation
   *        The breakpoint source location.
   * @param number aLineNumber
   *        The breakpoint line number.
   * @return object
   *         The corresponding item.
   */
  getBreakpoint: function DVB_getBreakpoint(aSourceLocation, aLineNumber) {
    return this._cache.get(this._key(aSourceLocation, aLineNumber));
  },

  /**
   * Gets the currently selected breakpoint client.
   * @return object
   */
  get selectedClient() {
    let selectedItem = this.selectedItem;
    if (selectedItem) {
      let { sourceLocation: url, lineNumber: line } = selectedItem.attachment;
      return DebuggerController.Breakpoints.getBreakpoint(url, line);
    }
    return null;
  },

  /**
   * Customization function for creating an item's UI.
   *
   * @param nsIDOMNode aElementNode
   *        The element associated with the displayed item.
   * @param string aInfo
   *        The breakpoint's line info.
   * @param string aText
   *        The breakpoint's line text.
   * @param any aAttachment [optional]
   *        Some attached primitive/object.
   */
  _createItemView: function DVB__createItemView(aElementNode, aInfo, aText, aAttachment) {
    let checkbox = document.createElement("checkbox");
    checkbox.setAttribute("checked", "true");
    checkbox.addEventListener("click", this._onCheckboxClick, false);

    let lineInfo = document.createElement("label");
    lineInfo.setAttribute("value", aInfo);
    lineInfo.setAttribute("crop", "end");

    let lineText = document.createElement("label");
    lineText.setAttribute("value", aText);
    lineText.setAttribute("crop", "end");
    lineText.setAttribute("tooltiptext",
      aText.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH));

    let state = document.createElement("vbox");
    state.className = "state";
    state.setAttribute("pack", "center");
    state.appendChild(checkbox);

    let content = document.createElement("vbox");
    content.className = "content";
    content.setAttribute("flex", "1");
    content.appendChild(lineInfo);
    content.appendChild(lineText);

    aElementNode.appendChild(state);
    aElementNode.appendChild(content);

    aElementNode.checkbox = checkbox;
    aElementNode.infoNode = lineInfo;
    aElementNode.textNode = lineText;
  },

  /**
   * Creates a context menu for a breakpoint element.
   *
   * @param nsIDOMNode aElementNode
   *        The element associated with the displayed breakpoint item.
   * @return string
   *         The newly created menupopup id.
   */
  _createContextMenu: function DVB__createContextMenu(aElementNode) {
    let breakpointId = aElementNode.id;
    let commandsetId = "breakpointCommandset-" + breakpointId;
    let menupopupId = "breakpointMenupopup-" + breakpointId;

    let commandset = document.createElement("commandset");
    let menupopup = document.createElement("menupopup");
    commandset.setAttribute("id", commandsetId);
    menupopup.setAttribute("id", menupopupId);

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

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

    aElementNode.commandset = commandset;
    aElementNode.menupopup = menupopup;
    return menupopupId;

    /**
     * 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-";
      let commandId = prefix + aName + "-" + breakpointId + "-command";
      let menuitemId = prefix + aName + "-" + breakpointId + "-menuitem";

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

      command.setAttribute("id", commandId);
      command.setAttribute("label", label);
      command.addEventListener("command", func.bind(this, aElementNode), false);

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

      commandset.appendChild(command);
      menupopup.appendChild(menuitem);
      aElementNode[aName] = { menuitem: menuitem, command: command };
    }

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

  /**
   * Destroys a context menu for a breakpoint.
   *
   * @param nsIDOMNode aElementNode
   *        The element associated with the displayed breakpoint item.
   */
  _destroyContextMenu: function DVB__destroyContextMenu(aElementNode) {
    let commandset = aElementNode.commandset;
    let menupopup = aElementNode.menupopup;

    commandset.parentNode.removeChild(commandset);
    menupopup.parentNode.removeChild(menupopup);
  },

  /**
   * Function called each time a breakpoint item is removed.
   */
  _onBreakpointRemoved: function DVB__onBreakpointRemoved(aItem) {
    this._destroyContextMenu(aItem.target);
  },

  /**
   * The load listener for the source editor.
   */
  _onEditorLoad: function DVB__onEditorLoad({ detail: editor }) {
    editor.addEventListener("Selection", this._onEditorSelection, false);
    editor.addEventListener("ContextMenu", this._onEditorContextMenu, false);
  },

  /**
   * The unload listener for the source editor.
   */
  _onEditorUnload: function DVB__onEditorUnload({ detail: editor }) {
    editor.removeEventListener("Selection", this._onEditorSelection, false);
    editor.removeEventListener("ContextMenu", this._onEditorContextMenu, false);
  },

  /**
   * The selection listener for the source editor.
   */
  _onEditorSelection: function DVB__onEditorSelection(e) {
    let { start, end } = e.newValue;

    let sourceLocation = DebuggerView.Sources.selectedValue;
    let lineStart = DebuggerView.editor.getLineAtOffset(start) + 1;
    let lineEnd = DebuggerView.editor.getLineAtOffset(end) + 1;

    if (this.getBreakpoint(sourceLocation, lineStart) && lineStart == lineEnd) {
      this.highlightBreakpoint(sourceLocation, lineStart);
    } else {
      this.unhighlightBreakpoint();
    }
  },

  /**
   * The context menu listener for the source editor.
   */
  _onEditorContextMenu: function DVB__onEditorContextMenu({ x, y }) {
    let offset = DebuggerView.editor.getOffsetAtLocation(x, y);
    let line = DebuggerView.editor.getLineAtOffset(offset);
    this._editorContextMenuLineNumber = line;
  },

  /**
   * The context menu popup hidden listener for the source editor.
   */
  _onEditorContextMenuPopupHidden: function DVB__onEditorContextMenuPopupHidden() {
    this._editorContextMenuLineNumber = -1;
  },

  /**
   * Called when the add breakpoint key sequence was pressed.
   */
  _onCmdAddBreakpoint: function BP__onCmdAddBreakpoint() {
    // If this command was executed via the context menu, add the breakpoint
    // on the currently hovered line in the source editor.
    if (this._editorContextMenuLineNumber >= 0) {
      DebuggerView.editor.setCaretPosition(this._editorContextMenuLineNumber);
    }
    // Avoid placing breakpoints incorrectly when using key shortcuts.
    this._editorContextMenuLineNumber = -1;

    let url = DebuggerView.Sources.selectedValue;
    let line = DebuggerView.editor.getCaretPosition().line + 1;
    let breakpointItem = this.getBreakpoint(url, line);

    // If a breakpoint already existed, remove it now.
    if (breakpointItem) {
      let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line)
      DebuggerController.Breakpoints.removeBreakpoint(breakpointClient);
      DebuggerView.Breakpoints.unhighlightBreakpoint();
    }
    // No breakpoint existed at the required location, add one now.
    else {
      let breakpointLocation = { url: url, line: line };
      DebuggerController.Breakpoints.addBreakpoint(breakpointLocation);
    }
  },

  /**
   * Called when the add conditional breakpoint key sequence was pressed.
   */
  _onCmdAddConditionalBreakpoint: function BP__onCmdAddConditionalBreakpoint() {
    // If this command was executed via the context menu, add the breakpoint
    // on the currently hovered line in the source editor.
    if (this._editorContextMenuLineNumber >= 0) {
      DebuggerView.editor.setCaretPosition(this._editorContextMenuLineNumber);
    }
    // Avoid placing breakpoints incorrectly when using key shortcuts.
    this._editorContextMenuLineNumber = -1;

    let url =  DebuggerView.Sources.selectedValue;
    let line = DebuggerView.editor.getCaretPosition().line + 1;
    let breakpointItem = this.getBreakpoint(url, line);

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

  /**
   * The popup showing listener for the breakpoints conditional expression panel.
   */
  _onConditionalPopupShowing: function DVB__onConditionalPopupShowing() {
    this._popupShown = true;
  },

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

  /**
   * The popup hiding listener for the breakpoints conditional expression panel.
   */
  _onConditionalPopupHiding: function DVB__onConditionalPopupHiding() {
    this._popupShown = false;
    this.selectedClient.conditionalExpression = this._cbTextbox.value;
  },

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

  /**
   * The click listener for the breakpoints container.
   */
  _onBreakpointClick: function DVB__onBreakpointClick(e) {
    let breakpointItem = this.getItemForElement(e.target);
    if (!breakpointItem) {
      // The container is empty or we didn't click on an actual item.
      return;
    }
    let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
    this.highlightBreakpoint(url, line, { updateEditor: true, openPopup: e.button == 0 });
  },

  /**
   * The click listener for a breakpoint checkbox.
   */
  _onCheckboxClick: function DVB__onCheckboxClick(e) {
    let breakpointItem = this.getItemForElement(e.target);
    if (!breakpointItem) {
      // The container is empty or we didn't click on an actual item.
      return;
    }
    let { sourceLocation: url, lineNumber: line, enabled } = breakpointItem.attachment;
    this[enabled ? "disableBreakpoint" : "enableBreakpoint"](url, line, { silent: true });

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

  /**
   * Listener handling the "setConditional" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onSetConditional: function DVB__onSetConditional(aTarget) {
    if (!aTarget) {
      return;
    }
    let breakpointItem = this.getItemForElement(aTarget);
    let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;

    breakpointItem.attachment.isConditional = true;
    this.highlightBreakpoint(url, line, { openPopup: true });
  },

  /**
   * Listener handling the "enableSelf" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onEnableSelf: function DVB__onEnableSelf(aTarget) {
    if (!aTarget) {
      return;
    }
    let breakpointItem = this.getItemForElement(aTarget);
    let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;

    if (this.enableBreakpoint(url, line)) {
      aTarget.enableSelf.menuitem.setAttribute("hidden", "true");
      aTarget.disableSelf.menuitem.removeAttribute("hidden");
    }
  },

  /**
   * Listener handling the "disableSelf" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onDisableSelf: function DVB__onDisableSelf(aTarget) {
    if (!aTarget) {
      return;
    }
    let breakpointItem = this.getItemForElement(aTarget);
    let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;

    if (this.disableBreakpoint(url, line)) {
      aTarget.enableSelf.menuitem.removeAttribute("hidden");
      aTarget.disableSelf.menuitem.setAttribute("hidden", "true");
    }
  },

  /**
   * Listener handling the "deleteSelf" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onDeleteSelf: function DVB__onDeleteSelf(aTarget) {
    if (!aTarget) {
      return;
    }
    let breakpointItem = this.getItemForElement(aTarget);
    let { sourceLocation: url, lineNumber: line } = breakpointItem.attachment;
    let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);

    this.removeBreakpoint(url, line);
    DebuggerController.Breakpoints.removeBreakpoint(breakpointClient);
  },

  /**
   * Listener handling the "enableOthers" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onEnableOthers: function DVB__onEnableOthers(aTarget) {
    for (let item in this) {
      if (item.target != aTarget) {
        this._onEnableSelf(item.target);
      }
    }
  },

  /**
   * Listener handling the "disableOthers" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onDisableOthers: function DVB__onDisableOthers(aTarget) {
    for (let item in this) {
      if (item.target != aTarget) {
        this._onDisableSelf(item.target);
      }
    }
  },

  /**
   * Listener handling the "deleteOthers" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onDeleteOthers: function DVB__onDeleteOthers(aTarget) {
    for (let item in this) {
      if (item.target != aTarget) {
        this._onDeleteSelf(item.target);
      }
    }
  },

  /**
   * Listener handling the "enableAll" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onEnableAll: function DVB__onEnableAll(aTarget) {
    this._onEnableOthers(aTarget);
    this._onEnableSelf(aTarget);
  },

  /**
   * Listener handling the "disableAll" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onDisableAll: function DVB__onDisableAll(aTarget) {
    this._onDisableOthers(aTarget);
    this._onDisableSelf(aTarget);
  },

  /**
   * Listener handling the "deleteAll" menuitem command.
   *
   * @param object aTarget
   *        The corresponding breakpoint element node.
   */
  _onDeleteAll: function DVB__onDeleteAll(aTarget) {
    this._onDeleteOthers(aTarget);
    this._onDeleteSelf(aTarget);
  },

  /**
   * Gets an identifier for a breakpoint item in the current cache.
   * @return string
   */
  _key: function DVB__key(aSourceLocation, aLineNumber) {
    return aSourceLocation + aLineNumber;
  },

  _popupset: null,
  _commandset: null,
  _cmPopup: null,
  _cbPanel: null,
  _cbTextbox: null,
  _popupShown: false,
  _cache: null,
  _editorContextMenuLineNumber: -1
});

/**
 * Functions handling the watch expressions UI.
 */
function WatchExpressionsView() {
  dumpn("WatchExpressionsView was instantiated");
  MenuContainer.call(this);
  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);
}

create({ constructor: WatchExpressionsView, proto: MenuContainer.prototype }, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function DVWE_initialize() {
    dumpn("Initializing the WatchExpressionsView");
    this._container = new StackList(document.getElementById("expressions"));
    this._variables = document.getElementById("variables");

    this._container.setAttribute("context", "debuggerWatchExpressionsContextMenu");
    this._container.permaText = L10N.getStr("addWatchExpressionText");
    this._container.itemFactory = this._createItemView;
    this._container.addEventListener("click", this._onClick, false);

    this._cache = [];
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function DVWE_destroy() {
    dumpn("Destroying the WatchExpressionsView");
    this._container.removeEventListener("click", this._onClick, false);
  },

  /**
   * Adds a watch expression in this container.
   *
   * @param string aExpression [optional]
   *        An optional initial watch expression text.
   */
  addExpression: function DVWE_addExpression(aExpression = "") {
    // Watch expressions are UI elements which benefit from visible panes.
    DebuggerView.showPanesSoon();

    // Append a watch expression item to this container.
    let expressionItem = this.push("", aExpression, {
      forced: { atIndex: 0 },
      unsorted: true,
      relaxed: true,
      attachment: {
        currentExpression: "",
        initialExpression: aExpression,
        id: this._generateId()
      }
    });

    // Check if watch expression was already appended.
    if (!expressionItem) {
      return;
    }

    let element = expressionItem.target;
    element.id = "expression-" + expressionItem.attachment.id;
    element.className = "dbg-expression list-item";
    element.arrowNode.className = "dbg-expression-arrow";
    element.inputNode.className = "dbg-expression-input plain";
    element.closeNode.className = "dbg-expression-delete plain devtools-closebutton";

    // Automatically focus the new watch expression input and
    // scroll the variables view to top.
    element.inputNode.value = aExpression;
    element.inputNode.select();
    element.inputNode.focus();
    this._variables.scrollTop = 0;

    this._cache.splice(0, 0, expressionItem);
  },

  /**
   * Removes the watch expression with the specified index from this container.
   *
   * @param number aIndex
   *        The index used to identify the watch expression.
   */
  removeExpressionAt: function DVWE_removeExpressionAt(aIndex) {
    this.remove(this._cache[aIndex]);
    this._cache.splice(aIndex, 1);
  },

  /**
   * Changes the watch expression corresponding to the specified variable item.
   *
   * @param Variable aVar
   *        The variable representing the watch expression evaluation.
   * @param string aExpression
   *        The new watch expression text.
   */
  switchExpression: function DVWE_switchExpression(aVar, aExpression) {
    let expressionItem =
      [i for (i of this._cache) if (i.attachment.currentExpression == aVar.name)][0];

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

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

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

  /**
   * Removes the watch expression corresponding to the specified variable item.
   *
   * @param Variable aVar
   *        The variable representing the watch expression evaluation.
   */
  deleteExpression: function DVWE_deleteExpression(aVar) {
    let expressionItem =
      [i for (i of this._cache) if (i.attachment.currentExpression == aVar.name)][0];

    // Remove the watch expression at its respective index.
    this.removeExpressionAt(this._cache.indexOf(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.
   */
  getExpression: function DVWE_getExpression(aIndex) {
    return this._cache[aIndex].attachment.currentExpression;
  },

  /**
   * Gets the watch expressions code strings for all items in this container.
   *
   * @return array
   *         The watch expressions code strings.
   */
  getExpressions: function DVWE_getExpressions() {
    return [item.attachment.currentExpression for (item of this._cache)];
  },

  /**
   * Customization function for creating an item's UI.
   *
   * @param nsIDOMNode aElementNode
   *        The element associated with the displayed item.
   * @param string aExpression
   *        The initial watch expression text.
   */
  _createItemView: function DVWE__createItemView(aElementNode, aExpression) {
    let arrowNode = document.createElement("box");
    let inputNode = document.createElement("textbox");
    let closeNode = document.createElement("toolbarbutton");

    inputNode.setAttribute("value", aExpression);
    inputNode.setAttribute("flex", "1");

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

    aElementNode.appendChild(arrowNode);
    aElementNode.appendChild(inputNode);
    aElementNode.appendChild(closeNode);
    aElementNode.arrowNode = arrowNode;
    aElementNode.inputNode = inputNode;
    aElementNode.closeNode = closeNode;
  },

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

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

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

  /**
   * The click listener for this container.
   */
  _onClick: function DVWE__onClick(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 DVWE__onClose(e) {
    let expressionItem = this.getItemForElement(e.target);
    this.removeExpressionAt(this._cache.indexOf(expressionItem));

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

    e.preventDefault();
    e.stopPropagation();
  },

  /**
   * The blur listener for a watch expression's textbox.
   */
  _onBlur: function DVWE__onBlur({ 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.removeExpressionAt(this._cache.indexOf(expressionItem));
    }
    // Remove the watch expression if it's a duplicate.
    else if (!oldExpression && this.getExpressions().indexOf(newExpression) != -1) {
      this.removeExpressionAt(this._cache.indexOf(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 DVWE__onKeyPress(e) {
    switch(e.keyCode) {
      case e.DOM_VK_RETURN:
      case e.DOM_VK_ENTER:
      case e.DOM_VK_ESCAPE:
        DebuggerView.editor.focus();
        return;
    }
  },

  /**
   * Gets an identifier for a new watch expression item in the current cache.
   * @return string
   */
  _generateId: (function() {
    let count = 0;
    return function DVWE__generateId() {
      return (++count) + "";
    };
  })(),

  _variables: null,
  _cache: null
});

/**
 * Functions handling the global search UI.
 */
function GlobalSearchView() {
  dumpn("GlobalSearchView was instantiated");
  MenuContainer.call(this);
  this._startSearch = this._startSearch.bind(this);
  this._onFetchSourceFinished = this._onFetchSourceFinished.bind(this);
  this._onFetchSourceTimeout = this._onFetchSourceTimeout.bind(this);
  this._onFetchSourcesFinished = this._onFetchSourcesFinished.bind(this);
  this._createItemView = this._createItemView.bind(this);
  this._onScroll = this._onScroll.bind(this);
  this._onHeaderClick = this._onHeaderClick.bind(this);
  this._onLineClick = this._onLineClick.bind(this);
  this._onMatchClick = this._onMatchClick.bind(this);
}

create({ constructor: GlobalSearchView, proto: MenuContainer.prototype }, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function DVGS_initialize() {
    dumpn("Initializing the GlobalSearchView");
    this._container = new StackList(document.getElementById("globalsearch"));
    this._splitter = document.getElementById("globalsearch-splitter");

    this._container.emptyText = L10N.getStr("noMatchingStringsText");
    this._container.itemFactory = this._createItemView;
    this._container.addEventListener("scroll", this._onScroll, false);

    this._cache = new Map();
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function DVGS_destroy() {
    dumpn("Destroying the GlobalSearchView");
    this._container.removeEventListener("scroll", this._onScroll, false);
  },

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

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

  /**
   * Removes all items from this container and hides it.
   */
  clearView: function DVGS_clearView() {
    this.hidden = true;
    this.empty();
    window.dispatchEvent("Debugger:GlobalSearch:ViewCleared");
  },

  /**
   * Clears all the fetched sources from cache.
   */
  clearCache: function DVGS_clearCache() {
    this._cache = new Map();
    window.dispatchEvent("Debugger:GlobalSearch:CacheCleared");
  },

  /**
   * Focuses the next found match in the source editor.
   */
  focusNextMatch: function DVGS_focusNextMatch() {
    let totalLineResults = LineResults.size();
    if (!totalLineResults) {
      return;
    }
    if (++this._currentlyFocusedMatch >= totalLineResults) {
      this._currentlyFocusedMatch = 0;
    }
    this._onMatchClick({
      target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
    });
  },

  /**
   * Focuses the previously found match in the source editor.
   */
  focusPrevMatch: function DVGS_focusPrevMatch() {
    let totalLineResults = LineResults.size();
    if (!totalLineResults) {
      return;
    }
    if (--this._currentlyFocusedMatch < 0) {
      this._currentlyFocusedMatch = totalLineResults - 1;
    }
    this._onMatchClick({
      target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
    });
  },

  /**
   * Allows searches to be scheduled and delayed to avoid redundant calls.
   */
  delayedSearch: true,

  /**
   * Schedules searching for a string in all of the sources.
   *
   * @param string aQuery
   *        The string to search for.
   */
  scheduleSearch: function DVGS_scheduleSearch(aQuery) {
    if (!this.delayedSearch) {
      this.performSearch(aQuery);
      return;
    }
    let delay = Math.max(GLOBAL_SEARCH_ACTION_MAX_DELAY / aQuery.length);

    window.clearTimeout(this._searchTimeout);
    this._searchFunction = this._startSearch.bind(this, aQuery);
    this._searchTimeout = window.setTimeout(this._searchFunction, delay);
  },

  /**
   * Immediately searches for a string in all of the sources.
   *
   * @param string aQuery
   *        The string to search for.
   */
  performSearch: function DVGS_performSearch(aQuery) {
    window.clearTimeout(this._searchTimeout);
    this._searchFunction = null;
    this._startSearch(aQuery);
  },

  /**
   * Starts searching for a string in all of the sources.
   *
   * @param string aQuery
   *        The string to search for.
   */
  _startSearch: function DVGS__startSearch(aQuery) {
    let locations = DebuggerView.Sources.values;
    this._sourcesCount = locations.length;
    this._searchedToken = aQuery;

    this._fetchSources(locations, {
      onFetch: this._onFetchSourceFinished,
      onTimeout: this._onFetchSourceTimeout,
      onFinished: this._onFetchSourcesFinished
    });
  },

  /**
   * Starts fetching all the sources, silently.
   *
   * @param array aLocations
   *        The locations for the sources to fetch.
   * @param object aCallbacks
   *        An object containing the callback functions to invoke:
   *          - onFetch: called after each source is fetched
   *          - onTimeout: called when a source's text takes too long to fetch
   *          - onFinished: called if all the sources were already fetched
   */
  _fetchSources:
  function DVGS__fetchSources(aLocations, { onFetch, onTimeout, onFinished }) {
    // If all the sources were already fetched, then don't do anything.
    if (this._cache.size == aLocations.length) {
      onFinished();
      return;
    }

    // Fetch each new source.
    for (let location of aLocations) {
      if (this._cache.has(location)) {
        continue;
      }
      let sourceItem = DebuggerView.Sources.getItemByValue(location);
      let sourceObject = sourceItem.attachment;
      DebuggerController.SourceScripts.getText(sourceObject, onFetch, onTimeout);
    }
  },

  /**
   * Called when a source has been fetched.
   *
   * @param string aLocation
   *        The location of the source.
   * @param string aContents
   *        The text contents of the source.
   */
  _onFetchSourceFinished: function DVGS__onFetchSourceFinished(aLocation, aContents) {
    // Remember the source in a cache so we don't have to fetch it again.
    this._cache.set(aLocation, aContents);

    // Check if all sources were fetched and stored in the cache.
    if (this._cache.size == this._sourcesCount) {
      this._onFetchSourcesFinished();
    }
  },

  /**
   * Called when a source's text takes too long to fetch.
   */
  _onFetchSourceTimeout: function DVGS__onFetchSourceTimeout() {
    // Remove the source from the load queue.
    this._sourcesCount--;

    // Check if the remaining sources were fetched and stored in the cache.
    if (this._cache.size == this._sourcesCount) {
      this._onFetchSourcesFinished();
    }
  },

  /**
   * Called when all the sources have been fetched.
   */
  _onFetchSourcesFinished: function DVGS__onFetchSourcesFinished() {
    // At least one source needs to be present to perform a global search.
    if (!this._sourcesCount) {
      return;
    }
    // All sources are fetched and stored in the cache, we can start searching.
    this._performGlobalSearch();
    this._sourcesCount = 0;
  },

  /**
   * Finds string matches in all the  sources stored in the cache, and groups
   * them by location and line number.
   */
  _performGlobalSearch: function DVGS__performGlobalSearch() {
    // Get the currently searched token from the filtering input.
    let token = this._searchedToken;

    // Make sure we're actually searching for something.
    if (!token) {
      this.clearView();
      window.dispatchEvent("Debugger:GlobalSearch:TokenEmpty");
      return;
    }

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

    // Prepare the results map, containing search details for each line.
    let globalResults = new GlobalResults();

    for (let [location, contents] of this._cache) {
      // Verify that the search token is found anywhere in the source.
      if (!contents.toLowerCase().contains(lowerCaseToken)) {
        continue;
      }
      let lines = contents.split("\n");
      let sourceResults = new SourceResults();

      for (let i = 0, len = lines.length; i < len; i++) {
        let line = lines[i];
        let lowerCaseLine = line.toLowerCase();

        // Search is not case sensitive, and is tied to each line in the source.
        if (!lowerCaseLine.contains(lowerCaseToken)) {
          continue;
        }

        let lineNumber = i;
        let lineResults = new LineResults();

        lowerCaseLine.split(lowerCaseToken).reduce(function(prev, curr, index, {length}) {
          let prevLength = prev.length;
          let currLength = curr.length;
          let unmatched = line.substr(prevLength, currLength);
          lineResults.add(unmatched);

          if (index != length - 1) {
            let matched = line.substr(prevLength + currLength, tokenLength);
            let range = {
              start: prevLength + currLength,
              length: matched.length
            };
            lineResults.add(matched, range, true);
            sourceResults.matchCount++;
          }
          return prev + token + curr;
        }, "");

        if (sourceResults.matchCount) {
          sourceResults.add(lineNumber, lineResults);
        }
      }
      if (sourceResults.matchCount) {
        globalResults.add(location, sourceResults);
      }
    }

    // Empty this container to rebuild the search results.
    this.empty();

    // Signal if there are any matches, and the rebuild the results.
    if (globalResults.itemCount) {
      this.hidden = false;
      this._currentlyFocusedMatch = -1;
      this._createGlobalResultsUI(globalResults);
      window.dispatchEvent("Debugger:GlobalSearch:MatchFound");
    } else {
      window.dispatchEvent("Debugger:GlobalSearch:MatchNotFound");
    }
  },

  /**
   * 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 DVGS__createGlobalResultsUI(aGlobalResults) {
    let i = 0;

    for (let [location, sourceResults] in aGlobalResults) {
      if (i++ == 0) {
        this._createSourceResultsUI(location, sourceResults, true);
      } 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, location, sourceResults) }, 0);
      }
    }
  },

  /**
   * Creates source search results entries and adds them to this container.
   *
   * @param string aLocation
   *        The location of the source.
   * @param SourceResults aSourceResults
   *        An object containing all the matched lines for a specific source.
   * @param boolean aExpandFlag
   *        True to expand the source results.
   */
  _createSourceResultsUI:
  function DVGS__createSourceResultsUI(aLocation, aSourceResults, aExpandFlag) {
    // Append a source results item to this container.
    let sourceResultsItem = this.push(aLocation, aSourceResults.matchCount, {
      forced: true,
      unsorted: true,
      relaxed: true,
      attachment: {
        sourceResults: aSourceResults,
        expandFlag: aExpandFlag
      }
    });
  },

  /**
   * Customization function for creating an item's UI.
   *
   * @param nsIDOMNode aElementNode
   *        The element associated with the displayed item.
   * @param string aLocation
   *        The source result's location.
   * @param string aMatchCount
   *        The source result's match count.
   * @param any aAttachment [optional]
   *        Some attached primitive/object.
   */
  _createItemView:
  function DVGS__createItemView(aElementNode, aLocation, aMatchCount, aAttachment) {
    let { sourceResults, expandFlag } = aAttachment;

    sourceResults.createView(aElementNode, aLocation, aMatchCount, expandFlag, {
      onHeaderClick: this._onHeaderClick,
      onLineClick: this._onLineClick,
      onMatchClick: this._onMatchClick
    });
  },

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

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

  /**
   * The click listener for a result match.
   */
  _onMatchClick: function DVGLS__onMatchClick(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 location = sourceResultsItem.location;
    let lineNumber = lineResultsItem.lineNumber;
    DebuggerView.updateEditor(location, lineNumber + 1, { noDebug: true });

    let editor = DebuggerView.editor;
    let offset = editor.getCaretOffset();
    let { start, length } = lineResultsItem.lineData.range;
    editor.setSelection(offset + start, offset + start + length);
  },

  /**
   * The scroll listener for the global search container.
   */
  _onScroll: function DVGS__onScroll(e) {
    for (let item in this) {
      this._expandResultsIfNeeded(item.target);
    }
  },

  /**
   * Expands the source results it they are currently visible.
   *
   * @param nsIDOMNode aTarget
   *        The element associated with the displayed item.
   */
  _expandResultsIfNeeded: function DVGS__expandResultsIfNeeded(aTarget) {
    let sourceResultsItem = SourceResults.getItemForElement(aTarget);
    if (sourceResultsItem.instance.toggled ||
        sourceResultsItem.instance.expanded) {
      return;
    }
    let { top, height } = aTarget.getBoundingClientRect();
    let { clientHeight } = this._container._parent;

    if (top - height <= clientHeight || this._forceExpandResults) {
      sourceResultsItem.instance.expand();
    }
  },

  /**
   * Scrolls a match into view.
   *
   * @param nsIDOMNode aMatch
   *        The match to scroll into view.
   */
  _scrollMatchIntoViewIfNeeded:  function DVGS__scrollMatchIntoViewIfNeeded(aMatch) {
    let { top, height } = aMatch.getBoundingClientRect();
    let { clientHeight } = this._container._parent;

    let style = window.getComputedStyle(aMatch);
    let topBorderSize = window.parseInt(style.getPropertyValue("border-top-width"));
    let bottomBorderSize = window.parseInt(style.getPropertyValue("border-bottom-width"));

    let marginY = top - (height + topBorderSize + bottomBorderSize) * 2;
    if (marginY <= 0) {
      this._container._parent.scrollTop += marginY;
    }
    if (marginY + height > clientHeight) {
      this._container._parent.scrollTop += height - (clientHeight - marginY);
    }
  },

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

  _splitter: null,
  _currentlyFocusedMatch: -1,
  _forceExpandResults: false,
  _searchTimeout: null,
  _searchFunction: null,
  _searchedToken: "",
  _sourcesCount: -1,
  _cache: null
});

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

GlobalResults.prototype = {
  /**
   * Adds source results to this store.
   *
   * @param string aLocation
   *        The location of the source.
   * @param SourceResults aSourceResults
   *        An object containing all the matched lines for a specific source.
   */
  add: function GR_add(aLocation, aSourceResults) {
    this._store.set(aLocation, aSourceResults);
  },

  /**
   * Gets the number of source results in this store.
   */
  get itemCount() this._store.size,

  _store: null
};

/**
 * An object containing all the matched lines for a specific source.
 * Iterable via "for (let [lineNumber, lineResults] in sourceResults) { }".
 */
function SourceResults() {
  this._store = new Map();
  this.matchCount = 0;
}

SourceResults.prototype = {
  /**
   * Adds line results to this store.
   *
   * @param number aLineNumber
   *        The line location in the source.
   * @param LineResults aLineResults
   *        An object containing all the matches for a specific line.
   */
  add: function SR_add(aLineNumber, aLineResults) {
    this._store.set(aLineNumber, aLineResults);
  },

  /**
   * The number of matches in this store. One line may have multiple matches.
   */
  matchCount: -1,

  /**
   * Expands the element, showing all the added details.
   *
   * @param boolean aSkipAnimationFlag
   *        Pass true to not show an opening animation.
   */
  expand: function SR_expand(aSkipAnimationFlag) {
    this._target.resultsContainer.setAttribute("open", "");
    this._target.arrow.setAttribute("open", "");

    if (!aSkipAnimationFlag) {
      this._target.resultsContainer.setAttribute("animated", "");
    }
  },

  /**
   * Collapses the element, hiding all the added details.
   */
  collapse: function SR_collapse() {
    this._target.resultsContainer.removeAttribute("animated");
    this._target.resultsContainer.removeAttribute("open");
    this._target.arrow.removeAttribute("open");
  },

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

  /**
   * Relaxes the auto-expand rules to always show as many results as possible.
   */
  alwaysExpand: true,

  /**
   * Gets this element's expanded state.
   * @return boolean
   */
  get expanded() this._target.resultsContainer.hasAttribute("open"),

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

  /**
   * Returns if this element was ever toggled via user interaction.
   * @return boolean
   */
  get toggled() this._userToggled,

  /**
   * 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 string aLocation
   *        The source result's location.
   * @param string aMatchCount
   *        The source result's match count.
   * @param boolean aExpandFlag
   *        True to expand the source results.
   * @param object aCallbacks
   *        An object containing all the necessary callback functions:
   *          - onHeaderClick
   *          - onMatchClick
   */
  createView:
  function SR_createView(aElementNode, aLocation, aMatchCount, aExpandFlag, aCallbacks) {
    this._target = aElementNode;

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

    let locationNode = document.createElement("label");
    locationNode.className = "plain location";
    locationNode.setAttribute("value", SourceUtils.trimUrlLength(aLocation));

    let matchCountNode = document.createElement("label");
    matchCountNode.className = "plain match-count";
    matchCountNode.setAttribute("value", "(" + aMatchCount + ")");

    let 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 = document.createElement("vbox");
    resultsContainer.className = "dbg-results-container";

    for (let [lineNumber, lineResults] of this._store) {
      lineResults.createView(resultsContainer, lineNumber, aCallbacks)
    }

    aElementNode.arrow = arrow;
    aElementNode.resultsHeader = resultsHeader;
    aElementNode.resultsContainer = resultsContainer;

    if ((aExpandFlag || this.alwaysExpand) &&
         aMatchCount < 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-" + aLocation;
    aElementNode.className = "dbg-source-results";
    aElementNode.appendChild(resultsBox);

    SourceResults._itemsByElement.set(aElementNode, {
      location: aLocation,
      matchCount: aMatchCount,
      autoExpand: aExpandFlag,
      instance: this
    });
  },

  _store: null,
  _target: null,
  _userToggled: false
};

/**
 * An object containing all the matches for a specific line.
 * Iterable via "for (let chunk in lineResults) { }".
 */
function LineResults() {
  this._store = [];
}

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 LC_add(aString, aRange, aMatchFlag) {
    this._store.push({
      string: aString,
      range: aRange,
      match: !!aMatchFlag
    });
  },

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

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

    let lineNumberNode = document.createElement("label");
    let lineContentsNode = document.createElement("hbox");
    let lineString = "";
    let lineLength = 0;
    let firstMatch = null;

    lineNumberNode.className = "plain line-number";
    lineNumberNode.setAttribute("value", aLineNumber + 1);
    lineContentsNode.className = "line-contents";
    lineContentsNode.setAttribute("flex", "1");

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

      let label = document.createElement("label");
      label.className = "plain string";
      label.setAttribute("value", lineString);
      label.setAttribute("match", match);
      lineContentsNode.appendChild(label);

      if (match) {
        this._entangleMatch(aLineNumber, label, chunk);
        label.addEventListener("click", aCallbacks.onMatchClick, false);
        firstMatch = firstMatch || label;
      }
      if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
        lineContentsNode.appendChild(this._ellipsis.cloneNode());
        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);
    aContainer.appendChild(searchResult);
  },

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

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

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

  _store: null,
  _target: null
};

/**
 * A generator-iterator over the global, source or line results.
 */
GlobalResults.prototype.__iterator__ =
SourceResults.prototype.__iterator__ =
LineResults.prototype.__iterator__ = function DVGS_iterator() {
  for (let item of this._store) {
    yield item;
  }
};

/**
 * 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 DVGS_getItemForElement(aElement) {
  return MenuContainer.prototype.getItemForElement.call(this, aElement);
};

/**
 * 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 DVGS_getElementAtIndex(aIndex) {
  for (let [element, item] of this._itemsByElement) {
    if (!item.nonenumerable && !aIndex--) {
      return element;
    }
  }
};

/**
 * Gets the index of an item associated with the specified element.
 *
 * @return number
 *         The index of the matched item, or -1 if nothing is found.
 */
SourceResults.indexOfElement =
LineResults.indexOfElement = function DVGS_indexOFElement(aElement) {
  let count = 0;
  for (let [element, item] of this._itemsByElement) {
    if (element == aElement) {
      return count;
    }
    if (!item.nonenumerable) {
      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 DVGS_size() {
  let count = 0;
  for (let [, item] of this._itemsByElement) {
    if (!item.nonenumerable) {
      count++;
    }
  }
  return count;
};

/**
 * Preliminary setup for the DebuggerView object.
 */
DebuggerView.StackFrames = new StackFramesView();
DebuggerView.Breakpoints = new BreakpointsView();
DebuggerView.WatchExpressions = new WatchExpressionsView();
DebuggerView.GlobalSearch = new GlobalSearchView();
