Bug 758157 - Clearly indicate overridden variables, r=past
authorVictor Porof <vporof@mozilla.com>
Thu, 12 Dec 2013 19:38:12 +0200
changeset 160196 a0021f958d1a4d54ad53f20263e42b6ef1dd150b
parent 160195 4bc1aa2876211d44f47983494f314dbf6ed5fbfd
child 160197 27e4e496ed0dc05ccde7f0592a724b0d09665e2b
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerspast
bugs758157
milestone29.0a1
Bug 758157 - Clearly indicate overridden variables, r=past
browser/devtools/debugger/test/browser.ini
browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js
browser/devtools/debugger/test/browser_dbg_variables-view-override-01.js
browser/devtools/debugger/test/browser_dbg_variables-view-override-02.js
browser/devtools/debugger/test/browser_dbg_variables-view-reexpand-01.js
browser/devtools/debugger/test/browser_dbg_variables-view-reexpand-02.js
browser/devtools/debugger/test/doc_scope-variable-2.html
browser/devtools/shared/widgets/VariablesView.jsm
browser/devtools/shared/widgets/VariablesViewController.jsm
browser/devtools/shared/widgets/widgets.css
browser/locales/en-US/chrome/browser/devtools/debugger.properties
browser/themes/linux/devtools/widgets.css
browser/themes/osx/devtools/widgets.css
browser/themes/shared/devtools/dark-theme.css
browser/themes/shared/devtools/light-theme.css
browser/themes/windows/devtools/widgets.css
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -46,16 +46,17 @@ support-files =
   doc_minified.html
   doc_minified_bogus_map.html
   doc_pause-exceptions.html
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_random-javascript.html
   doc_recursion-stack.html
   doc_scope-variable.html
+  doc_scope-variable-2.html
   doc_script-switching-01.html
   doc_script-switching-02.html
   doc_step-out.html
   doc_watch-expressions.html
   doc_with-frame.html
   head.js
   sjs_random-javascript.sjs
   testactors.js
@@ -206,16 +207,18 @@ support-files =
 [browser_dbg_variables-view-filter-searchbox.js]
 [browser_dbg_variables-view-frame-parameters-01.js]
 [browser_dbg_variables-view-frame-parameters-02.js]
 [browser_dbg_variables-view-frame-parameters-03.js]
 [browser_dbg_variables-view-frame-with.js]
 [browser_dbg_variables-view-frozen-sealed-nonext.js]
 [browser_dbg_variables-view-hide-non-enums.js]
 [browser_dbg_variables-view-large-array-buffer.js]
