Bug 723071 - Add a pane to display the list of breakpoints across all scripts in the debuggee; f=msucan,past r=past
authorVictor Porof <vporof@mozilla.com>
Sun, 15 Jul 2012 09:40:37 +0300
changeset 102112 f63242dea1f95c8e1307fdb0d0eb51b938805def
parent 102111 6a0dfbde408949420de42caaa5e97f92efdde035
child 102113 21d5e58533e06ce120e35acf6a1457750def9e9d
push id1729
push userlsblakk@mozilla.com
push dateMon, 16 Jul 2012 20:02:43 +0000
treeherdermozilla-aurora@f4e75e148951 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs723071
milestone16.0a1
Bug 723071 - Add a pane to display the list of breakpoints across all scripts in the debuggee; f=msucan,past r=past
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-view.js
browser/devtools/debugger/debugger.css
browser/devtools/debugger/debugger.xul
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js
browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-pane.js
browser/devtools/debugger/test/browser_dbg_panesize-inner.js
browser/devtools/debugger/test/browser_dbg_propertyview-01.js
browser/devtools/debugger/test/browser_dbg_scripts-sorting.js
browser/locales/en-US/chrome/browser/devtools/debugger.properties
browser/themes/gnomestripe/devtools/debugger.css
browser/themes/pinstripe/devtools/debugger.css
browser/themes/winstripe/devtools/debugger.css
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -47,16 +47,17 @@ let DebuggerController = {
       return;
     }
     this._isInitialized = true;
     window.removeEventListener("DOMContentLoaded", this._startupDebugger, true);
 
     DebuggerView.initializePanes();
     DebuggerView.initializeEditor();
     DebuggerView.StackFrames.initialize();
+    DebuggerView.Breakpoints.initialize();
     DebuggerView.Properties.initialize();
     DebuggerView.Scripts.initialize();
     DebuggerView.showCloseButton(!this._isRemoteDebugger && !this._isChromeDebugger);
 
     this.dispatchEvent("Debugger:Loaded");
     this._connect();
   },
 
@@ -70,16 +71,17 @@ let DebuggerController = {
     }
     this._isDestroyed = true;
     window.removeEventListener("unload", this._shutdownDebugger, true);
 
     DebuggerView.destroyPanes();
     DebuggerView.destroyEditor();
     DebuggerView.Scripts.destroy();
     DebuggerView.StackFrames.destroy();
+    DebuggerView.Breakpoints.destroy();
     DebuggerView.Properties.destroy();
 
     DebuggerController.Breakpoints.destroy();
     DebuggerController.SourceScripts.disconnect();
     DebuggerController.StackFrames.disconnect();
     DebuggerController.ThreadState.disconnect();
 
     this.dispatchEvent("Debugger:Unloaded");
