Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Sun, 18 Nov 2012 08:36:20 -0500
changeset 113641 174440fca7da0117c0bfd69e570a7fff0740f328
parent 113640 b3b972e271a7b2635f80b73447ce0d1f9cd2d2d2 (current diff)
parent 113617 87703fb491e0669aaba04707ba5354a4d25d9415 (diff)
child 113642 0ef0c9db3b2b0991a7d04a2cbf1e9d549bc17dd2
child 113643 bb6407777f229860f3b551f99f61c4fe4dc74bc2
push id23880
push userryanvm@gmail.com
push dateSun, 18 Nov 2012 13:36:46 +0000
treeherdermozilla-central@174440fca7da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone19.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound.
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -369,16 +369,18 @@ function StackFrames() {
   this._afterFramesCleared = this._afterFramesCleared.bind(this);
   this.evaluate = this.evaluate.bind(this);
 }
 
 StackFrames.prototype = {
   get activeThread() DebuggerController.activeThread,
   autoScopeExpand: false,
   currentFrame: null,
+  syncedWatchExpressions: null,
+  currentWatchExpressions: null,
   currentBreakpointLocation: null,
   currentEvaluation: null,
   currentException: null,
 
   /**
    * Connect to the current thread client.
    */
   connect: function SF_connect() {
@@ -423,90 +425,123 @@ StackFrames.prototype = {
   _onPaused: function SF__onPaused(aEvent, aPacket) {
     switch (aPacket.why.type) {
       // If paused by a breakpoint, store the breakpoint location.
       case "breakpoint":
         this.currentBreakpointLocation = aPacket.frame.where;
         break;
       // If paused by a client evaluation, store the evaluated value.
       case "clientEvaluated":
-        this.currentEvaluation = aPacket.why.frameFinished.return;
+        this.currentEvaluation = aPacket.why.frameFinished;
         break;
       // If paused by an exception, store the exception value.
       case "exception":
         this.currentException = aPacket.why.exception;
         break;
     }
 
     this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
     DebuggerView.editor.focus();
   },
 
   /**
    * Handler for the thread client's resumed notification.
    */
   _onResumed: function SF__onResumed() {
     DebuggerView.editor.setDebugLocation(-1);
+
+    // Prepare the watch expression evaluation string for the next pause.
+    if (!this._isWatchExpressionsEvaluation) {
+      this.currentWatchExpressions = this.syncedWatchExpressions;
+    }
   },
 
   /**
    * Handler for the thread client's framesadded notification.
    */
   _onFrames: function SF__onFrames() {
     // Ignore useless notifications.
     if (!this.activeThread.cachedFrames.length) {
       return;
     }
-    DebuggerView.StackFrames.empty();
 
     // Conditional breakpoints are { breakpoint, expression } tuples. The
     // boolean evaluation of the expression decides if the active thread
     // automatically resumes execution or not.
     if (this.currentBreakpointLocation) {
       let { url, line } = this.currentBreakpointLocation;
       let breakpointClient = DebuggerController.Breakpoints.getBreakpoint(url, line);
       let conditionalExpression = breakpointClient.conditionalExpression;
       if (conditionalExpression) {
         // Evaluating the current breakpoint's conditional expression will
         // cause the stack frames to be cleared and active thread to pause,
         // sending a 'clientEvaluated' packed and adding the frames again.
-        this.evaluate("(" + conditionalExpression + ")", 0);
+        this.evaluate(conditionalExpression, 0);
         this._isConditionalBreakpointEvaluation = true;
         return;
       }
     }
-
     // Got our evaluation of the current breakpoint's conditional expression.
     if (this._isConditionalBreakpointEvaluation) {
       this._isConditionalBreakpointEvaluation = false;
-
       // If the breakpoint's conditional expression evaluation is falsy,
       // automatically resume execution.
-      if (VariablesView.isFalsy({ value: this.currentEvaluation })) {
+      if (VariablesView.isFalsy({ value: this.currentEvaluation.return })) {
         this.activeThread.resume();
         return;
       }
     }
 
+
+    // Watch expressions are evaluated in the context of the topmost frame,
+    // and the results and displayed in the variables view.
+    if (this.currentWatchExpressions) {
+      // Evaluation causes the stack frames to be cleared and active thread to
+      // pause, sending a 'clientEvaluated' packed and adding the frames again.
+      this.evaluate(this.currentWatchExpressions, 0);
+      this._isWatchExpressionsEvaluation = true;
+      return;
+    }
+    // Got our evaluation of the current watch expressions.
+    if (this._isWatchExpressionsEvaluation) {
+      this._isWatchExpressionsEvaluation = false;
+      // If an error was thrown during the evaluation of the watch expressions,
+      // then at least one expression evaluation could not be performed.
+      if (this.currentEvaluation.throw) {
+        DebuggerView.WatchExpressions.removeExpression(0);
+        DebuggerController.StackFrames.syncWatchExpressions();
+        return;
+      }
+      // If the watch expressions were evaluated successfully, attach
+      // the results to the topmost frame.
+      let topmostFrame = this.activeThread.cachedFrames[0];
+      topmostFrame.watchExpressionsEvaluation = this.currentEvaluation.return;
+    }
+
+
+    // Make sure all the previous stackframes are removed before re-adding them.
+    DebuggerView.StackFrames.empty();
+
     for (let frame of this.activeThread.cachedFrames) {
       this._addFrame(frame);
     }
-    if (!this.currentFrame) {
+    if (this.currentFrame == null) {
       this.selectFrame(0);
     }
     if (this.activeThread.moreFrames) {
       DebuggerView.StackFrames.dirty = true;
     }
   },
 
   /**
    * Handler for the thread client's framescleared notification.
    */
   _onFramesCleared: function SF__onFramesCleared() {
     this.currentFrame = null;
+    this.currentWatchExpressions = null;
     this.currentBreakpointLocation = null;
     this.currentEvaluation = null;
     this.currentException = null;
     // After each frame step (in, over, out), framescleared is fired, which
     // forces the UI to be emptied and rebuilt on framesadded. Most of the times
     // this is not necessary, and will result in a brief redraw flicker.
     // To avoid it, invalidate the UI only after a short time if necessary.
     window.setTimeout(this._afterFramesCleared, FRAME_STEP_CLEAR_DELAY);
@@ -518,50 +553,67 @@ StackFrames.prototype = {
   _afterFramesCleared: function SF__afterFramesCleared() {
     // Ignore useless notifications.
     if (this.activeThread.cachedFrames.length) {
       return;
     }
     DebuggerView.StackFrames.empty();
     DebuggerView.Variables.empty(0);
     DebuggerView.Breakpoints.unhighlightBreakpoint();
+    DebuggerView.WatchExpressions.toggleContents(true);
     window.dispatchEvent("Debugger:AfterFramesCleared");
   },
 
   /**
    * Marks the stack frame at the specified depth as selected and updates the
    * properties view with the stack frame's data.
    *
    * @param number aDepth
    *        The depth of the frame in the stack.
    */
   selectFrame: function SF_selectFrame(aDepth) {
     let frame = this.activeThread.cachedFrames[this.currentFrame = aDepth];
     if (!frame) {
       return;
     }
-    let environment = frame.environment;
+    let { environment, watchExpressionsEvaluation } = frame;
     let { url, line } = frame.where;
 
     // Check if the frame does not represent the evaluation of debuggee code.
     if (!environment) {
       return;
     }
 
     // Move the editor's caret to the proper url and line.
     DebuggerView.updateEditor(url, line);
     // Highlight the stack frame at the specified depth.
     DebuggerView.StackFrames.highlightFrame(aDepth);
     // Highlight the breakpoint at the specified url and line if it exists.
     DebuggerView.Breakpoints.highlightBreakpoint(url, line);
+    // Don't display the watch expressions textbox inputs in the pane.
+    DebuggerView.WatchExpressions.toggleContents(false);
     // Start recording any added variables or properties in any scope.
     DebuggerView.Variables.createHierarchy();
     // Clear existing scopes and create each one dynamically.
     DebuggerView.Variables.empty();
 
+    // If watch expressions evaluation results are available, create a scope
+    // to contain all the values.
+    if (watchExpressionsEvaluation) {
+      let label = L10N.getStr("watchExpressionsScopeLabel");
+      let arrow = L10N.getStr("watchExpressionsSeparatorLabel");
+      let scope = DebuggerView.Variables.addScope(label);
+      scope.separator = arrow;
+
+      // The evaluation hasn't thrown, so display the returned results and
+      // always expand the watch expressions scope by default.
+      this._fetchWatchExpressions(scope, watchExpressionsEvaluation);
+      scope.expand();
+    }
+
     do {
       // Create a scope to contain all the inspected variables.
       let label = this._getScopeLabel(environment);
       let scope = DebuggerView.Variables.addScope(label);
 
       // Special additions to the innermost scope.
       if (environment == frame.environment) {
         this._insertScopeFrameReferences(scope, frame);
@@ -620,16 +672,49 @@ StackFrames.prototype = {
     if (aVar.name == "window" || aVar.name == "this") {
       aVar.onmouseover = callback;
     }
     // Make sure that properties are always available on expansion.
     aVar.onexpand = callback;
   },
 
   /**
+   * Adds the watch expressions evaluation results to a scope in the view.
+   *
+   * @param Scope aScope
+   *        The scope where the watch expressions will be placed into.
+   * @param object aExp
+   *        The grip of the evaluation results.
+   */
+  _fetchWatchExpressions: function SF__fetchWatchExpressions(aScope, aExp) {
+    // Retrieve the expressions only once.
+    if (aScope.fetched) {
+      return;
+    }
+    aScope.fetched = true;
+
+    // Add nodes for every watch expression in scope.
+    this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(function(aResponse) {
+      let ownProperties = aResponse.ownProperties;
+      let totalExpressions = DebuggerView.WatchExpressions.totalItems;
+
+      for (let i = 0; i < totalExpressions; i++) {
+        let name = DebuggerView.WatchExpressions.getExpression(i);
+        let expVal = ownProperties[i].value;
+        let expRef = aScope.addVar(name, ownProperties[i]);
+        this._addVarExpander(expRef, expVal);
+      }
+
+      // Signal that watch expressions have been fetched.
+      window.dispatchEvent("Debugger:FetchedWatchExpressions");
+      DebuggerView.Variables.commitHierarchy();
+    }.bind(this));
+  },
+
+  /**
    * Adds variables to a scope in the view. Triggered when a scope is
    * expanded or is hovered. It does not expand the scope.
    *
    * @param Scope aScope
    *        The scope where the variables will be placed into.
    * @param object aEnv
    *        The scope's environment.
    */
@@ -755,17 +840,17 @@ StackFrames.prototype = {
         aVar.addProperties(ownProperties);
         // Expansion handlers must be set after the properties are added.
         for (let name in ownProperties) {
           this._addVarExpander(aVar.get(name), ownProperties[name].value);
         }
       }
 
       // Add the variable's __proto__.
-      if (prototype.type != "null") {
+      if (prototype && prototype.type != "null") {
         aVar.addProperty("__proto__", { value: prototype });
         // Expansion handlers must be set after the properties are added.
         this._addVarExpander(aVar.get("__proto__"), prototype);
       }
 
       aVar._retrieved = true;
 
       // Signal that properties have been fetched.
@@ -826,25 +911,46 @@ StackFrames.prototype = {
    * Loads more stack frames from the debugger server cache.
    */
   addMoreFrames: function SF_addMoreFrames() {
     this.activeThread.fillFrames(
       this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
   },
 
   /**
+   * Updates a list of watch expressions to evaluate on each pause.
+   */
+  syncWatchExpressions: function SF_syncWatchExpressions() {
+    let list = DebuggerView.WatchExpressions.getExpressions();
+
+    if (list.length) {
+      this.syncedWatchExpressions =
+        this.currentWatchExpressions = "[" + list.map(function(str)
+          "(function() {" +
+            "try { return eval(\"" + str.replace(/"/g, "\\$&") + "\"); }" +
+            "catch(e) { return e.name + ': ' + e.message; }" +
+          "})()"
+        ).join(",") + "]";
+    } else {
+      this.syncedWatchExpressions =
+        this.currentWatchExpressions = null;
+    }
+    this._onFrames();
+  },
+
+  /**
    * Evaluate an expression in the context of the selected frame. This is used
    * for modifying the value of variables or properties in scope.
    *
    * @param string aExpression
    *        The expression to evaluate.
    * @param number aFrame [optional]
    *        The frame depth used for evaluation.
    */
-  evaluate: function SF_evaluate(aExpression, aFrame = this.currentFrame) {
+  evaluate: function SF_evaluate(aExpression, aFrame = this.currentFrame || 0) {
     let frame = this.activeThread.cachedFrames[aFrame];
     this.activeThread.eval(frame.actor, aExpression);
   }
 };
 
 /**
  * Keeps the source script list up-to-date, using the thread client's
  * source script cache.
@@ -953,16 +1059,20 @@ SourceScripts.prototype = {
   },
 
   /**
    * Callback for the debugger's active thread getScripts() method.
    */
   _onScriptsAdded: function SS__onScriptsAdded(aResponse) {
     // Add all the sources in the debugger view sources container.
     for (let script of aResponse.scripts) {
+      // Ignore scripts generated from 'clientEvaluate' packets.
+      if (script.url == "debugger eval code") {
+        continue;
+      }
       this._addSource(script);
     }
 
     let container = DebuggerView.Sources;
     let preferredValue = container.preferredValue;
 
     // Flushes all the prepared sources into the sources container.
     container.commit();
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -31,17 +31,17 @@ create({ constructor: StackFramesView, p
     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, true);
+    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
@@ -904,32 +904,274 @@ create({ constructor: BreakpointsView, p
    *        The corresponding breakpoint element node.
    */
   _onDeleteAll: function DVB__onDeleteAll(aTarget) {
     this._onDeleteOthers(aTarget);
     this._onDeleteSelf(aTarget);
   },
 
   /**
-   * Gets an identifier for a breakpoint item for the current cache.
+   * 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,
   _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._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);
+  this._onMouseOver = this._onMouseOver.bind(this);
+  this._onMouseOut = this._onMouseOut.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.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: {
+        expression: "",
+        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.
+   */
+  removeExpression: function DVWE_removeExpression(aIndex) {
+    this.remove(this._cache[aIndex]);
+    this._cache.splice(aIndex, 1);
+  },
+
+  /**
+   * 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.expression;
+  },
+
+  /**
+   * 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.expression 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.addEventListener("mouseover", this._onMouseOver, false);
+    aElementNode.addEventListener("mouseout", this._onMouseOut, false);
+
+    aElementNode.appendChild(arrowNode);
+    aElementNode.appendChild(inputNode);
+    aElementNode.appendChild(closeNode);
+    aElementNode.arrowNode = arrowNode;
+    aElementNode.inputNode = inputNode;
+    aElementNode.closeNode = closeNode;
+  },
+
+  /**
+   * The click listener for this container.
+   */
+  _onClick: function DVWE__onClick(e) {
+    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.removeExpression(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.expression;
+    let newExpression = textbox.value;
+
+    // Remove the watch expression if it's empty.
+    if (!newExpression) {
+      this.removeExpression(this._cache.indexOf(expressionItem));
+    }
+    // Remove the watch expression if it's a duplicate.
+    else if (!oldExpression && this.getExpressions().indexOf(newExpression) != -1) {
+      this.removeExpression(this._cache.indexOf(expressionItem));
+    }
+    // Expression is eligible.
+    else {
+      // Save the watch expression code string.
+      expressionItem.attachment.expression = newExpression;
+      // Make sure the close button is hidden when the textbox is unfocused.
+      expressionItem.target.closeNode.hidden = true;
+    }
+
+    // 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;
+    }
+  },
+
+  /**
+   * The mouse over listener for a watch expression.
+   */
+  _onMouseOver: function DVWE__onMouseOver({ target: element }) {
+    this.getItemForElement(element).target.closeNode.hidden = false;
+  },
+
+  /**
+   * The mouse out listener for a watch expression.
+   */
+  _onMouseOut: function DVWE__onMouseOut({ target: element }) {
+    this.getItemForElement(element).target.closeNode.hidden = true;
+  },
+
+  /**
+   * 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._onFetchSourcesFinished = this._onFetchSourcesFinished.bind(this);
@@ -1773,9 +2015,10 @@ LineResults.size = function DVGS_size() 
   return count;
 };
 
 /**
  * Preliminary setup for the DebuggerView object.
  */
 DebuggerView.StackFrames = new StackFramesView();
 DebuggerView.Breakpoints = new BreakpointsView();
+DebuggerView.WatchExpressions = new WatchExpressionsView();
 DebuggerView.GlobalSearch = new GlobalSearchView();
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -37,16 +37,17 @@ let DebuggerView = {
 
     this.Toolbar.initialize();
     this.Options.initialize();
     this.ChromeGlobals.initialize();
     this.Sources.initialize();
     this.Filtering.initialize();
     this.StackFrames.initialize();
     this.Breakpoints.initialize();
+    this.WatchExpressions.initialize();
     this.GlobalSearch.initialize();
 
     this.Variables = new VariablesView(document.getElementById("variables"));
     this.Variables.searchPlaceholder = L10N.getStr("emptyVariablesFilterText");
     this.Variables.emptyText = L10N.getStr("emptyVariablesText");
     this.Variables.nonEnumVisible = Prefs.variablesNonEnumVisible;
     this.Variables.searchEnabled = Prefs.variablesSearchboxVisible;
     this.Variables.eval = DebuggerController.StackFrames.evaluate;
@@ -66,16 +67,17 @@ let DebuggerView = {
 
     this.Toolbar.destroy();
     this.Options.destroy();
     this.ChromeGlobals.destroy();
     this.Sources.destroy();
     this.Filtering.destroy();
     this.StackFrames.destroy();
     this.Breakpoints.destroy();
+    this.WatchExpressions.destroy();
     this.GlobalSearch.destroy();
 
     this._destroyWindow();
     this._destroyPanes();
     this._destroyEditor();
     aCallback();
   },
 
@@ -117,38 +119,38 @@ let DebuggerView = {
   /**
    * Initializes the UI for all the displayed panes.
    */
   _initializePanes: function DV__initializePanes() {
     dumpn("Initializing the DebuggerView panes");
 
     this._togglePanesButton = document.getElementById("toggle-panes");
     this._stackframesAndBreakpoints = document.getElementById("stackframes+breakpoints");
-    this._variables = document.getElementById("variables");
+    this._variablesAndExpressions = document.getElementById("variables+expressions");
 
     this._stackframesAndBreakpoints.setAttribute("width", Prefs.stackframesWidth);
-    this._variables.setAttribute("width", Prefs.variablesWidth);
+    this._variablesAndExpressions.setAttribute("width", Prefs.variablesWidth);
     this.togglePanes({
       visible: Prefs.panesVisibleOnStartup,
       animated: false
     });
   },
 
   /**
    * Destroys the UI for all the displayed panes.
    */
   _destroyPanes: function DV__initializePanes() {
     dumpn("Destroying the DebuggerView panes");
 
     Prefs.stackframesWidth = this._stackframesAndBreakpoints.getAttribute("width");
-    Prefs.variablesWidth = this._variables.getAttribute("width");
+    Prefs.variablesWidth = this._variablesAndExpressions.getAttribute("width");
 
     this._togglePanesButton = null;
     this._stackframesAndBreakpoints = null;
-    this._variables = null;
+    this._variablesAndExpressions = null;
   },
 
   /**
    * Initializes the SourceEditor instance.
    *
    * @param function aCallback
    *        Called after the editor finishes initializing.
    */
@@ -396,45 +398,45 @@ let DebuggerView = {
     // Avoid useless toggles.
     if (aFlags.visible == !this.panesHidden) {
       aFlags.callback && aFlags.callback();
       return;
     }
 
     if (aFlags.visible) {
       this._stackframesAndBreakpoints.style.marginLeft = "0";
-      this._variables.style.marginRight = "0";
+      this._variablesAndExpressions.style.marginRight = "0";
       this._togglePanesButton.removeAttribute("panesHidden");
       this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("collapsePanes"));
     } else {
       let marginL = ~~(this._stackframesAndBreakpoints.getAttribute("width")) + 1;
-      let marginR = ~~(this._variables.getAttribute("width")) + 1;
+      let marginR = ~~(this._variablesAndExpressions.getAttribute("width")) + 1;
       this._stackframesAndBreakpoints.style.marginLeft = -marginL + "px";
-      this._variables.style.marginRight = -marginR + "px";
+      this._variablesAndExpressions.style.marginRight = -marginR + "px";
       this._togglePanesButton.setAttribute("panesHidden", "true");
       this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("expandPanes"));
     }
 
     if (aFlags.animated) {
       this._stackframesAndBreakpoints.setAttribute("animated", "");
-      this._variables.setAttribute("animated", "");
+      this._variablesAndExpressions.setAttribute("animated", "");
 
       // Displaying the panes may have the effect of triggering scrollbars to
       // appear in the source editor, which would render the currently
       // highlighted line to appear behind them in some cases.
       let self = this;
 
       window.addEventListener("transitionend", function onEvent() {
         window.removeEventListener("transitionend", onEvent, false);
         aFlags.callback && aFlags.callback();
         self.updateEditor();
       }, false);
     } else {
       this._stackframesAndBreakpoints.removeAttribute("animated");
-      this._variables.removeAttribute("animated");
+      this._variablesAndExpressions.removeAttribute("animated");
       aFlags.callback && aFlags.callback();
     }
   },
 
   /**
    * Sets all the panes visible after a short period of time.
    *
    * @param function aCallback
@@ -482,17 +484,17 @@ let DebuggerView = {
   StackFrames: null,
   Breakpoints: null,
   GlobalSearch: null,
   Variables: null,
   _editor: null,
   _editorSource: null,
   _togglePanesButton: null,
   _stackframesAndBreakpoints: null,
-  _variables: null,
+  _variablesAndExpressions: null,
   _isInitialized: false,
   _isDestroyed: false
 };
 
 /**
  * A generic item used to describe elements present in views like the
  * ChromeGlobals, Sources, Stackframes, Breakpoints etc.
  *
@@ -604,33 +606,37 @@ MenuContainer.prototype = {
    * overridden via the "relaxed" flag.
    *
    * @param string aLabel
    *        The label displayed in the container.
    * @param string aValue
    *        The actual internal value of the item.
    * @param object aOptions [optional]
    *        Additional options or flags supported by this operation:
-   *          - forced: true to force the item to be immediately added
-   *          - unsorted: true if the items should not remain sorted
+   *          - forced: true to force the item to be immediately appended
+   *          - unsorted: true if the items should not always remain sorted
    *          - relaxed: true if this container should allow dupes & degenerates
    *          - description: an optional description of the item
    *          - attachment: some attached primitive/object
    * @return MenuItem
    *         The item associated with the displayed element if a forced push,
    *         undefined if the item was staged for a later commit.
    */
   push: function DVMC_push(aLabel, aValue, aOptions = {}) {
     let item = new MenuItem(
       aLabel, aValue, aOptions.description, aOptions.attachment);
 
     // Batch the item to be added later.
     if (!aOptions.forced) {
       this._stagedItems.push(item);
     }
+    // Immediately insert the item at the specified index.
+    else if (aOptions.forced && aOptions.forced.atIndex !== undefined) {
+      return this._insertItemAt(aOptions.forced.atIndex, item, aOptions);
+    }
     // Find the target position in this container and insert the item there.
     else if (!aOptions.unsorted) {
       return this._insertItemAt(this._findExpectedIndex(aLabel), item, aOptions);
     }
     // Just append the item in this container.
     else {
       return this._appendItem(item, aOptions);
     }
@@ -705,16 +711,28 @@ MenuContainer.prototype = {
 
     this._itemsByLabel = new Map();
     this._itemsByValue = new Map();
     this._itemsByElement = new Map();
     this._stagedItems = [];
   },
 
   /**
+   * Toggles all the items in this container hidden or visible.
+   *
+   * @param boolean aVisibleFlag
+   *        Specifies the intended visibility.
+   */
+  toggleContents: function DVMC_toggleContents(aVisibleFlag) {
+    for (let [, item] of this._itemsByElement) {
+      item.target.hidden = !aVisibleFlag;
+    }
+  },
+
+  /**
    * Does not remove any item in this container. Instead, it overrides the
    * current label to signal that it is unavailable and removes the tooltip.
    */
   setUnavailable: function DVMC_setUnavailable() {
     this._container.setAttribute("label", this._unavailableLabel);
     this._container.removeAttribute("tooltiptext");
   },
 
@@ -836,16 +854,28 @@ MenuContainer.prototype = {
   set selectedValue(aValue) {
     let item = this._itemsByValue.get(aValue);
     if (item) {
       this._container.selectedItem = item.target;
     }
   },
 
   /**
+   * Gets the item in the container having the specified index.
+   *
+   * @param number aIndex
+   *        The index used to identify the element.
+   * @return MenuItem
+   *         The matched item, or null if nothing is found.
+   */
+  getItemAtIndex: function DVMC_getItemAtIndex(aIndex) {
+    return this.getItemForElement(this._container.getItemAtIndex(aIndex));
+  },
+
+  /**
    * Gets the item in the container having the specified label.
    *
    * @param string aLabel
    *        The label used to identify the element.
    * @return MenuItem
    *         The matched item, or null if nothing is found.
    */
   getItemByLabel: function DVMC_getItemByLabel(aLabel) {
@@ -904,16 +934,24 @@ MenuContainer.prototype = {
     let values = [];
     for (let [value] of this._itemsByValue) {
       values.push(value);
     }
     return values;
   },
 
   /**
+   * Gets the total items in this container.
+   * @return number
+   */
+  get totalItems() {
+    return this._itemsByElement.size;
+  },
+
+  /**
    * Gets the total visible (non-hidden) items in this container.
    * @return number
    */
   get visibleItems() {
     let count = 0;
     for (let [element] of this._itemsByElement) {
       count += element.hidden ? 0 : 1;
     }
@@ -1092,27 +1130,27 @@ MenuContainer.prototype = {
 };
 
 /**
  * A stacked list of items, compatible with MenuContainer instances, used for
  * displaying views like the StackFrames, Breakpoints etc.
  *
  * Custom methods introduced by this view, not necessary for a MenuContainer:
  * set emptyText(aValue:string)
+ * set permaText(aValue:string)
  * set itemType(aType:string)
  * set itemFactory(aCallback:function)
  *
  * TODO: Use this in #796135 - "Provide some obvious UI for scripts filtering".
  *
  * @param nsIDOMNode aAssociatedNode
  *        The element associated with the displayed container.
  */
 function StackList(aAssociatedNode) {
   this._parent = aAssociatedNode;
-  this._appendEmptyNotice();
 
   // Create an internal list container.
   this._list = document.createElement("vbox");
   this._parent.appendChild(this._list);
 }
 
 StackList.prototype = {
   /**
@@ -1315,24 +1353,37 @@ StackList.prototype = {
    *        True if the event was bubbling.
    */
   removeEventListener:
   function DVSL_removeEventListener(aName, aCallback, aBubbleFlag) {
     this._parent.removeEventListener(aName, aCallback, aBubbleFlag);
   },
 
   /**
+   * Sets the text displayed permanently in this container's header.
+   * @param string aValue
+   */
+  set permaText(aValue) {
+    if (this._permaTextNode) {
+      this._permaTextNode.setAttribute("value", aValue);
+    }
+    this._permaTextValue = aValue;
+    this._appendPermaNotice();
+  },
+
+  /**
    * Sets the text displayed in this container when there are no available items.
    * @param string aValue
    */
   set emptyText(aValue) {
     if (this._emptyTextNode) {
       this._emptyTextNode.setAttribute("value", aValue);
     }
     this._emptyTextValue = aValue;
+    this._appendEmptyNotice();
   },
 
   /**
    * Overrides the item's element type (e.g. "vbox" or "hbox").
    * @param string aType
    */
   itemType: "hbox",
 
@@ -1365,20 +1416,36 @@ StackList.prototype = {
     aElementNode.appendChild(spacer);
     aElementNode.appendChild(valueNode);
 
     aElementNode.labelNode = labelNode;
     aElementNode.valueNode = valueNode;
   },
 
   /**
+   * Creates and appends a label displayed permanently in this container's header.
+   */
+  _appendPermaNotice: function DVSL__appendPermaNotice() {
+    if (this._permaTextNode || !this._permaTextValue) {
+      return;
+    }
+
+    let label = document.createElement("label");
+    label.className = "empty list-item";
+    label.setAttribute("value", this._permaTextValue);
+
+    this._parent.insertBefore(label, this._list);
+    this._permaTextNode = label;
+  },
+
+  /**
    * Creates and appends a label signaling that this container is empty.
    */
   _appendEmptyNotice: function DVSL__appendEmptyNotice() {
-    if (this._emptyTextNode) {
+    if (this._emptyTextNode || !this._emptyTextValue) {
       return;
     }
 
     let label = document.createElement("label");
     label.className = "empty list-item";
     label.setAttribute("value", this._emptyTextValue);
 
     this._parent.appendChild(label);
@@ -1396,16 +1463,18 @@ StackList.prototype = {
     this._parent.removeChild(this._emptyTextNode);
     this._emptyTextNode = null;
   },
 
   _parent: null,
   _list: null,
   _selectedIndex: -1,
   _selectedItem: null,
+  _permaTextNode: null,
+  _permaTextValue: "",
   _emptyTextNode: null,
   _emptyTextValue: ""
 };
 
 /**
  * A simple way of displaying a "Connect to..." prompt.
  */
 function RemoteDebuggerPrompt() {
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -240,14 +240,18 @@
         <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 class="devtools-side-splitter"/>
-        <vbox id="variables"/>
+        <vbox id="variables+expressions">
+          <vbox id="expressions"/>
+          <splitter class="devtools-horizontal-splitter"/>
+          <vbox id="variables" flex="1"/>
+        </vbox>
       </hbox>
     </vbox>
 
   </vbox>
 </window>
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -69,16 +69,18 @@ MOCHITEST_BROWSER_TESTS = \
 	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_bug740825_conditional-breakpoints-01.js \
 	browser_dbg_bug740825_conditional-breakpoints-02.js \
+	browser_dbg_bug727429_watch-expressions-01.js \
+	browser_dbg_bug727429_watch-expressions-02.js \
 	browser_dbg_bug731394_editor-contextmenu.js \
 	browser_dbg_bug786070_hide_nonenums.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 \
@@ -103,13 +105,14 @@ MOCHITEST_BROWSER_PAGES = \
 	browser_dbg_update-editor-mode.html \
 	test-editor-mode \
 	browser_dbg_displayName.html \
 	browser_dbg_iframes.html \
 	browser_dbg_with-frame.html \
 	browser_dbg_pause-exceptions.html \
 	browser_dbg_breakpoint-new-script.html \
 	browser_dbg_conditional-breakpoints.html \
+	browser_dbg_watch-expressions.html \
 	$(NULL)
 
 MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-01.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 727429: test the debugger watch expressions.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_watch-expressions.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gWatch = null;
+
+function test()
+{
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPane = aPane;
+    gDebugger = gPane.contentWindow;
+    gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+    gDebugger.DebuggerView.togglePanes({ visible: true, animated: false });
+
+    executeSoon(function() {
+      performTest();
+    });
+  });
+
+  function performTest()
+  {
+    is(gWatch.getExpressions().length, 0,
+      "There should initially be no watch expressions");
+
+    addAndCheckExpressions(1, 0, "a");
+    addAndCheckExpressions(2, 0, "b");
+    addAndCheckExpressions(3, 0, "c");
+
+    removeAndCheckExpression(2, 1, "a");
+    removeAndCheckExpression(1, 0, "a");
+
+
+    addAndCheckExpressions(2, 0, "", true);
+    gDebugger.editor.focus();
+    is(gWatch.getExpressions().length, 1,
+      "Empty watch expressions are automatically removed");
+
+    addAndCheckExpressions(2, 0, "a", true);
+    gDebugger.editor.focus();
+    is(gWatch.getExpressions().length, 1,
+      "Duplicate watch expressions are automatically removed");
+
+
+    addAndCheckCustomExpression(2, 0, "bazΩΩka");
+    addAndCheckCustomExpression(3, 0, "bambøøcha");
+
+
+    EventUtils.sendMouseEvent({ type: "click" },
+      gWatch.getItemAtIndex(0).target.closeNode,
+      gDebugger);
+
+    is(gWatch.getExpressions().length, 2,
+      "Watch expressions are removed when the close button is pressed");
+    is(gWatch.getExpressions()[0], "bazΩΩka",
+      "The expression at index " + 0 + " should be correct (1)");
+    is(gWatch.getExpressions()[1], "a",
+      "The expression at index " + 1 + " should be correct (2)");
+
+
+    EventUtils.sendMouseEvent({ type: "click" },
+      gWatch.getItemAtIndex(0).target.closeNode,
+      gDebugger);
+
+    is(gWatch.getExpressions().length, 1,
+      "Watch expressions are removed when the close button is pressed");
+    is(gWatch.getExpressions()[0], "a",
+      "The expression at index " + 0 + " should be correct (3)");
+
+
+    EventUtils.sendMouseEvent({ type: "click" },
+      gWatch.getItemAtIndex(0).target.closeNode,
+      gDebugger);
+
+    is(gWatch.getExpressions().length, 0,
+      "Watch expressions are removed when the close button is pressed");
+
+
+    EventUtils.sendMouseEvent({ type: "click" },
+      gWatch._container._parent,
+      gDebugger);
+
+    is(gWatch.getExpressions().length, 1,
+      "Watch expressions are added when the view container is pressed");
+
+
+    closeDebuggerAndFinish();
+  }
+
+  function addAndCheckCustomExpression(total, index, string, noBlur) {
+    addAndCheckExpressions(total, index, "", true);
+
+    for (let i = 0; i < string.length; i++) {
+      EventUtils.sendChar(string[i]);
+    }
+
+    gDebugger.editor.focus();
+
+    let id = gWatch.getItemAtIndex(index).attachment.id;
+    let element = gDebugger.document.getElementById("expression-" + id);
+
+    is(gWatch.getItemAtIndex(index).attachment.initialExpression, "",
+      "The initial expression at index " + index + " should be correct (1)");
+    is(gWatch.getItemForElement(element).attachment.initialExpression, "",
+      "The initial expression at index " + index + " should be correct (2)");
+
+    is(gWatch.getItemAtIndex(index).attachment.expression, string,
+      "The expression at index " + index + " should be correct (1)");
+    is(gWatch.getItemForElement(element).attachment.expression, string,
+      "The expression at index " + index + " should be correct (2)");
+
+    is(gWatch.getExpression(index), string,
+      "The expression at index " + index + " should be correct (3)");
+    is(gWatch.getExpressions()[index], string,
+      "The expression at index " + index + " should be correct (4)");
+  }
+
+  function addAndCheckExpressions(total, index, string, noBlur) {
+    gWatch.addExpression(string);
+
+    is(gWatch.getExpressions().length, total,
+      "There should be " + total + " watch expressions available (1)");
+    is(gWatch.totalItems, total,
+      "There should be " + total + " watch expressions available (2)");
+
+    ok(gWatch.getItemAtIndex(index),
+      "The expression at index " + index + " should be available");
+    ok(gWatch.getItemAtIndex(index).attachment.id,
+      "The expression at index " + index + " should have an id");
+    is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+      "The expression at index " + index + " should have an initial expression");
+
+    let id = gWatch.getItemAtIndex(index).attachment.id;
+    let element = gDebugger.document.getElementById("expression-" + id);
+
+    ok(element,
+      "Three should be a new expression item in the view");
+    ok(gWatch.getItemForElement(element),
+      "The watch expression item should be accessible");
+    is(gWatch.getItemForElement(element), gWatch.getItemAtIndex(index),
+      "The correct watch expression item was accessed");
+
+    ok(gWatch.getItemAtIndex(index) instanceof gDebugger.MenuItem,
+      "The correct watch expression element was accessed (1)");
+    ok(gWatch._container.getItemAtIndex(index) instanceof XULElement,
+      "The correct watch expression element was accessed (2)");
+    is(element, gWatch._container.getItemAtIndex(index),
+      "The correct watch expression element was accessed (3)");
+
+    is(element.arrowNode.hidden, false,
+      "The arrow node should be visible");
+    is(element.closeNode.hidden, false,
+      "The close button should be visible");
+    is(element.inputNode.getAttribute("focused"), "true",
+      "The textbox input should be focused");
+
+    is(gWatch._variables.scrollTop, 0,
+      "The variables view should be scrolled to top");
+
+    is(gWatch._cache[0], gWatch.getItemAtIndex(index),
+      "The correct watch expression was added to the cache (1)");
+    is(gWatch._cache[0], gWatch.getItemForElement(element),
+      "The correct watch expression was added to the cache (2)");
+
+    if (!noBlur) {
+      gDebugger.editor.focus();
+
+      is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+        "The initial expression at index " + index + " should be correct (1)");
+      is(gWatch.getItemForElement(element).attachment.initialExpression, string,
+        "The initial expression at index " + index + " should be correct (2)");
+
+      is(gWatch.getItemAtIndex(index).attachment.expression, string,
+        "The expression at index " + index + " should be correct (1)");
+      is(gWatch.getItemForElement(element).attachment.expression, string,
+        "The expression at index " + index + " should be correct (2)");
+
+      is(gWatch.getExpression(index), string,
+        "The expression at index " + index + " should be correct (3)");
+      is(gWatch.getExpressions()[index], string,
+        "The expression at index " + index + " should be correct (4)");
+    }
+  }
+
+  function removeAndCheckExpression(total, index, string) {
+    gWatch.removeExpression(index);
+
+    is(gWatch.getExpressions().length, total,
+      "There should be " + total + " watch expressions available (1)");
+    is(gWatch.totalItems, total,
+      "There should be " + total + " watch expressions available (2)");
+
+    ok(gWatch.getItemAtIndex(index),
+      "The expression at index " + index + " should still be available");
+    ok(gWatch.getItemAtIndex(index).attachment.id,
+      "The expression at index " + index + " should still have an id");
+    is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+      "The expression at index " + index + " should still have an initial expression");
+
+    let id = gWatch.getItemAtIndex(index).attachment.id;
+    let element = gDebugger.document.getElementById("expression-" + id);
+
+    is(gWatch.getItemAtIndex(index).attachment.initialExpression, string,
+      "The initial expression at index " + index + " should be correct (1)");
+    is(gWatch.getItemForElement(element).attachment.initialExpression, string,
+      "The initial expression at index " + index + " should be correct (2)");
+
+    is(gWatch.getItemAtIndex(index).attachment.expression, string,
+      "The expression at index " + index + " should be correct (1)");
+    is(gWatch.getItemForElement(element).attachment.expression, string,
+      "The expression at index " + index + " should be correct (2)");
+
+    is(gWatch.getExpression(index), string,
+      "The expression at index " + index + " should be correct (3)");
+    is(gWatch.getExpressions()[index], string,
+      "The expression at index " + index + " should be correct (4)");
+  }
+
+  registerCleanupFunction(function() {
+    removeTab(gTab);
+    gPane = null;
+    gTab = null;
+    gDebuggee = null;
+    gDebugger = null;
+    gWatch = null;
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-02.js
@@ -0,0 +1,275 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 727429: test the debugger watch expressions.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_watch-expressions.html";
+
+let gPane = null;
+let gTab = null;
+let gDebuggee = null;
+let gDebugger = null;
+let gWatch = null;
+let gVars = null;
+
+function test()
+{
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPane = aPane;
+    gDebugger = gPane.contentWindow;
+    gWatch = gDebugger.DebuggerView.WatchExpressions;
+    gVars = gDebugger.DebuggerView.Variables;
+
+    gDebugger.DebuggerView.togglePanes({ visible: true, animated: false });
+    addExpressions();
+    performTest();
+  });
+
+  function addExpressions()
+  {
+    gWatch.addExpression("'a'");
+    gWatch.addExpression("\"a\"");
+    gWatch.addExpression("'a\"\"'");
+    gWatch.addExpression("\"a''\"");
+    gWatch.addExpression("?");
+    gWatch.addExpression("a");
+    gWatch.addExpression("[1, 2, 3]");
+    gWatch.addExpression("x = [1, 2, 3]");
+    gWatch.addExpression("y = [1, 2, 3]; y.test = 4");
+    gWatch.addExpression("z = [1, 2, 3]; z.test = 4; z");
+    gWatch.addExpression("t = [1, 2, 3]; t.test = 4; !t");
+    gWatch.addExpression("encodeURI(\"\\\")");
+    gWatch.addExpression("decodeURI(\"\\\")");
+  }
+
+  function performTest()
+  {
+    is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+      "There should be 0 hidden nodes in the watch expressions container");
+    is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 13,
+      "There should be 13 visible nodes in the watch expressions container");
+
+    test1(function() {
+      test2(function() {
+        test3(function() {
+          test4(function() {
+            test5(function() {
+              test6(function() {
+                test7(function() {
+                  test8(function() {
+                    test9(function() {
+                      finishTest();
+                    });
+                  });
+                });
+              });
+            });
+          });
+        });
+      });
+    });
+  }
+
+  function finishTest()
+  {
+    is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
+      "There should be 0 hidden nodes in the watch expressions container");
+    is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 12,
+      "There should be 12 visible nodes in the watch expressions container");
+
+    closeDebuggerAndFinish();
+  }
+
+  function test1(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test1");
+      checkWatchExpressions("ReferenceError: a is not defined");
+      callback();
+    });
+    executeSoon(function() {
+      gDebuggee.ermahgerd(); // ermahgerd!!
+    });
+  }
+
+  function test2(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test2");
+      checkWatchExpressions(undefined);
+      callback();
+    });
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      gDebugger.document.getElementById("resume"),
+      gDebugger);
+  }
+
+  function test3(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test3");
+      checkWatchExpressions({ type: "object", class: "Object" });
+      callback();
+    });
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      gDebugger.document.getElementById("resume"),
+      gDebugger);
+  }
+
+  function test4(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test4");
+      checkWatchExpressions(5, 12);
+      callback();
+    });
+    executeSoon(function() {
+      gWatch.addExpression("a = 5");
+      EventUtils.sendKey("RETURN");
+    });
+  }
+
+  function test5(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test5");
+      checkWatchExpressions(5, 12);
+      callback();
+    });
+    executeSoon(function() {
+      gWatch.addExpression("encodeURI(\"\\\")");
+      EventUtils.sendKey("RETURN");
+    });
+  }
+
+  function test6(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test6");
+      checkWatchExpressions(5, 12);
+      callback();
+    })
+    executeSoon(function() {
+      gWatch.addExpression("decodeURI(\"\\\")");
+      EventUtils.sendKey("RETURN");
+    });
+  }
+
+  function test7(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test7");
+      checkWatchExpressions(5, 12);
+      callback();
+    });
+    executeSoon(function() {
+      gWatch.addExpression("?");
+      EventUtils.sendKey("RETURN");
+    });
+  }
+
+  function test8(callback) {
+    waitForWatchExpressions(function() {
+      info("Performing test8");
+      checkWatchExpressions(5, 12);
+      callback();
+    });
+    executeSoon(function() {
+      gWatch.addExpression("a");
+      EventUtils.sendKey("RETURN");
+    });
+  }
+
+  function test9(callback) {
+    waitForAfterFramesCleared(function() {
+      info("Performing test9");
+      callback();
+    });
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      gDebugger.document.getElementById("resume"),
+      gDebugger);
+  }
+
+  function waitForAfterFramesCleared(callback) {
+    gDebugger.addEventListener("Debugger:AfterFramesCleared", function onClear() {
+      gDebugger.removeEventListener("Debugger:AfterFramesCleared", onClear, false);
+      executeSoon(callback);
+    }, false);
+  }
+
+  function waitForWatchExpressions(callback) {
+    gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
+      gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
+      executeSoon(callback);
+    }, false);
+  }
+
+  function checkWatchExpressions(expected, total = 11) {
+    is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, total,
+      "There should be " + total + " hidden nodes in the watch expressions container");
+    is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
+      "There should be 0 visible nodes in the watch expressions container");
+
+    let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+    let scope = gVars._currHierarchy.get(label);
+
+    ok(scope, "There should be a wach expressions scope in the variables view");
+    is(scope._store.size, total, "There should be " + total + " evaluations availalble");
+
+    let w1 = scope.get("'a'");
+    let w2 = scope.get("\"a\"");
+    let w3 = scope.get("'a\"\"'");
+    let w4 = scope.get("\"a''\"");
+    let w5 = scope.get("?");
+    let w6 = scope.get("a");
+    let w7 = scope.get("x = [1, 2, 3]");
+    let w8 = scope.get("y = [1, 2, 3]; y.test = 4");
+    let w9 = scope.get("z = [1, 2, 3]; z.test = 4; z");
+    let w10 = scope.get("t = [1, 2, 3]; t.test = 4; !t");
+    let w11 = scope.get("encodeURI(\"\\\")");
+    let w12 = scope.get("decodeURI(\"\\\")");
+
+    ok(w1, "The first watch expression should be present in the scope");
+    ok(w2, "The second watch expression should be present in the scope");
+    ok(w3, "The third watch expression should be present in the scope");
+    ok(w4, "The fourth watch expression should be present in the scope");
+    ok(w5, "The fifth watch expression should be present in the scope");
+    ok(w6, "The sixth watch expression should be present in the scope");
+    ok(w7, "The seventh watch expression should be present in the scope");
+    ok(w8, "The eight watch expression should be present in the scope");
+    ok(w9, "The ninth watch expression should be present in the scope");
+    ok(w10, "The tenth watch expression should be present in the scope");
+    ok(!w11, "The eleventh watch expression should not be present in the scope");
+    ok(!w12, "The twelveth watch expression should not be present in the scope");
+
+    is(w1.value, "a", "The first value is correct");
+    is(w2.value, "a", "The second value is correct");
+    is(w3.value, "a\"\"", "The third value is correct");
+    is(w4.value, "a''", "The fourth value is correct");
+    is(w5.value, "SyntaxError: syntax error", "The fifth value is correct");
+
+    if (typeof expected == "object") {
+      is(w6.value.type, expected.type, "The sixth value type is correct");
+      is(w6.value.class, expected.class, "The sixth value class is correct");
+    } else {
+      is(w6.value, expected, "The sixth value is correct");
+    }
+
+    is(w7.value.type, "object", "The seventh value type is correct");
+    is(w7.value.class, "Array", "The seventh value class is correct");
+
+    is(w8.value, "4", "The eight value is correct");
+
+    is(w9.value.type, "object", "The ninth value type is correct");
+    is(w9.value.class, "Array", "The ninth value class is correct");
+
+    is(w10.value, false, "The tenth value is correct");
+  }
+
+  registerCleanupFunction(function() {
+    removeTab(gTab);
+    gPane = null;
+    gTab = null;
+    gDebuggee = null;
+    gDebugger = null;
+    gWatch = null;
+    gVars = null;
+  });
+}
--- a/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
@@ -221,20 +221,20 @@ function test()
                         });
                       }, {
                         conditionalExpression: "a"
                       });
                     }, {
                       conditionalExpression: "(function() { return false; })()"
                     });
                   }, {
-                    conditionalExpression: "function() {}"
+                    conditionalExpression: "(function() {})"
                   });
                 }, {
-                  conditionalExpression: "{}"
+                  conditionalExpression: "({})"
                 });
               }, {
                 conditionalExpression: "/regexp/"
               });
             }, {
               conditionalExpression: "'nasu'"
             });
           }, {
--- a/browser/devtools/debugger/test/browser_dbg_pane-collapse.js
+++ b/browser/devtools/debugger/test/browser_dbg_pane-collapse.js
@@ -86,70 +86,70 @@ function testPaneCollapse1() {
     "The stackframes and breakpoints pane has an incorrect left margin after uncollapsing.");
   ok(!stackframesAndBrekpoints.hasAttribute("animated"),
     "The stackframes and breakpoints pane has an incorrect attribute after an unanimated uncollapsing.");
   ok(!togglePanesButton.getAttribute("panesHidden"),
     "The stackframes and breakpoints pane should be visible again after uncollapsing.");
 }
 
 function testPaneCollapse2() {
-  let variables =
-    gDebugger.document.getElementById("variables");
+  let variablesAndExpressions =
+    gDebugger.document.getElementById("variables+expressions");
   let togglePanesButton =
     gDebugger.document.getElementById("toggle-panes");
 
-  let width = parseInt(variables.getAttribute("width"));
+  let width = parseInt(variablesAndExpressions.getAttribute("width"));
   is(width, gDebugger.Prefs.variablesWidth,
-    "The variables pane has an incorrect width.");
-  is(variables.style.marginRight, "0px",
-    "The variables pane has an incorrect right margin.");
-  ok(!variables.hasAttribute("animated"),
-    "The variables pane has an incorrect animated attribute.");
+    "The variables and expressions pane has an incorrect width.");
+  is(variablesAndExpressions.style.marginRight, "0px",
+    "The variables and expressions pane has an incorrect right margin.");
+  ok(!variablesAndExpressions.hasAttribute("animated"),
+    "The variables and expressions pane has an incorrect animated attribute.");
   ok(!togglePanesButton.getAttribute("panesHidden"),
-    "The variables pane should at this point be visible.");
+    "The variables and expressions pane should at this point be visible.");
 
   gView.togglePanes({ visible: false, animated: true });
 
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
   isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
 
   let margin = -(width + 1) + "px";
   is(width, gDebugger.Prefs.variablesWidth,
-    "The variables pane has an incorrect width after collapsing.");
-  is(variables.style.marginRight, margin,
-    "The variables pane has an incorrect right margin after collapsing.");
-  ok(variables.hasAttribute("animated"),
-    "The variables pane has an incorrect attribute after an animated collapsing.");
+    "The variables and expressions pane has an incorrect width after collapsing.");
+  is(variablesAndExpressions.style.marginRight, margin,
+    "The variables and expressions pane has an incorrect right margin after collapsing.");
+  ok(variablesAndExpressions.hasAttribute("animated"),
+    "The variables and expressions pane has an incorrect attribute after an animated collapsing.");
   ok(togglePanesButton.hasAttribute("panesHidden"),
-    "The variables pane should not be visible after collapsing.");
+    "The variables and expressions pane should not be visible after collapsing.");
 
   gView.togglePanes({ visible: true, animated: false });
 
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
   isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
 
   is(width, gDebugger.Prefs.variablesWidth,
-    "The variables pane has an incorrect width after uncollapsing.");
-  is(variables.style.marginRight, "0px",
-    "The variables pane has an incorrect right margin after uncollapsing.");
-  ok(!variables.hasAttribute("animated"),
-    "The variables pane has an incorrect attribute after an unanimated uncollapsing.");
+    "The variables and expressions pane has an incorrect width after uncollapsing.");
+  is(variablesAndExpressions.style.marginRight, "0px",
+    "The variables and expressions pane has an incorrect right margin after uncollapsing.");
+  ok(!variablesAndExpressions.hasAttribute("animated"),
+    "The variables and expressions pane has an incorrect attribute after an unanimated uncollapsing.");
   ok(!togglePanesButton.getAttribute("panesHidden"),
-    "The variables pane should be visible again after uncollapsing.");
+    "The variables and expressions pane should be visible again after uncollapsing.");
 }
 
 function testPanesStartupPref(aCallback) {
   let stackframesAndBrekpoints =
     gDebugger.document.getElementById("stackframes+breakpoints");
-  let variables =
-    gDebugger.document.getElementById("variables");
+  let variablesAndExpressions =
+    gDebugger.document.getElementById("variables+expressions");
   let togglePanesButton =
     gDebugger.document.getElementById("toggle-panes");
 
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
 
   ok(!togglePanesButton.getAttribute("panesHidden"),
     "The debugger panes should at this point be visible.");
--- a/browser/devtools/debugger/test/browser_dbg_panesize-inner.js
+++ b/browser/devtools/debugger/test/browser_dbg_panesize-inner.js
@@ -38,17 +38,17 @@ function test() {
 
     wait_for_connect_and_resume(function() {
       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+breakpoints");
-      variables = content.document.getElementById("variables");
+      variables = content.document.getElementById("variables+expressions");
 
       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);
       variables.setAttribute("width", someWidth2);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_watch-expressions.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset='utf-8'/>
+    <title>Browser Debugger Watch Expressions Test</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+    <script type="text/javascript">
+      function ermahgerd() {
+        debugger;
+        (function() {
+          var a = undefined;
+          debugger;
+          var a = {};
+          debugger;
+        }());
+      }
+    </script>
+  </head>
+  <body>
+  </body>
+</html>
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -441,17 +441,25 @@ ResponsiveUI.prototype = {
    */
   addPreset: function RUI_addPreset() {
     let w = this.customPreset.width;
     let h = this.customPreset.height;
     let newName = {};
 
     let title = this.strings.GetStringFromName("responsiveUI.customNamePromptTitle");
     let message = this.strings.formatStringFromName("responsiveUI.customNamePromptMsg", [w, h], 2);
-    Services.prompt.prompt(null, title, message, newName, null, {});
+    let promptOk = Services.prompt.prompt(null, title, message, newName, null, {});
+
+    if (!promptOk) {
+      // Prompt has been cancelled
+      let menuitem = this.customMenuitem;
+      this.menulist.selectedItem = menuitem;
+      this.currentPresetKey = this.customPreset.key;
+      return;
+    }
 
     let newPreset = {
       key: w + "x" + h,
       name: newName.value,
       width: w,
       height: h
     };
 
--- a/browser/devtools/responsivedesign/test/browser_responsiveuiaddcustompreset.js
+++ b/browser/devtools/responsivedesign/test/browser_responsiveuiaddcustompreset.js
@@ -13,18 +13,21 @@ function test() {
   }, true);
 
   content.location = "data:text/html,foo";
 
   function startTest() {
     // Mocking prompt
     oldPrompt = Services.prompt;
     Services.prompt = {
+      value: "",
+      returnBool: true,
       prompt: function(aParent, aDialogTitle, aText, aValue, aCheckMsg, aCheckState) {
-        aValue.value = "Testing preset";
+        aValue.value = this.value;
+        return this.returnBool;
       }
     };
 
     document.getElementById("Tools:ResponsiveUI").removeAttribute("disabled");
     synthesizeKeyFromKeyTag("key_responsiveUI");
     executeSoon(onUIOpen);
   }
 
@@ -37,20 +40,33 @@ function test() {
     ok(instance, "instance of the module is attached to the tab.");
 
     instance.transitionsEnabled = false;
 
     testAddCustomPreset();
   }
 
   function testAddCustomPreset() {
+    // Tries to add a custom preset and cancel the prompt
+    let idx = instance.menulist.selectedIndex;
+    let presetCount = instance.presets.length;
+
+    Services.prompt.value = "";
+    Services.prompt.returnBool = false;
+    instance.addbutton.doCommand();
+
+    is(idx, instance.menulist.selectedIndex, "selected item didn't change after add preset and cancel");
+    is(presetCount, instance.presets.length, "number of presets didn't change after add preset and cancel");
+
     let customHeight = 123, customWidth = 456;
     instance.setSize(customWidth, customHeight);
 
-    // Adds the custom preset with "Testing preset" as label (look at mock upper)
+    // Adds the custom preset with "Testing preset"
+    Services.prompt.value = "Testing preset";
+    Services.prompt.returnBool = true;
     instance.addbutton.doCommand();
 
     instance.menulist.selectedIndex = 1;
 
     EventUtils.synthesizeKey("VK_ESCAPE", {});
     executeSoon(restart);
   }
 
--- a/browser/devtools/shared/VariablesView.jsm
+++ b/browser/devtools/shared/VariablesView.jsm
@@ -1,18 +1,20 @@
 /* -*- 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 DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const LAZY_EMPTY_DELAY = 150; // ms
 
 Components.utils.import('resource://gre/modules/Services.jsm');
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 this.EXPORTED_SYMBOLS = ["VariablesView", "create"];
 
 /**
  * A tree view for inspecting scopes, objects and properties.
  * Iterable via "for (let [id, scope] in instance) { }".
  * Requires the devtools common.css and debugger.css skin stylesheets.
  *
@@ -135,17 +137,17 @@ VariablesView.prototype = {
   /**
    * Specifies if enumerable properties and variables should be displayed.
    * @param boolean aFlag
    */
   set enumVisible(aFlag) {
     this._enumVisible = aFlag;
 
     for (let [, scope] in this) {
-      scope._nonEnumVisible = aFlag;
+      scope._enumVisible = aFlag;
     }
   },
 
   /**
    * Specifies if non-enumerable properties and variables should be displayed.
    * @param boolean aFlag
    */
   set nonEnumVisible(aFlag) {
@@ -1114,28 +1116,31 @@ create({ constructor: Variable, proto: S
    * @param object aDescriptor
    *        The property's descriptor.
    */
   _displayVariable: function V__displayVariable(aDescriptor) {
     let document = this.document;
 
     let separatorLabel = this._separatorLabel = document.createElement("label");
     separatorLabel.className = "plain";
-    separatorLabel.setAttribute("value", ":");
+    separatorLabel.setAttribute("value", this.ownerView.separator);
 
     let valueLabel = this._valueLabel = document.createElement("label");
     valueLabel.className = "value plain";
 
     this._title.appendChild(separatorLabel);
     this._title.appendChild(valueLabel);
 
-    if (VariablesView.isPrimitive(aDescriptor)) {
+    let isPrimitive = VariablesView.isPrimitive(aDescriptor);
+    let isUndefined = VariablesView.isUndefined(aDescriptor);
+
+    if (isPrimitive || isUndefined) {
       this.hideArrow();
     }
-    if (aDescriptor.get || aDescriptor.set) {
+    if (!isUndefined && (aDescriptor.get || aDescriptor.set)) {
       this.addProperty("get", { value: aDescriptor.get });
       this.addProperty("set", { value: aDescriptor.set });
       this.expand();
       separatorLabel.hidden = true;
       valueLabel.hidden = true;
     }
   },
 
@@ -1489,16 +1494,43 @@ VariablesView.isPrimitive = function VV_
   if (type == "undefined" || type == "null") {
     return true;
   }
 
   return false;
 };
 
 /**
+ * Returns true if the descriptor represents an undefined value.
+ *
+ * @param object aDescriptor
+ *        The variable's descriptor.
+ */
+VariablesView.isUndefined = function VV_isUndefined(aDescriptor) {
+  // For accessor property descriptors, the getter and setter need to be
+  // contained in 'get' and 'set' properties.
+  let getter = aDescriptor.get;
+  let setter = aDescriptor.set;
+  if (typeof getter == "object" && getter.type == "undefined" &&
+      typeof setter == "object" && setter.type == "undefined") {
+    return true;
+  }
+
+  // As described in the remote debugger protocol, the value grip
+  // must be contained in a 'value' property.
+  // For convenience, undefined is considered a type.
+  let grip = aDescriptor.value;
+  if (grip && grip.type == "undefined") {
+    return true;
+  }
+
+  return false;
+};
+
+/**
  * Returns true if the descriptor represents a falsy value.
  *
  * @param object aDescriptor
  *        The variable's descriptor.
  */
 VariablesView.isFalsy = function VV_isFalsy(aDescriptor) {
   if (!aDescriptor || typeof aDescriptor != "object") {
     return true;
@@ -1605,29 +1637,53 @@ VariablesView.getClass = function VV_get
       case "number":
         return "token-number";
     }
   }
   return "token-other";
 };
 
 /**
+ * Localization convenience methods.
+ */
+let L10N = {
+  /**
+   * L10N shortcut function.
+   *
+   * @param string aName
+   * @return string
+   */
+  getStr: function L10N_getStr(aName) {
+    return this.stringBundle.GetStringFromName(aName);
+  }
+};
+
+XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() {
+  return Services.strings.createBundle(DBG_STRINGS_URI);
+});
+
+/**
+ * The separator label between the variables or properties name and value.
+ */
+Scope.prototype.separator = L10N.getStr("variablesSeparatorLabel");
+
+/**
  * A monotonically-increasing counter, that guarantees the uniqueness of scope,
  * variables and properties ids.
  *
  * @param string aName
  *        An optional string to prefix the id with.
  * @return number
  *         A unique id.
  */
 let generateId = (function() {
   let count = 0;
-  return function(aName = "") {
+  return function VV_generateId(aName = "") {
     return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
-  }
+  };
 })();
 
 /**
  * Sugar for prototypal inheritance using Object.create.
  * Creates a new object with the specified prototype object and properties.
  *
  * @param object target
  * @param object properties
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -54,19 +54,19 @@ this.EXPORTED_SYMBOLS = ["CssRuleView",
  *     property declaration.
  *   Changes to the TextProperty are sent to its related Rule for
  *     application.
  */
 
 /**
  * ElementStyle maintains a list of Rule objects for a given element.
  *
- * @param Element aElement
+ * @param {Element} aElement
  *        The element whose style we are viewing.
- * @param object aStore
+ * @param {object} aStore
  *        The ElementStyle can use this object to store metadata
  *        that might outlast the rule view, particularly the current
  *        set of disabled properties.
  *
  * @constructor
  */
 function ElementStyle(aElement, aStore)
 {
@@ -184,17 +184,17 @@ ElementStyle.prototype = {
 
   /**
    * Add a rule if it's one we care about.  Filters out duplicates and
    * inherited styles with no inherited properties.
    *
    * @param {object} aOptions
    *        Options for creating the Rule, see the Rule constructor.
    *
-   * @return true if we added the rule.
+   * @return {bool} true if we added the rule.
    */
   _maybeAddRule: function ElementStyle_maybeAddRule(aOptions)
   {
     // If we've already included this domRule (for example, when a
     // common selector is inherited), ignore it.
     if (aOptions.domRule &&
         this.rules.some(function(rule) rule.domRule === aOptions.domRule)) {
       return false;
@@ -300,17 +300,17 @@ ElementStyle.prototype = {
   /**
    * Mark a given TextProperty as overridden or not depending on the
    * state of its computed properties.  Clears the _overriddenDirty state
    * on all computed properties.
    *
    * @param {TextProperty} aProp
    *        The text property to update.
    *
-   * @return True if the TextProperty's overridden state (or any of its
+   * @return {bool} true if the TextProperty's overridden state (or any of its
    *         computed properties overridden state) changed.
    */
   _updatePropertyOverridden: function ElementStyle_updatePropertyOverridden(aProp)
   {
     let overridden = true;
     let dirty = false;
     for each (let computedProp in aProp.computed) {
       if (!computedProp.overridden) {
@@ -415,19 +415,18 @@ Rule.prototype = {
     }
     return this.elementStyle.domUtils.getRuleLine(this.domRule);
   },
 
   /**
    * Returns true if the rule matches the creation options
    * specified.
    *
-   * @param object aOptions
-   *        Creation options.  See the Rule constructor for
-   *        documentation.
+   * @param {object} aOptions
+   *        Creation options.  See the Rule constructor for documentation.
    */
   matches: function Rule_matches(aOptions)
   {
     return (this.style === (aOptions.style || aOptions.domRule.style));
   },
 
   /**
    * Create a new TextProperty to include in the rule.
@@ -668,22 +667,22 @@ Rule.prototype = {
    *   Name, value, and priority match, disabled. (5)
    *   Name and value match, enabled. (4)
    *   Name and value match, disabled. (3)
    *   Name matches, enabled. (2)
    *   Name matches, disabled. (1)
    *
    * If no existing properties match the property, nothing happens.
    *
-   * @param TextProperty aNewProp
+   * @param {TextProperty} aNewProp
    *        The current version of the property, as parsed from the
    *        cssText in Rule._getTextProperties().
    *
-   * @returns true if a property was updated, false if no properties
-   *          were updated.
+   * @return {bool} true if a property was updated, false if no properties
+   *         were updated.
    */
   _updateTextProperty: function Rule__updateTextProperty(aNewProp) {
     let match = { rank: 0, prop: null };
 
     for each (let prop in this.textProps) {
       if (prop.name != aNewProp.name)
         continue;
 
@@ -796,17 +795,17 @@ TextProperty.prototype = {
       });
     }
   },
 
   /**
    * Set all the values from another TextProperty instance into
    * this TextProperty instance.
    *
-   * @param TextProperty aOther
+   * @param {TextProperty} aOther
    *        The other TextProperty instance.
    */
   set: function TextProperty_set(aOther)
   {
     let changed = false;
     for (let item of ["name", "value", "priority", "enabled"]) {
       if (this[item] != aOther[item]) {
         this[item] = aOther[item];
@@ -861,19 +860,19 @@ TextProperty.prototype = {
  *   Can mark a property disabled or enabled.
  */
 
 /**
  * CssRuleView is a view of the style rules and declarations that
  * apply to a given element.  After construction, the 'element'
  * property will be available with the user interface.
  *
- * @param Document aDoc
+ * @param {Document} aDoc
  *        The document that will contain the rule view.
- * @param object aStore
+ * @param {object} aStore
  *        The CSS rule view can use this object to store metadata
  *        that might outlast the rule view, particularly the current
  *        set of disabled properties.
  * @constructor
  */
 this.CssRuleView = function CssRuleView(aDoc, aStore)
 {
   this.doc = aDoc;
@@ -890,17 +889,17 @@ this.CssRuleView = function CssRuleView(
   this._showEmpty();
 }
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
   _viewedElement: null,
 
   /**
-   * Returns true if the rule view currently has an input editor visible.
+   * Return {bool} true if the rule view currently has an input editor visible.
    */
   get isEditing() {
     return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0;
   },
 
   destroy: function CssRuleView_destroy()
   {
     this.clear();
@@ -1115,17 +1114,18 @@ CssRuleView.prototype = {
 
     this._contextMenu = menu;
   },
 
   /**
    * Update the rule view's context menu by disabling irrelevant menuitems and
    * enabling relevant ones.
    *
-   * @param aEvent The event object
+   * @param {Event} aEvent
+   *        The event object.
    */
   _onMenuUpdate: function CssRuleView_onMenuUpdate(aEvent)
   {
     let node = this.doc.popupNode;
 
     // Copy selection.
     let editorSelection = node.className == "styleinspector-propertyeditor" &&
                           node.selectionEnd - node.selectionStart != 0;
@@ -1154,17 +1154,18 @@ CssRuleView.prototype = {
     this._declarationItem.disabled = disablePropertyItems;
     this._propertyItem.disabled = disablePropertyItems;
     this._propertyValueItem.disabled = disablePropertyItems;
   },
 
   /**
    * Copy selected text from the rule view.
    *
-   * @param aEvent The event object
+   * @param {Event} aEvent
+   *        The event object.
    */
   _onCopy: function CssRuleView_onCopy(aEvent)
   {
     let target = this.doc.popupNode || aEvent.target;
     let text;
 
     if (target.nodeName == "input") {
       let start = Math.min(target.selectionStart, target.selectionEnd);
@@ -1189,17 +1190,18 @@ CssRuleView.prototype = {
     if (aEvent) {
       aEvent.preventDefault();
     }
   },
 
   /**
    * Copy a rule from the rule view.
    *
-   * @param aEvent The event object
+   * @param {Event} aEvent
+   *        The event object.
    */
   _onCopyRule: function CssRuleView_onCopyRule(aEvent)
   {
     let terminator;
     let node = this.doc.popupNode;
     if (!node) {
       return;
     }
@@ -1242,17 +1244,18 @@ CssRuleView.prototype = {
     out += "}" + terminator;
 
     clipboardHelper.copyString(out, this.doc);
   },
 
   /**
    * Copy a declaration from the rule view.
    *
-   * @param aEvent The event object
+   * @param {Event} aEvent
+   *        The event object.
    */
   _onCopyDeclaration: function CssRuleView_onCopyDeclaration(aEvent)
   {
     let node = this.doc.popupNode;
     if (!node) {
       return;
     }
 
@@ -1281,17 +1284,18 @@ CssRuleView.prototype = {
     let out = propertyName + ": " + propertyValue + ";";
 
     clipboardHelper.copyString(out, this.doc);
   },
 
   /**
    * Copy a property name from the rule view.
    *
-   * @param aEvent The event object
+   * @param {Event} aEvent
+   *        The event object.
    */
   _onCopyProperty: function CssRuleView_onCopyProperty(aEvent)
   {
     let node = this.doc.popupNode;
 
     if (!node) {
       return;
     }
@@ -1303,17 +1307,18 @@ CssRuleView.prototype = {
     if (node) {
       clipboardHelper.copyString(node.textContent, this.doc);
     }
   },
 
  /**
    * Copy a property value from the rule view.
    *
-   * @param aEvent The event object
+   * @param {Event} aEvent
+   *        The event object.
    */
   _onCopyPropertyValue: function CssRuleView_onCopyPropertyValue(aEvent)
   {
     let node = this.doc.popupNode;
     if (!node) {
       return;
     }
 
@@ -1325,19 +1330,19 @@ CssRuleView.prototype = {
       clipboardHelper.copyString(node.textContent, this.doc);
     }
   }
 };
 
 /**
  * Create a RuleEditor.
  *
- * @param CssRuleView aRuleView
+ * @param {CssRuleView} aRuleView
  *        The CssRuleView containg the document holding this rule editor.
- * @param Rule aRule
+ * @param {Rule} aRule
  *        The Rule object we're editing.
  * @constructor
  */
 function RuleEditor(aRuleView, aRule)
 {
   this.ruleView = aRuleView;
   this.doc = this.ruleView.doc;
   this.rule = aRule;
@@ -1446,17 +1451,18 @@ RuleEditor.prototype = {
       for (let i = 0; i < selectors.length; i++) {
         let selector = selectors[i];
         if (i != 0) {
           createChild(this.selectorText, "span", {
             class: "ruleview-selector-separator",
             textContent: ", "
           });
         }
-        let cls = element.mozMatchesSelector(selector) ? "ruleview-selector-matched" : "ruleview-selector-unmatched";
+        let cls = element.mozMatchesSelector(selector) ? "ruleview-selector-matched" :
+                                                         "ruleview-selector-unmatched";
         createChild(this.selectorText, "span", {
           class: cls,
           textContent: selector
         });
       }
     } else {
       this.selectorText.textContent = this.rule.selectorText;
     }
@@ -1467,21 +1473,21 @@ RuleEditor.prototype = {
         this.propertyList.appendChild(prop.editor.element);
       }
     }
   },
 
   /**
    * Programatically add a new property to the rule.
    *
-   * @param string aName
+   * @param {string} aName
    *        Property name.
-   * @param string aValue
+   * @param {string} aValue
    *        Property value.
-   * @param string aPriority
+   * @param {string} aPriority
    *        Property priority.
    */
   addProperty: function RuleEditor_addProperty(aName, aValue, aPriority)
   {
     let prop = this.rule.createProperty(aName, aValue, aPriority);
     let editor = new TextPropertyEditor(this, prop);
     this.propertyList.appendChild(editor.element);
   },
@@ -1519,19 +1525,19 @@ RuleEditor.prototype = {
       advanceChars: ":"
     });
   },
 
   /**
    * Called when the new property input has been dismissed.
    * Will create a new TextProperty if necessary.
    *
-   * @param string aValue
+   * @param {string} aValue
    *        The value in the editor.
-   * @param bool aCommit
+   * @param {bool} aCommit
    *        True if the value should be committed.
    */
   _onNewProperty: function RuleEditor__onNewProperty(aValue, aCommit)
   {
     if (!aValue || !aCommit) {
       return;
     }
 
@@ -1831,34 +1837,34 @@ TextPropertyEditor.prototype = {
   },
 
   /**
    * Pull priority (!important) out of the value provided by a
    * value editor.
    *
    * @param {string} aValue
    *        The value from the text editor.
-   * @return an object with 'value' and 'priority' properties.
+   * @return {object} an object with 'value' and 'priority' properties.
    */
   _parseValue: function TextPropertyEditor_parseValue(aValue)
   {
     let pieces = aValue.split("!", 2);
     return {
       value: pieces[0].trim(),
       priority: (pieces.length > 1 ? pieces[1].trim() : "")
     };
   },
 
   /**
    * Called when a value editor closes.  If the user pressed escape,
    * revert to the value this property had before editing.
    *
    * @param {string} aValue
    *        The value contained in the editor.
-   * @param {boolean} aCommit
+   * @param {bool} aCommit
    *        True if the change should be applied.
    */
    _onValueDone: function PropertyEditor_onValueDone(aValue, aCommit)
   {
     if (aCommit) {
       let val = this._parseValue(aValue);
       this.prop.setValue(val.value, val.priority);
       this.committed.value = this.prop.value;
@@ -1866,22 +1872,21 @@ TextPropertyEditor.prototype = {
     } else {
       this.prop.setValue(this.committed.value, this.committed.priority);
     }
   },
 
   /**
    * Validate this property.
    *
-   * @param {String} [aValue]
+   * @param {string} [aValue]
    *        Override the actual property value used for validation without
    *        applying property values e.g. validate as you type.
    *
-   * @returns {Boolean}
-   *          True if the property value is valid, false otherwise.
+   * @return {bool} true if the property value is valid, false otherwise.
    */
   _validate: function TextPropertyEditor_validate(aValue)
   {
     let name = this.prop.name;
     let value = typeof aValue == "undefined" ? this.prop.value : aValue;
     let val = this._parseValue(value);
     let style = this.doc.createElementNS(HTML_NS, "div").style;
     let prefs = Services.prefs;
@@ -1940,22 +1945,22 @@ function editableField(aOptions)
   });
 }
 
 /**
  * Handle events for an element that should respond to
  * clicks and sit in the editing tab order, and call
  * a callback when it is activated.
  *
- * @param object aOptions
+ * @param {object} aOptions
  *    The options for this editor, including:
  *    {Element} element: The DOM element.
  *    {string} trigger: The DOM event that should trigger editing,
  *      defaults to "click"
- * @param function aCallback
+ * @param {function} aCallback
  *        Called when the editor is activated.
  */
 this.editableItem = function editableItem(aOptions, aCallback)
 {
   let trigger = aOptions.trigger || "click"
   let element = aOptions.element;
   element.addEventListener(trigger, function(evt) {
     let win = this.ownerDocument.defaultView;
@@ -2154,48 +2159,432 @@ InplaceEditor.prototype = {
       width += 15;
       this._measurement.textContent += "M";
       this.input.style.height = this._measurement.offsetHeight + "px";
     }
 
     this.input.style.width = width + "px";
   },
 
+   /**
+   * Increment property values in rule view.
+   *
+   * @param {number} increment 
+   *        The amount to increase/decrease the property value.
+   * @return {bool} true if value has been incremented.
+   */
+  _incrementValue: function InplaceEditor_incrementValue(increment)
+  {
+    let value = this.input.value;
+    let selectionStart = this.input.selectionStart;
+    let selectionEnd = this.input.selectionEnd;
+
+    let newValue = this._incrementCSSValue(value, increment, selectionStart, selectionEnd);
+
+    if (!newValue) {
+      return false;
+    }
+
+    this.input.value = newValue.value;
+    this.input.setSelectionRange(newValue.start, newValue.end);
+
+    return true;
+  },
+
+  /**
+   * Increment the property value based on the property type.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} increment
+   *        Amount to increase/decrease the property value.
+   * @param {number} selStart
+   *        Starting index of the value.
+   * @param {number} selEnd
+   *        Ending index of the value.
+   * @return {object} object with properties 'value', 'start', and 'end'.
+   */
+  _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment, selStart, 
+                                                               selEnd)
+  {
+    let range = this._parseCSSValue(value, selStart);
+    let type = (range && range.type) || "";
+    let rawValue = (range ? value.substring(range.start, range.end) : "");
+    let incrementedValue = null, selection;
+
+    if (type === "num") {
+      let newValue = this._incrementRawValue(rawValue, increment);
+      if (newValue !== null) {
+        incrementedValue = newValue;
+        selection = [0, incrementedValue.length];
+      }
+    } else if (type === "hex") {
+      let exprOffset = selStart - range.start;
+      let exprOffsetEnd = selEnd - range.start;
+      let newValue = this._incHexColor(rawValue, increment, exprOffset, exprOffsetEnd);
+      if (newValue) {
+        incrementedValue = newValue.value;
+        selection = newValue.selection;
+      }
+    } else {
+      let info;
+      if (type === "rgb" || type === "hsl") {
+        info = {};
+        let part = value.substring(range.start, selStart).split(",").length - 1;
+        if (part === 3) { // alpha
+          info.minValue = 0;
+          info.maxValue = 1;
+        } else if (type === "rgb") {
+          info.minValue = 0;
+          info.maxValue = 255;
+        } else if (part !== 0) { // hsl percentage
+          info.minValue = 0;
+          info.maxValue = 100;
+
+          // select the previous number if the selection is at the end of a percentage sign
+          if (value.charAt(selStart - 1) === "%") {
+            --selStart;
+          }
+        }
+      }
+      return this._incrementGenericValue(value, increment, selStart, selEnd, info);
+    }
+
+    if (incrementedValue === null) {
+      return;
+    }
+
+    let preRawValue = value.substr(0, range.start);
+    let postRawValue = value.substr(range.end);
+
+    return {
+      value: preRawValue + incrementedValue + postRawValue,
+      start: range.start + selection[0],
+      end: range.start + selection[1]
+    };
+  },
+
+  /**
+   * Parses the property value and type.
+   *
+   * @param {string} value 
+   *        Property value.
+   * @param {number} offset 
+   *        Starting index of value.
+   * @return {object} object with properties 'value', 'start', 'end', and 'type'.
+   */
+   _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
+  {
+    const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
+    let start = 0;
+    let m;
+
+    // retreive values from left to right until we find the one at our offset
+    while ((m = reSplitCSS.exec(value)) &&
+          (m.index + m[0].length < offset)) {
+      value = value.substr(m.index + m[0].length);
+      start += m.index + m[0].length;
+      offset -= m.index + m[0].length;
+    }
+
+    if (!m) {
+      return;
+    }
+
+    let type;
+    if (m[1]) {
+      type = "url";
+    } else if (m[2]) {
+      type = "rgb";
+    } else if (m[3]) {
+      type = "hsl";
+    } else if (m[4]) {
+      type = "hex";
+    } else if (m[5]) {
+      type = "num";
+    }
+
+    return {
+      value: m[0],
+      start: start + m.index,
+      end: start + m.index + m[0].length,
+      type: type
+    };
+  },
+
+  /**
+   * Increment the property value for types other than
+   * number or hex, such as rgb, hsl, and file names.
+   *
+   * @param {string} value 
+   *        Property value.
+   * @param {number} increment 
+   *        Amount to increment/decrement.
+   * @param {number} offset 
+   *        Starting index of the property value.
+   * @param {number} offsetEnd 
+   *        Ending index of the property value.
+   * @param {object} info 
+   *        Object with details about the property value.
+   * @return {object} object with properties 'value', 'start', and 'end'.
+   */
+  _incrementGenericValue: function InplaceEditor_incrementGenericValue(value, increment, offset,
+                                                                       offsetEnd, info)
+  {
+    // Try to find a number around the cursor to increment.
+    let start, end;
+    // Check if we are incrementing in a non-number context (such as a URL)
+    if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
+      !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
+      // We have a number selected, possibly with a suffix, and we are not in
+      // the disallowed case of just part of a known number being selected.
+      // Use that number.
+      start = offset;
+      end = offsetEnd;
+    } else {
+      // Parse periods as belonging to the number only if we are in a known number
+      // context. (This makes incrementing the 1 in 'image1.gif' work.)
+      let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
+      let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
+      let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
+
+      start = offset - before;
+      end = offset + after;
+
+      // Expand the number to contain an initial minus sign if it seems
+      // free-standing.
+      if (value.charAt(start - 1) === "-" &&
+         (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
+        --start;
+      }
+    }
+
+    if (start !== end)
+    {
+      // Include percentages as part of the incremented number (they are
+      // common enough).
+      if (value.charAt(end) === "%") {
+        ++end;
+      }
+
+      let first = value.substr(0, start);
+      let mid = value.substring(start, end);
+      let last = value.substr(end);
+
+      mid = this._incrementRawValue(mid, increment, info);
+
+      if (mid !== null) {
+        return {
+          value: first + mid + last,
+          start: start,
+          end: start + mid.length
+        };
+      }
+    }
+  },
+
+  /**
+   * Increment the property value for numbers.
+   *
+   * @param {string} rawValue 
+   *        Raw value to increment.
+   * @param {number} increment 
+   *        Amount to increase/decrease the raw value.
+   * @param {object} info 
+   *        Object with info about the property value.
+   * @return {string} the incremented value.
+   */
+  _incrementRawValue: function InplaceEditor_incrementRawValue(rawValue, increment, info)
+  {
+    let num = parseFloat(rawValue);
+
+    if (isNaN(num)) {
+      return null;
+    }
+
+    let number = /\d+(\.\d+)?/.exec(rawValue);
+    let units = rawValue.substr(number.index + number[0].length);
+
+    // avoid rounding errors
+    let newValue = Math.round((num + increment) * 1000) / 1000;
+
+    if (info && "minValue" in info) {
+      newValue = Math.max(newValue, info.minValue);
+    }
+    if (info && "maxValue" in info) {
+      newValue = Math.min(newValue, info.maxValue);
+    }
+
+    newValue = newValue.toString();
+
+    return newValue + units;
+  },
+
+  /**
+   * Increment the property value for hex.
+   *
+   * @param {string} value 
+   *        Property value.
+   * @param {number} increment 
+   *        Amount to increase/decrease the property value.
+   * @param {number} offset 
+   *        Starting index of the property value.
+   * @param {number} offsetEnd 
+   *        Ending index of the property value.
+   * @return {object} object with properties 'value' and 'selection'.
+   */
+  _incHexColor: function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
+  {
+    // Return early if no part of the rawValue is selected.
+    if (offsetEnd > rawValue.length && offset >= rawValue.length) {
+      return;
+    }
+    if (offset < 1 && offsetEnd <= 1) {
+      return;
+    }
+    // Ignore the leading #.
+    rawValue = rawValue.substr(1);
+    --offset;
+    --offsetEnd;
+
+    // Clamp the selection to within the actual value.
+    offset = Math.max(offset, 0);
+    offsetEnd = Math.min(offsetEnd, rawValue.length);
+    offsetEnd = Math.max(offsetEnd, offset);
+
+    // Normalize #ABC -> #AABBCC.
+    if (rawValue.length === 3) {
+      rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+                 rawValue.charAt(1) + rawValue.charAt(1) +
+                 rawValue.charAt(2) + rawValue.charAt(2);
+      offset *= 2;
+      offsetEnd *= 2;
+    }
+
+    if (rawValue.length !== 6) {
+      return;
+    }
+
+    // If no selection, increment an adjacent color, preferably one to the left.
+    if (offset === offsetEnd) {
+      if (offset === 0) {
+        offsetEnd = 1;
+      } else {
+        offset = offsetEnd - 1;
+      }
+    }
+
+    // Make the selection cover entire parts.
+    offset -= offset % 2;
+    offsetEnd += offsetEnd % 2;
+
+    // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
+    if (-1 < increment && increment < 1) {
+      increment = (increment < 0 ? -1 : 1);
+    }
+    if (Math.abs(increment) === 10) {
+      increment = (increment < 0 ? -16 : 16);
+    }
+
+    let isUpper = (rawValue.toUpperCase() === rawValue);
+
+    for (let pos = offset; pos < offsetEnd; pos += 2) {
+      // Increment the part in [pos, pos+2).
+      let mid = rawValue.substr(pos, 2);
+      let value = parseInt(mid, 16);
+
+      if (isNaN(value)) {
+        return;
+      }
+
+      mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
+
+      while (mid.length < 2) {
+        mid = "0" + mid;
+      }
+      if (isUpper) {
+        mid = mid.toUpperCase();
+      }
+
+      rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
+    }
+
+    return {
+      value: "#" + rawValue,
+      selection: [offset + 1, offsetEnd + 1]
+    };
+  },
+
   /**
    * Call the client's done handler and clear out.
    */
   _apply: function InplaceEditor_apply(aEvent)
   {
     if (this._applied) {
       return;
     }
 
     this._applied = true;
 
     if (this.done) {
       let val = this.input.value.trim();
       return this.done(this.cancelled ? this.initial : val, !this.cancelled);
     }
+
     return null;
   },
 
   /**
    * Handle loss of focus by calling done if it hasn't been called yet.
    */
   _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
   {
     this._apply();
     if (!aDoNotClear) {
       this._clear();
     }
   },
 
+  /**
+   * Handle the input field's keypress event.
+   */
   _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
   {
     let prevent = false;
+
+    const largeIncrement = 100;
+    const mediumIncrement = 10;
+    const smallIncrement = 0.1;
+
+    let increment = 0;
+
+    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
+      increment = 1;
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+      increment = -1;
+    }
+
+    if (aEvent.shiftKey && !aEvent.altKey) {
+      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
+           ||  aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+        increment *= largeIncrement;
+      } else {
+        increment *= mediumIncrement;
+      }
+    } else if (aEvent.altKey && !aEvent.shiftKey) {
+      increment *= smallIncrement;
+    }
+
+    if (increment && this._incrementValue(increment) ) {
+      this._updateSize();
+      prevent = true;
+    }
+
     if (this.multiline &&
         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
         aEvent.shiftKey) {
       prevent = false;
     } else if (aEvent.charCode in this._advanceCharCodes
        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
       prevent = true;
@@ -2253,17 +2642,17 @@ InplaceEditor.prototype = {
   _onKeyup: function(aEvent) {
     // Validate the entered value.
     this.warning.hidden = this.validate(this.input.value);
     this._applied = false;
     this._onBlur(null, true);
   },
 
   /**
-   * Handle changes the input text.
+   * Handle changes to the input text.
    */
   _onInput: function InplaceEditor_onInput(aEvent)
   {
     // Validate the entered value.
     if (this.warning && this.validate) {
       this.warning.hidden = this.validate(this.input.value);
     }
 
@@ -2280,17 +2669,20 @@ InplaceEditor.prototype = {
 };
 
 /*
  * Various API consumers (especially tests) sometimes want to grab the
  * inplaceEditor expando off span elements. However, when each global has its
  * own compartment, those expandos live on Xray wrappers that are only visible
  * within this JSM. So we provide a little workaround here.
  */
-this._getInplaceEditorForSpan = function _getInplaceEditorForSpan(aSpan) { return aSpan.inplaceEditor; };
+this._getInplaceEditorForSpan = function _getInplaceEditorForSpan(aSpan)
+{
+  return aSpan.inplaceEditor;
+};
 
 /**
  * Store of CSSStyleDeclarations mapped to properties that have been changed by
  * the user.
  */
 function UserProperties()
 {
   // FIXME: This should be a WeakMap once bug 753517 is fixed.
@@ -2299,23 +2691,23 @@ function UserProperties()
 }
 
 UserProperties.prototype = {
   /**
    * Get a named property for a given CSSStyleDeclaration.
    *
    * @param {CSSStyleDeclaration} aStyle
    *        The CSSStyleDeclaration against which the property is mapped.
-   * @param {String} aName
+   * @param {string} aName
    *        The name of the property to get.
-   * @param {String} aComputedValue
+   * @param {string} aComputedValue
    *        The computed value of the property.  The user value will only be
    *        returned if the computed value hasn't changed since, and this will
    *        be returned as the default if no user value is available.
-   * @returns {String}
+   * @return {string}
    *          The property value if it has previously been set by the user, null
    *          otherwise.
    */
   getProperty: function UP_getProperty(aStyle, aName, aComputedValue) {
     let entry = this.map.get(aStyle, null);
 
     if (entry && aName in entry) {
       let item = entry[aName];
--- a/browser/devtools/styleinspector/test/Makefile.in
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -31,16 +31,17 @@ MOCHITEST_BROWSER_FILES = \
   browser_bug705707_is_content_stylesheet.js \
   browser_bug722196_property_view_media_queries.js \
   browser_bug722196_rule_view_media_queries.js \
   browser_bug_592743_specificity.js \
   browser_ruleview_bug_703643_context_menu_copy.js \
   browser_computedview_bug_703643_context_menu_copy.js \
   browser_ruleview_734259_style_editor_link.js \
   browser_computedview_734259_style_editor_link.js \
+  browser_bug722691_rule_view_increment.js \
   head.js \
   $(NULL)
 
 MOCHITEST_BROWSER_FILES += \
   browser_bug683672.html \
   browser_bug705707_is_content_stylesheet.html \
   browser_bug705707_is_content_stylesheet_imported.css \
   browser_bug705707_is_content_stylesheet_imported2.css \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
@@ -0,0 +1,205 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that increasing/decreasing values in rule view using
+// arrow keys works correctly.
+
+let tempScope = {};
+Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
+let CssRuleView = tempScope.CssRuleView;
+let _ElementStyle = tempScope._ElementStyle;
+let _editableField = tempScope._editableField;
+let inplaceEditor = tempScope._getInplaceEditorForSpan;
+
+let doc;
+let ruleDialog;
+let ruleView;
+
+function setUpTests()
+{
+  doc.body.innerHTML = '<div id="test" style="' +
+                           'margin-top:0px;' +
+                           'padding-top: 0px;' +
+                           'color:#000000;' +
+                           'background-color: #000000; >"'+
+                       '</div>';
+  let testElement = doc.getElementById("test");
+  ruleDialog = openDialog("chrome://browser/content/devtools/cssruleview.xul",
+                          "cssruleviewtest",
+                          "width=350,height=350");
+  ruleDialog.addEventListener("load", function onLoad(evt) {
+    ruleDialog.removeEventListener("load", onLoad, true);
+    let doc = ruleDialog.document;
+    ruleView = new CssRuleView(doc);
+    doc.documentElement.appendChild(ruleView.element);
+    ruleView.highlight(testElement);
+    waitForFocus(runTests, ruleDialog);
+  }, true);
+}
+
+function runTests()
+{
+  let idRuleEditor = ruleView.element.children[0]._ruleEditor;
+  let marginPropEditor = idRuleEditor.rule.textProps[0].editor;
+  let paddingPropEditor = idRuleEditor.rule.textProps[1].editor;
+  let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor;
+  let rgbColorPropEditor = idRuleEditor.rule.textProps[3].editor;
+
+  (function() {
+    info("INCREMENTS");
+    newTest( marginPropEditor, {
+      1: { alt: true, start: "0px", end: "0.1px", selectAll: true },
+      2: { start: "0px", end: "1px", selectAll: true },
+      3: { shift: true, start: "0px", end: "10px", selectAll: true },
+      4: { down: true, alt: true, start: "0.1px", end: "0px", selectAll: true },
+      5: { down: true, start: "0px", end: "-1px", selectAll: true },
+      6: { down: true, shift: true, start: "0px", end: "-10px", selectAll: true },
+      7: { pageUp: true, shift: true, start: "0px", end: "100px", selectAll: true },
+      8: { pageDown: true, shift: true, start: "0px", end: "-100px", selectAll: true,
+           nextTest: test2 }
+    });
+    EventUtils.synthesizeMouse(marginPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+  })();
+
+  function test2() {
+    info("UNITS");
+    newTest( paddingPropEditor, {
+      1: { start: "0px", end: "1px", selectAll: true },
+      2: { start: "0pt", end: "1pt", selectAll: true },
+      3: { start: "0pc", end: "1pc", selectAll: true },
+      4: { start: "0em", end: "1em", selectAll: true },
+      5: { start: "0%",  end: "1%",  selectAll: true },
+      6: { start: "0in", end: "1in", selectAll: true },
+      7: { start: "0cm", end: "1cm", selectAll: true },
+      8: { start: "0mm", end: "1mm", selectAll: true },
+      9: { start: "0ex", end: "1ex", selectAll: true,
+           nextTest: test3 }
+    });
+    EventUtils.synthesizeMouse(paddingPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+  };
+
+  function test3() {
+    info("HEX COLORS");
+    newTest( hexColorPropEditor, {
+      1: { start: "#CCCCCC", end: "#CDCDCD", selectAll: true},
+      2: { shift: true, start: "#CCCCCC", end: "#DCDCDC", selectAll: true },
+      3: { start: "#CCCCCC", end: "#CDCCCC", selection: [1,3] },
+      4: { shift: true, start: "#CCCCCC", end: "#DCCCCC", selection: [1,3] },
+      5: { start: "#FFFFFF", end: "#FFFFFF", selectAll: true },
+      6: { down: true, shift: true, start: "#000000", end: "#000000", selectAll: true,
+           nextTest: test4 }
+    });
+    EventUtils.synthesizeMouse(hexColorPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+  };
+
+  function test4() {
+    info("RGB COLORS");
+    newTest( rgbColorPropEditor, {
+      1: { start: "rgb(0,0,0)", end: "rgb(0,1,0)", selection: [6,7] },
+      2: { shift: true, start: "rgb(0,0,0)", end: "rgb(0,10,0)", selection: [6,7] },
+      3: { start: "rgb(0,255,0)", end: "rgb(0,255,0)", selection: [6,9] },
+      4: { shift: true, start: "rgb(0,250,0)", end: "rgb(0,255,0)", selection: [6,9] },
+      5: { down: true, start: "rgb(0,0,0)", end: "rgb(0,0,0)", selection: [6,7] },
+      6: { down: true, shift: true, start: "rgb(0,5,0)", end: "rgb(0,0,0)", selection: [6,7],
+           nextTest: test5 }
+    });
+    EventUtils.synthesizeMouse(rgbColorPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+  };
+
+  function test5() {
+    info("SHORTHAND");
+    newTest( paddingPropEditor, {
+      1: { start: "0px 0px 0px 0px", end: "0px 1px 0px 0px", selection: [4,7] },
+      2: { shift: true, start: "0px 0px 0px 0px", end: "0px 10px 0px 0px", selection: [4,7] },
+      3: { start: "0px 0px 0px 0px", end: "1px 0px 0px 0px", selectAll: true },
+      4: { shift: true, start: "0px 0px 0px 0px", end: "10px 0px 0px 0px", selectAll: true },
+      5: { down: true, start: "0px 0px 0px 0px", end: "0px 0px -1px 0px", selection: [8,11] },
+      6: { down: true, shift: true, start: "0px 0px 0px 0px", end: "-10px 0px 0px 0px", selectAll: true,
+           nextTest: test6 }
+    });
+    EventUtils.synthesizeMouse(paddingPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+  };
+
+  function test6() {
+    info("ODD CASES");
+    newTest( marginPropEditor, {
+      1: { start: "98.7%", end: "99.7%", selection: [3,3] },
+      2: { alt: true, start: "98.7%", end: "98.8%", selection: [3,3] },
+      3: { start: "0", end: "1" },
+      4: { down: true, start: "0", end: "-1" },
+      5: { start: "'a=-1'", end: "'a=0'", selection: [4,4] },
+      6: { start: "0 -1px", end: "0 0px", selection: [2,2] },
+      7: { start: "url(-1)", end: "url(-1)", selection: [4,4] },
+      8: { start: "url('test1.1.png')", end: "url('test1.2.png')", selection: [11,11] },
+      9: { start: "url('test1.png')", end: "url('test2.png')", selection: [9,9] },
+      10: { shift: true, start: "url('test1.1.png')", end: "url('test11.1.png')", selection: [9,9] },
+      11: { down: true, start: "url('test-1.png')", end: "url('test-2.png')", selection: [9,11] },
+      12: { start: "url('test1.1.png')", end: "url('test1.2.png')", selection: [11,12] },
+      13: { down: true, alt: true, start: "url('test-0.png')", end: "url('test--0.1.png')", selection: [10,11] },
+      14: { alt: true, start: "url('test--0.1.png')", end: "url('test-0.png')", selection: [10,14],
+           endTest: true }
+    });
+    EventUtils.synthesizeMouse(marginPropEditor.valueSpan, 1, 1, {}, ruleDialog);
+  };
+}
+
+function newTest( propEditor, tests )
+{
+  waitForEditorFocus(propEditor.element, function onElementFocus(aEditor) {
+    for( test in tests) {
+      testIncrement( aEditor, tests[test] );
+    }
+  }, false);
+}
+
+function testIncrement( aEditor, aOptions )
+{
+  aEditor.input.value = aOptions.start;
+  let input = aEditor.input;
+  if ( aOptions.selectAll ) {
+    input.select();
+  } else if ( aOptions.selection ) {
+    input.setSelectionRange(aOptions.selection[0], aOptions.selection[1]);
+  }
+  is(input.value, aOptions.start, "Value initialized at " + aOptions.start);
+  input.addEventListener("keyup", function onIncrementUp() {
+    input.removeEventListener("keyup", onIncrementUp, false);
+    input = aEditor.input;
+    is(input.value, aOptions.end, "Value changed to " + aOptions.end);
+    if( aOptions.nextTest) {
+      aOptions.nextTest();
+    }
+    else if( aOptions.endTest ) {
+      finishTest();
+    }
+  }, false);
+  let key;
+  key = ( aOptions.down ) ? "VK_DOWN" : "VK_UP";
+  key = ( aOptions.pageDown ) ? "VK_PAGE_DOWN" : ( aOptions.pageUp ) ? "VK_PAGE_UP" : key;
+  EventUtils.synthesizeKey(key,
+                          {altKey: aOptions.alt, shiftKey: aOptions.shift},
+                          ruleDialog);
+}
+
+function finishTest()
+{
+  ruleView.clear();
+  ruleDialog.close();
+  ruleDialog = ruleView = null;
+  doc = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, onload, true);
+    doc = content.document;
+    waitForFocus(setUpTests, content);
+  }, true);
+  content.location = "data:text/html,sample document for bug 722691";
+}
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -72,21 +72,21 @@ stepOverTooltip=Step Over (%S)
 stepInTooltip=Step In (%S)
 
 # LOCALIZATION NOTE (stepOutTooltip): The label that is displayed on the
 # button that steps out of a function call.
 stepOutTooltip=Step Out (%S)
 
 # 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.
+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.
+emptyBreakpointsText=No breakpoints to display
 
 # LOCALIZATION NOTE (emptyGlobalsText): The text to display in the menulist
 # when there are no chrome globals available.
 noGlobalsText=No globals
 
 # LOCALIZATION NOTE (noMatchingScriptsText): The text to display in the
 # menulist when there are no matching chrome globals after filtering.
 noMatchingGlobalsText=No matching globals
@@ -145,21 +145,38 @@ breakpointMenuItem.enableAll=Enable all 
 breakpointMenuItem.disableAll=Disable all breakpoints
 breakpointMenuItem.deleteAll=Remove all breakpoints
 
 # LOCALIZATION NOTE (loadingText): The text that is displayed in the script
 # editor when the loading process has started but there is no file to display
 # yet.
 loadingText=Loading\u2026
 
+# LOCALIZATION NOTE (emptyStackText): The text that is displayed in the watch
+# expressions list to add a new item.
+addWatchExpressionText=Add watch expression
+
 # LOCALIZATION NOTE (emptyVariablesText): The text that is displayed in the
 # variables pane when there are no variables to display.
-emptyVariablesText=No variables to display.
+emptyVariablesText=No variables to display
 
 # LOCALIZATION NOTE (scopeLabel): The text that is displayed in the variables
 # pane as a header for each variable scope (e.g. "Global scope, "With scope",
 # etc.).
 scopeLabel=%S scope
 
+# LOCALIZATION NOTE (watchExpressionsScopeLabel): The name of the watch
+# expressions scope. This text is displayed in the variables pane as a header for
+# the watch expressions scope.
+watchExpressionsScopeLabel=Watch expressions
+
 # LOCALIZATION NOTE (globalScopeLabel): The name of the global scope. This text
 # is added to scopeLabel and displayed in the variables pane as a header for
 # the global scope.
 globalScopeLabel=Global
+
+# LOCALIZATION NOTE (variablesSeparatorLabel): The text that is displayed
+# in the variables list as a separator between the name and value.
+variablesSeparatorLabel=:
+
+# LOCALIZATION NOTE (watchExpressionsSeparatorLabel): The text that is displayed
+# in the watch expressions list as a separator between the code and evaluation.
+watchExpressionsSeparatorLabel=\ →
--- a/browser/themes/gnomestripe/devtools/debugger.css
+++ b/browser/themes/gnomestripe/devtools/debugger.css
@@ -30,17 +30,17 @@
 
 .list-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-item.empty {
   color: GrayText;
-  padding: 4px;
+  padding: 2px;
 }
 
 /**
  * Sources searching
  */
 
 #globalsearch {
   background-color: white;
@@ -126,16 +126,29 @@
   min-width: 50px;
 }
 
 #stackframes\+breakpoints[animated] {
   transition: margin 0.25s ease-in-out;
 }
 
 /**
+ * Variables and watch expressions pane
+ */
+
+#variables\+expressions {
+  background-color: white;
+  min-width: 50px;
+}
+
+#variables\+expressions[animated] {
+  transition: margin 0.25s ease-in-out;
+}
+
+/**
  * Stack frames view
  */
 
 #stackframes {
   background-color: white;
   min-height: 10px;
 }
 
@@ -178,26 +191,54 @@
   margin: -6px 0 8px 0;
 }
 
 #conditional-breakpoint-panel textbox {
   margin: 0 0 -2px 0;
 }
 
 /**
+ * Watch expressions view
+ */
+
+#expressions {
+  background-color: white;
+  min-height: 10px;
+}
+
+.dbg-expression {
+  height: 20px;
+  -moz-padding-start: 8px;
+}
+
+.dbg-expression:last-child {
+  margin-bottom: 4px;
+}
+
+.dbg-expression-arrow {
+  width: 10px;
+  height: auto;
+  background: url("chrome://browser/skin/devtools/commandline.png") 0px 4px no-repeat;
+}
+
+.dbg-expression-input {
+  font: 9pt monospace;
+}
+
+.dbg-expression-delete {
+  -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+/**
  * Variables view
  */
 
 #variables {
   background-color: white;
-  min-width: 50px;
-}
-
-#variables[animated] {
-  transition: margin 0.25s ease-in-out;
+  min-height: 10px;
 }
 
 /**
  * Scope element
  */
 
 .scope > .title {
   text-shadow: 0 1px #222;
--- a/browser/themes/pinstripe/devtools/debugger.css
+++ b/browser/themes/pinstripe/devtools/debugger.css
@@ -32,17 +32,17 @@
 
 .list-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-item.empty {
   color: GrayText;
-  padding: 4px;
+  padding: 2px;
 }
 
 /**
  * Sources searching
  */
 
 #globalsearch {
   background-color: white;
@@ -128,16 +128,29 @@
   min-width: 50px;
 }
 
 #stackframes\+breakpoints[animated] {
   transition: margin 0.25s ease-in-out;
 }
 
 /**
+ * Variables and watch expressions pane
+ */
+
+#variables\+expressions {
+  background-color: white;
+  min-width: 50px;
+}
+
+#variables\+expressions[animated] {
+  transition: margin 0.25s ease-in-out;
+}
+
+/**
  * Stack frames view
  */
 
 #stackframes {
   background-color: white;
   min-height: 10px;
 }
 
@@ -180,26 +193,54 @@
   margin: -6px 0 8px 0;
 }
 
 #conditional-breakpoint-panel textbox {
   margin: 0 0 -2px 0;
 }
 
 /**
+ * Watch expressions view
+ */
+
+#expressions {
+  background-color: white;
+  min-height: 10px;
+}
+
+.dbg-expression {
+  height: 20px;
+  -moz-padding-start: 8px;
+}
+
+.dbg-expression:last-child {
+  margin-bottom: 4px;
+}
+
+.dbg-expression-arrow {
+  width: 10px;
+  height: auto;
+  background: url("chrome://browser/skin/devtools/commandline.png") 0px 4px no-repeat;
+}
+
+.dbg-expression-input {
+  font: 9pt monospace;
+}
+
+.dbg-expression-delete {
+  -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+/**
  * Variables view
  */
 
 #variables {
   background-color: white;
-  min-width: 50px;
-}
-
-#variables[animated] {
-  transition: margin 0.25s ease-in-out;
+  min-height: 10px;
 }
 
 /**
  * Scope element
  */
 
 .scope > .title {
   text-shadow: 0 1px #222;
--- a/browser/themes/winstripe/devtools/debugger.css
+++ b/browser/themes/winstripe/devtools/debugger.css
@@ -38,17 +38,17 @@
 
 .list-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-item.empty {
   color: GrayText;
-  padding: 4px;
+  padding: 2px;
 }
 
 /**
  * Sources searching
  */
 
 #globalsearch {
   background-color: white;
@@ -134,16 +134,29 @@
   min-width: 50px;
 }
 
 #stackframes\+breakpoints[animated] {
   transition: margin 0.25s ease-in-out;
 }
 
 /**
+ * Variables and watch expressions pane
+ */
+
+#variables\+expressions {
+  background-color: white;
+  min-width: 50px;
+}
+
+#variables\+expressions[animated] {
+  transition: margin 0.25s ease-in-out;
+}
+
+/**
  * Stack frames view
  */
 
 #stackframes {
   background-color: white;
   min-height: 10px;
 }
 
@@ -186,26 +199,54 @@
   margin: -6px 0 8px 0;
 }
 
 #conditional-breakpoint-panel textbox {
   margin: 0 0 -2px 0;
 }
 
 /**
+ * Watch expressions view
+ */
+
+#expressions {
+  background-color: white;
+  min-height: 10px;
+}
+
+.dbg-expression {
+  height: 20px;
+  -moz-padding-start: 8px;
+}
+
+.dbg-expression:last-child {
+  margin-bottom: 4px;
+}
+
+.dbg-expression-arrow {
+  width: 10px;
+  height: auto;
+  background: url("chrome://browser/skin/devtools/commandline.png") 0px 4px no-repeat;
+}
+
+.dbg-expression-input {
+  font: 9pt monospace;
+}
+
+.dbg-expression-delete {
+  -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+/**
  * Variables view
  */
 
 #variables {
   background-color: white;
-  min-width: 50px;
-}
-
-#variables[animated] {
-  transition: margin 0.25s ease-in-out;
+  min-height: 10px;
 }
 
 /**
  * Scope element
  */
 
 .scope > .title {
   text-shadow: 0 1px #222;