+[browser_dbg_variables-view-override-01.js]
+[browser_dbg_variables-view-override-02.js]
 [browser_dbg_variables-view-popup-01.js]
 [browser_dbg_variables-view-popup-02.js]
 [browser_dbg_variables-view-popup-03.js]
 [browser_dbg_variables-view-popup-04.js]
 [browser_dbg_variables-view-popup-05.js]
 [browser_dbg_variables-view-popup-06.js]
 [browser_dbg_variables-view-popup-07.js]
 [browser_dbg_variables-view-popup-08.js]
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js
@@ -46,24 +46,24 @@ function initialChecks() {
   ok(scopeNodes[1].querySelector(".name").getAttribute("value").contains("[Window]"),
     "The global scope should be properly identified.");
 
   is(gVariables.getScopeAtIndex(0).target, scopeNodes[0],
     "getScopeAtIndex(0) didn't return the expected scope.");
   is(gVariables.getScopeAtIndex(1).target, scopeNodes[1],
     "getScopeAtIndex(1) didn't return the expected scope.");
 
-  is(gVariables.getScopeForNode(scopeNodes[0]).target, scopeNodes[0],
-    "getScopeForNode([0]) didn't return the expected scope.");
-  is(gVariables.getScopeForNode(scopeNodes[1]).target, scopeNodes[1],
-    "getScopeForNode([1]) didn't return the expected scope.");
+  is(gVariables.getItemForNode(scopeNodes[0]).target, scopeNodes[0],
+    "getItemForNode([0]) didn't return the expected scope.");
+  is(gVariables.getItemForNode(scopeNodes[1]).target, scopeNodes[1],
+    "getItemForNode([1]) didn't return the expected scope.");
 
-  is(gVariables.getScopeForNode(scopeNodes[0]).expanded, true,
+  is(gVariables.getItemForNode(scopeNodes[0]).expanded, true,
     "The local scope should be expanded by default.");
-  is(gVariables.getScopeForNode(scopeNodes[1]).expanded, false,
+  is(gVariables.getItemForNode(scopeNodes[1]).expanded, false,
     "The global scope should not be collapsed by default.");
 }
 
 function testExpandVariables() {
   let deferred = promise.defer();
 
   let localScope = gVariables.getScopeAtIndex(0);
   let localEnums = localScope.target.querySelector(".variables-view-element-details.enum").childNodes;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-override-01.js
@@ -0,0 +1,219 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that VariablesView methods responsible for styling variables
+ * as overridden work properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_scope-variable-2.html";
+
+function test() {
+  Task.spawn(function() {
+    let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
+    let win = panel.panelWin;
+    let events = win.EVENTS;
+    let variables = win.DebuggerView.Variables;
+
+    // Allow this generator function to yield first.
+    executeSoon(() => debuggee.test());
+    yield waitForSourceAndCaretAndScopes(panel, ".html", 23);
+
+    let firstScope = variables.getScopeAtIndex(0);
+    let secondScope = variables.getScopeAtIndex(1);
+    let thirdScope = variables.getScopeAtIndex(2);
+    let globalScope = variables.getScopeAtIndex(3);
+
+    ok(firstScope, "The first scope is available.");
+    ok(secondScope, "The second scope is available.");
+    ok(thirdScope, "The third scope is available.");
+    ok(globalScope, "The global scope is available.");
+
+    is(firstScope.name, "Function scope [secondNest]",
+      "The first scope's name is correct.");
+    is(secondScope.name, "Function scope [firstNest]",
+      "The second scope's name is correct.");
+    is(thirdScope.name, "Function scope [test]",
+      "The third scope's name is correct.");
+    is(globalScope.name, "Global scope [Window]",
+      "The global scope's name is correct.");
+
+    is(firstScope.expanded, true,
+      "The first scope's expansion state is correct.");
+    is(secondScope.expanded, false,
+      "The second scope's expansion state is correct.");
+    is(thirdScope.expanded, false,
+      "The third scope's expansion state is correct.");
+    is(globalScope.expanded, false,
+      "The global scope's expansion state is correct.");
+
+    is(firstScope._store.size, 3,
+      "The first scope should have all the variables available.");
+    is(secondScope._store.size, 3,
+      "The second scope shoild have all the variables available.");
+    is(thirdScope._store.size, 3,
+      "The third scope shoild have all the variables available.");
+    is(globalScope._store.size, 0,
+      "The global scope shoild have no variables available.");
+
+    // Test getOwnerScopeForVariableOrProperty with simple variables.
+
+    let thisVar = firstScope.get("this");
+    let thisOwner = variables.getOwnerScopeForVariableOrProperty(thisVar);
+    is(thisOwner, firstScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (1).");
+
+    let someVar1 = firstScope.get("a");
+    let someOwner1 = variables.getOwnerScopeForVariableOrProperty(someVar1);
+    is(someOwner1, firstScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (2).");
+
+    // Test getOwnerScopeForVariableOrProperty with first-degree properties.
+
+    let argsVar1 = firstScope.get("arguments");
+    let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+    argsVar1.expand();
+    yield fetched;
+
+    let calleeProp1 = argsVar1.get("callee");
+    let calleeOwner1 = variables.getOwnerScopeForVariableOrProperty(calleeProp1);
+    is(calleeOwner1, firstScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (3).");
+
+    // Test getOwnerScopeForVariableOrProperty with second-degree properties.
+
+    let protoVar1 = argsVar1.get("__proto__");
+    let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+    protoVar1.expand();
+    yield fetched;
+
+    let constrProp1 = protoVar1.get("constructor");
+    let constrOwner1 = variables.getOwnerScopeForVariableOrProperty(constrProp1);
+    is(constrOwner1, firstScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (4).");
+
+    // Test getOwnerScopeForVariableOrProperty with a simple variable
+    // from non-topmost scopes.
+
+    let someVar2 = secondScope.get("a");
+    let someOwner2 = variables.getOwnerScopeForVariableOrProperty(someVar2);
+    is(someOwner2, secondScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (5).");
+
+    let someVar3 = thirdScope.get("a");
+    let someOwner3 = variables.getOwnerScopeForVariableOrProperty(someVar3);
+    is(someOwner3, thirdScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (6).");
+
+    // Test getOwnerScopeForVariableOrProperty with first-degree properies
+    // from non-topmost scopes.
+
+    let argsVar2 = secondScope.get("arguments");
+    let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+    argsVar2.expand();
+    yield fetched;
+
+    let calleeProp2 = argsVar2.get("callee");
+    let calleeOwner2 = variables.getOwnerScopeForVariableOrProperty(calleeProp2);
+    is(calleeOwner2, secondScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (7).");
+
+    let argsVar3 = thirdScope.get("arguments");
+    let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+    argsVar3.expand();
+    yield fetched;
+
+    let calleeProp3 = argsVar3.get("callee");
+    let calleeOwner3 = variables.getOwnerScopeForVariableOrProperty(calleeProp3);
+    is(calleeOwner3, thirdScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (8).");
+
+    // Test getOwnerScopeForVariableOrProperty with second-degree properties
+    // from non-topmost scopes.
+
+    let protoVar2 = argsVar2.get("__proto__");
+    let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+    protoVar2.expand();
+    yield fetched;
+
+    let constrProp2 = protoVar2.get("constructor");
+    let constrOwner2 = variables.getOwnerScopeForVariableOrProperty(constrProp2);
+    is(constrOwner2, secondScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (9).");
+
+    let protoVar3 = argsVar3.get("__proto__");
+    let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES);
+    protoVar3.expand();
+    yield fetched;
+
+    let constrProp3 = protoVar3.get("constructor");
+    let constrOwner3 = variables.getOwnerScopeForVariableOrProperty(constrProp3);
+    is(constrOwner3, thirdScope,
+      "The getOwnerScopeForVariableOrProperty method works properly (10).");
+
+    // Test getParentScopesForVariableOrProperty with simple variables.
+
+    let varOwners1 = variables.getParentScopesForVariableOrProperty(someVar1);
+    let varOwners2 = variables.getParentScopesForVariableOrProperty(someVar2);
+    let varOwners3 = variables.getParentScopesForVariableOrProperty(someVar3);
+
+    is(varOwners1.length, 0,
+      "There should be no owner scopes for the first variable.");
+
+    is(varOwners2.length, 1,
+      "There should be one owner scope for the second variable.");
+    is(varOwners2[0], firstScope,
+      "The only owner scope for the second variable is correct.");
+
+    is(varOwners3.length, 2,
+      "There should be two owner scopes for the third variable.");
+    is(varOwners3[0], firstScope,
+      "The first owner scope for the third variable is correct.");
+    is(varOwners3[1], secondScope,
+      "The second owner scope for the third variable is correct.");
+
+    // Test getParentScopesForVariableOrProperty with first-degree properties.
+
+    let propOwners1 = variables.getParentScopesForVariableOrProperty(calleeProp1);
+    let propOwners2 = variables.getParentScopesForVariableOrProperty(calleeProp2);
+    let propOwners3 = variables.getParentScopesForVariableOrProperty(calleeProp3);
+
+    is(propOwners1.length, 0,
+      "There should be no owner scopes for the first property.");
+
+    is(propOwners2.length, 1,
+      "There should be one owner scope for the second property.");
+    is(propOwners2[0], firstScope,
+      "The only owner scope for the second property is correct.");
+
+    is(propOwners3.length, 2,
+      "There should be two owner scopes for the third property.");
+    is(propOwners3[0], firstScope,
+      "The first owner scope for the third property is correct.");
+    is(propOwners3[1], secondScope,
+      "The second owner scope for the third property is correct.");
+
+    // Test getParentScopesForVariableOrProperty with second-degree properties.
+
+    let secPropOwners1 = variables.getParentScopesForVariableOrProperty(constrProp1);
+    let secPropOwners2 = variables.getParentScopesForVariableOrProperty(constrProp2);
+    let secPropOwners3 = variables.getParentScopesForVariableOrProperty(constrProp3);
+
+    is(secPropOwners1.length, 0,
+      "There should be no owner scopes for the first inner property.");
+
+    is(secPropOwners2.length, 1,
+      "There should be one owner scope for the second inner property.");
+    is(secPropOwners2[0], firstScope,
+      "The only owner scope for the second inner property is correct.");
+
+    is(secPropOwners3.length, 2,
+      "There should be two owner scopes for the third inner property.");
+    is(secPropOwners3[0], firstScope,
+      "The first owner scope for the third inner property is correct.");
+    is(secPropOwners3[1], secondScope,
+      "The second owner scope for the third inner property is correct.");
+
+    yield resumeDebuggerThenCloseAndFinish(panel);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-override-02.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that overridden variables in the VariablesView are styled properly.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_scope-variable-2.html";
+
+function test() {
+  Task.spawn(function() {
+    let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
+    let win = panel.panelWin;
+    let events = win.EVENTS;
+    let variables = win.DebuggerView.Variables;
+
+    // Allow this generator function to yield first.
+    executeSoon(() => debuggee.test());
+    yield waitForSourceAndCaretAndScopes(panel, ".html", 23);
+
+    // Wait for the hierarchy to be committed by the VariablesViewController.
+    let committed = promise.defer();
+    variables.oncommit = committed.resolve;
+    yield committed.promise;
+
+    let firstScope = variables.getScopeAtIndex(0);
+    let secondScope = variables.getScopeAtIndex(1);
+    let thirdScope = variables.getScopeAtIndex(2);
+
+    let someVar1 = firstScope.get("a");
+    let someVar2 = secondScope.get("a");
+    let someVar3 = thirdScope.get("a");
+
+    let argsVar1 = firstScope.get("arguments");
+    let argsVar2 = secondScope.get("arguments");
+    let argsVar3 = thirdScope.get("arguments");
+
+    is(someVar1.target.hasAttribute("overridden"), false,
+      "The first 'a' variable should not be marked as being overridden.");
+    is(someVar2.target.hasAttribute("overridden"), true,
+      "The second 'a' variable should be marked as being overridden.");
+    is(someVar3.target.hasAttribute("overridden"), true,
+      "The third 'a' variable should be marked as being overridden.");
+
+    is(argsVar1.target.hasAttribute("overridden"), false,
+      "The first 'arguments' variable should not be marked as being overridden.");
+    is(argsVar2.target.hasAttribute("overridden"), true,
+      "The second 'arguments' variable should be marked as being overridden.");
+    is(argsVar3.target.hasAttribute("overridden"), true,
+      "The third 'arguments' variable should be marked as being overridden.");
+
+    yield resumeDebuggerThenCloseAndFinish(panel);
+  });
+}
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-reexpand-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-reexpand-01.js
@@ -18,16 +18,19 @@ function test() {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gBreakpoints = gDebugger.DebuggerController.Breakpoints;
     gSources = gDebugger.DebuggerView.Sources;
     gVariables = gDebugger.DebuggerView.Variables;
 
+    // Always expand all items between pauses except 'window' variables.
+    gVariables.commitHierarchyIgnoredItems = Object.create(null, { window: { value: true } });
+
     waitForSourceShown(gPanel, ".html")
       .then(addBreakpoint)
       .then(() => ensureThreadClientState(gPanel, "resumed"))
       .then(pauseDebuggee)
       .then(prepareVariablesAndProperties)
       .then(stepInDebuggee)
       .then(testVariablesExpand)
       .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-reexpand-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-reexpand-02.js
@@ -19,17 +19,17 @@ function test() {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gBreakpoints = gDebugger.DebuggerController.Breakpoints;
     gSources = gDebugger.DebuggerView.Sources;
     gVariables = gDebugger.DebuggerView.Variables;
 
-    // Always expand all scopes between pauses.
+    // Always expand all items between pauses.
     gVariables.commitHierarchyIgnoredItems = Object.create(null);
 
     waitForSourceShown(gPanel, ".html")
       .then(addBreakpoint)
       .then(() => ensureThreadClientState(gPanel, "resumed"))
       .then(pauseDebuggee)
       .then(prepareVariablesAndProperties)
       .then(stepInDebuggee)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_scope-variable-2.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger test page</title>
+  </head>
+
+  <body>
+    <script type="text/javascript">
+      function test() {
+        var a = "first scope";
+        firstNest();
+
+        function firstNest() {
+          var a = "second scope";
+          secondNest();
+
+          function secondNest() {
+            var a = "third scope";
+            debugger;
+          }
+        }
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -11,16 +11,17 @@ const Cu = Components.utils;
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const LAZY_EMPTY_DELAY = 150; // ms
 const LAZY_EXPAND_DELAY = 50; // ms
 const LAZY_APPEND_DELAY = 100; // ms
 const LAZY_APPEND_BATCH = 100; // nodes
 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
 const PAGE_SIZE_MAX_JUMPS = 30;
 const SEARCH_ACTION_MAX_DELAY = 300; // ms
+const ITEM_FLASH_DURATION = 300 // ms
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
@@ -112,30 +113,34 @@ VariablesView.prototype = {
     this.addScope()
         .addItem("", { enumerable: true })
         .populate(aObject, { sorted: true });
   },
 
   /**
    * Adds a scope to contain any inspected variables.
    *
+   * This new scope will be considered the parent of any other scope
+   * added afterwards.
+   *
    * @param string aName
    *        The scope's name (e.g. "Local", "Global" etc.).
    * @return Scope
    *         The newly created Scope instance.
    */
   addScope: function(aName = "") {
     this._removeEmptyNotice();
     this._toggleSearchVisibility(true);
 
     let scope = new Scope(this, aName);
     this._store.push(scope);
     this._itemsByElement.set(scope._target, scope);
     this._currHierarchy.set(aName, scope);
     scope.header = !!aName;
+
     return scope;
   },
 
   /**
    * Removes all items from this container.
    *
    * @param number aTimeout [optional]
    *        The number of milliseconds to delay the operation if
@@ -600,46 +605,66 @@ VariablesView.prototype = {
    * @return Scope
    *         The scope if found, undefined if not.
    */
   getScopeAtIndex: function(aIndex) {
     return this._store[aIndex];
   },
 
   /**
-   * Searches for the scope in this container displayed by the specified node.
-   *
-   * @param nsIDOMNode aNode
-   *        The node to search for.
-   * @return Scope
-   *         The matched scope, or null if nothing is found.
-   */
-  getScopeForNode: function(aNode) {
-    let item = this._itemsByElement.get(aNode);
-    // Match only Scopes, not Variables or Properties.
-    if (item && !(item instanceof Variable)) {
-      return item;
-    }
-    return null;
-  },
-
-  /**
    * Recursively searches this container for the scope, variable or property
    * displayed by the specified node.
    *
    * @param nsIDOMNode aNode
    *        The node to search for.
    * @return Scope | Variable | Property
    *         The matched scope, variable or property, or null if nothing is found.
    */
   getItemForNode: function(aNode) {
     return this._itemsByElement.get(aNode);
   },
 
   /**
+   * Gets the scope owning a Variable or Property.
+   *
+   * @param Variable | Property
+   *        The variable or property to retrieven the owner scope for.
+   * @return Scope
+   *         The owner scope.
+   */
+  getOwnerScopeForVariableOrProperty: function(aItem) {
+    if (!aItem) {
+      return null;
+    }
+    // If this is a Scope, return it.
+    if (!(aItem instanceof Variable)) {
+      return aItem;
+    }
+    // If this is a Variable or Property, find its owner scope.
+    if (aItem instanceof Variable && aItem.ownerView) {
+      return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
+    }
+    return null;
+  },
+
+  /**
+   * Gets the parent scopes for a specified Variable or Property.
+   * The returned list will not include the owner scope.
+   *
+   * @param Variable | Property
+   *        The variable or property for which to find the parent scopes.
+   * @return array
+   *         A list of parent Scopes.
+   */
+  getParentScopesForVariableOrProperty: function(aItem) {
+    let scope = this.getOwnerScopeForVariableOrProperty(aItem);
+    return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
+  },
+
+  /**
    * Gets the currently focused scope, variable or property in this view.
    *
    * @return Scope | Variable | Property
    *         The focused scope, variable or property, or null if nothing is found.
    */
   getFocusedItem: function() {
     let focused = this.document.commandDispatcher.focusedElement;
     return this.getItemForNode(focused);
@@ -971,22 +996,25 @@ VariablesView.prototype = {
    * @return nsIDOMWindow
    */
   get window() this._window || (this._window = this.document.defaultView),
 
   _document: null,
   _window: null,
 
   _store: null,
+  _itemsByElement: null,
   _prevHierarchy: null,
   _currHierarchy: null,
+
   _enumVisible: true,
   _nonEnumVisible: true,
   _alignedValues: false,
   _actionsFirst: false,
+
   _parent: null,
   _list: null,
   _searchboxNode: null,
   _searchboxContainer: null,
   _searchboxPlaceholder: "",
   _emptyTextNode: null,
   _emptyTextValue: ""
 };
@@ -1239,16 +1267,17 @@ Scope.prototype = {
       return null;
     }
 
     let child = this._createChild(aName, aDescriptor);
     this._store.set(aName, child);
     this._variablesView._itemsByElement.set(child._target, child);
     this._variablesView._currHierarchy.set(child._absoluteName, child);
     child.header = !!aName;
+
     return child;
   },
 
   /**
    * Adds items for this variable.
    *
    * @param object aItems
    *        An object containing some { name: descriptor } data properties,
@@ -1289,17 +1318,19 @@ Scope.prototype = {
   /**
    * Remove this Scope from its parent and remove all children recursively.
    */
   remove: function() {
     let view = this._variablesView;
     view._store.splice(view._store.indexOf(this), 1);
     view._itemsByElement.delete(this._target);
     view._currHierarchy.delete(this._nameString);
+
     this._target.remove();
+
     for (let variable of this._store.values()) {
       variable.remove();
     }
   },
 
   /**
    * Gets the variable in this container having the specified name.
    *
@@ -1720,17 +1751,18 @@ Scope.prototype = {
   },
 
   /**
    * The click listener for this scope's title.
    */
   _onClick: function(e) {
     if (e.button != 0 ||
         e.target == this._editNode ||
-        e.target == this._deleteNode) {
+        e.target == this._deleteNode ||
+        e.target == this._addPropertyNode) {
       return;
     }
     this.toggle();
     this.focus();
   },
 
   /**
    * Lazily appends a node to this scope's enumerable or non-enumerable
@@ -1931,35 +1963,16 @@ Scope.prototype = {
       this.target.removeAttribute("unmatched");
     } else {
       this._isMatch = false;
       this.target.setAttribute("unmatched", "");
     }
   },
 
   /**
-   * Gets the first search results match in this scope.
-   * @return Variable | Property
-   */
-  get _firstMatch() {
-    for (let [, variable] of this._store) {
-      let match;
-      if (variable._isMatch) {
-        match = variable;
-      } else {
-        match = variable._firstMatch;
-      }
-      if (match) {
-        return match;
-      }
-    }
-    return null;
-  },
-
-  /**
    * Find the first item in the tree of visible items in this item that matches
    * the predicate. Searches in visual order (the order seen by the user).
    * Tests itself, then descends into first the enumerable children and then
    * the non-enumerable children (since they are presented in separate groups).
    *
    * @param function aPredicate
    *        A function that returns true when a match is found.
    * @return Scope | Variable | Property
@@ -2073,21 +2086,22 @@ Scope.prototype = {
   _document: null,
   _window: null,
 
   ownerView: null,
   eval: null,
   switch: null,
   delete: null,
   new: null,
+  preventDisableOnChange: false,
+  preventDescriptorModifiers: false,
+  editableNameTooltip: "",
   editableValueTooltip: "",
-  editableNameTooltip: "",
   editButtonTooltip: "",
   deleteButtonTooltip: "",
-  preventDescriptorModifiers: false,
   contextMenuId: "",
   separatorStr: "",
 
   _store: null,
   _enumItems: null,
   _nonEnumItems: null,
   _fetched: false,
   _retrieved: false,
@@ -2173,17 +2187,19 @@ Variable.prototype = Heritage.extend(Sco
 
   /**
    * Remove this Variable from its parent and remove all children recursively.
    */
   remove: function() {
     this.ownerView._store.delete(this._nameString);
     this._variablesView._itemsByElement.delete(this._target);
     this._variablesView._currHierarchy.delete(this._absoluteName);
+
     this._target.remove();
+
     for (let property of this._store.values()) {
       property.remove();
     }
   },
 
   /**
    * Populates this variable to contain all the properties of an object.
    *
@@ -2351,16 +2367,47 @@ Variable.prototype = Heritage.extend(Sco
     this._valueString = VariablesView.getString(aGrip, true);
     this._valueClassName = VariablesView.getClass(aGrip);
 
     this._valueLabel.classList.add(this._valueClassName);
     this._valueLabel.setAttribute("value", this._valueString);
   },
 
   /**
+   * Marks this variable as overridden.
+   *
+   * @param boolean aFlag
+   *        Whether this variable is overridden or not.
+   */
+  setOverridden: function(aFlag) {
+    if (aFlag) {
+      this._target.setAttribute("overridden", "");
+    } else {
+      this._target.removeAttribute("overridden");
+    }
+  },
+
+  /**
+   * Briefly flashes this variable.
+   *
+   * @param number aDuration [optional]
+   *        An optional flash animation duration.
+   */
+  flash: function(aDuration = ITEM_FLASH_DURATION) {
+    let fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
+    let fadeOutDelay = fadeInDelay + aDuration;
+
+    setNamedTimeout("vview-flash-in" + this._absoluteName,
+      fadeInDelay, () => this._target.setAttribute("changed", ""));
+
+    setNamedTimeout("vview-flash-out" + this._absoluteName,
+      fadeOutDelay, () => this._target.removeAttribute("changed"));
+  },
+
+  /**
    * Initializes this variable's id, view and binds event listeners.
    *
    * @param string aName
    *        The variable's name.
    * @param object aDescriptor
    *        The variable's descriptor.
    */
   _init: function(aName, aDescriptor) {
@@ -2400,17 +2447,17 @@ Variable.prototype = Heritage.extend(Sco
    * Creates the necessary nodes for this variable.
    */
   _displayVariable: function() {
     let document = this.document;
     let descriptor = this._initialDescriptor;
 
     let separatorLabel = this._separatorLabel = document.createElement("label");
     separatorLabel.className = "plain separator";
-    separatorLabel.setAttribute("value", this.ownerView.separatorStr);
+    separatorLabel.setAttribute("value", this.ownerView.separatorStr + " ");
 
     let valueLabel = this._valueLabel = document.createElement("label");
     valueLabel.className = "plain value";
     valueLabel.setAttribute("crop", "center");
 
     let spacer = this._spacer = document.createElement("spacer");
     spacer.setAttribute("optional-visibility", "");
     spacer.setAttribute("flex", "1");
@@ -2423,16 +2470,18 @@ Variable.prototype = Heritage.extend(Sco
       this.hideArrow();
     }
 
     // If no value will be displayed, we don't need the separator.
     if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
       separatorLabel.hidden = true;
     }
 
+    // If this is a getter/setter property, create two child pseudo-properties
+    // called "get" and "set" that display the corresponding functions.
     if (descriptor.get || descriptor.set) {
       separatorLabel.hidden = true;
       valueLabel.hidden = true;
 
       // Changing getter/setter names is never allowed.
       this.switch = null;
 
       // Getter/setter properties require special handling when it comes to
@@ -2475,25 +2524,26 @@ Variable.prototype = Heritage.extend(Sco
 
     if (ownerView.delete) {
       let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
       deleteNode.className = "plain variables-view-delete";
       deleteNode.addEventListener("click", this._onDelete.bind(this), false);
       this._title.appendChild(deleteNode);
     }
 
-    let { actionsFirst } = this._variablesView;
-    if (ownerView.new || actionsFirst) {
+    if (ownerView.new) {
       let addPropertyNode = this._addPropertyNode = this.document.createElement("toolbarbutton");
       addPropertyNode.className = "plain variables-view-add-property";
       addPropertyNode.addEventListener("mousedown", this._onAddProperty.bind(this), false);
-      if (actionsFirst && VariablesView.isPrimitive(descriptor)) {
+      this._title.appendChild(addPropertyNode);
+
+      // Can't add properties to primitive values, hide the node in those cases.
+      if (VariablesView.isPrimitive(descriptor)) {
         addPropertyNode.setAttribute("invisible", "");
       }
-      this._title.appendChild(addPropertyNode);
     }
 
     if (ownerView.contextMenuId) {
       this._title.setAttribute("context", ownerView.contextMenuId);
     }
 
     if (ownerView.preventDescriptorModifiers) {
       return;
@@ -2549,21 +2599,22 @@ Variable.prototype = Heritage.extend(Sco
     }
 
     let tooltip = this.document.createElement("tooltip");
     tooltip.id = "tooltip-" + this._idString;
     tooltip.setAttribute("orient", "horizontal");
 
     let labels = [
       "configurable", "enumerable", "writable",
-      "frozen", "sealed", "extensible", "WebIDL"];
-
-    for (let label of labels) {
+      "frozen", "sealed", "extensible", "overridden", "WebIDL"];
+
+    for (let type of labels) {
       let labelElement = this.document.createElement("label");
-      labelElement.setAttribute("value", label);
+      labelElement.className = type;
+      labelElement.setAttribute("value", STR.GetStringFromName(type + "Tooltip"));
       tooltip.appendChild(labelElement);
     }
 
     this._target.appendChild(tooltip);
     this._target.setAttribute("tooltip", tooltip.id);
 
     if (this._editNode && ownerView.eval) {
       this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
@@ -2618,16 +2669,17 @@ Variable.prototype = Heritage.extend(Sco
       if (!descriptor.value.extensible) {
         target.setAttribute("non-extensible", "");
       }
     }
 
     if (descriptor && "getterValue" in descriptor) {
       target.setAttribute("safe-getter", "");
     }
+
     if (name == "this") {
       target.setAttribute("self", "");
     }
     else if (name == "<exception>") {
       target.setAttribute("exception", "");
     }
     else if (name == "<return>") {
       target.setAttribute("return", "");
@@ -2769,18 +2821,18 @@ Variable.prototype = Heritage.extend(Sco
       }
     }, e);
   },
 
   _symbolicName: "",
   _absoluteName: "",
   _initialDescriptor: null,
   _separatorLabel: null,
+  _valueLabel: null,
   _spacer: null,
-  _valueLabel: null,
   _editNode: null,
   _deleteNode: null,
   _addPropertyNode: null,
   _tooltip: null,
   _valueGrip: null,
   _valueString: "",
   _valueClassName: "",
   _prevExpandable: false,
@@ -2872,80 +2924,112 @@ VariablesView.prototype.createHierarchy 
   this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
 };
 
 /**
  * Briefly flash the variables that changed between the previous and current
  * scope/variable/property hierarchies and reopen previously expanded nodes.
  */
 VariablesView.prototype.commitHierarchy = function() {
-  let prevHierarchy = this._prevHierarchy;
-  let currHierarchy = this._currHierarchy;
-
-  for (let [absoluteName, currVariable] of currHierarchy) {
-    // Ignore variables which were already commmitted.
-    if (currVariable._committed) {
-      continue;
-    }
+  for (let [, currItem] of this._currHierarchy) {
     // Avoid performing expensive operations.
-    if (this.commitHierarchyIgnoredItems[currVariable._nameString]) {
+    if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
       continue;
     }
-
-    // Try to get the previous instance of the inspected variable to
-    // determine the difference in state.
-    let prevVariable = prevHierarchy.get(absoluteName);
-    let expanded = false;
-    let changed = false;
-
-    // If the inspected variable existed in a previous hierarchy, check if
-    // the displayed value (a representation of the grip) has changed and if
-    // it was previously expanded.
-    if (prevVariable) {
-      expanded = prevVariable._isExpanded;
-
-      // Only analyze Variables and Properties for displayed value changes.
-      if (currVariable instanceof Variable) {
-        changed = prevVariable._valueString != currVariable._valueString;
-      }
+    let overridden = this.isOverridden(currItem);
+    if (overridden) {
+      currItem.setOverridden(true);
+    }
+    let expanded = !currItem._committed && this.wasExpanded(currItem);
+    if (expanded) {
+      currItem.expand();
     }
-
-    // Make sure this variable is not handled in ulteror commits for the
-    // same hierarchy.
-    currVariable._committed = true;
-
-    // Re-expand the variable if not previously collapsed.
-    if (expanded) {
-      currVariable.expand();
+    let changed = !currItem._committed && this.hasChanged(currItem);
+    if (changed) {
+      currItem.flash();
     }
-    // This variable was either not changed or removed, no need to continue.
-    if (!changed) {
-      continue;
-    }
-
-    // Apply an attribute determining the flash type and duration.
-    // Dispatch this action after all the nodes have been drawn, so that
-    // the transition efects can take place.
-    this.window.setTimeout(function(aTarget) {
-      aTarget.addEventListener("transitionend", function onEvent() {
-        aTarget.removeEventListener("transitionend", onEvent, false);
-        aTarget.removeAttribute("changed");
-      }, false);
-      aTarget.setAttribute("changed", "");
-    }.bind(this, currVariable.target), this.lazyEmptyDelay + 1);
+    currItem._committed = true;
+  }
+  if (this.oncommit) {
+    this.oncommit(this);
   }
 };
 
 // Some variables are likely to contain a very large number of properties.
 // It would be a bad idea to re-expand them or perform expensive operations.
-VariablesView.prototype.commitHierarchyIgnoredItems = Object.create(null, {
-  "window": { value: true }
+VariablesView.prototype.commitHierarchyIgnoredItems = Heritage.extend(null, {
+  "window": true,
+  "this": true
 });
 
 /**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ *        The item to verify.
+ * @return boolean
+ *         Whether the item was expanded.
+ */
+VariablesView.prototype.wasExpanded = function(aItem) {
+  if (!(aItem instanceof Scope)) {
+    return false;
+  }
+  let prevItem = this._prevHierarchy.get(aItem._absoluteName || aItem._nameString);
+  return prevItem ? prevItem._isExpanded : false;
+};
+
+/**
+ * Checks if the an item's displayed value (a representation of the grip)
+ * has changed, if it existed in a previous hierarchy.
+ *
+ * @param Variable | Property aItem
+ *        The item to verify.
+ * @return boolean
+ *         Whether the item has changed.
+ */
+VariablesView.prototype.hasChanged = function(aItem) {
+  // Only analyze Variables and Properties for displayed value changes.
+  // Scopes are just collections of Variables and Properties and
+  // don't have a "value", so they can't change.
+  if (!(aItem instanceof Variable)) {
+    return false;
+  }
+  let prevItem = this._prevHierarchy.get(aItem._absoluteName);
+  return prevItem ? prevItem._valueString != aItem._valueString : false;
+};
+
+/**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ *        The item to verify.
+ * @return boolean
+ *         Whether the item was expanded.
+ */
+VariablesView.prototype.isOverridden = function(aItem) {
+  // Only analyze Variables for being overridden in different Scopes.
+  if (!(aItem instanceof Variable) || aItem instanceof Property) {
+    return false;
+  }
+  let currVariableName = aItem._nameString;
+  let parentScopes = this.getParentScopesForVariableOrProperty(aItem);
+
+  for (let otherScope of parentScopes) {
+    for (let [otherVariableName] of otherScope) {
+      if (otherVariableName == currVariableName) {
+        return true;
+      }
+    }
+  }
+  return false;
+};
+
+/**
  * Returns true if the descriptor represents an undefined, null or
  * primitive value.
  *
  * @param object aDescriptor
  *        The variable's descriptor.
  */
 VariablesView.isPrimitive = function(aDescriptor) {
   // For accessor property descriptors, the getter and setter need to be
@@ -3242,19 +3326,16 @@ Editable.prototype = {
       e.stopPropagation();
     }
 
     // Create a texbox input element which will be shown in the current
     // element's specified label location.
     let input = this._input = this._variable.document.createElement("textbox");
     input.className = "plain " + this.className;
     input.setAttribute("value", initialString);
-    if (!this._variable._variablesView.alignedValues) {
-      input.setAttribute("flex", "1");
-    }
 
     // Replace the specified label with a textbox input element.
     label.parentNode.replaceChild(input, label);
     this._variable._variablesView.boxObject.ensureElementIsVisible(input);
     input.select();
 
     // When the value is a string (displayed as "value"), then we probably want
     // to change it to another string in the textbox, so to avoid typing the ""
--- a/browser/devtools/shared/widgets/VariablesViewController.jsm
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -347,45 +347,53 @@ VariablesViewController.prototype = {
       return;
     }
 
     // If the source is a long string then show the arrow.
     if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
       aTarget.showArrow();
     }
 
-    // Make sure that properties are always available on expansion.
-    aTarget.onexpand = () => this.expand(aTarget, aSource);
+    if (aSource.type == "block" || aSource.type == "function") {
+      // Block and function environments already contain scope arguments and
+      // corresponding variables as bindings.
+      this.populate(aTarget, aSource);
+    } else {
+      // Make sure that properties are always available on expansion.
+      aTarget.onexpand = () => this.populate(aTarget, aSource);
 
-    // Some variables are likely to contain a very large number of properties.
-    // It's a good idea to be prepared in case of an expansion.
-    if (aTarget.shouldPrefetch) {
-      aTarget.addEventListener("mouseover", aTarget.onexpand, false);
+      // Some variables are likely to contain a very large number of properties.
+      // It's a good idea to be prepared in case of an expansion.
+      if (aTarget.shouldPrefetch) {
+        aTarget.addEventListener("mouseover", aTarget.onexpand, false);
+      }
     }
 
     // Register all the actors that this controller now depends on.
     for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
       if (WebConsoleUtils.isActorGrip(grip)) {
         this._actors.add(grip.actor);
       }
     }
   },
 
   /**
    * Adds properties to a Scope, Variable, or Property in the view. Triggered
    * when a scope is expanded or certain variables are hovered.
    *
+   * This does not expand the target, it only populates it.
+   *
    * @param Scope aTarget
    *        The Scope to be expanded.
    * @param object aSource
    *        The source to use to populate the target.
    * @return Promise
    *         The promise that is resolved once the target has been expanded.
    */
-  expand: function(aTarget, aSource) {
+  populate: function(aTarget, aSource) {
     // Fetch the variables only once.
     if (aTarget._fetched) {
       return aTarget._fetched;
     }
     // Make sure the source grip is available.
     if (!aSource) {
       return promise.reject(new Error("No actor grip was given for the variable."));
     }
@@ -505,26 +513,27 @@ VariablesViewController.prototype = {
     this._setEvaluationMacros(aConfiguration);
     this.view.empty();
 
     let scope = this.view.addScope(aOptions.label);
     scope.expanded = true; // Expand the scope by default.
     scope.locked = true; // Prevent collpasing the scope.
 
     let variable = scope.addItem("", { enumerable: true });
-    let expanded;
+    let populated;
 
     if (aOptions.objectActor) {
-      expanded = this.expand(variable, aOptions.objectActor);
+      populated = this.populate(variable, aOptions.objectActor);
+      variable.expand();
     } else if (aOptions.rawObject) {
       variable.populate(aOptions.rawObject, { expanded: true });
-      expanded = promise.resolve();
+      populated = promise.resolve();
     }
 
-    return { variable: variable, expanded: expanded };
+    return { variable: variable, expanded: populated };
   },
 };
 
 
 /**
  * Attaches a VariablesViewController to a VariablesView if it doesn't already
  * have one.
  *
--- a/browser/devtools/shared/widgets/widgets.css
+++ b/browser/devtools/shared/widgets/widgets.css
@@ -51,28 +51,29 @@
 }
 
 .variables-view-scope[untitled] > .title,
 .variable-or-property[untitled] > .title,
 .variable-or-property[unmatched] > .title {
   display: none;
 }
 
-.variable-or-property:not([safe-getter]) > tooltip > label[value=WebIDL],
-.variable-or-property:not([non-extensible]) > tooltip > label[value=extensible],
-.variable-or-property:not([frozen]) > tooltip > label[value=frozen],
-.variable-or-property:not([sealed]) > tooltip > label[value=sealed] {
+.variable-or-property:not([safe-getter]) > tooltip > label.WebIDL,
+.variable-or-property:not([overridden]) > tooltip > label.overridden,
+.variable-or-property:not([non-extensible]) > tooltip > label.extensible,
+.variable-or-property:not([frozen]) > tooltip > label.frozen,
+.variable-or-property:not([sealed]) > tooltip > label.sealed {
   display: none;
 }
 
 *:not(:hover) .variables-view-delete,
 *:not(:hover) .variables-view-add-property {
   visibility: hidden;
 }
 
 .variables-view-delete > .toolbarbutton-text,
 .variables-view-add-property > .toolbarbutton-text {
   display: none;
 }
 
-.variables-view-container[aligned-values] .title > [optional-visibility] {
+.variables-view-container[aligned-values] [optional-visibility] {
   display: none;
 }
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -228,16 +228,33 @@ variablesEditableValueTooltip=Click to c
 # LOCALIZATION NOTE (variablesCloseButtonTooltip): The text that is displayed
 # in the variables list on an item which can be removed.
 variablesCloseButtonTooltip=Click to remove
 
 # LOCALIZATION NOTE (variablesEditButtonTooltip): The text that is displayed
 # in the variables list on a getter or setter which can be edited.
 variablesEditButtonTooltip=Click to set value
 
+# LOCALIZATION NOTE (configurable|...|Tooltip): The text that is displayed
+# in the variables list on certain variables or properties as tooltips.
+# Expanations of what these represent can be found at the following links:
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen
+# https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed
+# It's probably best to keep these in English.
+configurableTooltip=configurable
+enumerableTooltip=enumerable
+writableTooltip=writable
+frozenTooltip=frozen
+sealedTooltip=sealed
+extensibleTooltip=extensible
+overriddenTooltip=overridden
+WebIDLTooltip=WebIDL
+
 # 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/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -441,60 +441,79 @@
   color: #fff;
 }
 
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
-/* Generic traits applied to both variables and properties */
-
-.variable-or-property {
-  transition: background 1s ease-in-out, color 1s ease-in-out;
-}
-
-.variable-or-property[changed] {
-  color: black;
-  transition-duration: .4s;
-}
-
-.variable-or-property > .title > .value {
-  -moz-box-flex: 1;
-  -moz-padding-start: 6px;
-  -moz-padding-end: 4px;
-}
-
-.variable-or-property[editable] > .title > .value {
-  cursor: text;
-}
-
-.variable-or-property:not([untitled]) > .variables-view-element-details {
-  -moz-margin-start: 10px;
-}
-
-/* Custom variables and properties traits */
+/* Generic variables traits */
 
 .variables-view-variable {
   -moz-margin-start: 1px;
   -moz-margin-end: 1px;
 }
 
 .variables-view-variable:not(:last-child) {
   border-bottom: 1px solid rgba(128, 128, 128, .15);
 }
 
 .variables-view-variable > .title > .name {
   font-weight: 600;
 }
 
+/* Generic variables *and* properties traits */
+
 .variable-or-property:focus > .title > label {
   color: inherit !important;
 }
 
+.variable-or-property > .title > .value {
+  -moz-box-flex: 1;
+}
+
+.variable-or-property:not([untitled]) > .variables-view-element-details {
+  -moz-margin-start: 10px;
+}
+
+/* Traits applied when variables or properties are changed or overridden */
+
+.variable-or-property:not([overridden]) {
+  transition: background 1s ease-in-out;
+}
+
+.variable-or-property:not([overridden])[changed] {
+  transition-duration: .4s;
+}
+
+.variable-or-property[overridden] {
+  background: rgba(128,128,128,0.05);
+}
+
+.variable-or-property[overridden] .title > label {
+  /* Cross out the title for this variable and all child properties. */
+  font-style: italic;
+  text-decoration: line-through;
+  border-bottom: none !important;
+  color: rgba(128,128,128,0.9);
+  opacity: 0.7;
+}
+
+/* Traits applied when variables or properties are editable */
+
+.variable-or-property[editable] > .title > .value {
+  cursor: text;
+}
+
+.variable-or-property[overridden] .title > .value {
+  /* Disallow editing this variable and all child properties. */
+  pointer-events: none;
+}
+
 /* Custom configurable/enumerable/writable or frozen/sealed/extensible
  * variables and properties */
 
 .variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
   opacity: 0.6;
 }
 
 .variable-or-property[non-configurable] > .title > .name {
@@ -562,25 +581,30 @@
 }
 
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
-.variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
-.variable-or-property[non-configurable] > tooltip > label[value=configurable],
-.variable-or-property[non-writable] > tooltip > label[value=writable],
-.variable-or-property[non-extensible] > tooltip > label[value=extensible] {
+.variable-or-property[non-enumerable] > tooltip > label.enumerable,
+.variable-or-property[non-configurable] > tooltip > label.configurable,
+.variable-or-property[non-writable] > tooltip > label.writable,
+.variable-or-property[non-extensible] > tooltip > label.extensible {
   color: #800;
   text-decoration: line-through;
 }
 
-.variable-or-property[safe-getter] > tooltip > label[value=WebIDL] {
+.variable-or-property[overridden] > tooltip > label.overridden {
+  -moz-padding-start: 4px;
+  -moz-border-start: 1px dotted #000;
+}
+
+.variable-or-property[safe-getter] > tooltip > label.WebIDL {
   -moz-padding-start: 4px;
   -moz-border-start: 1px dotted #000;
   color: #080;
 }
 
 /* Variables and properties editing */
 
 .variables-view-delete {
@@ -605,29 +629,30 @@
 
 .variables-view-throbber {
   background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
   width: 16px;
   height: 16px;
 }
 
 .element-value-input {
-  -moz-margin-start: 4px !important;
+  -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
 }
 
 .element-name-input {
   -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
   font-weight: 600;
 }
 
 .element-value-input,
 .element-name-input {
   border: 1px solid rgba(128, 128, 128, .5) !important;
+  border-radius: 0;
   color: inherit;
 }
 
 /* Variables and properties searching */
 
 .variables-view-searchinput {
   min-height: 24px;
 }
--- a/browser/themes/osx/devtools/widgets.css
+++ b/browser/themes/osx/devtools/widgets.css
@@ -435,60 +435,79 @@
   color: #fff;
 }
 
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
-/* Generic traits applied to both variables and properties */
-
-.variable-or-property {
-  transition: background 1s ease-in-out, color 1s ease-in-out;
-}
-
-.variable-or-property[changed] {
-  color: black;
-  transition-duration: .4s;
-}
-
-.variable-or-property > .title > .value {
-  -moz-box-flex: 1;
-  -moz-padding-start: 6px;
-  -moz-padding-end: 4px;
-}
-
-.variable-or-property[editable] > .title > .value {
-  cursor: text;
-}
-
-.variable-or-property:not([untitled]) > .variables-view-element-details {
-  -moz-margin-start: 10px;
-}
-
-/* Custom variables and properties traits */
+/* Generic variables traits */
 
 .variables-view-variable {
   -moz-margin-start: 1px;
   -moz-margin-end: 1px;
 }
 
 .variables-view-variable:not(:last-child) {
   border-bottom: 1px solid rgba(128, 128, 128, .15);
 }
 
 .variables-view-variable > .title > .name {
   font-weight: 600;
 }
 
+/* Generic variables *and* properties traits */
+
 .variable-or-property:focus > .title > label {
   color: inherit !important;
 }
 
+.variable-or-property > .title > .value {
+  -moz-box-flex: 1;
+}
+
+.variable-or-property:not([untitled]) > .variables-view-element-details {
+  -moz-margin-start: 10px;
+}
+
+/* Traits applied when variables or properties are changed or overridden */
+
+.variable-or-property:not([overridden]) {
+  transition: background 1s ease-in-out;
+}
+
+.variable-or-property:not([overridden])[changed] {
+  transition-duration: .4s;
+}
+
+.variable-or-property[overridden] {
+  background: rgba(128,128,128,0.05);
+}
+
+.variable-or-property[overridden] .title > label {
+  /* Cross out the title for this variable and all child properties. */
+  font-style: italic;
+  text-decoration: line-through;
+  border-bottom: none !important;
+  color: rgba(128,128,128,0.9);
+  opacity: 0.7;
+}
+
+/* Traits applied when variables or properties are editable */
+
+.variable-or-property[editable] > .title > .value {
+  cursor: text;
+}
+
+.variable-or-property[overridden] .title > .value {
+  /* Disallow editing this variable and all child properties. */
+  pointer-events: none;
+}
+
 /* Custom configurable/enumerable/writable or frozen/sealed/extensible
  * variables and properties */
 
 .variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
   opacity: 0.6;
 }
 
 .variable-or-property[non-configurable] > .title > .name {
@@ -556,25 +575,30 @@
 }
 
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
-.variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
-.variable-or-property[non-configurable] > tooltip > label[value=configurable],
-.variable-or-property[non-writable] > tooltip > label[value=writable],
-.variable-or-property[non-extensible] > tooltip > label[value=extensible] {
+.variable-or-property[non-enumerable] > tooltip > label.enumerable,
+.variable-or-property[non-configurable] > tooltip > label.configurable,
+.variable-or-property[non-writable] > tooltip > label.writable,
+.variable-or-property[non-extensible] > tooltip > label.extensible {
   color: #800;
   text-decoration: line-through;
 }
 
-.variable-or-property[safe-getter] > tooltip > label[value=WebIDL] {
+.variable-or-property[overridden] > tooltip > label.overridden {
+  -moz-padding-start: 4px;
+  -moz-border-start: 1px dotted #000;
+}
+
+.variable-or-property[safe-getter] > tooltip > label.WebIDL {
   -moz-padding-start: 4px;
   -moz-border-start: 1px dotted #000;
   color: #080;
 }
 
 /* Variables and properties editing */
 
 .variables-view-delete {
@@ -599,29 +623,30 @@
 
 .variables-view-throbber {
   background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
   width: 16px;
   height: 16px;
 }
 
 .element-value-input {
-  -moz-margin-start: 4px !important;
+  -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
 }
 
 .element-name-input {
   -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
   font-weight: 600;
 }
 
 .element-value-input,
 .element-name-input {
   border: 1px solid rgba(128, 128, 128, .5) !important;
+  border-radius: 0;
   color: inherit;
 }
 
 /* Variables and properties searching */
 
 .variables-view-searchinput {
   min-height: 24px;
 }
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -47,17 +47,17 @@
   background: #26394D;
 }
 
 .theme-bg-darker {
   background-color: rgba(0,0,0,0.5);
 }
 
 .theme-bg-contrast,
-.variable-or-property[changed] { /* contrast bg color to attract attention on a container */
+.variable-or-property:not([overridden])[changed] { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
 .theme-link,
 .cm-s-mozilla .cm-link { /* blue */
   color: #3689b2;
 }
 
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -47,17 +47,17 @@
   background-color: #CCC;
 }
 
 .theme-bg-darker {
   background: #EFEFEF;
 }
 
 .theme-bg-contrast,
-.variable-or-property[changed] { /* contrast bg color to attract attention on a container */
+.variable-or-property:not([overridden])[changed] { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
 .theme-link,
 .cm-s-mozilla .cm-link { /* blue */
   color: hsl(208,56%,40%);
 }
 
@@ -292,9 +292,9 @@ div.CodeMirror span.eval-text {
   color: black;
   border-bottom: 1px solid #d9e1e8;
 }
 
 .theme-tooltip-panel .devtools-tooltip-simple-text:last-child {
   border-bottom: 0;
 }
 
-%include toolbars.inc.css
\ No newline at end of file
+%include toolbars.inc.css
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -438,60 +438,79 @@
   color: #fff;
 }
 
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
-/* Generic traits applied to both variables and properties */
-
-.variable-or-property {
-  transition: background 1s ease-in-out, color 1s ease-in-out;
-}
-
-.variable-or-property[changed] {
-  color: black;
-  transition-duration: .4s;
-}
-
-.variable-or-property > .title > .value {
-  -moz-box-flex: 1;
-  -moz-padding-start: 6px;
-  -moz-padding-end: 4px;
-}
-
-.variable-or-property[editable] > .title > .value {
-  cursor: text;
-}
-
-.variable-or-property:not([untitled]) > .variables-view-element-details {
-  -moz-margin-start: 10px;
-}
-
-/* Custom variables and properties traits */
+/* Generic variables traits */
 
 .variables-view-variable {
   -moz-margin-start: 1px;
   -moz-margin-end: 1px;
 }
 
 .variables-view-variable:not(:last-child) {
   border-bottom: 1px solid rgba(128, 128, 128, .15);
 }
 
 .variables-view-variable > .title > .name {
   font-weight: 600;
 }
 
+/* Generic variables *and* properties traits */
+
 .variable-or-property:focus > .title > label {
   color: inherit !important;
 }
 
+.variable-or-property > .title > .value {
+  -moz-box-flex: 1;
+}
+
+.variable-or-property:not([untitled]) > .variables-view-element-details {
+  -moz-margin-start: 10px;
+}
+
+/* Traits applied when variables or properties are changed or overridden */
+
+.variable-or-property:not([overridden]) {
+  transition: background 1s ease-in-out;
+}
+
+.variable-or-property:not([overridden])[changed] {
+  transition-duration: .4s;
+}
+
+.variable-or-property[overridden] {
+  background: rgba(128,128,128,0.05);
+}
+
+.variable-or-property[overridden] .title > label {
+  /* Cross out the title for this variable and all child properties. */
+  font-style: italic;
+  text-decoration: line-through;
+  border-bottom: none !important;
+  color: rgba(128,128,128,0.9);
+  opacity: 0.7;
+}
+
+/* Traits applied when variables or properties are editable */
+
+.variable-or-property[editable] > .title > .value {
+  cursor: text;
+}
+
+.variable-or-property[overridden] .title > .value {
+  /* Disallow editing this variable and all child properties. */
+  pointer-events: none;
+}
+
 /* Custom configurable/enumerable/writable or frozen/sealed/extensible
  * variables and properties */
 
 .variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
   opacity: 0.6;
 }
 
 .variable-or-property[non-configurable] > .title > .name {
@@ -559,25 +578,30 @@
 }
 
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
-.variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
-.variable-or-property[non-configurable] > tooltip > label[value=configurable],
-.variable-or-property[non-writable] > tooltip > label[value=writable],
-.variable-or-property[non-extensible] > tooltip > label[value=extensible] {
+.variable-or-property[non-enumerable] > tooltip > label.enumerable,
+.variable-or-property[non-configurable] > tooltip > label.configurable,
+.variable-or-property[non-writable] > tooltip > label.writable,
+.variable-or-property[non-extensible] > tooltip > label.extensible {
   color: #800;
   text-decoration: line-through;
 }
 
-.variable-or-property[safe-getter] > tooltip > label[value=WebIDL] {
+.variable-or-property[overridden] > tooltip > label.overridden {
+  -moz-padding-start: 4px;
+  -moz-border-start: 1px dotted #000;
+}
+
+.variable-or-property[safe-getter] > tooltip > label.WebIDL {
   -moz-padding-start: 4px;
   -moz-border-start: 1px dotted #000;
   color: #080;
 }
 
 /* Variables and properties editing */
 
 .variables-view-delete {
@@ -602,29 +626,30 @@
 
 .variables-view-throbber {
   background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
   width: 16px;
   height: 16px;
 }
 
 .element-value-input {
-  -moz-margin-start: 4px !important;
+  -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
 }
 
 .element-name-input {
   -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
   font-weight: 600;
 }
 
 .element-value-input,
 .element-name-input {
   border: 1px solid rgba(128, 128, 128, .5) !important;
+  border-radius: 0;
   color: inherit;
 }
 
 /* Variables and properties searching */
 
 .variables-view-searchinput {
   min-height: 24px;
 }