@@ -390,18 +392,16 @@ StackFrames.prototype = {
    * Watch the given thread client.
    *
    * @param function aCallback
    *        The next function in the initialization sequence.
    */
   connect: function SF_connect(aCallback) {
     window.addEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);
 
-    this._onFramesCleared();
-
     this.activeThread.addListener("paused", this._onPaused);
     this.activeThread.addListener("resumed", this._onResume);
     this.activeThread.addListener("framesadded", this._onFrames);
     this.activeThread.addListener("framescleared", this._onFramesCleared);
 
     this.updatePauseOnExceptions(this.pauseOnExceptions);
 
     aCallback && aCallback();
@@ -501,22 +501,57 @@ StackFrames.prototype = {
     if (!frame) {
       return;
     }
 
     let url = frame.where.url;
     let line = frame.where.line;
     let editor = DebuggerView.editor;
 
-    // Move the editor's caret to the proper line.
-    if (DebuggerView.Scripts.isSelected(url) && line) {
-      editor.setDebugLocation(line - 1);
-    } else {
-      editor.setDebugLocation(-1);
+    this.updateEditorToLocation(url, line, true);
+  },
+
+  /**
+   * Update the source editor's current caret and debug location based on
+   * a specified url and line.
+   *
+   * @param string aUrl
+   *        The target source url.
+   * @param number aLine
+   *        The target line number in the source.
+   * @param boolean aNoSwitch
+   *        Pass true to not switch to the script if not currently selected.
+   * @param boolean aNoCaretFlag
+   *        Pass true to not set the caret location at the specified line.
+   * @param boolean aNoDebugFlag
+   *        Pass true to not set the debug location at the specified line.
+   */
+  updateEditorToLocation:
+  function SF_updateEditorToLocation(aUrl, aLine, aNoSwitch, aNoCaretFlag, aNoDebugFlag) {
+    let editor = DebuggerView.editor;
+
+    function set() {
+      if (!aNoCaretFlag) {
+        editor.setCaretPosition(aLine - 1);
+      }
+      if (!aNoDebugFlag) {
+        editor.setDebugLocation(aLine - 1);
+      }
     }
+
+    // Move the editor's caret to the proper url and line.
+    if (DebuggerView.Scripts.isSelected(aUrl)) {
+      return set();
+    }
+    if (!aNoSwitch && DebuggerView.Scripts.contains(aUrl)) {
+      DebuggerView.Scripts.selectScript(aUrl);
+      return set();
+    }
+    editor.setCaretPosition(-1);
+    editor.setDebugLocation(-1);
   },
 
   /**
    * Inform the debugger client whether the debuggee should be paused whenever
    * an exception is thrown.
    *
    * @param boolean aFlag
    *        The new value of the flag: true for pausing, false otherwise.
@@ -545,30 +580,19 @@ StackFrames.prototype = {
 
     let frame = this.activeThread.cachedFrames[aDepth];
     if (!frame) {
       return;
     }
 
     let url = frame.where.url;
     let line = frame.where.line;
-    let editor = DebuggerView.editor;
 
     // Move the editor's caret to the proper line.
-    if (DebuggerView.Scripts.isSelected(url) && line) {
-      editor.setCaretPosition(line - 1);
-      editor.setDebugLocation(line - 1);
-    }
-    else if (DebuggerView.Scripts.contains(url)) {
-      DebuggerView.Scripts.selectScript(url);
-      editor.setCaretPosition(line - 1);
-    }
-    else {
-      editor.setDebugLocation(-1);
-    }
+    this.updateEditorToLocation(url, line);
 
     // Start recording any added variables or properties in any scope.
     DebuggerView.Properties.createHierarchyStore();
 
     // Clear existing scopes and create each one dynamically.
     DebuggerView.Properties.empty();
 
     if (frame.environment) {
@@ -750,17 +774,17 @@ StackFrames.prototype = {
   /**
    * Adds the specified stack frame to the list.
    *
    * @param Debugger.Frame aFrame
    *        The new frame to add.
    */
   _addFrame: function SF__addFrame(aFrame) {
     let depth = aFrame.depth;
-    let label = DebuggerController.SourceScripts._getScriptLabel(aFrame.where.url);
+    let label = DebuggerController.SourceScripts.getScriptLabel(aFrame.where.url);
 
     let startText = this._getFrameTitle(aFrame);
     let endText = label + ":" + aFrame.where.line;
 
     let frame = DebuggerView.StackFrames.addFrame(depth, startText, endText);
     if (frame) {
       frame.debuggerFrame = aFrame;
     }
@@ -877,39 +901,43 @@ SourceScripts.prototype = {
    */
   _onNewScript: function SS__onNewScript(aNotification, aPacket) {
     // Ignore scripts generated from 'clientEvaluate' packets.
     if (aPacket.url == "debugger eval code") {
       return;
     }
 
     this._addScript({ url: aPacket.url, startLine: aPacket.startLine }, true);
-    // If there are any stored breakpoints for this script, display them again.
-    for each (let bp in DebuggerController.Breakpoints.store) {
-      if (bp.location.url == aPacket.url) {
-        DebuggerController.Breakpoints.displayBreakpoint(bp.location);
+
+    // If there are any stored breakpoints for this script, display them again,
+    // both in the editor and the pane.
+    for each (let breakpoint in DebuggerController.Breakpoints.store) {
+      if (breakpoint.location.url == aPacket.url) {
+        DebuggerController.Breakpoints.displayBreakpoint(breakpoint);
       }
     }
   },
 
   /**
    * Handler for the thread client's scriptsadded notification.
    */
   _onScriptsAdded: function SS__onScriptsAdded() {
     for each (let script in this.activeThread.cachedScripts) {
       this._addScript(script, false);
     }
     DebuggerView.Scripts.commitScripts();
+    DebuggerController.Breakpoints.updatePaneBreakpoints();
   },
 
   /**
    * Handler for the thread client's scriptscleared notification.
    */
   _onScriptsCleared: function SS__onScriptsCleared() {
     DebuggerView.Scripts.empty();
+    DebuggerView.Breakpoints.emptyText();
   },
 
   /**
    * Sets the proper editor mode (JS or HTML) according to the specified
    * content type, or by determining the type from the URL.
    *
    * @param string aUrl
    *        The script URL.
@@ -962,17 +990,17 @@ SourceScripts.prototype = {
    *        The script 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 SS__trimURL(aUrl, aLabel, aSeq) {
+  _trimUrl: function SS__trimUrl(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;
       }
@@ -1007,65 +1035,65 @@ SourceScripts.prototype = {
         return aLabel;
       }
     }
 
     // Append the url query.
     if (aSeq === 1) {
       let query = aUrl.query;
       if (query) {
-        return this._trimURL(aUrl, aLabel + "?" + query, aSeq + 1);
+        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);
+        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);
+        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);
+        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);
+      return this._trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
     }
     // Give up.
     return aUrl.spec;
   },
 
   /**
    * Gets a unique, simplified label from a script url.
    *
    * @param string aUrl
    *        The script url.
    * @param string aHref
    *        The content location href to be used. If unspecified, it will
    *        default to the script url prepath.
    * @return string
    *         The simplified label.
    */
-  _getScriptLabel: function SS__getScriptLabel(aUrl, aHref) {
-    return this._labelsCache[aUrl] || (this._labelsCache[aUrl] = this._trimURL(aUrl));
+  getScriptLabel: function SS_getScriptLabel(aUrl, aHref) {
+    return this._labelsCache[aUrl] || (this._labelsCache[aUrl] = this._trimUrl(aUrl));
   },
 
   /**
    * Clears the labels cache, populated by SS_getScriptLabel.
    * This should be done every time the content location changes.
    */
   _clearLabelsCache: function SS__clearLabelsCache() {
     this._labelsCache = {};
@@ -1076,17 +1104,17 @@ SourceScripts.prototype = {
    *
    * @param object aScript
    *        The script object coming from the active thread.
    * @param boolean aForceFlag
    *        True to force the script to be immediately added.
    */
   _addScript: function SS__addScript(aScript, aForceFlag) {
     DebuggerView.Scripts.addScript(
-      this._getScriptLabel(aScript.url), aScript, aForceFlag);
+      this.getScriptLabel(aScript.url), aScript, aForceFlag);
   },
 
   /**
    * Load the editor with the script text if available, otherwise fire an event
    * to load and display the script text.
    *
    * @param object aScript
    *        The script object coming from the active thread.
@@ -1229,16 +1257,33 @@ SourceScripts.prototype = {
     script.text = aSourceText;
     script.contentType = aContentType;
     element.setUserData("sourceScript", script, null);
 
     this.showScript(script, aOptions);
   },
 
   /**
+   * Gets the text in a source editor's specified line.
+   *
+   * @param number aLine [optional]
+   *        The line to get the text from.
+   *        If unspecified, it defaults to the current caret position line.
+   * @return string
+   *         The specified line text
+   */
+  getLineText: function SS_getLineText(aLine) {
+    let editor = DebuggerView.editor;
+    let line = aLine || editor.getCaretPosition().line;
+    let start = editor.getLineStart(line);
+    let end = editor.getLineEnd(line);
+    return editor.getText(start, end);
+  },
+
+  /**
    * Log an error message in the error console when a script fails to load.
    *
    * @param string aUrl
    *        The URL of the source script.
    * @param string aStatus
    *        The failure status code.
    */
   _logError: function SS__logError(aUrl, aStatus) {
@@ -1391,102 +1436,141 @@ Breakpoints.prototype = {
       if (breakpoint.location.url == url) {
         this.editor.addBreakpoint(breakpoint.location.line - 1);
       }
     }
     this._skipEditorBreakpointChange = false;
   },
 
   /**
+   * Update the breakpoints in the pane view. This function is invoked when the
+   * scripts are added (typically after a page navigation).
+   */
+  updatePaneBreakpoints: function BP_updatePaneBreakpoints() {
+    let url = DebuggerView.Scripts.selected;
+    if (!url) {
+      return;
+    }
+
+    this._skipEditorBreakpointChange = true;
+    for each (let breakpoint in this.store) {
+      if (DebuggerView.Scripts.contains(breakpoint.location.url)) {
+        this.displayBreakpoint(breakpoint, true);
+      }
+    }
+    this._skipEditorBreakpointChange = false;
+  },
+
+  /**
    * Add a breakpoint.
    *
    * @param object aLocation
    *        The location where you want the breakpoint. This object must have
    *        two properties:
    *          - url - the URL of the script.
    *          - line - the line number (starting from 1).
    * @param function [aCallback]
    *        Optional function to invoke once the breakpoint is added. The
    *        callback is invoked with two arguments:
    *          - aBreakpointClient - the BreakpointActor client object, if the
    *          breakpoint has been added successfully.
    *          - aResponseError - if there was any error.
    * @param boolean [aNoEditorUpdate=false]
    *        Tells if you want to skip editor updates. Typically the editor is
    *        updated to visually indicate that a breakpoint has been added.
+   * @param boolean [aNoPaneUpdate=false]
+   *        Tells if you want to skip any breakpoint pane updates.
    */
   addBreakpoint:
-  function BP_addBreakpoint(aLocation, aCallback, aNoEditorUpdate) {
+  function BP_addBreakpoint(aLocation, aCallback, aNoEditorUpdate, aNoPaneUpdate) {
     let breakpoint = this.getBreakpoint(aLocation.url, aLocation.line);
     if (breakpoint) {
       aCallback && aCallback(breakpoint);
       return;
     }
 
     this.activeThread.setBreakpoint(aLocation, function(aResponse, aBpClient) {
       this.store[aBpClient.actor] = aBpClient;
-      this.displayBreakpoint(aLocation, aNoEditorUpdate);
+      this.displayBreakpoint(aBpClient, aNoEditorUpdate, aNoPaneUpdate);
       aCallback && aCallback(aBpClient, aResponse.error);
     }.bind(this));
   },
 
   /**
    * Update the editor to display the specified breakpoint in the gutter.
    *
-   * @param object aLocation
-   *        The location where you want the breakpoint. This object must have
-   *        two properties:
-   *          - url - the URL of the script.
-   *          - line - the line number (starting from 1).
+   * @param object aBreakpoint
+   *        The breakpoint you want to display.
    * @param boolean [aNoEditorUpdate=false]
    *        Tells if you want to skip editor updates. Typically the editor is
    *        updated to visually indicate that a breakpoint has been added.
+   * @param boolean [aNoPaneUpdate=false]
+   *        Tells if you want to skip any breakpoint pane updates.
    */
-  displayBreakpoint: function BP_displayBreakpoint(aLocation, aNoEditorUpdate) {
+  displayBreakpoint:
+  function BP_displayBreakpoint(aBreakpoint, aNoEditorUpdate, aNoPaneUpdate) {
     if (!aNoEditorUpdate) {
       let url = DebuggerView.Scripts.selected;
-      if (url == aLocation.url) {
+      if (url == aBreakpoint.location.url) {
         this._skipEditorBreakpointChange = true;
-        this.editor.addBreakpoint(aLocation.line - 1);
+        this.editor.addBreakpoint(aBreakpoint.location.line - 1);
         this._skipEditorBreakpointChange = false;
       }
     }
+    if (!aNoPaneUpdate) {
+      let { url: url, line: line } = aBreakpoint.location;
+
+      if (!aBreakpoint.lineText || !aBreakpoint.lineInfo) {
+        let scripts = DebuggerController.SourceScripts;
+        aBreakpoint.lineText = scripts.getLineText(line - 1);
+        aBreakpoint.lineInfo = scripts.getScriptLabel(url) + ":" + line;
+      }
+      DebuggerView.Breakpoints.addBreakpoint(
+        aBreakpoint.actor,
+        aBreakpoint.lineInfo,
+        aBreakpoint.lineText, url, line);
+    }
   },
 
   /**
    * Remove a breakpoint.
    *
    * @param object aBreakpoint
    *        The breakpoint you want to remove.
    * @param function [aCallback]
    *        Optional function to invoke once the breakpoint is removed. The
    *        callback is invoked with one argument: the breakpoint location
    *        object which holds the url and line properties.
    * @param boolean [aNoEditorUpdate=false]
    *        Tells if you want to skip editor updates. Typically the editor is
    *        updated to visually indicate that a breakpoint has been removed.
+   * @param boolean [aNoPaneUpdate=false]
+   *        Tells if you want to skip any breakpoint pane updates.
    */
   removeBreakpoint:
-  function BP_removeBreakpoint(aBreakpoint, aCallback, aNoEditorUpdate) {
+  function BP_removeBreakpoint(aBreakpoint, aCallback, aNoEditorUpdate, aNoPaneUpdate) {
     if (!(aBreakpoint.actor in this.store)) {
       aCallback && aCallback(aBreakpoint.location);
       return;
     }
 
     aBreakpoint.remove(function() {
       delete this.store[aBreakpoint.actor];
 
       if (!aNoEditorUpdate) {
         let url = DebuggerView.Scripts.selected;
         if (url == aBreakpoint.location.url) {
           this._skipEditorBreakpointChange = true;
           this.editor.removeBreakpoint(aBreakpoint.location.line - 1);
           this._skipEditorBreakpointChange = false;
         }
       }
+      if (!aNoPaneUpdate) {
+        DebuggerView.Breakpoints.removeBreakpoint(aBreakpoint.actor);
+      }
 
       aCallback && aCallback(aBreakpoint.location);
     }.bind(this));
   },
 
   /**
    * Get the breakpoint object at the given location.
    *
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -1,33 +1,34 @@
 /* -*- 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";
 
 const PROPERTY_VIEW_FLASH_DURATION = 400; // ms
+const BREAKPOINT_LINE_TOOLTIP_MAX_SIZE = 1000;
 
 /**
  * Object mediating visual changes and event listeners between the debugger and
  * the html view.
  */
 let DebuggerView = {
 
   /**
    * An instance of SourceEditor.
    */
   editor: null,
 
   /**
    * Initializes UI properties for all the displayed panes.
    */
   initializePanes: function DV_initializePanes() {
-    let stackframes = document.getElementById("stackframes");
+    let stackframes = document.getElementById("stackframes+breakpoints");
     stackframes.setAttribute("width", Prefs.stackframesWidth);
 
     let variables = document.getElementById("variables");
     variables.setAttribute("width", Prefs.variablesWidth);
   },
 
   /**
    * Initializes the SourceEditor instance.
@@ -46,17 +47,17 @@ let DebuggerView = {
     this.editor = new SourceEditor();
     this.editor.init(placeholder, config, this._onEditorLoad.bind(this));
   },
 
   /**
    * Removes the displayed panes and saves any necessary state.
    */
   destroyPanes: function DV_destroyPanes() {
-    let stackframes = document.getElementById("stackframes");
+    let stackframes = document.getElementById("stackframes+breakpoints");
     Prefs.stackframesWidth = stackframes.getAttribute("width");
 
     let variables = document.getElementById("variables");
     Prefs.variablesWidth = variables.getAttribute("width");
   },
 
   /**
    * Removes the SourceEditor instance and added breakpoints.
@@ -802,16 +803,17 @@ StackFramesView.prototype = {
     stepOver.addEventListener("click", this._onStepOverClick, false);
     stepIn.addEventListener("click", this._onStepInClick, false);
     stepOut.addEventListener("click", this._onStepOutClick, false);
     frames.addEventListener("click", this._onFramesClick, false);
     frames.addEventListener("scroll", this._onFramesScroll, false);
     window.addEventListener("resize", this._onFramesScroll, false);
 
     this._frames = frames;
+    this.emptyText();
   },
 
   /**
    * Destruction function, called when the debugger is shut down.
    */
   destroy: function DVF_destroy() {
     let close = document.getElementById("close");
     let pauseOnExceptions = document.getElementById("pause-exceptions");
@@ -833,25 +835,565 @@ StackFramesView.prototype = {
     frames.removeEventListener("scroll", this._onFramesScroll, false);
     window.removeEventListener("resize", this._onFramesScroll, false);
 
     this._frames = null;
   }
 };
 
 /**
+ * Functions handling the breakpoints view.
+ */
+function BreakpointsView() {
+  this._onBreakpointClick = this._onBreakpointClick.bind(this);
+  this._onBreakpointCheckboxChange = this._onBreakpointCheckboxChange.bind(this);
+}
+
+BreakpointsView.prototype = {
+
+  /**
+   * Removes all elements from the breakpoints container, leaving it empty.
+   */
+  empty: function DVB_empty() {
+    let firstChild;
+    while (firstChild = this._breakpoints.firstChild) {
+      this._destroyContextMenu(firstChild);
+      this._breakpoints.removeChild(firstChild);
+    }
+  },
+
+  /**
+   * Removes all elements from the breakpoints container, and adds a child node
+   * with an empty text note attached.
+   */
+  emptyText: function DVB_emptyText() {
+    // Make sure the container is empty first.
+    this.empty();
+
+    let item = document.createElement("label");
+
+    // The empty node should look grayed out to avoid confusion.
+    item.className = "list-item empty";
+    item.setAttribute("value", L10N.getStr("emptyBreakpointsText"));
+
+    this._breakpoints.appendChild(item);
+  },
+
+  /**
+   * Checks whether the breakpoint with the specified script URL and line is
+   * among the breakpoints known to the debugger and shown in the list, and
+   * returns the matched result or null if nothing is found.
+   *
+   * @param string aUrl
+   *        The original breakpoint script url.
+   * @param number aLine
+   *        The original breakpoint script line.
+   * @return object | null
+   *         The queried breakpoint
+   */
+  getBreakpoint: function DVB_getBreakpoint(aUrl, aLine) {
+    return this._breakpoints.getElementsByAttribute("location", aUrl + ":" + aLine)[0];
+  },
+
+  /**
+   * Removes a breakpoint only from the breakpoints container.
+   * This doesn't remove the breakpoint from the DebuggerController!
+   *
+   * @param string aId
+   *        A breakpoint identifier specified by the debugger.
+   */
+  removeBreakpoint: function DVB_removeBreakpoint(aId) {
+    let breakpoint = document.getElementById("breakpoint-" + aId);
+
+    // Make sure we have something to remove.
+    if (!breakpoint) {
+      return;
+    }
+    this._destroyContextMenu(breakpoint);
+    this._breakpoints.removeChild(breakpoint);
+
+    if (!this.count) {
+      this.emptyText();
+    }
+  },
+
+  /**
+   * Adds a breakpoint to the breakpoints container.
+   * If the breakpoint already exists (was previously added), null is returned.
+   * If it's already added but disabled, it will be enabled and null is returned.
+   * Otherwise, the newly created element is returned.
+   *
+   * @param string aId
+   *        A breakpoint identifier specified by the debugger.
+   * @param string aLineInfo
+   *        The script line information to be displayed in the list.
+   * @param string aLineText
+   *        The script line text to be displayed in the list.
+   * @param string aUrl
+   *        The original breakpoint script url.
+   * @param number aLine
+   *        The original breakpoint script line.
+   * @return object
+   *         The newly created html node representing the added breakpoint.
+   */
+  addBreakpoint: function DVB_addBreakpoint(aId, aLineInfo, aLineText, aUrl, aLine) {
+    // Make sure we don't duplicate anything.
+    if (document.getElementById("breakpoint-" + aId)) {
+      return null;
+    }
+    // Remove the empty list text if it was there.
+    if (!this.count) {
+      this.empty();
+    }
+
+    // If the breakpoint was already added but disabled, enable it now.
+    let breakpoint = this.getBreakpoint(aUrl, aLine);
+    if (breakpoint) {
+      breakpoint.id = "breakpoint-" + aId;
+      breakpoint.breakpointActor = aId;
+      breakpoint.getElementsByTagName("checkbox")[0].setAttribute("checked", "true");
+      return;
+    }
+
+    breakpoint = document.createElement("box");
+    let bkpCheckbox = document.createElement("checkbox");
+    let bkpLineInfo = document.createElement("label");
+    let bkpLineText = document.createElement("label");
+
+    // Create a list item to be added to the stackframes container.
+    breakpoint.id = "breakpoint-" + aId;
+    breakpoint.className = "dbg-breakpoint list-item";
+    breakpoint.setAttribute("location", aUrl + ":" + aLine);
+    breakpoint.breakpointUrl = aUrl;
+    breakpoint.breakpointLine = aLine;
+    breakpoint.breakpointActor = aId;
+
+    aLineInfo = aLineInfo.trim();
+    aLineText = aLineText.trim();
+
+    // A checkbox specifies if the breakpoint is enabled or not.
+    bkpCheckbox.setAttribute("checked", "true");
+    bkpCheckbox.addEventListener("click", this._onBreakpointCheckboxChange, false);
+
+    // This list should display the line info and text for the breakpoint.
+    bkpLineInfo.className = "dbg-breakpoint-info plain";
+    bkpLineText.className = "dbg-breakpoint-text plain";
+    bkpLineInfo.setAttribute("value", aLineInfo);
+    bkpLineText.setAttribute("value", aLineText);
+    bkpLineInfo.setAttribute("crop", "end");
+    bkpLineText.setAttribute("crop", "end");
+    bkpLineText.setAttribute("tooltiptext", aLineText.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_SIZE));
+
+    // Create a context menu for the breakpoint.
+    let menupopupId = this._createContextMenu(breakpoint);
+    breakpoint.setAttribute("contextmenu", menupopupId);
+
+    let state = document.createElement("vbox");
+    state.className = "state";
+    state.appendChild(bkpCheckbox);
+
+    let content = document.createElement("vbox");
+    content.className = "content";
+    content.setAttribute("flex", "1");
+    content.appendChild(bkpLineInfo);
+    content.appendChild(bkpLineText);
+
+    breakpoint.appendChild(state);
+    breakpoint.appendChild(content);
+
+    this._breakpoints.appendChild(breakpoint);
+
+    // Return the element for later use if necessary.
+    return breakpoint;
+  },
+
+  /**
+   * Enables a breakpoint.
+   *
+   * @param object aBreakpoint
+   *        An element representing a breakpoint.
+   * @param function aCallback
+   *        Optional function to invoke once the breakpoint is enabled.
+   * @param boolean aNoCheckboxUpdate
+   *        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).
+   */
+  enableBreakpoint:
+  function DVB_enableBreakpoint(aTarget, aCallback, aNoCheckboxUpdate) {
+    let { breakpointUrl: url, breakpointLine: line } = aTarget;
+    let breakpoint = DebuggerController.Breakpoints.getBreakpoint(url, line)
+
+    if (!breakpoint) {
+      if (!aNoCheckboxUpdate) {
+        aTarget.getElementsByTagName("checkbox")[0].setAttribute("checked", "true");
+      }
+      DebuggerController.Breakpoints.
+        addBreakpoint({ url: url, line: line }, aCallback);
+
+      return true;
+    }
+    return false;
+  },
+
+  /**
+   * Disables a breakpoint.
+   *
+   * @param object aTarget
+   *        An element representing a breakpoint.
+   * @param function aCallback
+   *        Optional function to invoke once the breakpoint is disabled.
+   * @param boolean aNoCheckboxUpdate
+   *        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).
+   */
+  disableBreakpoint:
+  function DVB_disableBreakpoint(aTarget, aCallback, aNoCheckboxUpdate) {
+    let { breakpointUrl: url, breakpointLine: line } = aTarget;
+    let breakpoint = DebuggerController.Breakpoints.getBreakpoint(url, line)
+
+    if (breakpoint) {
+      if (!aNoCheckboxUpdate) {
+        aTarget.getElementsByTagName("checkbox")[0].removeAttribute("checked");
+      }
+      DebuggerController.Breakpoints.
+        removeBreakpoint(breakpoint, aCallback, false, true);
+
+      return true;
+    }
+    return false;
+  },
+
+  /**
+   * Gets the current number of added breakpoints.
+   */
+  get count() {
+    return this._breakpoints.getElementsByClassName("dbg-breakpoint").length;
+  },
+
+  /**
+   * Iterates through all the added breakpoints.
+   *
+   * @param function aCallback
+   *        Function called for each element.
+   */
+  _iterate: function DVB_iterate(aCallback) {
+    Array.forEach(Array.slice(this._breakpoints.childNodes), aCallback);
+  },
+
+  /**
+   * Gets the real breakpoint target when an event is handled.
+   * @return object
+   */
+  _getBreakpointTarget: function DVB__getBreakpointTarget(aEvent) {
+    let target = aEvent.target;
+
+    while (target) {
+      if (target.breakpointActor) {
+        return target;
+      }
+      target = target.parentNode;
+    }
+  },
+
+  /**
+   * Listener handling the breakpoint click event.
+   */
+  _onBreakpointClick: function DVB__onBreakpointClick(aEvent) {
+    let target = this._getBreakpointTarget(aEvent);
+    let { breakpointUrl: url, breakpointLine: line } = target;
+
+    DebuggerController.StackFrames.updateEditorToLocation(url, line, 0, 0, 1);
+  },
+
+  /**
+   * Listener handling the breakpoint checkbox change event.
+   */
+  _onBreakpointCheckboxChange: function DVB__onBreakpointCheckboxChange(aEvent) {
+    aEvent.stopPropagation();
+
+    let target = this._getBreakpointTarget(aEvent);
+    let { breakpointUrl: url, breakpointLine: line } = target;
+
+    if (aEvent.target.getAttribute("checked") === "true") {
+      this.disableBreakpoint(target, null, true);
+    } else {
+      this.enableBreakpoint(target, null, true);
+    }
+  },
+
+  /**
+   * Listener handling the "enableSelf" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onEnableSelf: function DVB__onEnableSelf(aTarget) {
+    if (!aTarget) {
+      return;
+    }
+    if (this.enableBreakpoint(aTarget)) {
+      aTarget.enableSelf.menuitem.setAttribute("hidden", "true");
+      aTarget.disableSelf.menuitem.removeAttribute("hidden");
+    }
+  },
+
+  /**
+   * Listener handling the "disableSelf" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onDisableSelf: function DVB__onDisableSelf(aTarget) {
+    if (!aTarget) {
+      return;
+    }
+    if (this.disableBreakpoint(aTarget)) {
+      aTarget.enableSelf.menuitem.removeAttribute("hidden");
+      aTarget.disableSelf.menuitem.setAttribute("hidden", "true");
+    }
+  },
+
+  /**
+   * Listener handling the "deleteSelf" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onDeleteSelf: function DVB__onDeleteSelf(aTarget) {
+    let { breakpointUrl: url, breakpointLine: line } = aTarget;
+    let breakpoint = DebuggerController.Breakpoints.getBreakpoint(url, line)
+
+    if (aTarget) {
+      this.removeBreakpoint(aTarget.breakpointActor);
+    }
+    if (breakpoint) {
+      DebuggerController.Breakpoints.removeBreakpoint(breakpoint);
+    }
+  },
+
+  /**
+   * Listener handling the "enableOthers" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onEnableOthers: function DVB__onEnableOthers(aTarget) {
+    this._iterate(function(element) {
+      if (element !== aTarget) {
+        this._onEnableSelf(element);
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Listener handling the "disableOthers" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onDisableOthers: function DVB__onDisableOthers(aTarget) {
+    this._iterate(function(element) {
+      if (element !== aTarget) {
+        this._onDisableSelf(element);
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Listener handling the "deleteOthers" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onDeleteOthers: function DVB__onDeleteOthers(aTarget) {
+    this._iterate(function(element) {
+      if (element !== aTarget) {
+        this._onDeleteSelf(element);
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Listener handling the "disableAll" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onEnableAll: function DVB__onEnableAll(aTarget) {
+    this._onEnableOthers(aTarget);
+    this._onEnableSelf(aTarget);
+  },
+
+  /**
+   * Listener handling the "disableAll" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onDisableAll: function DVB__onDisableAll(aTarget) {
+    this._onDisableOthers(aTarget);
+    this._onDisableSelf(aTarget);
+  },
+
+  /**
+   * Listener handling the "deleteAll" menuitem command.
+   *
+   * @param object aTarget
+   *        The corresponding breakpoint element.
+   */
+  _onDeleteAll: function DVB__onDeleteAll(aTarget) {
+    this._onDeleteOthers(aTarget);
+    this._onDeleteSelf(aTarget);
+  },
+
+  /**
+   * The cached breakpoints container.
+   */
+  _breakpoints: null,
+
+  /**
+   * Creates a breakpoint context menu.
+   *
+   * @param object aBreakpoint
+   *        An element representing a breakpoint.
+   * @return string
+   *         The popup id.
+   */
+  _createContextMenu: function DVB_createContextMenu(aBreakpoint) {
+    let commandsetId = "breakpointMenuCommands-" + aBreakpoint.id;
+    let menupopupId = "breakpointContextMenu-" + aBreakpoint.id;
+
+    let commandsset = document.createElement("commandsset");
+    commandsset.setAttribute("id", commandsetId);
+
+    let menupopup = document.createElement("menupopup");
+    menupopup.setAttribute("id", menupopupId);
+
+    /**
+     * Creates a menu item specified by a name with the appropriate attributes
+     * (label and command 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 func = this["_on" + aName.charAt(0).toUpperCase() + aName.slice(1)];
+      let label = L10N.getStr("breakpointMenuItem." + aName);
+
+      let prefix = "bp-cMenu-";
+      let commandId = prefix + aName + "-" + aBreakpoint.id + "-command";
+      let menuitemId = prefix + aName + "-" + aBreakpoint.id + "-menuitem";
+
+      command.setAttribute("id", commandId);
+      command.setAttribute("label", label);
+      command.addEventListener("command", func.bind(this, aBreakpoint), true);
+
+      menuitem.setAttribute("id", menuitemId);
+      menuitem.setAttribute("command", commandId);
+      menuitem.setAttribute("hidden", aHiddenFlag);
+
+      commandsset.appendChild(command);
+      menupopup.appendChild(menuitem);
+
+      aBreakpoint[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);
+    }
+
+    createMenuItem.call(this, "enableSelf", true);
+    createMenuItem.call(this, "disableSelf");
+    createMenuItem.call(this, "deleteSelf");
+    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");
+
+    let popupset = document.getElementById("debugger-popups");
+    popupset.appendChild(menupopup);
+    document.documentElement.appendChild(commandsset);
+
+    return menupopupId;
+  },
+
+  /**
+   * Destroys a breakpoint context menu.
+   *
+   * @param object aBreakpoint
+   *        An element representing a breakpoint.
+   */
+  _destroyContextMenu: function DVB__destroyContextMenu(aBreakpoint) {
+    let commandsetId = "breakpointMenuCommands-" + aBreakpoint.id;
+    let menupopupId = "breakpointContextMenu-" + aBreakpoint.id;
+
+    let commandset = document.getElementById(commandsetId);
+    let menupopup = document.getElementById(menupopupId);
+
+    if (commandset) {
+      commandset.parentNode.removeChild(commandset);
+    }
+    if (menupopup) {
+      menupopup.parentNode.removeChild(menupopup);
+    }
+  },
+
+  /**
+   * Initialization function, called when the debugger is initialized.
+   */
+  initialize: function DVB_initialize() {
+    let breakpoints = document.getElementById("breakpoints");
+    breakpoints.addEventListener("click", this._onBreakpointClick, false);
+
+    this._breakpoints = breakpoints;
+    this.emptyText();
+  },
+
+  /**
+   * Destruction function, called when the debugger is shut down.
+   */
+  destroy: function DVB_destroy() {
+    let breakpoints = this._breakpoints;
+    breakpoints.removeEventListener("click", this._onBreakpointClick, false);
+
+    this._breakpoints = null;
+  }
+};
+
+/**
  * Functions handling the properties view.
  */
 function PropertiesView() {
   this.addScope = this._addScope.bind(this);
   this._addVar = this._addVar.bind(this);
   this._addProperties = this._addProperties.bind(this);
 }
 
 PropertiesView.prototype = {
+
   /**
    * A monotonically-increasing counter, that guarantees the uniqueness of scope
    * IDs.
    */
   _idCount: 1,
 
   /**
    * Adds a scope to contain any inspected variables.
@@ -1918,19 +2460,20 @@ PropertiesView.prototype = {
    * The cached variable properties container.
    */
   _vars: null,
 
   /**
    * Initialization function, called when the debugger is initialized.
    */
   initialize: function DVP_initialize() {
-    this.createHierarchyStore();
+    this._vars = document.getElementById("variables");
 
-    this._vars = document.getElementById("variables");
+    this.emptyText();
+    this.createHierarchyStore();
   },
 
   /**
    * Destruction function, called when the debugger is shut down.
    */
   destroy: function DVP_destroy() {
     this._currHierarchy = null;
     this._prevHierarchy = null;
@@ -1938,16 +2481,17 @@ PropertiesView.prototype = {
   }
 };
 
 /**
  * Preliminary setup for the DebuggerView object.
  */
 DebuggerView.Scripts = new ScriptsView();
 DebuggerView.StackFrames = new StackFramesView();
+DebuggerView.Breakpoints = new BreakpointsView();
 DebuggerView.Properties = new PropertiesView();
 
 /**
  * Export the source editor to the global scope for easier access in tests.
  */
 Object.defineProperty(window, "editor", {
   get: function() { return DebuggerView.editor; }
 });
--- a/browser/devtools/debugger/debugger.css
+++ b/browser/devtools/debugger/debugger.css
@@ -8,16 +8,30 @@
  * Stack frames
  */
 
 #stackframes {
   overflow: auto;
 }
 
 /**
+ * Breakpoints view
+ */
+
+#breakpoints {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.dbg-breakpoint > .state,
+.dbg-breakpoint > .content {
+  overflow: hidden;
+}
+
+/**
  * Properties elements
  */
 
 #variables {
   overflow: auto;
 }
 
 /**
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -75,16 +75,20 @@
       <spacer flex="1"/>
 #ifndef XP_MACOSX
       <toolbarbutton id="close"
                      tooltiptext="&debuggerUI.closeButton.tooltip;"
                      class="devtools-closebutton"/>
 #endif
     </toolbar>
     <hbox id="dbg-content" flex="1">
-      <vbox id="stackframes"/>
-      <splitter id="stack-script-splitter" class="devtools-side-splitter"/>
+      <vbox id="stackframes+breakpoints">
+        <vbox id="stackframes" flex="1"/>
+        <splitter class="devtools-horizontal-splitter"/>
+        <vbox id="breakpoints"/>
+      </vbox>
+      <splitter class="devtools-side-splitter"/>
       <vbox id="editor" flex="1"/>
-      <splitter id="script-properties-splitter" class="devtools-side-splitter"/>
+      <splitter class="devtools-side-splitter"/>
       <vbox id="variables"/>
     </hbox>
   </vbox>
 </window>
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -46,16 +46,17 @@ MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_scripts-sorting.js \
 	browser_dbg_scripts-searching-01.js \
 	browser_dbg_scripts-searching-02.js \
 	browser_dbg_pause-resume.js \
 	browser_dbg_update-editor-mode.js \
 	$(warning browser_dbg_select-line.js temporarily disabled due to oranges, see bug 726609) \
 	browser_dbg_clean-exit.js \
 	browser_dbg_bug723069_editor-breakpoints.js \
+	browser_dbg_bug723071_editor-breakpoints-pane.js \
 	browser_dbg_bug731394_editor-contextmenu.js \
 	browser_dbg_displayName.js \
 	browser_dbg_iframes.js \
 	browser_dbg_pause-exceptions.js \
 	browser_dbg_multiple-windows.js \
 	browser_dbg_menustatus.js \
 	browser_dbg_bfcache.js \
 	browser_dbg_breakpoint-new-script.js \
--- a/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js
@@ -16,16 +16,17 @@ let gScripts = null;
 let gEditor = null;
 let gBreakpoints = null;
 
 function test()
 {
   let tempScope = {};
   Cu.import("resource:///modules/source-editor.jsm", tempScope);
   let SourceEditor = tempScope.SourceEditor;
+
   let scriptShown = false;
   let framesAdded = false;
   let resumed = false;
   let testStarted = false;
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug723071_editor-breakpoints-pane.js
@@ -0,0 +1,281 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 723071: test adding a pane to display the list of breakpoints across
+ * all scripts in the debuggee.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gScripts = null;
+let gBreakpoints = null;
+let gBreakpointsElement = null;
+
+function test()
+{
+  let scriptShown = false;
+  let framesAdded = false;
+  let resumed = false;
+  let testStarted = false;
+
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPane = aPane;
+    gDebugger = gPane.contentWindow;
+    resumed = true;
+
+    gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
+      framesAdded = true;
+      executeSoon(startTest);
+    });
+
+    executeSoon(function() {
+      gDebuggee.firstCall();
+    });
+  });
+
+  function onScriptShown(aEvent)
+  {
+    scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
+    executeSoon(startTest);
+  }
+
+  window.addEventListener("Debugger:ScriptShown", onScriptShown);
+
+  function startTest()
+  {
+    if (scriptShown && framesAdded && resumed && !testStarted) {
+      window.removeEventListener("Debugger:ScriptShown", onScriptShown);
+      testStarted = true;
+      Services.tm.currentThread.dispatch({ run: performTest }, 0);
+    }
+  }
+
+  let breakpointsAdded = 0;
+  let breakpointsDisabled = 0;
+  let breakpointsRemoved = 0;
+
+  function performTest()
+  {
+    gScripts = gDebugger.DebuggerView.Scripts;
+
+    is(gDebugger.DebuggerController.activeThread.state, "paused",
+      "Should only be getting stack frames while paused.");
+
+    is(gScripts._scripts.itemCount, 2, "Found the expected number of scripts.");
+
+    let editor = gDebugger.editor;
+
+    isnot(editor.getText().indexOf("debugger"), -1,
+          "The correct script was loaded initially.");
+    isnot(gScripts.selected, gScripts.scriptLocations[0],
+          "the correct script is selected");
+
+    gBreakpoints = gPane.breakpoints;
+    is(Object.keys(gBreakpoints), 0, "no breakpoints");
+    ok(!gPane.getBreakpoint("chocolate", 3), "getBreakpoint('chocolate', 3) returns falsey");
+
+    is(editor.getBreakpoints().length, 0, "no breakpoints in the editor");
+
+    gBreakpointsElement = gDebugger.DebuggerView.Breakpoints._breakpoints;
+    is(gBreakpointsElement.childNodes.length, 1,
+      "The breakpoints pane should be empty, but showing a " +
+      "'no breakpoints' information message.");
+    is(gBreakpointsElement.childNodes.length,
+       gBreakpointsElement.querySelectorAll(".list-item.empty").length,
+       "Found junk in the breakpoints container.");
+
+    addBreakpoints(function() {
+      is(breakpointsAdded, 3,
+        "Should have added 3 breakpoints so far.");
+      is(breakpointsDisabled, 0,
+        "Shouldn't have disabled anything so far.");
+      is(breakpointsRemoved, 0,
+        "Shouldn't have removed anything so far.");
+
+      is(gBreakpointsElement.childNodes.length,
+         gBreakpointsElement.querySelectorAll(".dbg-breakpoint").length,
+         "Found junk in the breakpoints container.");
+
+      disableBreakpoints(function() {
+        is(breakpointsAdded, 3,
+          "Should still have 3 breakpoints added so far.");
+        is(breakpointsDisabled, 3,
+          "Should have 3 disabled breakpoints.");
+        is(breakpointsRemoved, 0,
+          "Shouldn't have removed anything so far.");
+
+        is(gBreakpointsElement.childNodes.length, breakpointsAdded,
+          "Should have the same number of breakpoints in the pane.");
+        is(gBreakpointsElement.childNodes.length, breakpointsDisabled,
+          "Should have the same number of disabled breakpoints.");
+
+        addBreakpoints(function() {
+          is(breakpointsAdded, 3,
+            "Should still have only 3 breakpoints added so far.");
+          is(breakpointsDisabled, 3,
+            "Should still have 3 disabled breakpoints.");
+          is(breakpointsRemoved, 0,
+            "Shouldn't have removed anything so far.");
+
+          is(gBreakpointsElement.childNodes.length, breakpointsAdded,
+            "Since half of the breakpoints already existed, but disabled, " +
+            "only half of the added breakpoints are actually in the pane.");
+          is(gBreakpointsElement.childNodes.length,
+             gBreakpointsElement.querySelectorAll(".dbg-breakpoint").length,
+             "Found junk in the breakpoints container.");
+
+          removeBreakpoints(function() {
+            is(breakpointsRemoved, 3,
+              "Should have 3 removed breakpoints.");
+
+            is(gBreakpointsElement.childNodes.length, 1,
+              "The breakpoints pane should be empty, but showing a " +
+              "'no breakpoints' information message.");
+            is(gBreakpointsElement.childNodes.length,
+               gBreakpointsElement.querySelectorAll(".list-item.empty").length,
+               "Found junk in the breakpoints container.");
+
+            finish();
+          });
+        });
+      });
+    }, true);
+
+    function addBreakpoints(callback, increment)
+    {
+      let line;
+
+      executeSoon(function()
+      {
+        line = 4;
+        gPane.addBreakpoint({url: gScripts.selected, line: line},
+          function(cl, err) {
+          onBreakpointAdd.call({ increment: increment, line: line }, cl, err);
+
+          line = 5;
+          gPane.addBreakpoint({url: gScripts.selected, line: line},
+            function(cl, err) {
+            onBreakpointAdd.call({ increment: increment, line: line }, cl, err);
+
+            line = 6;
+            gPane.addBreakpoint({url: gScripts.selected, line: line},
+              function(cl, err) {
+              onBreakpointAdd.call({ increment: increment, line: line }, cl, err);
+
+              executeSoon(function() {
+                callback();
+              });
+            });
+          });
+        });
+      });
+    }
+
+    function disableBreakpoints(callback)
+    {
+      let nodes = Array.slice(gBreakpointsElement.childNodes);
+      info("Nodes to disable: " + breakpointsAdded);
+      is(nodes.length, breakpointsAdded,
+        "The number of nodes to disable is incorrect.");
+
+      Array.forEach(nodes, function(bkp) {
+        info("Disabling breakpoint: " + bkp.id);
+
+        gDebugger.DebuggerView.Breakpoints.disableBreakpoint(bkp, function() {
+          if (++breakpointsDisabled !== breakpointsAdded) {
+            return;
+          }
+          executeSoon(function() {
+            callback();
+          });
+        });
+      });
+    }
+
+    function removeBreakpoints(callback)
+    {
+      let nodes = Array.slice(gBreakpointsElement.childNodes);
+      info("Nodes to remove: " + breakpointsAdded);
+      is(nodes.length, breakpointsAdded,
+        "The number of nodes to remove is incorrect.");
+
+      Array.forEach(nodes, function(bkp) {
+        info("Removing breakpoint: " + bkp.id);
+
+        let [url, line, actor] =
+          [bkp.breakpointUrl, bkp.breakpointLine, bkp.breakpointActor];
+
+        gDebugger.DebuggerView.Breakpoints.removeBreakpoint(actor);
+        gPane.removeBreakpoint(gPane.getBreakpoint(url, line), function() {
+          if (++breakpointsRemoved !== breakpointsAdded) {
+            return;
+          }
+          executeSoon(function() {
+            callback();
+          });
+        });
+      });
+    }
+
+    function onBreakpointAdd(aBreakpointClient, aResponseError)
+    {
+      if (this.increment) {
+        breakpointsAdded++;
+      }
+
+      is(gBreakpointsElement.childNodes.length, breakpointsAdded, this.increment
+        ? "Should have added a breakpoint in the pane."
+        : "Should have the same number of breakpoints in the pane.");
+
+      let id = "breakpoint-" + aBreakpointClient.actor;
+      let bkp = gDebugger.document.getElementById(id);
+      let info = bkp.getElementsByClassName("dbg-breakpoint-info")[0];
+      let text = bkp.getElementsByClassName("dbg-breakpoint-text")[0];
+      let check = bkp.querySelector("checkbox");
+
+      is(bkp.id, id,
+        "Breakpoint element " + id + " found succesfully.");
+      is(info.getAttribute("value"), getExpectedBreakpointInfo(this.line),
+        "The expected information wasn't found in the breakpoint element.");
+      is(text.getAttribute("value"), getExpectedLineText(this.line).trim(),
+        "The expected line text wasn't found in the breakpoint element.");
+      is(check.getAttribute("checked"), "true",
+        "The breakpoint enable checkbox is checked as expected.");
+    }
+
+    function getExpectedBreakpointInfo(line) {
+      let url = gDebugger.DebuggerView.Scripts.selected;
+      let label = gDebugger.DebuggerController.SourceScripts.getScriptLabel(url);
+      return label + ":" + line;
+    }
+
+    function getExpectedLineText(line) {
+      return gDebugger.DebuggerController.SourceScripts.getLineText(line - 1);
+    }
+  }
+
+  registerCleanupFunction(function() {
+    is(Object.keys(gBreakpoints).length, 0, "no breakpoint in the debugger");
+    ok(!gPane.getBreakpoint(gScripts.scriptLocations[0], 5),
+       "getBreakpoint(scriptLocations[0], 5) returns no breakpoint");
+
+    is(breakpointsAdded, 3, "correct number of breakpoints have been added");
+    is(breakpointsDisabled, 3, "correct number of breakpoints have been disabled");
+    is(breakpointsRemoved, 3, "correct number of breakpoints have been removed");
+    removeTab(gTab);
+    gPane = null;
+    gTab = null;
+    gDebuggee = null;
+    gDebugger = null;
+    gScripts = null;
+    gBreakpoints = null;
+    gBreakpointsElement = null;
+  });
+}
--- a/browser/devtools/debugger/test/browser_dbg_panesize-inner.js
+++ b/browser/devtools/debugger/test/browser_dbg_panesize-inner.js
@@ -28,17 +28,17 @@ function test() {
     frame.addEventListener("Debugger:Loaded", function dbgLoaded() {
       frame.removeEventListener("Debugger:Loaded", dbgLoaded, true);
 
       ok(content.Prefs.stackframesWidth,
         "The debugger preferences should have a saved stackframesWidth value.");
       ok(content.Prefs.variablesWidth,
         "The debugger preferences should have a saved variablesWidth value.");
 
-      stackframes = content.document.getElementById("stackframes");
+      stackframes = content.document.getElementById("stackframes+breakpoints");
       variables = content.document.getElementById("variables");
 
       is(content.Prefs.stackframesWidth, stackframes.getAttribute("width"),
         "The stackframes pane width should be the same as the preferred value.");
       is(content.Prefs.variablesWidth, variables.getAttribute("width"),
         "The variables pane width should be the same as the preferred value.");
 
       stackframes.setAttribute("width", someWidth1);
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-01.js
@@ -63,17 +63,17 @@ function testScriptLabelShortening() {
       { href: "resource://random/", leaf: "script_t3_1.js#id?a=1&b=2" },
       { href: "resource://random/", leaf: "script_t3_2.js?a=1&b=2#id" },
       { href: "resource://random/", leaf: "script_t3_3.js&a=1&b=2#id" }
     ];
 
     urls.forEach(function(url) {
       executeSoon(function() {
         let loc = url.href + url.leaf;
-        vs.addScript(ss._getScriptLabel(loc, url.href), { url: loc }, true);
+        vs.addScript(ss.getScriptLabel(loc, url.href), { url: loc }, true);
       });
     });
 
     executeSoon(function() {
       info("Script labels:");
       info(vs.scriptLabels.toSource());
 
       info("Script locations:");
--- a/browser/devtools/debugger/test/browser_dbg_scripts-sorting.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-sorting.js
@@ -62,41 +62,41 @@ function addScriptsAndCheckOrder(method,
   urls.sort(function(a, b) {
     return Math.random() - 0.5;
   });
 
   switch (method) {
     case 1:
       urls.forEach(function(url) {
         let loc = url.href + url.leaf;
-        vs.addScript(ss._getScriptLabel(loc, url.href), { url: loc });
+        vs.addScript(ss.getScriptLabel(loc, url.href), { url: loc });
       });
       vs.commitScripts();
       break;
 
     case 2:
       urls.forEach(function(url) {
         let loc = url.href + url.leaf;
-        vs.addScript(ss._getScriptLabel(loc, url.href), { url: loc }, true);
+        vs.addScript(ss.getScriptLabel(loc, url.href), { url: loc }, true);
       });
       break;
 
     case 3:
       let i = 0
       for (; i < urls.length / 2; i++) {
         let url = urls[i];
         let loc = url.href + url.leaf;
-        vs.addScript(ss._getScriptLabel(loc, url.href), { url: loc });
+        vs.addScript(ss.getScriptLabel(loc, url.href), { url: loc });
       }
       vs.commitScripts();
 
       for (; i < urls.length; i++) {
         let url = urls[i];
         let loc = url.href + url.leaf;
-        vs.addScript(ss._getScriptLabel(loc, url.href), { url: loc }, true);
+        vs.addScript(ss.getScriptLabel(loc, url.href), { url: loc }, true);
       }
       break;
   }
 
   executeSoon(function() {
     checkScriptsOrder(method);
     callback();
   });
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -50,16 +50,32 @@ pauseTooltip=Click to pause
 # LOCALIZATION NOTE (resumeLabel): The label that is displayed on the pause
 # button when the debugger is in a paused state.
 resumeTooltip=Click to resume
 
 # LOCALIZATION NOTE (emptyStackText): The text that is displayed in the stack
 # frames list when there are no frames to display.
 emptyStackText=No stacks to display.
 
+# LOCALIZATION NOTE (emptyBreakpointsText): The text that is displayed in the
+# breakpoints list when there are no breakpoints to display.
+emptyBreakpointsText=No breakpoints to display.
+
+# LOCALIZATION NOTE (breakpointMenuItem): The text for all the elements that
+# are displayed in the breakpoints menu item popup.
+breakpointMenuItem.enableSelf=Enable breakpoint
+breakpointMenuItem.disableSelf=Disable breakpoint
+breakpointMenuItem.deleteSelf=Remove breakpoint
+breakpointMenuItem.enableOthers=Enable others
+breakpointMenuItem.disableOthers=Disable others
+breakpointMenuItem.deleteOthers=Remove others
+breakpointMenuItem.enableAll=Enable all breakpoints
+breakpointMenuItem.disableAll=Disable all breakpoints
+breakpointMenuItem.deleteAll=Remove all breakpoints
+
 # LOCALIZATION NOTE (loadingText): The text that is displayed in the script
 # editor when the laoding process has started but there is no file to display
 # yet.
 loadingText=Loading\u2026
 
 # LOCALIZATION NOTE (loadingError):
 # This is the error message that is displayed on failed attempts to load an
 # external resource file.
--- a/browser/themes/gnomestripe/devtools/debugger.css
+++ b/browser/themes/gnomestripe/devtools/debugger.css
@@ -23,23 +23,28 @@
 /**
  * Lists and headers
  */
 
 .list-item {
   padding: 2px;
 }
 
+.list-item:not(.selected):not(.empty):hover {
+  background: #cddae5;
+}
+
 .list-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-item.empty {
   color: GrayText;
+  padding: 4px;
 }
 
 /**
  * Stack frames
  */
 
 #stackframes {
   background-color: white;
@@ -51,16 +56,32 @@
 }
 
 .dbg-stackframe-name {
   -moz-padding-end: 4px;
   font-weight: 600;
 }
 
 /**
+ * Breakpoints view
+ */
+
+#breakpoints {
+  background-color: white;
+}
+
+.dbg-breakpoint-info {
+  font-weight: 600;
+}
+
+.dbg-breakpoint-text {
+  font: 8pt monospace;
+}
+
+/**
  * Properties view
  */
 
 #variables {
   background-color: white;
 }
 
 /**
--- a/browser/themes/pinstripe/devtools/debugger.css
+++ b/browser/themes/pinstripe/devtools/debugger.css
@@ -25,23 +25,28 @@
 /**
  * Lists and headers
  */
 
 .list-item {
   padding: 2px;
 }
 
+.list-item:not(.selected):not(.empty):hover {
+  background: #cddae5;
+}
+
 .list-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-item.empty {
   color: GrayText;
+  padding: 4px;
 }
 
 /**
  * Stack frames
  */
 
 #stackframes {
   background-color: white;
@@ -53,16 +58,32 @@
 }
 
 .dbg-stackframe-name {
   -moz-padding-end: 4px;
   font-weight: 600;
 }
 
 /**
+ * Breakpoints view
+ */
+
+#breakpoints {
+  background-color: white;
+}
+
+.dbg-breakpoint-info {
+  font-weight: 600;
+}
+
+.dbg-breakpoint-text {
+  font: 8pt monospace;
+}
+
+/**
  * Properties view
  */
 
 #variables {
   background-color: white;
 }
 
 /**
--- a/browser/themes/winstripe/devtools/debugger.css
+++ b/browser/themes/winstripe/devtools/debugger.css
@@ -31,23 +31,28 @@
 /**
  * Lists and headers
  */
 
 .list-item {
   padding: 2px;
 }
 
+.list-item:not(.selected):not(.empty):hover {
+  background: #cddae5;
+}
+
 .list-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-item.empty {
   color: GrayText;
+  padding: 4px;
 }
 
 /**
  * Stack frames
  */
 
 #stackframes {
   background-color: white;
@@ -59,16 +64,32 @@
 }
 
 .dbg-stackframe-name {
   -moz-padding-end: 4px;
   font-weight: 600;
 }
 
 /**
+ * Breakpoints view
+ */
+
+#breakpoints {
+  background-color: white;
+}
+
+.dbg-breakpoint-info {
+  font-weight: 600;
+}
+
+.dbg-breakpoint-text {
+  font: 8pt monospace;
+}
+
+/**
  * Properties view
  */
 
 #variables {
   background-color: white;
 }
 
 /**