merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 07 Oct 2015 11:44:06 +0200
changeset 266347 d1c5a7c5b4331ee9ea5443de893fcfd0a5b80e2a
parent 266334 eb95242f2414246cf281041ccc70df936702cb9b (current diff)
parent 266346 624f967efe43130cf4a74bebad06179620be5317 (diff)
child 266348 bfb9436756a0df8078460bd3520ebc9dde3dbe68
push id29488
push usercbook@mozilla.com
push dateWed, 07 Oct 2015 09:48:25 +0000
treeherdermozilla-central@d1c5a7c5b433 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone44.0a1
first release with
nightly linux32
d1c5a7c5b433 / 44.0a1 / 20151007030205 / files
nightly linux64
d1c5a7c5b433 / 44.0a1 / 20151007030205 / files
nightly mac
d1c5a7c5b433 / 44.0a1 / 20151007030205 / files
nightly win32
d1c5a7c5b433 / 44.0a1 / 20151007030205 / files
nightly win64
d1c5a7c5b433 / 44.0a1 / 20151007030205 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
devtools/server/tests/browser/browser_animation_actors_01.js
devtools/server/tests/browser/browser_animation_actors_02.js
devtools/server/tests/browser/browser_animation_actors_03.js
devtools/server/tests/browser/browser_animation_actors_04.js
devtools/server/tests/browser/browser_animation_actors_06.js
devtools/server/tests/browser/browser_animation_actors_07.js
devtools/server/tests/browser/browser_animation_actors_08.js
devtools/server/tests/browser/browser_animation_actors_09.js
devtools/server/tests/browser/browser_animation_actors_10.js
devtools/server/tests/browser/browser_animation_actors_11.js
devtools/server/tests/browser/browser_animation_actors_12.js
devtools/server/tests/browser/browser_animation_actors_13.js
devtools/server/tests/browser/browser_animation_actors_14.js
devtools/server/tests/browser/browser_animation_actors_15.js
devtools/server/tests/browser/browser_animation_actors_16.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1374,17 +1374,17 @@ pref("devtools.inspector.imagePreviewToo
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
 // Show all native anonymous content (like controls in <video> tags)
 pref("devtools.inspector.showAllAnonymousContent", false);
 // Enable the MDN docs tooltip
 pref("devtools.inspector.mdnDocsTooltip.enabled", true);
 
 // DevTools default color unit
-pref("devtools.defaultColorUnit", "hex");
+pref("devtools.defaultColorUnit", "authored");
 
 // Enable the Responsive UI tool
 pref("devtools.responsiveUI.no-reload-notification", false);
 
 // Enable the Debugger
 pref("devtools.debugger.enabled", true);
 pref("devtools.debugger.chrome-debugging-host", "localhost");
 pref("devtools.debugger.chrome-debugging-port", 6080);
--- a/browser/base/content/social-content.js
+++ b/browser/base/content/social-content.js
@@ -80,16 +80,29 @@ SocialErrorListener = {
           failure = aStatus != Components.results.NS_OK;
         }
       }
     }
 
     // Calling cancel() will raise some OnStateChange notifications by itself,
     // so avoid doing that more than once
     if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
+      // if tp is enabled and we get a failure, ignore failures (ie. STATE_STOP)
+      // on child resources since they *may* have been blocked. We don't have an
+      // easy way to know if a particular url is blocked by TP, only that
+      // something was.
+      if (docShell.hasTrackingContentBlocked) {
+        let frame = docShell.chromeEventHandler;
+        let src = frame.getAttribute("src");
+        if (aRequest && aRequest.name != src) {
+          Cu.reportError("SocialErrorListener ignoring blocked content error for " + aRequest.name);
+          return;
+        }
+      }
+
       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
       this.setErrorPage();
     }
   },
 
   onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
     if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -264,16 +264,17 @@ html[dir="rtl"] .conversation-toolbar-bt
 }
 
 .call-action-group .btn-group-chevron,
 .call-action-group .btn-group {
   width: 100%;
 }
 
 .call-action-group > .invite-button {
+  cursor: pointer;
   margin: 0 4px;
   position: relative;
 }
 
 .call-action-group > .invite-button > img {
   background-color: #00a9dc;
   border-radius: 100%;
   height: 28px;
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -64,16 +64,20 @@ values from browser.dtd.  -->
   -  This label is visible in the options panel. -->
 <!ENTITY options.defaultColorUnit.label "Default color unit">
 
 <!-- LOCALIZATION NOTE (options.defaultColorUnit.accesskey): This is the access
   -  key for a dropdown list that controls the default color unit used in the
   -  inspector. This is visible in the options panel. -->
 <!ENTITY options.defaultColorUnit.accesskey "U">
 
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.authored): This is used in the
+  -  'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.authored "As Authored">
+
 <!-- LOCALIZATION NOTE (options.defaultColorUnit.hex): This is used in the
   -  'Default color unit' dropdown list and is visible in the options panel. -->
 <!ENTITY options.defaultColorUnit.hex "Hex">
 
 <!-- LOCALIZATION NOTE (options.defaultColorUnit.hsl): This is used in the
   -  'Default color unit' dropdown list and is visible in the options panel. -->
 <!ENTITY options.defaultColorUnit.hsl "HSL(A)">
 
--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -84,17 +84,19 @@ var getServerTraits = Task.async(functio
       method: "setCurrentTime" },
     { name: "hasMutationEvents", actor: "animations",
      method: "stopAnimationPlayerUpdates" },
     { name: "hasSetPlaybackRate", actor: "animationplayer",
       method: "setPlaybackRate" },
     { name: "hasTargetNode", actor: "domwalker",
       method: "getNodeFromActor" },
     { name: "hasSetCurrentTimes", actor: "animations",
-      method: "setCurrentTimes" }
+      method: "setCurrentTimes" },
+    { name: "hasGetFrames", actor: "animationplayer",
+      method: "getFrames" }
   ];
 
   let traits = {};
   for (let {name, actor, method} of config) {
     traits[name] = yield target.actorHasMethod(actor, method);
   }
 
   return traits;
--- a/devtools/client/framework/toolbox-options.xul
+++ b/devtools/client/framework/toolbox-options.xul
@@ -50,16 +50,17 @@
           <description>
             <label control="defaultColorUnitMenuList"
                    accesskey="&options.defaultColorUnit.accesskey;"
             >&options.defaultColorUnit.label;</label>
             <hbox>
               <menulist id="defaultColorUnitMenuList"
                         data-pref="devtools.defaultColorUnit">
                 <menupopup>
+                  <menuitem label="&options.defaultColorUnit.authored;" value="authored"/>
                   <menuitem label="&options.defaultColorUnit.hex;" value="hex"/>
                   <menuitem label="&options.defaultColorUnit.hsl;" value="hsl"/>
                   <menuitem label="&options.defaultColorUnit.rgb;" value="rgb"/>
                   <menuitem label="&options.defaultColorUnit.name;" value="name"/>
                 </menupopup>
               </menulist>
             </hbox>
           </description>
--- a/devtools/client/layoutview/view.js
+++ b/devtools/client/layoutview/view.js
@@ -90,37 +90,37 @@ EditingSession.prototype = {
 
     for (let property of properties) {
       if (!this._modifications.has(property.name)) {
         this._modifications.set(property.name,
           this.getPropertyFromRule(this._rules[0], property.name));
       }
 
       if (property.value == "") {
-        modifications.removeProperty(property.name);
+        modifications.removeProperty(-1, property.name);
       } else {
-        modifications.setProperty(property.name, property.value, "");
+        modifications.setProperty(-1, property.name, property.value, "");
       }
     }
 
     return modifications.apply().then(null, console.error);
   },
 
   /**
    * Reverts all of the property changes made by this instance. Returns a
    * promise that will be resolved when complete.
    */
   revert: function() {
     let modifications = this._rules[0].startModifyingProperties();
 
     for (let [property, value] of this._modifications) {
       if (value != "") {
-        modifications.setProperty(property, value, "");
+        modifications.setProperty(-1, property, value, "");
       } else {
-        modifications.removeProperty(property);
+        modifications.removeProperty(-1, property);
       }
     }
 
     return modifications.apply().then(null, console.error);
   },
 
   destroy: function() {
     this._doc = null;
--- a/devtools/client/shared/test/browser_filter-editor-01.js
+++ b/devtools/client/shared/test/browser_filter-editor-01.js
@@ -13,24 +13,24 @@ add_task(function *() {
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   const container = doc.querySelector("#container");
   let widget = new CSSFilterEditorWidget(container, "none");
 
   info("Test parsing of a valid CSS Filter value");
   widget.setCssValue("blur(2px) contrast(200%)");
   is(widget.getCssValue(),
-     "blur(2px) contrast(200%)", "setCssValue should work for computed values");
+     "blur(2px) contrast(200%)",
+     "setCssValue should work for computed values");
 
   info("Test parsing of space-filled value");
   widget.setCssValue("blur(   2px  )   contrast(  2  )");
   is(widget.getCssValue(),
-     "blur(2px) contrast(200%)", "setCssValue should work for spaced values");
+     "blur(2px) contrast(200%)",
+     "setCssValue should work for spaced values");
 
   info("Test parsing of string-typed values");
   widget.setCssValue("drop-shadow( 2px  1px 5px black) url( example.svg#filter )");
 
-  const computedURI =
-    "chrome://devtools/content/shared/widgets/example.svg#filter";
-  const expected = `drop-shadow(rgb(0, 0, 0) 2px 1px 5px) url(${computedURI})`;
-  is(widget.getCssValue(), expected,
+  is(widget.getCssValue(),
+     "drop-shadow(2px  1px 5px black) url(example.svg#filter)",
      "setCssValue should work for string-typed values");
 });
--- a/devtools/client/shared/test/browser_filter-editor-02.js
+++ b/devtools/client/shared/test/browser_filter-editor-02.js
@@ -32,27 +32,27 @@ add_task(function*() {
         },
         {
           label: "hue-rotate",
           value: "20.2",
           unit: "deg"
         },
         {
           label: "drop-shadow",
-          value: "rgb(0, 0, 0) 5px 5px 0px",
+          value: "5px 5px black",
           unit: null
         }
       ]
     },
     {
       cssValue: "url(example.svg)",
       expected: [
         {
           label: "url",
-          value: "chrome://devtools/content/shared/widgets/example.svg",
+          value: "example.svg",
           unit: null
         }
       ]
     },
     {
       cssValue: "none",
       expected: []
     }
--- a/devtools/client/shared/test/browser_filter-editor-05.js
+++ b/devtools/client/shared/test/browser_filter-editor-05.js
@@ -18,56 +18,59 @@ const GRAYSCALE_MAX = 100,
 add_task(function*() {
   yield promiseTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   const container = doc.querySelector("#container");
   let widget = new CSSFilterEditorWidget(container, "grayscale(0%) url(test.svg)");
 
   const filters = widget.el.querySelector("#filters");
-  const grayscale = filters.children[0],
-        url = filters.children[1];
+  const grayscale = filters.children[0];
+  const url = filters.children[1];
 
   info("Test label-dragging on number-type filters without modifiers");
   widget._mouseDown({
     target: grayscale.querySelector("label"),
     pageX: 0,
     altKey: false,
     shiftKey: false
   });
 
   widget._mouseMove({
     pageX: 12,
     altKey: false,
     shiftKey: false
   });
   let expected = DEFAULT_VALUE_MULTIPLIER * 12;
-  is(widget.getValueAt(0), `${expected}%`,
+  is(widget.getValueAt(0),
+     `${expected}%`,
      "Should update value correctly without modifiers");
 
   info("Test label-dragging on number-type filters with alt");
   widget._mouseMove({
     pageX: 20, // 20 - 12 = 8
     altKey: true,
     shiftKey: false
   });
 
   expected = expected + SLOW_VALUE_MULTIPLIER * 8;
-  is(widget.getValueAt(0), `${expected}%`,
+  is(widget.getValueAt(0),
+     `${expected}%`,
      "Should update value correctly with alt key");
 
   info("Test label-dragging on number-type filters with shift");
   widget._mouseMove({
     pageX: 25, // 25 - 20 = 5
     altKey: false,
     shiftKey: true
   });
 
   expected = expected + FAST_VALUE_MULTIPLIER * 5;
-  is(widget.getValueAt(0), `${expected}%`,
+  is(widget.getValueAt(0),
+     `${expected}%`,
      "Should update value correctly with shift key");
 
   info("Test releasing mouse and dragging again");
 
   widget._mouseUp();
 
   widget._mouseDown({
     target: grayscale.querySelector("label"),
@@ -78,39 +81,42 @@ add_task(function*() {
 
   widget._mouseMove({
     pageX: 5,
     altKey: false,
     shiftKey: false
   });
 
   expected = expected + DEFAULT_VALUE_MULTIPLIER * 5;
-  is(widget.getValueAt(0), `${expected}%`,
+  is(widget.getValueAt(0),
+     `${expected}%`,
      "Should reset multiplier to default");
 
   info("Test value ranges");
 
   widget._mouseMove({
     pageX: 30, // 30 - 25 = 5
     altKey: false,
     shiftKey: true
   });
 
   expected = GRAYSCALE_MAX;
-  is(widget.getValueAt(0), `${expected}%`,
+  is(widget.getValueAt(0),
+     `${expected}%`,
      "Shouldn't allow values higher than max");
 
   widget._mouseMove({
     pageX: -11,
     altKey: false,
     shiftKey: true
   });
 
   expected = GRAYSCALE_MIN;
-  is(widget.getValueAt(0), `${expected}%`,
+  is(widget.getValueAt(0),
+     `${expected}%`,
      "Shouldn't allow values less than min");
 
   widget._mouseUp();
 
   info("Test label-dragging on string-type filters");
   widget._mouseDown({
     target: url.querySelector("label"),
     pageX: 0,
@@ -122,11 +128,12 @@ add_task(function*() {
      "Label-dragging should not work for string-type filters");
 
   widget._mouseMove({
     pageX: -11,
     altKey: false,
     shiftKey: true
   });
 
-  is(widget.getValueAt(1), "chrome://devtools/content/shared/widgets/test.svg",
+  is(widget.getValueAt(1),
+     "test.svg",
      "Label-dragging on string-type filters shouldn't affect their value");
 });
--- a/devtools/client/shared/test/browser_outputparser.js
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -28,82 +28,78 @@ function* performTest() {
 
 // Class name used in color swatch.
 var COLOR_TEST_CLASS = "test-class";
 
 // Create a new CSS color-parsing test.  |name| is the name of the CSS
 // property.  |value| is the CSS text to use.  |segments| is an array
 // describing the expected result.  If an element of |segments| is a
 // string, it is simply appended to the expected string.  Otherwise,
-// it must be an object with a |value| property and a |name| property.
-// These describe the color and are both used in the generated
-// expected output -- |name| is the color name as it appears in the
-// input (e.g., "red"); and |value| is the hash-style numeric value
-// for the color, which parseCssProperty emits in some spots (e.g.,
-// "#F00").
+// it must be an object with a |value| property, which is the color
+// name as it appears in the input.
 //
 // This approach is taken to reduce boilerplate and to make it simpler
 // to modify the test when the parseCssProperty output changes.
 function makeColorTest(name, value, segments) {
   let result = {
     name,
     value,
     expected: ""
   };
 
   for (let segment of segments) {
     if (typeof (segment) === "string") {
       result.expected += segment;
     } else {
-      result.expected += "<span data-color=\"" + segment.value + "\">" +
+      result.expected += "<span data-color=\"" + segment.name + "\">" +
         "<span style=\"background-color:" + segment.name +
         "\" class=\"" + COLOR_TEST_CLASS + "\"></span><span>" +
-        segment.value + "</span></span>";
+        segment.name + "</span></span>";
     }
   }
 
   result.desc = "Testing " + name + ": " + value;
 
   return result;
 }
 
 function testParseCssProperty(doc, parser) {
   let tests = [
     makeColorTest("border", "1px solid red",
-                  ["1px solid ", {name: "red", value: "#F00"}]),
+                  ["1px solid ", {name: "red"}]),
 
     makeColorTest("background-image",
                   "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))",
-                  ["linear-gradient(to right, ", {name: "#F60", value: "#F60"},
-                   " 10%, ", {name: "rgba(0,0,0,1)", value: "#000"},
+                  ["linear-gradient(to right, ", {name: "#F60"},
+                   " 10%, ", {name: "rgba(0,0,0,1)"},
                    ")"]),
 
     // In "arial black", "black" is a font, not a color.
     makeColorTest("font-family", "arial black", ["arial black"]),
 
     makeColorTest("box-shadow", "0 0 1em red",
-                  ["0 0 1em ", {name: "red", value: "#F00"}]),
+                  ["0 0 1em ", {name: "red"}]),
 
     makeColorTest("box-shadow",
                   "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)",
-                  ["0 0 1em ", {name: "red", value: "#F00"},
+                  ["0 0 1em ", {name: "red"},
                    ", 2px 2px 0 0 ",
-                   {name: "rgba(0,0,0,.5)", value: "rgba(0,0,0,.5)"}]),
+                   {name: "rgba(0,0,0,.5)"}]),
 
     makeColorTest("content", "\"red\"", ["\"red\""]),
 
     // Invalid property names should not cause exceptions.
     makeColorTest("hellothere", "'red'", ["'red'"]),
 
     makeColorTest("filter",
                   "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)",
                   ["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ",
                    "url(red.svg#blue)\"><span>",
                    "blur(1px) drop-shadow(0 0 0 ",
-                   {name: "blue", value: "#00F"},
+                   {name: "blue"},
                    ") url(red.svg#blue)</span></span>"]),
 
     makeColorTest("color", "currentColor", ["currentColor"]),
   ];
 
   let target = doc.querySelector("div");
   ok(target, "captain, we have the div");
 
--- a/devtools/client/shared/widgets/FilterWidget.js
+++ b/devtools/client/shared/widgets/FilterWidget.js
@@ -702,24 +702,17 @@ CSSFilterEditorWidget.prototype = {
     this.filters = [];
 
     if (cssValue === "none") {
       this.emit("updated", this.getCssValue());
       this.render();
       return;
     }
 
-    // Apply filter to a temporary element
-    // and get the computed value to make parsing
-    // easier
-    let tmp = this.doc.createElement("i");
-    tmp.style.filter = cssValue;
-    const computedValue = this.win.getComputedStyle(tmp).filter;
-
-    for (let {name, value} of tokenizeComputedFilter(computedValue)) {
+    for (let {name, value} of tokenizeFilterValue(cssValue)) {
       this.add(name, value);
     }
 
     this.emit("updated", this.getCssValue());
     this.render();
   },
 
   /**
@@ -870,29 +863,22 @@ function fixFloat(a, number) {
  * @param {Number} b
  *        index of second element
  */
 function swapArrayIndices(array, a, b) {
   array[a] = array.splice(b, 1, array[a])[0];
 }
 
 /**
-  * Tokenizes CSS Filter value and returns an array of {name, value} pairs
-  *
-  * This is only a very simple tokenizer that only works its way through
-  * parenthesis in the string to detect function names and values.
-  * It assumes that the string actually is a well-formed filter value
-  * (e.g. "blur(2px) hue-rotate(100deg)").
-  *
-  * @param {String} css
-  *        CSS Filter value to be parsed
-  * @return {Array}
-  *        An array of {name, value} pairs
-  */
-function tokenizeComputedFilter(css) {
+ * Tokenizes a CSS Filter value and returns an array of {name, value} pairs.
+ *
+ * @param {String} css CSS Filter value to be parsed
+ * @return {Array} An array of {name, value} pairs
+ */
+function tokenizeFilterValue(css) {
   let filters = [];
   let depth = 0;
 
   if (css === "none") {
     return filters;
   }
 
   let state = "initial";
@@ -902,27 +888,27 @@ function tokenizeComputedFilter(css) {
     switch (state) {
       case "initial":
         if (token.tokenType === "function") {
           name = token.text;
           contents = "";
           state = "function";
           depth = 1;
         } else if (token.tokenType === "url" || token.tokenType === "bad_url") {
-          filters.push({name: "url", value: token.text});
+          filters.push({name: "url", value: token.text.trim()});
           // Leave state as "initial" because the URL token includes
           // the trailing close paren.
         }
         break;
 
       case "function":
         if (token.tokenType === "symbol" && token.text === ")") {
           --depth;
           if (depth === 0) {
-            filters.push({name: name, value: contents});
+            filters.push({name: name, value: contents.trim()});
             state = "initial";
             break;
           }
         }
         contents += css.substring(token.startOffset, token.endOffset);
         if (token.tokenType === "function" ||
             (token.tokenType === "symbol" && token.text === "(")) {
           ++depth;
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -1140,16 +1140,18 @@ SwatchColorPickerTooltip.prototype = Her
    * color.
    */
   show: function() {
     // Call then parent class' show function
     SwatchBasedEditorTooltip.prototype.show.call(this);
     // Then set spectrum's color and listen to color changes to preview them
     if (this.activeSwatch) {
       this.currentSwatchColor = this.activeSwatch.nextSibling;
+      this._colorUnit =
+        colorUtils.classifyColor(this.currentSwatchColor.textContent);
       let color = this.activeSwatch.style.backgroundColor;
       this.spectrum.then(spectrum => {
         spectrum.off("changed", this._onSpectrumColorChange);
         spectrum.rgb = this._colorToRgba(color);
         spectrum.on("changed", this._onSpectrumColorChange);
         spectrum.updateUI();
       });
     }
@@ -1217,16 +1219,17 @@ SwatchColorPickerTooltip.prototype = Her
   _colorToRgba: function(color) {
     color = new colorUtils.CssColor(color);
     let rgba = color._getRGBATuple();
     return [rgba.r, rgba.g, rgba.b, rgba.a];
   },
 
   _toDefaultType: function(color) {
     let colorObj = new colorUtils.CssColor(color);
+    colorObj.colorUnit = this._colorUnit;
     return colorObj.toString();
   },
 
   destroy: function() {
     SwatchBasedEditorTooltip.prototype.destroy.call(this);
     this.currentSwatchColor = null;
     this.spectrum.then(spectrum => {
       spectrum.off("changed", this._onSpectrumColorChange);
--- a/devtools/client/styleeditor/StyleSheetEditor.jsm
+++ b/devtools/client/styleeditor/StyleSheetEditor.jsm
@@ -81,16 +81,22 @@ function StyleSheetEditor(styleSheet, wi
   this.styleSheet = styleSheet;
   this._inputElement = null;
   this.sourceEditor = null;
   this._window = win;
   this._isNew = isNew;
   this.walker = walker;
   this.highlighter = highlighter;
 
+  // True when we've called update() on the style sheet.
+  this._isUpdating = false;
+  // True when we've just set the editor text based on a style-applied
+  // event from the StyleSheetActor.
+  this._justSetText = false;
+
   this._state = {   // state to use when inputElement attaches
     text: "",
     selection: {
       start: {line: 0, ch: 0},
       end: {line: 0, ch: 0}
     }
   };
 
@@ -98,32 +104,34 @@ function StyleSheetEditor(styleSheet, wi
   if (styleSheet.href &&
       Services.io.extractScheme(this.styleSheet.href) == "file") {
     this._styleSheetFilePath = this.styleSheet.href;
   }
 
   this._onPropertyChange = this._onPropertyChange.bind(this);
   this._onError = this._onError.bind(this);
   this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this);
-  this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this)
+  this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this);
+  this._onStyleApplied = this._onStyleApplied.bind(this);
   this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this);
   this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
   this.saveToFile = this.saveToFile.bind(this);
   this.updateStyleSheet = this.updateStyleSheet.bind(this);
   this._updateStyleSheet = this._updateStyleSheet.bind(this);
   this._onMouseMove = this._onMouseMove.bind(this);
 
   this._focusOnSourceEditorReady = false;
   this.cssSheet.on("property-change", this._onPropertyChange);
   this.styleSheet.on("error", this._onError);
   this.mediaRules = [];
   if (this.cssSheet.getMediaRules) {
     this.cssSheet.getMediaRules().then(this._onMediaRulesChanged, Cu.reportError);
   }
   this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged);
+  this.cssSheet.on("style-applied", this._onStyleApplied);
   this.savedFile = file;
   this.linkCSSFile();
 }
 
 StyleSheetEditor.prototype = {
   /**
    * Whether there are unsaved changes in the editor
    */
@@ -240,35 +248,48 @@ StyleSheetEditor.prototype = {
     OS.File.stat(path).then((info) => {
       this._fileModDate = info.lastModificationDate.getTime();
     }, this.markLinkedFileBroken);
 
     this.emit("linked-css-file");
   },
 
   /**
+   * A helper function that fetches the source text from the style
+   * sheet.  The text is possibly prettified using
+   * CssLogic.prettifyCSS.  This also sets |this._state.text| to the
+   * new text.
+   *
+   * @return {Promise} a promise that resolves to the new text
+   */
+  _getSourceTextAndPrettify: function() {
+    return this.styleSheet.getText().then((longStr) => {
+      return longStr.string();
+    }).then((source) => {
+      let ruleCount = this.styleSheet.ruleCount;
+      if (!this.styleSheet.isOriginalSource) {
+        source = CssLogic.prettifyCSS(source, ruleCount);
+      }
+      this._state.text = source;
+      return source;
+    });
+  },
+
+  /**
    * Start fetching the full text source for this editor's sheet.
    *
    * @return {Promise}
    *         A promise that'll resolve with the source text once the source
    *         has been loaded or reject on unexpected error.
    */
-  fetchSource: function () {
-    return Task.spawn(function* () {
-      let longStr = yield this.styleSheet.getText();
-      let source = yield longStr.string();
-      let ruleCount = this.styleSheet.ruleCount;
-      if (!this.styleSheet.isOriginalSource) {
-        source = CssLogic.prettifyCSS(source, ruleCount);
-      }
-      this._state.text = source;
+  fetchSource: function() {
+    return this._getSourceTextAndPrettify().then((source) => {
       this.sourceLoaded = true;
-
       return source;
-    }.bind(this)).then(null, e => {
+    }).then(null, e => {
       if (this._isDestroyed) {
         console.warn("Could not fetch the source for " +
                      this.styleSheet.href +
                      ", the editor was destroyed");
         Cu.reportError(e);
       } else {
         this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href });
         throw e;
@@ -319,16 +340,36 @@ StyleSheetEditor.prototype = {
    * @param  {string} property
    *         Property that has changed on sheet
    */
   _onPropertyChange: function(property, value) {
     this.emit("property-change", property, value);
   },
 
   /**
+   * Called when the stylesheet text changes.
+   */
+  _onStyleApplied: function() {
+    if (this._isUpdating) {
+      // We just applied an edit in the editor, so we can drop this
+      // notification.
+      this._isUpdating = false;
+    } else if (this.sourceEditor) {
+      this._getSourceTextAndPrettify().then((newText) => {
+        this._justSetText = true;
+        let firstLine = this.sourceEditor.getFirstVisibleLine();
+        let pos = this.sourceEditor.getCursor();
+        this.sourceEditor.setText(newText);
+        this.sourceEditor.setFirstVisibleLine(firstLine);
+        this.sourceEditor.setCursor(pos);
+      });
+    }
+  },
+
+  /**
    * Handles changes to the list of @media rules in the stylesheet.
    * Emits 'media-rules-changed' if the list has changed.
    *
    * @param  {array} rules
    *         Array of MediaRuleFronts for new media rules of sheet.
    */
   _onMediaRulesChanged: function(rules) {
     if (!rules.length && !this.mediaRules.length) {
@@ -484,25 +525,31 @@ StyleSheetEditor.prototype = {
   /**
    * Update live style sheet according to modifications.
    */
   _updateStyleSheet: function() {
     if (this.styleSheet.disabled) {
       return;  // TODO: do we want to do this?
     }
 
+    if (this._justSetText) {
+      this._justSetText = false;
+      return;
+    }
+
     this._updateTask = null; // reset only if we actually perform an update
                              // (stylesheet is enabled) so that 'missed' updates
                              // while the stylesheet is disabled can be performed
                              // when it is enabled back. @see enableStylesheet
 
     if (this.sourceEditor) {
       this._state.text = this.sourceEditor.getText();
     }
 
+    this._isUpdating = true;
     this.styleSheet.update(this._state.text, this.transitionsEnabled)
                    .then(null, Cu.reportError);
   },
 
   /**
    * Handle mousemove events, calling _highlightSelectorAt after a delay only
    * and reseting the delay everytime.
    */
@@ -721,20 +768,21 @@ StyleSheetEditor.prototype = {
       if (this.highlighter && this.walker && this._sourceEditor.container) {
         this._sourceEditor.container.removeEventListener("mousemove",
           this._onMouseMove);
       }
       this._sourceEditor.destroy();
     }
     this.cssSheet.off("property-change", this._onPropertyChange);
     this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged);
+    this.cssSheet.off("style-applied", this._onStyleApplied);
     this.styleSheet.off("error", this._onError);
     this._isDestroyed = true;
   }
-}
+};
 
 /**
  * Find a path on disk for a file given it's hosted uri, the uri of the
  * original resource that generated it (e.g. Sass file), and the location of the
  * local file for that source.
  *
  * @param {nsIURI} uri
  *        The uri of the resource
--- a/devtools/client/styleeditor/test/browser.ini
+++ b/devtools/client/styleeditor/test/browser.ini
@@ -43,16 +43,17 @@ support-files =
   sourcemaps-large.html
   sourcemaps-watching.html
   test_private.css
   test_private.html
   doc_long.css
   doc_uncached.css
   doc_uncached.html
   doc_xulpage.xul
+  sync.html
 
 [browser_styleeditor_autocomplete.js]
 [browser_styleeditor_autocomplete-disabled.js]
 [browser_styleeditor_bug_740541_iframes.js]
 [browser_styleeditor_bug_851132_middle_click.js]
 [browser_styleeditor_bug_870339.js]
 [browser_styleeditor_cmd_edit.js]
 skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s
@@ -78,10 +79,15 @@ skip-if = e10s # Bug 1055333 - style edi
 [browser_styleeditor_scroll.js]
 [browser_styleeditor_sv_keynav.js]
 [browser_styleeditor_sv_resize.js]
 [browser_styleeditor_selectstylesheet.js]
 [browser_styleeditor_sourcemaps.js]
 [browser_styleeditor_sourcemap_large.js]
 [browser_styleeditor_sourcemap_watching.js]
 skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s
+[browser_styleeditor_sync.js]
+[browser_styleeditor_syncAddRule.js]
+[browser_styleeditor_syncAlreadyOpen.js]
+[browser_styleeditor_syncEditSelector.js]
+[browser_styleeditor_syncIntoRuleView.js]
 [browser_styleeditor_transition_rule.js]
 [browser_styleeditor_xul.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sync.js
@@ -0,0 +1,74 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+  body {
+    border-width: 15px;
+    /*! color: red; */
+  }
+
+  #testid {
+    /*! font-size: 4em; */
+  }
+  `;
+
+function* closeAndReopenToolbox() {
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  yield gDevTools.closeToolbox(target);
+  let { ui: newui } = yield openStyleEditor();
+  return newui;
+}
+
+add_task(function*() {
+  yield addTab(TESTCASE_URI);
+  let { inspector, view } = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+  // Disable the "font-size" property.
+  let propEditor = ruleEditor.rule.textProps[0].editor;
+  let onModification = view.once("ruleview-changed");
+  propEditor.enable.click();
+  yield onModification;
+
+  // Disable the "color" property.  Note that this property is in a
+  // rule that also contains a non-inherited property -- so this test
+  // is also testing that property editing works properly in this
+  // situation.
+  ruleEditor = getRuleViewRuleEditor(view, 3);
+  propEditor = ruleEditor.rule.textProps[1].editor;
+  onModification = view.once("ruleview-changed");
+  propEditor.enable.click();
+  yield onModification;
+
+  let { ui } = yield openStyleEditor();
+
+  let editor = yield ui.editors[0].getSourceEditor();
+  let text = editor.sourceEditor.getText();
+  is(text, expectedText, "style inspector changes are synced");
+
+  // Close and reopen the toolbox, to see that the edited text remains
+  // available.
+  ui = yield closeAndReopenToolbox();
+  editor = yield ui.editors[0].getSourceEditor();
+  text = editor.sourceEditor.getText();
+  is(text, expectedText, "changes remain after close and reopen");
+
+  // For the time being, the actor does not update the style's owning
+  // node's textContent.  See bug 1205380.
+  yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+    let style = content.document.querySelector("style");
+    return style.textContent;
+  }).then((textContent) => {
+    isnot(textContent, expectedText, "changes not written back to style node");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js
@@ -0,0 +1,33 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that adding a new rule is synced to the style editor.
+
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+#testid {
+}`;
+
+add_task(function*() {
+  yield addTab(TESTCASE_URI);
+  let { inspector, view } = yield openRuleView();
+  yield selectNode("#testid", inspector);
+
+  let onRuleViewChanged = once(view, "ruleview-changed");
+  view.addRuleButton.click();
+  yield onRuleViewChanged;
+
+  let { ui } = yield openStyleEditor();
+
+  info("Selecting the second editor");
+  yield ui.selectStyleSheet(ui.editors[1].styleSheet);
+
+  let editor = ui.editors[1];
+  let text = editor.sourceEditor.getText();
+  is(text, expectedText, "selector edits are synced");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js
@@ -0,0 +1,52 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+  body {
+    border-width: 15px;
+    color: red;
+  }
+
+  #testid {
+    /*! font-size: 4em; */
+  }
+  `;
+
+add_task(function*() {
+  yield addTab(TESTCASE_URI);
+
+  let { inspector, view } = yield openRuleView();
+
+  // In this test, make sure the style editor is open before making
+  // changes in the inspector.
+  let { ui } = yield openStyleEditor();
+  let editor = yield ui.editors[0].getSourceEditor();
+
+  let onEditorChange = promise.defer();
+  editor.sourceEditor.on("change", onEditorChange.resolve);
+
+  yield openRuleView();
+  yield selectNode("#testid", inspector);
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+  // Disable the "font-size" property.
+  let propEditor = ruleEditor.rule.textProps[0].editor;
+  let onModification = view.once("ruleview-changed");
+  propEditor.enable.click();
+  yield onModification;
+
+  yield openStyleEditor();
+  yield onEditorChange.promise;
+
+  let text = editor.sourceEditor.getText();
+  is(text, expectedText, "style inspector changes are synced");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js
@@ -0,0 +1,41 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+  body {
+    border-width: 15px;
+    color: red;
+  }
+
+  #testid, span {
+    font-size: 4em;
+  }
+  `;
+
+add_task(function*() {
+  yield addTab(TESTCASE_URI);
+  let { inspector, view } = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+  let editor = yield focusEditableField(view, ruleEditor.selectorText);
+  editor.input.value = "#testid, span";
+  let onRuleViewChanged = once(view, "ruleview-changed");
+  EventUtils.synthesizeKey("VK_RETURN", {});
+  yield onRuleViewChanged;
+
+  let { ui } = yield openStyleEditor();
+
+  editor = yield ui.editors[0].getSourceEditor();
+  let text = editor.sourceEditor.getText();
+  is(text, expectedText, "selector edits are synced");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js
@@ -0,0 +1,51 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style editor are synchronized into the
+// style inspector.
+
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
+
+const TEST_URI = `
+  <style type='text/css'>
+    div { background-color: seagreen; }
+  </style>
+  <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+const TESTCASE_CSS_SOURCE = "#testid { color: chartreuse; }";
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+
+  let { panel, ui } = yield openStyleEditor();
+
+  let editor = yield ui.editors[0].getSourceEditor();
+
+  let waitForRuleView = view.once("ruleview-refreshed");
+  yield typeInEditor(editor, panel.panelWindow);
+  yield waitForRuleView;
+
+  let value = getRuleViewPropertyValue(view, "#testid", "color");
+  is(value, "chartreuse", "check that edits were synced to rule view");
+});
+
+function typeInEditor(aEditor, panelWindow) {
+  let deferred = promise.defer();
+
+  waitForFocus(function() {
+    for (let c of TESTCASE_CSS_SOURCE) {
+      EventUtils.synthesizeKey(c, {}, panelWindow);
+    }
+    ok(aEditor.unsaved, "new editor has unsaved flag");
+
+    deferred.resolve();
+  }, panelWindow);
+
+  return deferred.promise;
+}
--- a/devtools/client/styleeditor/test/head.js
+++ b/devtools/client/styleeditor/test/head.js
@@ -66,27 +66,39 @@ function* cleanup()
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     yield gDevTools.closeToolbox(target);
 
     gBrowser.removeCurrentTab();
   }
 }
 
 /**
+ * Open the style editor for the current tab.
+ */
+var openStyleEditor = Task.async(function*(tab) {
+  if (!tab) {
+    tab = gBrowser.selectedTab;
+  }
+  let target = TargetFactory.forTab(tab);
+  let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
+  let panel = toolbox.getPanel("styleeditor");
+  let ui = panel.UI;
+
+  return { toolbox, panel, ui };
+});
+
+/**
  * Creates a new tab in specified window navigates it to the given URL and
  * opens style editor in it.
  */
 var openStyleEditorForURL = Task.async(function* (url, win) {
   let tab = yield addTab(url, win);
-  let target = TargetFactory.forTab(tab);
-  let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
-  let panel = toolbox.getPanel("styleeditor");
-  let ui = panel.UI;
-
-  return { tab, toolbox, panel, ui };
+  let result = yield openStyleEditor(tab);
+  result.tab = tab;
+  return result;
 });
 
 /**
  * Loads shared/frame-script-utils.js in the specified tab.
  *
  * @param tab
  *        Optional tab to load the frame script in. Defaults to the current tab.
  */
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/test/sync.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>simple testcase</title>
+  <style type="text/css">
+  body {
+    border-width: 15px;
+    color: red;
+  }
+
+  #testid {
+    font-size: 4em;
+  }
+  </style>
+</head>
+<body>
+	<div id="testid">simple testcase</div>
+</body>
+</html>
--- a/devtools/client/styleinspector/rule-view.js
+++ b/devtools/client/styleinspector/rule-view.js
@@ -104,17 +104,17 @@ function createDummyDocument() {
  * ElementStyle:
  *   Responsible for keeping track of which properties are overridden.
  *   Maintains a list of Rule objects that apply to the element.
  * Rule:
  *   Manages a single style declaration or rule.
  *   Responsible for applying changes to the properties in a rule.
  *   Maintains a list of TextProperty objects.
  * TextProperty:
- *   Manages a single property from the cssText attribute of the
+ *   Manages a single property from the authoredText attribute of the
  *     relevant declaration.
  *   Maintains a list of computed properties that come from this
  *     property declaration.
  *   Changes to the TextProperty are sent to its related Rule for
  *     application.
  */
 
 /**
@@ -178,16 +178,22 @@ ElementStyle.prototype = {
   },
 
   destroy: function() {
     if (this.destroyed) {
       return;
     }
     this.destroyed = true;
 
+    for (let rule of this.rules) {
+      if (rule.editor) {
+        rule.editor.destroy();
+      }
+    }
+
     this.dummyElement = null;
     this.dummyElementPromise.then(dummyElement => {
       dummyElement.remove();
       this.dummyElementPromise = null;
     }, console.error);
   },
 
   /**
@@ -221,31 +227,35 @@ ElementStyle.prototype = {
       return this.dummyElementPromise.then(() => {
         if (this.populated !== populated) {
           // Don't care anymore.
           return;
         }
 
         // Store the current list of rules (if any) during the population
         // process.  They will be reused if possible.
-        this._refreshRules = this.rules;
+        let existingRules = this.rules;
 
         this.rules = [];
 
         for (let entry of entries) {
-          this._maybeAddRule(entry);
+          this._maybeAddRule(entry, existingRules);
         }
 
         // Mark overridden computed styles.
         this.markOverriddenAll();
 
         this._sortRulesForPseudoElement();
 
         // We're done with the previous list of rules.
-        delete this._refreshRules;
+        for (let r of existingRules) {
+          if (r && r.editor) {
+            r.editor.destroy();
+          }
+        }
       });
     }).then(null, e => {
       // populate is often called after a setTimeout,
       // the connection may already be closed.
       if (this.destroyed) {
         return promise.resolve(undefined);
       }
       return promiseWarn(e);
@@ -264,51 +274,53 @@ ElementStyle.prototype = {
    },
 
   /**
    * Add a rule if it's one we care about.  Filters out duplicates and
    * inherited styles with no inherited properties.
    *
    * @param {Object} options
    *        Options for creating the Rule, see the Rule constructor.
+   * @param {Array} existingRules
+   *        Rules to reuse if possible.  If a rule is reused, then it
+   *        it will be deleted from this array.
    * @return {Boolean} true if we added the rule.
    */
-  _maybeAddRule: function(options) {
+  _maybeAddRule: function(options, existingRules) {
     // If we've already included this domRule (for example, when a
     // common selector is inherited), ignore it.
     if (options.rule &&
         this.rules.some(rule => rule.domRule === options.rule)) {
       return false;
     }
 
     if (options.system) {
       return false;
     }
 
     let rule = null;
 
     // If we're refreshing and the rule previously existed, reuse the
     // Rule object.
-    if (this._refreshRules) {
-      for (let r of this._refreshRules) {
-        if (r.matches(options)) {
-          rule = r;
-          rule.refresh(options);
-          break;
-        }
+    if (existingRules) {
+      let ruleIndex = existingRules.findIndex((r) => r.matches(options));
+      if (ruleIndex >= 0) {
+        rule = existingRules[ruleIndex];
+        rule.refresh(options);
+        existingRules.splice(ruleIndex, 1);
       }
     }
 
     // If this is a new rule, create its Rule object.
     if (!rule) {
       rule = new Rule(this, options);
     }
 
-    // Ignore inherited rules with no properties.
-    if (options.inherited && rule.textProps.length === 0) {
+    // Ignore inherited rules with no visible properties.
+    if (options.inherited && !rule.hasAnyVisibleProperties()) {
       return false;
     }
 
     this.rules.push(rule);
     return true;
   },
 
   /**
@@ -367,16 +379,26 @@ ElementStyle.prototype = {
     //   If the new property is a lower or equal priority, mark it as
     //   overridden.
     //
     // _overriddenDirty will be set on each prop, indicating whether its
     // dirty status changed during this pass.
     let taken = {};
     for (let computedProp of computedProps) {
       let earlier = taken[computedProp.name];
+
+      // Prevent -webkit-gradient from being selected after unchecking
+      // linear-gradient in this case:
+      //  -moz-linear-gradient: ...;
+      //  -webkit-linear-gradient: ...;
+      //  linear-gradient: ...;
+      if (!computedProp.textProp.isValid()) {
+        computedProp.overridden = true;
+        continue;
+      }
       let overridden;
       if (earlier &&
           computedProp.priority === "important" &&
           earlier.priority !== "important" &&
           (earlier.textProp.rule.inherited ||
            !computedProp.textProp.rule.inherited)) {
         // New property is higher priority.  Mark the earlier property
         // overridden (which will reverse its dirty state).
@@ -458,37 +480,32 @@ function Rule(elementStyle, options) {
   this.inherited = options.inherited || null;
   this.keyframes = options.keyframes || null;
   this._modificationDepth = 0;
 
   if (this.domRule && this.domRule.mediaText) {
     this.mediaText = this.domRule.mediaText;
   }
 
-  // Populate the text properties with the style's current cssText
+  // Populate the text properties with the style's current authoredText
   // value, and add in any disabled properties from the store.
   this.textProps = this._getTextProperties();
   this.textProps = this.textProps.concat(this._getDisabledProperties());
 }
 
 Rule.prototype = {
   mediaText: "",
 
   get title() {
-    if (this._title) {
-      return this._title;
-    }
-    this._title = CssLogic.shortSource(this.sheet);
+    let title = CssLogic.shortSource(this.sheet);
     if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
-      this._title += ":" + this.ruleLine;
+      title += ":" + this.ruleLine;
     }
 
-    this._title = this._title +
-      (this.mediaText ? " @media " + this.mediaText : "");
-    return this._title;
+    return title + (this.mediaText ? " @media " + this.mediaText : "");
   },
 
   get inheritedSource() {
     if (this._inheritedSource) {
       return this._inheritedSource;
     }
     this._inheritedSource = "";
     if (this.inherited) {
@@ -546,30 +563,25 @@ Rule.prototype = {
    * Get display name for this rule based on the original source
    * for this rule's style sheet.
    *
    * @return {Promise}
    *         Promise which resolves with location as an object containing
    *         both the full and short version of the source string.
    */
   getOriginalSourceStrings: function() {
-    if (this._originalSourceStrings) {
-      return promise.resolve(this._originalSourceStrings);
-    }
-
     return this.domRule.getOriginalLocation().then(({href, line, mediaText}) => {
       let mediaString = mediaText ? " @" + mediaText : "";
 
       let sourceStrings = {
         full: (href || CssLogic.l10n("rule.sourceInline")) + ":" +
           line + mediaString,
         short: CssLogic.shortSource({href: href}) + ":" + line + mediaString
       };
 
-      this._originalSourceStrings = sourceStrings;
       return sourceStrings;
     });
   },
 
   /**
    * Returns true if the rule matches the creation options
    * specified.
    *
@@ -590,69 +602,73 @@ Rule.prototype = {
    * @param {String} priority
    *        The property's priority (either "important" or an empty string).
    * @param {TextProperty} siblingProp
    *        Optional, property next to which the new property will be added.
    */
   createProperty: function(name, value, priority, siblingProp) {
     let prop = new TextProperty(this, name, value, priority);
 
+    let ind;
     if (siblingProp) {
-      let ind = this.textProps.indexOf(siblingProp);
-      this.textProps.splice(ind + 1, 0, prop);
+      ind = this.textProps.indexOf(siblingProp) + 1;
+      this.textProps.splice(ind, 0, prop);
     } else {
+      ind = this.textProps.length;
       this.textProps.push(prop);
     }
 
-    this.applyProperties();
+    this.applyProperties((modifications) => {
+      modifications.createProperty(ind, name, value, priority);
+    });
     return prop;
   },
 
   /**
-   * Reapply all the properties in this rule, and update their
-   * computed styles. Store disabled properties in the element
-   * style's store. Will re-mark overridden properties.
+   * Helper function for applyProperties that is called when the actor
+   * does not support as-authored styles.  Store disabled properties
+   * in the element style's store.
    */
-  applyProperties: function(modifications) {
+  _applyPropertiesNoAuthored: function(modifications) {
     this.elementStyle.markOverriddenAll();
 
-    if (!modifications) {
-      modifications = this.style.startModifyingProperties();
-    }
     let disabledProps = [];
 
     for (let prop of this.textProps) {
+      if (prop.invisible) {
+        continue;
+      }
       if (!prop.enabled) {
         disabledProps.push({
           name: prop.name,
           value: prop.value,
           priority: prop.priority
         });
         continue;
       }
       if (prop.value.trim() === "") {
         continue;
       }
 
-      modifications.setProperty(prop.name, prop.value, prop.priority);
+      modifications.setProperty(-1, prop.name, prop.value, prop.priority);
 
       prop.updateComputed();
     }
 
     // Store disabled properties in the disabled store.
     let disabled = this.elementStyle.store.disabled;
     if (disabledProps.length > 0) {
       disabled.set(this.style, disabledProps);
     } else {
       disabled.delete(this.style);
     }
 
-    let modificationsPromise = modifications.apply().then(() => {
+    return modifications.apply().then(() => {
       let cssProps = {};
-      for (let cssProp of parseDeclarations(this.style.cssText)) {
+      for (let cssProp of parseDeclarations(this.style.authoredText)) {
         cssProps[cssProp.name] = cssProp;
       }
 
       for (let textProp of this.textProps) {
         if (!textProp.enabled) {
           continue;
         }
         let cssProp = cssProps[textProp.name];
@@ -662,46 +678,97 @@ Rule.prototype = {
             name: textProp.name,
             value: "",
             priority: ""
           };
         }
 
         textProp.priority = cssProp.priority;
       }
-
-      this.elementStyle.markOverriddenAll();
-
-      if (modificationsPromise === this._applyingModifications) {
-        this._applyingModifications = null;
+    });
+  },
+
+  /**
+   * A helper for applyProperties that applies properties in the "as
+   * authored" case; that is, when the StyleRuleActor supports
+   * setRuleText.
+   */
+  _applyPropertiesAuthored: function(modifications) {
+    return modifications.apply().then(() => {
+      // The rewriting may have required some other property values to
+      // change, e.g., to insert some needed terminators.  Update the
+      // relevant properties here.
+      for (let index in modifications.changedDeclarations) {
+        let newValue = modifications.changedDeclarations[index];
+        this.textProps[index].noticeNewValue(newValue);
+      }
+      // Recompute and redisplay the computed properties.
+      for (let prop of this.textProps) {
+        if (!prop.invisible && prop.enabled) {
+          prop.updateComputed();
+          prop.updateEditor();
+        }
       }
-
-      this.elementStyle._changed();
-    }).then(null, promiseWarn);
-
-    this._applyingModifications = modificationsPromise;
-    return modificationsPromise;
+    });
+  },
+
+  /**
+   * Reapply all the properties in this rule, and update their
+   * computed styles.  Will re-mark overridden properties.  Sets the
+   * |_applyingModifications| property to a promise which will resolve
+   * when the edit has completed.
+   *
+   * @param {Function} modifier a function that takes a RuleModificationList
+   *        (or RuleRewriter) as an argument and that modifies it
+   *        to apply the desired edit
+   * @return {Promise} a promise which will resolve when the edit
+   *        is complete
+   */
+  applyProperties: function(modifier) {
+    // If there is already a pending modification, we have to wait
+    // until it settles before applying the next modification.
+    let resultPromise =
+        promise.resolve(this._applyingModifications).then(() => {
+          let modifications = this.style.startModifyingProperties();
+          modifier(modifications);
+          if (this.style.canSetRuleText) {
+            return this._applyPropertiesAuthored(modifications);
+          }
+          return this._applyPropertiesNoAuthored(modifications);
+        }).then(() => {
+          this.elementStyle.markOverriddenAll();
+
+          if (resultPromise === this._applyingModifications) {
+            this._applyingModifications = null;
+            this.elementStyle._changed();
+          }
+        }).catch(promiseWarn);
+
+    this._applyingModifications = resultPromise;
+    return resultPromise;
   },
 
   /**
    * Renames a property.
    *
    * @param {TextProperty} property
    *        The property to rename.
    * @param {String} name
    *        The new property name (such as "background" or "border-top").
    */
   setPropertyName: function(property, name) {
     if (name === property.name) {
       return;
     }
-    let modifications = this.style.startModifyingProperties();
-    modifications.removeProperty(property.name);
+
     property.name = name;
-    this.applyProperties(modifications, name);
+    let index = this.textProps.indexOf(property);
+    this.applyProperties((modifications) => {
+      modifications.renameProperty(index, property.name, name);
+    });
   },
 
   /**
    * Sets the value and priority of a property, then reapply all properties.
    *
    * @param {TextProperty} property
    *        The property to manipulate.
    * @param {String} value
@@ -711,88 +778,100 @@ Rule.prototype = {
    */
   setPropertyValue: function(property, value, priority) {
     if (value === property.value && priority === property.priority) {
       return;
     }
 
     property.value = value;
     property.priority = priority;
-    this.applyProperties(null, property.name);
+
+    let index = this.textProps.indexOf(property);
+    this.applyProperties((modifications) => {
+      modifications.setProperty(index, property.name, value, priority);
+    });
   },
 
   /**
    * Just sets the value and priority of a property, in order to preview its
    * effect on the content document.
    *
    * @param {TextProperty} property
    *        The property which value will be previewed
    * @param {String} value
    *        The value to be used for the preview
    * @param {String} priority
    *        The property's priority (either "important" or an empty string).
    */
   previewPropertyValue: function(property, value, priority) {
     let modifications = this.style.startModifyingProperties();
-    modifications.setProperty(property.name, value, priority);
+    modifications.setProperty(this.textProps.indexOf(property),
+                              property.name, value, priority);
     modifications.apply().then(() => {
       // Ensure dispatching a ruleview-changed event
       // also for previews
       this.elementStyle._changed();
     });
   },
 
   /**
    * Disables or enables given TextProperty.
    *
    * @param {TextProperty} property
    *        The property to enable/disable
    * @param {Boolean} value
    */
   setPropertyEnabled: function(property, value) {
+    if (property.enabled === !!value) {
+      return;
+    }
     property.enabled = !!value;
-    let modifications = this.style.startModifyingProperties();
-    if (!property.enabled) {
-      modifications.removeProperty(property.name);
-    }
-    this.applyProperties(modifications);
+    let index = this.textProps.indexOf(property);
+    this.applyProperties((modifications) => {
+      modifications.setPropertyEnabled(index, property.name, property.enabled);
+    });
   },
 
   /**
    * Remove a given TextProperty from the rule and update the rule
    * accordingly.
    *
    * @param {TextProperty} property
    *        The property to be removed
    */
   removeProperty: function(property) {
-    this.textProps = this.textProps.filter(prop => prop !== property);
-    let modifications = this.style.startModifyingProperties();
-    modifications.removeProperty(property.name);
+    let index = this.textProps.indexOf(property);
+    this.textProps.splice(index, 1);
     // Need to re-apply properties in case removing this TextProperty
     // exposes another one.
-    this.applyProperties(modifications);
+    this.applyProperties((modifications) => {
+      modifications.removeProperty(index, property.name);
+    });
   },
 
   /**
    * Get the list of TextProperties from the style. Needs
-   * to parse the style's cssText.
+   * to parse the style's authoredText.
    */
   _getTextProperties: function() {
     let textProps = [];
     let store = this.elementStyle.store;
-    let props = parseDeclarations(this.style.cssText);
+    let props = parseDeclarations(this.style.authoredText, true);
     for (let prop of props) {
       let name = prop.name;
-      if (this.inherited && !domUtils.isInheritedProperty(name)) {
-        continue;
-      }
+      // In an inherited rule, we only show inherited properties.
+      // However, we must keep all properties in order for rule
+      // rewriting to work properly.  So, compute the "invisible"
+      // property here.
+      let invisible = this.inherited && !domUtils.isInheritedProperty(name);
       let value = store.userProperties.getProperty(this.style, name,
                                                    prop.value);
-      let textProp = new TextProperty(this, name, value, prop.priority);
+      let textProp = new TextProperty(this, name, value, prop.priority,
+                                      !("commentOffsets" in prop),
+                                      invisible);
       textProps.push(textProp);
     }
 
     return textProps;
   },
 
   /**
    * Return the list of disabled properties from the store for this rule.
@@ -859,33 +938,33 @@ Rule.prototype = {
     // Refresh the editor if one already exists.
     if (this.editor) {
       this.editor.populate();
     }
   },
 
   /**
    * Update the current TextProperties that match a given property
-   * from the cssText.  Will choose one existing TextProperty to update
+   * from the authoredText.  Will choose one existing TextProperty to update
    * with the new property's value, and will disable all others.
    *
    * When choosing the best match to reuse, properties will be chosen
    * by assigning a rank and choosing the highest-ranked property:
    *   Name, value, and priority match, enabled. (6)
    *   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} newProp
    *        The current version of the property, as parsed from the
-   *        cssText in Rule._getTextProperties().
+   *        authoredText in Rule._getTextProperties().
    * @return {Boolean} true if a property was updated, false if no properties
    *         were updated.
    */
   _updateTextProperty: function(newProp) {
     let match = { rank: 0, prop: null };
 
     for (let prop of this.textProps) {
       if (prop.name !== newProp.name) {
@@ -948,66 +1027,99 @@ Rule.prototype = {
    *        The text property that will be left to focus on a sibling.
    * @param {Number} direction
    *        The move focus direction number.
    */
   editClosestTextProperty: function(textProperty, direction) {
     let index = this.textProps.indexOf(textProperty);
 
     if (direction === Ci.nsIFocusManager.MOVEFOCUS_FORWARD) {
-      if (index === this.textProps.length - 1) {
+      for (++index; index < this.textProps.length; ++index) {
+        if (!this.textProps[index].invisible) {
+          break;
+        }
+      }
+      if (index === this.textProps.length) {
         textProperty.rule.editor.closeBrace.click();
       } else {
-        let nextProp = this.textProps[index + 1];
-        nextProp.editor.nameSpan.click();
+        this.textProps[index].editor.nameSpan.click();
       }
     } else if (direction === Ci.nsIFocusManager.MOVEFOCUS_BACKWARD) {
-      if (index === 0) {
+      for (--index; index >= 0; --index) {
+        if (!this.textProps[index].invisible) {
+          break;
+        }
+      }
+      if (index < 0) {
         textProperty.editor.ruleEditor.selectorText.click();
       } else {
-        let prevProp = this.textProps[index - 1];
-        prevProp.editor.valueSpan.click();
+        this.textProps[index].editor.valueSpan.click();
       }
     }
   },
 
   /**
    * Return a string representation of the rule.
    */
   stringifyRule: function() {
     let selectorText = this.selectorText;
     let cssText = "";
     let terminator = osString === "WINNT" ? "\r\n" : "\n";
 
     for (let textProp of this.textProps) {
-      cssText += "\t" + textProp.stringifyProperty() + terminator;
+      if (!textProp.invisible) {
+        cssText += "\t" + textProp.stringifyProperty() + terminator;
+      }
     }
 
     return selectorText + " {" + terminator + cssText + "}";
+  },
+
+  /**
+   * See whether this rule has any non-invisible properties.
+   * @return {Boolean} true if there is any visible property, or false
+   *         if all properties are invisible
+   */
+  hasAnyVisibleProperties: function() {
+    for (let prop of this.textProps) {
+      if (!prop.invisible) {
+        return true;
+      }
+    }
+    return false;
   }
 };
 
 /**
- * A single property in a rule's cssText.
+ * A single property in a rule's authoredText.
  *
  * @param {Rule} rule
  *        The rule this TextProperty came from.
  * @param {String} name
  *        The text property name (such as "background" or "border-top").
  * @param {String} value
  *        The property's value (not including priority).
  * @param {String} priority
  *        The property's priority (either "important" or an empty string).
+ * @param {Boolean} enabled
+ *        Whether the property is enabled.
+ * @param {Boolean} invisible
+ *        Whether the property is invisible.  An invisible property
+ *        does not show up in the UI; these are needed so that the
+ *        index of a property in Rule.textProps is the same as the index
+ *        coming from parseDeclarations.
  */
-function TextProperty(rule, name, value, priority) {
+function TextProperty(rule, name, value, priority, enabled = true,
+                      invisible = false) {
   this.rule = rule;
   this.name = name;
   this.value = value;
   this.priority = priority;
-  this.enabled = true;
+  this.enabled = !!enabled;
+  this.invisible = invisible;
   this.updateComputed();
 }
 
 TextProperty.prototype = {
   /**
    * Update the editor associated with this text property,
    * if any.
    */
@@ -1082,16 +1194,27 @@ TextProperty.prototype = {
     if (this.editor && value !== this.editor.committed.value || force) {
       store.userProperties.setProperty(this.rule.style, this.name, value);
     }
 
     this.rule.setPropertyValue(this, value, priority);
     this.updateEditor();
   },
 
+  /**
+   * Called when the property's value has been updated externally, and
+   * the property and editor should update.
+   */
+  noticeNewValue: function(value) {
+    if (value !== this.value) {
+      this.value = value;
+      this.updateEditor();
+    }
+  },
+
   setName: function(name) {
     let store = this.rule.elementStyle.store;
 
     if (name !== this.name) {
       store.userProperties.setProperty(this.rule.style, name,
                                        this.editor.committed.value);
     }
 
@@ -1117,16 +1240,43 @@ TextProperty.prototype = {
       ";";
 
     // Comment out property declarations that are not enabled
     if (!this.enabled) {
       declaration = "/* " + escapeCSSComment(declaration) + " */";
     }
 
     return declaration;
+  },
+
+  /**
+   * See whether this property's name is known.
+   *
+   * @return {Boolean} true if the property name is known, false otherwise.
+   */
+  isKnownProperty: function() {
+    try {
+      // If the property name is invalid, the cssPropertyIsShorthand
+      // will throw an exception.  But if it is valid, no exception will
+      // be thrown; so we just ignore the return value.
+      domUtils.cssPropertyIsShorthand(this.name);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  },
+
+  /**
+   * Validate this property. Does it make sense for this value to be assigned
+   * to this property name? This does not apply the property value
+   *
+   * @return {Boolean} true if the property value is valid, false otherwise.
+   */
+  isValid: function() {
+    return domUtils.cssPropertyIsValid(this.name, this.value);
   }
 };
 
 /**
  * View hierarchy mostly follows the model hierarchy.
  *
  * CssRuleView:
  *   Owns an ElementStyle and creates a list of RuleEditors for its
@@ -1480,29 +1630,25 @@ CssRuleView.prototype = {
 
       clipboardHelper.copyString(text);
     } catch(e) {
       console.error(e);
     }
   },
 
   /**
-   * Add a new rule to the current element.
+   * A helper for _onAddRule that handles the case where the actor
+   * does not support as-authored styles.
    */
-  _onAddRule: function() {
+  _onAddNewRuleNonAuthored: function() {
     let elementStyle = this._elementStyle;
     let element = elementStyle.element;
     let rules = elementStyle.rules;
-    let client = this.inspector.toolbox._target.client;
     let pseudoClasses = element.pseudoClassLocks;
 
-    if (!client.traits.addNewRule) {
-      return;
-    }
-
     this.pageStyle.addNewRule(element, pseudoClasses).then(options => {
       let newRule = new Rule(elementStyle, options);
       rules.push(newRule);
       let editor = new RuleEditor(this, newRule);
       newRule.editor = editor;
 
       // Insert the new rule editor after the inline element rule
       if (rules.length <= 1) {
@@ -1519,16 +1665,54 @@ CssRuleView.prototype = {
 
       // Focus and make the new rule's selector editable
       editor.selectorText.click();
       elementStyle._changed();
     });
   },
 
   /**
+   * Add a new rule to the current element.
+   */
+  _onAddRule: function() {
+    let elementStyle = this._elementStyle;
+    let element = elementStyle.element;
+    let client = this.inspector.toolbox._target.client;
+    let pseudoClasses = element.pseudoClassLocks;
+
+    if (!client.traits.addNewRule) {
+      return;
+    }
+
+    if (!this.pageStyle.supportsAuthoredStyles) {
+      // We're talking to an old server.
+      this._onAddNewRuleNonAuthored();
+      return;
+    }
+
+    // Adding a new rule with authored styles will cause the actor to
+    // emit an event, which will in turn cause the rule view to be
+    // updated.  So, we wait for this update and for the rule creation
+    // request to complete, and then focus the new rule's selector.
+    let eventPromise = this.once("ruleview-refreshed");
+    let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses);
+    promise.all([eventPromise, newRulePromise]).then((values) => {
+      let options = values[1];
+      // Be sure the reference the correct |rules| here.
+      for (let rule of this._elementStyle.rules) {
+        if (options.rule === rule.domRule) {
+          rule.editor.selectorText.click();
+          elementStyle._changed();
+          break;
+        }
+      }
+    });
+  },
+
+  /**
    * Disables add rule button when needed
    */
   refreshAddRuleButtonState: function() {
     let shouldBeDisabled = !this._viewedElement ||
                            !this.inspector.selection.isElementNode() ||
                            this.inspector.selection.isAnonymousNode();
     this.addRuleButton.disabled = shouldBeDisabled;
   },
@@ -2141,17 +2325,17 @@ CssRuleView.prototype = {
    */
   highlightRule: function(rule) {
     let isRuleSelectorHighlighted = this._highlightRuleSelector(rule);
     let isStyleSheetHighlighted = this._highlightStyleSheet(rule);
     let isHighlighted = isRuleSelectorHighlighted || isStyleSheetHighlighted;
 
     // Highlight search matches in the rule properties
     for (let textProp of rule.textProps) {
-      if (this._highlightProperty(textProp.editor)) {
+      if (!textProp.invisible && this._highlightProperty(textProp.editor)) {
         isHighlighted = true;
       }
     }
 
     return isHighlighted;
   },
 
   /**
@@ -2430,21 +2614,28 @@ function RuleEditor(aRuleView, aRule) {
   this.isEditable = !aRule.isSystem;
   // Flag that blocks updates of the selector and properties when it is
   // being edited
   this.isEditing = false;
 
   this._onNewProperty = this._onNewProperty.bind(this);
   this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
   this._onSelectorDone = this._onSelectorDone.bind(this);
+  this._locationChanged = this._locationChanged.bind(this);
+
+  this.rule.domRule.on("location-changed", this._locationChanged);
 
   this._create();
 }
 
 RuleEditor.prototype = {
+  destroy: function() {
+    this.rule.domRule.off("location-changed");
+  },
+
   get isSelectorEditable() {
     let toolbox = this.ruleView.inspector.toolbox;
     let trait = this.isEditable &&
       toolbox.target.client.traits.selectorEditable &&
       this.rule.domRule.type !== ELEMENT_STYLE &&
       this.rule.domRule.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE;
 
     // Do not allow editing anonymousselectors until we can
@@ -2549,38 +2740,47 @@ RuleEditor.prototype = {
 
       // Create a property editor when the close brace is clicked.
       editableItem({ element: this.closeBrace }, () => {
         this.newProperty();
       });
     }
   },
 
+  /**
+   * Event handler called when a property changes on the
+   * StyleRuleActor.
+   */
+  _locationChanged: function(line, column) {
+    this.updateSourceLink();
+  },
+
   updateSourceLink: function() {
     let sourceLabel = this.element.querySelector(".ruleview-rule-source-label");
+    let title = this.rule.title;
     let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
-      this.rule.sheet.href : this.rule.title;
+      this.rule.sheet.href : title;
     let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : "";
 
     sourceLabel.setAttribute("tooltiptext", sourceHref + sourceLine);
 
     if (this.rule.isSystem) {
       let uaLabel = _strings.GetStringFromName("rule.userAgentStyles");
-      sourceLabel.setAttribute("value", uaLabel + " " + this.rule.title);
+      sourceLabel.setAttribute("value", uaLabel + " " + title);
 
       // Special case about:PreferenceStyleSheet, as it is generated on the
       // fly and the URI is not registered with the about: handler.
       // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
       if (sourceHref === "about:PreferenceStyleSheet") {
         sourceLabel.parentNode.setAttribute("unselectable", "true");
         sourceLabel.setAttribute("value", uaLabel);
         sourceLabel.removeAttribute("tooltiptext");
       }
     } else {
-      sourceLabel.setAttribute("value", this.rule.title);
+      sourceLabel.setAttribute("value", title);
       if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) {
         sourceLabel.parentNode.setAttribute("unselectable", "true");
       }
     }
 
     let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     if (showOrig && !this.rule.isSystem &&
         this.rule.domRule.type !== ELEMENT_STYLE) {
@@ -2649,17 +2849,17 @@ RuleEditor.prototype = {
             textContent: selectorText.value,
             class: selectorClass
           });
         }
       });
     }
 
     for (let prop of this.rule.textProps) {
-      if (!prop.editor) {
+      if (!prop.editor && !prop.invisible) {
         let editor = new TextPropertyEditor(this, prop);
         this.propertyList.appendChild(editor.element);
       }
     }
   },
 
   /**
    * Programatically add a new property to the rule.
@@ -2857,23 +3057,23 @@ RuleEditor.prototype = {
 
       let newRule = new Rule(elementStyle, ruleProps);
       let editor = new RuleEditor(ruleView, newRule);
       let rules = elementStyle.rules;
 
       rules.splice(rules.indexOf(this.rule), 1);
       rules.push(newRule);
       elementStyle._changed();
+      elementStyle.markOverriddenAll();
 
       editor.element.setAttribute("unmatched", !isMatching);
       this.element.parentNode.replaceChild(editor.element, this.element);
 
       // Remove highlight for modified selector
-      if (ruleView.highlightedSelector &&
-          ruleView.highlightedSelector === this.rule.selectorText) {
+      if (ruleView.highlightedSelector) {
         ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
           ruleView.highlightedSelector);
       }
 
       editor._moveSelectorFocus(direction);
     }).then(null, err => {
       this.isEditing = false;
       promiseWarn(err);
@@ -3167,17 +3367,18 @@ TextPropertyEditor.prototype = {
       this.enable.removeAttribute("checked");
     }
 
     this.warning.hidden = this.editing || this.isValid();
     this.filterProperty.hidden = this.editing ||
                                  !this.isValid() ||
                                  !this.prop.overridden;
 
-    if (this.prop.overridden || !this.prop.enabled) {
+    if (this.prop.overridden || !this.prop.enabled ||
+        !this.prop.isKnownProperty()) {
       this.element.classList.add("ruleview-overridden");
     } else {
       this.element.classList.remove("ruleview-overridden");
     }
 
     let name = this.prop.name;
     this.nameSpan.textContent = name;
 
@@ -3497,18 +3698,20 @@ TextPropertyEditor.prototype = {
   _onValueDone: function(value="", commit, direction) {
     let parsedProperties = this._getValueAndExtraProperties(value);
     let val = parseSingleValue(parsedProperties.firstValue);
     let isValueUnchanged = (!commit && !this.ruleEditor.isEditing) ||
                            !parsedProperties.propertiesToAdd.length &&
                            this.committed.value === val.value &&
                            this.committed.priority === val.priority;
     // If the value is not empty and unchanged, revert the property back to
-    // its original enabled or disabled state
+    // its original value and enabled or disabled state
     if (value.trim() && isValueUnchanged) {
+      this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
+                                                val.priority);
       this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
       return;
     }
 
     // First, set this property value (common case, only modified a property)
     this.prop.setValue(val.value, val.priority);
 
     if (!this.prop.enabled) {
@@ -3550,17 +3753,17 @@ TextPropertyEditor.prototype = {
     this._previewValue(this.valueSpan.textContent);
   },
 
   /**
    * Called when the swatch editor closes from an ESC. Revert to the original
    * value of this property before editing.
    */
   _onSwatchRevert: function() {
-    this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
+    this._previewValue(this.prop.value);
     this.update();
   },
 
   /**
    * Parse a value string and break it into pieces, starting with the
    * first value, and into an array of additional properties (if any).
    *
    * Example: Calling with "red; width: 100px" would return
@@ -3626,17 +3829,17 @@ TextPropertyEditor.prototype = {
 
   /**
    * Validate this property. Does it make sense for this value to be assigned
    * to this property name? This does not apply the property value
    *
    * @return {Boolean} true if the property value is valid, false otherwise.
    */
   isValid: function() {
-    return domUtils.cssPropertyIsValid(this.prop.name, this.prop.value);
+    return this.prop.isValid();
   }
 };
 
 /**
  * Store of CSSStyleDeclarations mapped to properties that have been changed by
  * the user.
  */
 function UserProperties() {
--- a/devtools/client/styleinspector/style-inspector.js
+++ b/devtools/client/styleinspector/style-inspector.js
@@ -43,16 +43,17 @@ function RuleViewTool(inspector, window)
   this.view.on("ruleview-linked-clicked", this.onLinkClicked);
 
   this.inspector.selection.on("detached", this.onSelected);
   this.inspector.selection.on("new-node-front", this.onSelected);
   this.inspector.on("layout-change", this.refresh);
   this.inspector.selection.on("pseudoclass", this.refresh);
   this.inspector.target.on("navigate", this.clearUserProperties);
   this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
+  this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
 
   this.onSelected();
 }
 
 RuleViewTool.prototype = {
   isSidebarActive: function() {
     if (!this.view) {
       return false;
@@ -147,16 +148,19 @@ RuleViewTool.prototype = {
   },
 
   destroy: function() {
     this.inspector.off("layout-change", this.refresh);
     this.inspector.selection.off("pseudoclass", this.refresh);
     this.inspector.selection.off("new-node-front", this.onSelected);
     this.inspector.target.off("navigate", this.clearUserProperties);
     this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected);
+    if (this.inspector.pageStyle) {
+      this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
+    }
 
     this.view.off("ruleview-linked-clicked", this.onLinkClicked);
     this.view.off("ruleview-changed", this.onPropertyChanged);
     this.view.off("ruleview-refreshed", this.onViewRefreshed);
 
     this.view.destroy();
 
     this.view = this.document = this.inspector = null;
@@ -174,16 +178,17 @@ function ComputedViewTool(inspector, win
   this.refresh = this.refresh.bind(this);
   this.onPanelSelected = this.onPanelSelected.bind(this);
 
   this.inspector.selection.on("detached", this.onSelected);
   this.inspector.selection.on("new-node-front", this.onSelected);
   this.inspector.on("layout-change", this.refresh);
   this.inspector.selection.on("pseudoclass", this.refresh);
   this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
+  this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
 
   this.view.selectElement(null);
 
   this.onSelected();
 }
 
 ComputedViewTool.prototype = {
   isSidebarActive: function() {
@@ -239,16 +244,19 @@ ComputedViewTool.prototype = {
   },
 
   destroy: function() {
     this.inspector.off("layout-change", this.refresh);
     this.inspector.sidebar.off("computedview-selected", this.refresh);
     this.inspector.selection.off("pseudoclass", this.refresh);
     this.inspector.selection.off("new-node-front", this.onSelected);
     this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
+    if (this.inspector.pageStyle) {
+      this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
+    }
 
     this.view.destroy();
 
     this.view = this.document = this.inspector = null;
   }
 };
 
 exports.RuleViewTool = RuleViewTool;
--- a/devtools/client/styleinspector/test/browser.ini
+++ b/devtools/client/styleinspector/test/browser.ini
@@ -14,16 +14,17 @@ support-files =
   doc_custom.html
   doc_filter.html
   doc_frame_script.js
   doc_keyframeanimation.html
   doc_keyframeanimation.css
   doc_matched_selectors.html
   doc_media_queries.html
   doc_pseudoelement.html
+  doc_ruleLineNumbers.html
   doc_sourcemaps.css
   doc_sourcemaps.css.map
   doc_sourcemaps.html
   doc_sourcemaps.scss
   doc_style_editor_link.css
   doc_test_image.png
   doc_urls_clickable.css
   doc_urls_clickable.html
@@ -55,16 +56,17 @@ support-files =
 [browser_ruleview_add-property_01.js]
 [browser_ruleview_add-property_02.js]
 [browser_ruleview_add-property-svg.js]
 [browser_ruleview_add-rule_01.js]
 [browser_ruleview_add-rule_02.js]
 [browser_ruleview_add-rule_03.js]
 [browser_ruleview_add-rule_04.js]
 [browser_ruleview_add-rule_pseudo_class.js]
+[browser_ruleview_authored.js]
 [browser_ruleview_colorpicker-and-image-tooltip_01.js]
 [browser_ruleview_colorpicker-and-image-tooltip_02.js]
 [browser_ruleview_colorpicker-appears-on-swatch-click.js]
 [browser_ruleview_colorpicker-commit-on-ENTER.js]
 [browser_ruleview_colorpicker-edit-gradient.js]
 [browser_ruleview_colorpicker-hides-on-tooltip.js]
 [browser_ruleview_colorpicker-multiple-changes.js]
 [browser_ruleview_colorpicker-release-outside-frame.js]
@@ -113,28 +115,31 @@ skip-if = e10s # Bug 1039528: "inspect e
 [browser_ruleview_edit-selector_06.js]
 [browser_ruleview_editable-field-focus_01.js]
 [browser_ruleview_editable-field-focus_02.js]
 [browser_ruleview_eyedropper.js]
 [browser_ruleview_filtereditor-appears-on-swatch-click.js]
 [browser_ruleview_filtereditor-commit-on-ENTER.js]
 [browser_ruleview_filtereditor-revert-on-ESC.js]
 skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s.
+[browser_ruleview_guessIndentation.js]
 [browser_ruleview_inherited-properties_01.js]
 [browser_ruleview_inherited-properties_02.js]
 [browser_ruleview_inherited-properties_03.js]
 [browser_ruleview_keybindings.js]
 [browser_ruleview_keyframes-rule_01.js]
 [browser_ruleview_keyframes-rule_02.js]
+[browser_ruleview_lineNumbers.js]
 [browser_ruleview_livepreview.js]
 [browser_ruleview_mark_overridden_01.js]
 [browser_ruleview_mark_overridden_02.js]
 [browser_ruleview_mark_overridden_03.js]
 [browser_ruleview_mark_overridden_04.js]
 [browser_ruleview_mark_overridden_05.js]
+[browser_ruleview_mark_overridden_06.js]
 [browser_ruleview_mark_overridden_07.js]
 [browser_ruleview_mathml-element.js]
 [browser_ruleview_media-queries.js]
 [browser_ruleview_multiple-properties-duplicates.js]
 [browser_ruleview_multiple-properties-priority.js]
 [browser_ruleview_multiple-properties-unfinished_01.js]
 [browser_ruleview_multiple-properties-unfinished_02.js]
 [browser_ruleview_multiple_properties_01.js]
--- a/devtools/client/styleinspector/test/browser_computedview_cycle_color.js
+++ b/devtools/client/styleinspector/test/browser_computedview_cycle_color.js
@@ -29,17 +29,23 @@ add_task(function*() {
   checkColorCycling(container, inspector);
 });
 
 function checkColorCycling(container, inspector) {
   let swatch = container.querySelector(".computedview-colorswatch");
   let valueNode = container.querySelector(".computedview-color");
   let win = inspector.sidebar.getWindowForTab("computedview");
 
-  // Hex (default)
+  // "Authored" (default; currently the computed value)
+  is(valueNode.textContent, "rgb(255, 0, 0)",
+                            "Color displayed as an RGB value.");
+
+  // Hex
+  EventUtils.synthesizeMouseAtCenter(swatch,
+                                     {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
 
   // HSL
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "hsl(0, 100%, 50%)",
                             "Color displayed as an HSL value.");
 
@@ -50,20 +56,14 @@ function checkColorCycling(container, in
                             "Color displayed as an RGB value.");
 
   // Color name
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "red",
                             "Color displayed as a color name.");
 
-  // "Authored" (currently the computed value)
+  // Back to "Authored"
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "rgb(255, 0, 0)",
                             "Color displayed as an RGB value.");
-
-  // Back to hex
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "#F00",
-                            "Color displayed as hex again.");
 }
--- a/devtools/client/styleinspector/test/browser_computedview_getNodeInfo.js
+++ b/devtools/client/styleinspector/test/browser_computedview_getNodeInfo.js
@@ -70,30 +70,30 @@ const TEST_DATA = [
     getHoveredNode: function*(view) {
       return getComputedViewProperty(view, "color").nameSpan;
     },
     assertNodeInfo: function(nodeInfo) {
       is(nodeInfo.type, VIEW_NODE_PROPERTY_TYPE);
       ok("property" in nodeInfo.value);
       ok("value" in nodeInfo.value);
       is(nodeInfo.value.property, "color");
-      is(nodeInfo.value.value, "#F00");
+      is(nodeInfo.value.value, "rgb(255, 0, 0)");
     }
   },
   {
     desc: "Testing a property value",
     getHoveredNode: function*(view) {
       return getComputedViewProperty(view, "color").valueSpan;
     },
     assertNodeInfo: function(nodeInfo) {
       is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
       ok("property" in nodeInfo.value);
       ok("value" in nodeInfo.value);
       is(nodeInfo.value.property, "color");
-      is(nodeInfo.value.value, "#F00");
+      is(nodeInfo.value.value, "rgb(255, 0, 0)");
     }
   },
   {
     desc: "Testing an image url",
     getHoveredNode: function*(view) {
       let {valueSpan} = getComputedViewProperty(view, "background-image");
       return valueSpan.querySelector(".theme-link");
     },
@@ -144,17 +144,17 @@ const TEST_DATA = [
     desc: "Testing a matched rule value",
     getHoveredNode: function*(view) {
       let content = yield getComputedViewMatchedRules(view, "color");
       return content.querySelector(".other-property-value");
     },
     assertNodeInfo: function(nodeInfo) {
       is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
       is(nodeInfo.value.property, "color");
-      is(nodeInfo.value.value, "#F00");
+      is(nodeInfo.value.value, "red");
     }
   },
   {
     desc: "Testing a matched rule stylesheet link",
     getHoveredNode: function*(view) {
       let content = yield getComputedViewMatchedRules(view, "color");
       return content.querySelector(".rule-link .theme-link");
     },
--- a/devtools/client/styleinspector/test/browser_computedview_refresh-on-style-change_01.js
+++ b/devtools/client/styleinspector/test/browser_computedview_refresh-on-style-change_01.js
@@ -20,10 +20,10 @@ add_task(function*() {
   info("Changing the node's style and waiting for the update");
   let onUpdated = inspector.once("computed-view-refreshed");
   getNode("#testdiv").style.cssText = "font-size: 15px; color: red;";
   yield onUpdated;
 
   fontSize = getComputedViewPropertyValue(view, "font-size");
   is(fontSize, "15px", "The computed view shows the updated font-size");
   let color = getComputedViewPropertyValue(view, "color");
-  is(color, "#F00", "The computed view also shows the color now");
+  is(color, "rgb(255, 0, 0)", "The computed view also shows the color now");
 });
--- a/devtools/client/styleinspector/test/browser_computedview_select-and-copy-styles.js
+++ b/devtools/client/styleinspector/test/browser_computedview_select-and-copy-styles.js
@@ -76,17 +76,17 @@ function checkSelectAll(view) {
   info("Testing select-all copy");
 
   let contentDoc = view.styleDocument;
   let prop = contentDoc.querySelector(".property-view");
 
   info("Checking that _onSelectAll() then copy returns the correct " +
     "clipboard value");
   view._contextmenu._onSelectAll();
-  let expectedPattern = "color: #FF0;[\\r\\n]+" +
+  let expectedPattern = "color: rgb\\(255, 255, 0\\);[\\r\\n]+" +
                         "font-family: helvetica,sans-serif;[\\r\\n]+" +
                         "font-size: 16px;[\\r\\n]+" +
                         "font-variant-caps: small-caps;[\\r\\n]*";
 
   return waitForClipboard(() => {
     fireCopyEvent(prop);
   }, () => {
     return checkClipboardData(expectedPattern);
--- a/devtools/client/styleinspector/test/browser_ruleview_add-property_01.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_add-property_01.js
@@ -64,11 +64,11 @@ function* testCreateNew(view) {
   editor.input.value = "#XYZ";
   let onBlur = once(editor.input, "blur");
   onModifications = elementRuleEditor.rule._applyingModifications;
   editor.input.blur();
   yield onBlur;
   yield onModifications;
 
   is(textProp.value, "#XYZ", "Text prop should have been changed.");
-  is(textProp.overridden, false, "Property should not be overridden");
+  is(textProp.overridden, true, "Property should be overridden");
   is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry");
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleinspector/test/browser_ruleview_authored.js
@@ -0,0 +1,127 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for as-authored styles.
+
+add_task(function*() {
+  yield basicTest();
+  yield overrideTest();
+  yield colorEditingTest();
+});
+
+function* createTestContent(style) {
+  let content = `<style type="text/css">
+      ${style}
+      </style>
+      <div id="testid" class="testclass">Styled Node</div>`;
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(content));
+
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  return view;
+}
+
+function* basicTest() {
+  let view = yield createTestContent("#testid {" +
+                                     // Invalid property.
+                                     "  something: random;" +
+                                     // Invalid value.
+                                     "  color: orang;" +
+                                     // Override.
+                                     "  background-color: blue;" +
+                                     "  background-color: #f0c;" +
+                                     "} ");
+
+  let elementStyle = view._elementStyle;
+
+  let expected = [
+    {name: "something", overridden: true},
+    {name: "color", overridden: true},
+    {name: "background-color", overridden: true},
+    {name: "background-color", overridden: false}
+  ];
+
+  let rule = elementStyle.rules[1];
+
+  for (let i = 0; i < expected.length; ++i) {
+    let prop = rule.textProps[i];
+    is(prop.name, expected[i].name, "test name for prop " + i);
+    is(prop.overridden, expected[i].overridden,
+       "test overridden for prop " + i);
+  }
+}
+
+function* overrideTest() {
+  let gradientText = "(45deg, rgba(255,255,255,0.2) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.2) 75%, transparent 75%, transparent);";
+
+  let view =
+      yield createTestContent("#testid {" +
+                              "  background-image: -moz-linear-gradient" +
+                              gradientText +
+                              "  background-image: -webkit-linear-gradient" +
+                              gradientText +
+                              "  background-image: linear-gradient" +
+                              gradientText +
+                              "} ");
+
+  let elementStyle = view._elementStyle;
+  let rule = elementStyle.rules[1];
+
+  // Initially the last property should be active.
+  for (let i = 0; i < 3; ++i) {
+    let prop = rule.textProps[i];
+    is(prop.name, "background-image", "check the property name");
+    is(prop.overridden, i !== 2, "check overridden for " + i);
+  }
+
+  rule.textProps[2].setEnabled(false);
+  yield rule._applyingModifications;
+
+  // Now the first property should be active.
+  for (let i = 0; i < 3; ++i) {
+    let prop = rule.textProps[i];
+    is(prop.overridden || !prop.enabled, i !== 0,
+       "post-change check overridden for " + i);
+  }
+}
+
+function* colorEditingTest() {
+  let colors = [
+    {name: "hex", text: "#f0c", result: "#0F0"},
+    {name: "rgb", text: "rgb(0,128,250)", result: "rgb(0, 255, 0)"}
+  ];
+
+  Services.prefs.setCharPref("devtools.defaultColorUnit", "authored");
+
+  for (let color of colors) {
+    let view = yield createTestContent("#testid {" +
+                                       "  color: " + color.text + ";" +
+                                       "} ");
+
+    let cPicker = view.tooltips.colorPicker;
+    let swatch = getRuleViewProperty(view, "#testid", "color").valueSpan
+        .querySelector(".ruleview-colorswatch");
+    let onShown = cPicker.tooltip.once("shown");
+    swatch.click();
+    yield onShown;
+
+    let testNode = yield getNode("#testid");
+
+    yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
+      element: testNode,
+      name: "color",
+      value: "rgb(0, 255, 0)"
+    });
+
+    let spectrum = yield cPicker.spectrum;
+    let onHidden = cPicker.tooltip.once("hidden");
+    EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
+    yield onHidden;
+
+    is(getRuleViewPropertyValue(view, "#testid", "color"), color.result,
+       "changing the color preserved the unit for " + color.name);
+  }
+}
--- a/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js
@@ -16,17 +16,17 @@ const TEST_URI = `
   </style>
   Testing the color picker tooltip!
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {view} = yield openRuleView();
   let value = getRuleViewProperty(view, "body", "background").valueSpan;
-  let swatch = value.querySelectorAll(".ruleview-colorswatch")[1];
+  let swatch = value.querySelectorAll(".ruleview-colorswatch")[0];
   let url = value.querySelector(".theme-link");
   yield testImageTooltipAfterColorChange(swatch, url, view);
 });
 
 function* testImageTooltipAfterColorChange(swatch, url, ruleView) {
   info("First, verify that the image preview tooltip works");
   let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip,
                                           url);
--- a/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js
@@ -56,12 +56,12 @@ function* testColorChangeIsntRevertedWhe
   onShown = ruleView.tooltips.previewTooltip.once("shown");
   let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip,
                                           url);
   ruleView.tooltips.previewTooltip.show(anchor);
   yield onShown;
 
   info("Image tooltip is shown, verify that the swatch is still correct");
   swatch = value.querySelector(".ruleview-colorswatch");
-  is(swatch.style.backgroundColor, "rgb(0, 0, 0)",
+  is(swatch.style.backgroundColor, "black",
     "The swatch's color is correct");
-  is(swatch.nextSibling.textContent, "#000", "The color name is correct");
+  is(swatch.nextSibling.textContent, "black", "The color name is correct");
 }
--- a/devtools/client/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js
@@ -34,17 +34,17 @@ function testColorParsing(view) {
   let swatchEls = ruleEl.valueSpan.querySelectorAll(".ruleview-colorswatch");
   ok(swatchEls, "The color swatch elements were found");
   is(swatchEls.length, 3, "There are 3 color swatches");
 
   let colorEls = ruleEl.valueSpan.querySelectorAll(".ruleview-color");
   ok(colorEls, "The color elements were found");
   is(colorEls.length, 3, "There are 3 color values");
 
-  let colors = ["#F06", "#333", "#000"];
+  let colors = ["#f06", "#333", "#000"];
   for (let i = 0; i < colors.length; i++) {
     is(colorEls[i].textContent, colors[i], "The right color value was found");
   }
 }
 
 function* testPickingNewColor(view) {
   // Grab the first color swatch and color in the gradient
   let ruleEl = getRuleViewProperty(view, "body", "background-image");
--- a/devtools/client/styleinspector/test/browser_ruleview_cycle-color.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_cycle-color.js
@@ -22,17 +22,17 @@ add_task(function*() {
   checkColorCycling(container, inspector);
 });
 
 function checkColorCycling(container, inspector) {
   let swatch = container.querySelector(".ruleview-colorswatch");
   let valueNode = container.querySelector(".ruleview-color");
   let win = inspector.sidebar.getWindowForTab("ruleview");
 
-  // Hex (default)
+  // Hex
   is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
 
   // HSL
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "hsl(0, 100%, 50%)",
                             "Color displayed as an HSL value.");
 
@@ -43,20 +43,21 @@ function checkColorCycling(container, in
                             "Color displayed as an RGB value.");
 
   // Color name
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "red",
                             "Color displayed as a color name.");
 
-  // "Authored" (currently the computed value)
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "rgb(255, 0, 0)",
-                            "Color displayed as an RGB value.");
-
-  // Back to hex
+  // "Authored"
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "#F00",
-                            "Color displayed as hex again.");
+                            "Color displayed as an authored value.");
+
+  // One more click skips hex, because it is the same as authored, and
+  // instead goes back to HSL.
+  EventUtils.synthesizeMouseAtCenter(swatch,
+                                     {type: "mousedown", shiftKey: true}, win);
+  is(valueNode.textContent, "hsl(0, 100%, 50%)",
+                            "Color displayed as an HSL value again.");
 }
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property-remove_02.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property-remove_02.js
@@ -43,19 +43,20 @@ function* testEditPropertyAndRemove(insp
 
   propEditor = ruleEditor.rule.textProps[0].editor;
 
   let editor = inplaceEditor(view.styleDocument.activeElement);
   is(inplaceEditor(propEditor.nameSpan), editor,
     "Focus should have moved to the next property name");
 
   info("Focus the property value and remove the property");
+  let onChanged = view.once("ruleview-changed");
   yield sendKeysAndWaitForFocus(view, ruleEditor.element,
     ["VK_TAB", "VK_DELETE", "VK_RETURN"]);
-  yield ruleEditor.rule._applyingModifications;
+  yield onChanged;
 
   newValue = yield executeInContent("Test:GetRulePropertyValue", {
     styleSheetIndex: 0,
     ruleIndex: 0,
     name: "color"
   });
   is(newValue, "", "color should have been unset.");
 
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property_01.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property_01.js
@@ -54,32 +54,31 @@ function* testEditProperty(ruleEditor, n
 
   is(inplaceEditor(propEditor.nameSpan), editor,
     "The property name editor got focused");
   let input = editor.input;
 
   info("Entering a new property name, including : to commit and " +
     "focus the value");
   let onValueFocus = once(ruleEditor.element, "focus", true);
-  let onModifications = ruleEditor.rule._applyingModifications;
+  let onModifications = ruleEditor.ruleView.once("ruleview-changed");
   EventUtils.sendString(name + ":", doc.defaultView);
   yield onValueFocus;
   yield onModifications;
 
   // Getting the value editor after focus
   editor = inplaceEditor(doc.activeElement);
   input = editor.input;
   is(inplaceEditor(propEditor.valueSpan), editor, "Focus moved to the value.");
 
   info("Entering a new value, including ; to commit and blur the value");
   let onBlur = once(input, "blur");
-  onModifications = ruleEditor.rule._applyingModifications;
   EventUtils.sendString(value + ";", doc.defaultView);
   yield onBlur;
-  yield onModifications;
+  yield ruleEditor.rule._applyingModifications;
 
   is(propEditor.isValid(), isValid,
     value + " is " + isValid ? "valid" : "invalid");
 
   info("Checking that the style property was changed on the content page");
   let propValue = yield executeInContent("Test:GetRulePropertyValue", {
     styleSheetIndex: 0,
     ruleIndex: 0,
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property_03.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property_03.js
@@ -28,29 +28,29 @@ add_task(function*() {
   yield selectNode("#testid", inspector);
 
   let ruleEditor = getRuleViewRuleEditor(view, 1);
   let propEditor = ruleEditor.rule.textProps[1].editor;
 
   yield focusEditableField(view, propEditor.valueSpan);
 
   info("Deleting all the text out of a value field");
+  let waitForUpdates = view.once("ruleview-changed");
   yield sendCharsAndWaitForFocus(view, ruleEditor.element,
     ["VK_DELETE", "VK_RETURN"]);
+  yield waitForUpdates;
 
   info("Pressing enter a couple times to cycle through editors");
   yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]);
   yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]);
 
   isnot(ruleEditor.rule.textProps[1].editor.nameSpan.style.display, "none",
     "The name span is visible");
   is(ruleEditor.rule.textProps.length, 2, "Correct number of props");
 });
 
 function* sendCharsAndWaitForFocus(view, element, chars) {
   let onFocus = once(element, "focus", true);
   for (let ch of chars) {
-    let onRuleViewChanged = view.once("ruleview-changed");
     EventUtils.sendChar(ch, element.ownerDocument.defaultView);
-    yield onRuleViewChanged;
   }
   yield onFocus;
 }
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property_07.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property_07.js
@@ -5,17 +5,17 @@
 "use strict";
 
 // Tests that adding multiple values will enable the property even if the
 // property does not change, and that the extra values are added correctly.
 
 const TEST_URI = `
   <style type='text/css'>
   #testid {
-    background-color: red;
+    background-color: #f00;
   }
   </style>
   <div id='testid'>Styled Node</div>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-selector_05.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_edit-selector_05.js
@@ -93,16 +93,16 @@ function* testAddProperty(view) {
   is(ruleEditor.rule.textProps.length, 1, "Created a new text property.");
   is(ruleEditor.propertyList.children.length, 1, "Created a property editor.");
   is(editor, inplaceEditor(textProp.editor.valueSpan),
     "Editing the value span now.");
 
   info("Entering a value and bluring the field to expect a rule change");
   editor.input.value = "center";
   let onBlur = once(editor.input, "blur");
-  onRuleViewChanged = view.once("ruleview-changed");
+  onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
   editor.input.blur();
   yield onBlur;
   yield onRuleViewChanged;
 
   is(textProp.value, "center", "Text prop should have been changed.");
   is(textProp.overridden, false, "Property should not be overridden");
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleinspector/test/browser_ruleview_guessIndentation.js
@@ -0,0 +1,76 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that we can guess indentation from a style sheet, not just a
+// rule.
+
+// Needed for openStyleEditor.
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleeditor/test/head.js", this);
+
+// Use a weird indentation depth to avoid accidental success.
+const TEST_URI = `
+  <style type='text/css'>
+div {
+       background-color: blue;
+}
+
+* {
+}
+</style>
+  <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+const expectedText = `
+div {
+       background-color: blue;
+}
+
+* {
+       color: chartreuse;
+}
+`;
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testIndentation(inspector, view);
+});
+
+function* testIndentation(inspector, view) {
+  let ruleEditor = getRuleViewRuleEditor(view, 2);
+
+  info("Focusing a new property name in the rule-view");
+  let editor = yield focusEditableField(view, ruleEditor.closeBrace);
+
+  let input = editor.input;
+
+  info("Entering color in the property name editor");
+  input.value = "color";
+
+  info("Pressing return to commit and focus the new value field");
+  let onValueFocus = once(ruleEditor.element, "focus", true);
+  let onModifications = ruleEditor.rule._applyingModifications;
+  EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+  yield onValueFocus;
+  yield onModifications;
+
+  // Getting the new value editor after focus
+  editor = inplaceEditor(view.styleDocument.activeElement);
+  info("Entering a value and bluring the field to expect a rule change");
+  editor.input.value = "chartreuse";
+  let onBlur = once(editor.input, "blur");
+  onModifications = ruleEditor.rule._applyingModifications;
+  editor.input.blur();
+  yield onBlur;
+  yield onModifications;
+
+  let { ui } = yield openStyleEditor();
+
+  let styleEditor = yield ui.editors[0].getSourceEditor();
+  let text = styleEditor.sourceEditor.getText();
+  is(text, expectedText, "style inspector changes are synced");
+}
--- a/devtools/client/styleinspector/test/browser_ruleview_inherited-properties_01.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_inherited-properties_01.js
@@ -33,13 +33,17 @@ function* simpleInherit(inspector, view)
   let elementRule = elementStyle.rules[0];
   ok(!elementRule.inherited,
     "Element style attribute should not consider itself inherited.");
 
   let inheritRule = elementStyle.rules[1];
   is(inheritRule.selectorText, "#test2",
     "Inherited rule should be the one that includes inheritable properties.");
   ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
-  is(inheritRule.textProps.length, 1,
-    "Should only display one inherited style");
-  let inheritProp = inheritRule.textProps[0];
+  is(inheritRule.textProps.length, 2,
+    "Rule should have two styles");
+  let bgcProp = inheritRule.textProps[0];
+  is(bgcProp.name, "background-color",
+     "background-color property should exist");
+  ok(bgcProp.invisible, "background-color property should be invisible");
+  let inheritProp = inheritRule.textProps[1];
   is(inheritProp.name, "color", "color should have been inherited.");
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleinspector/test/browser_ruleview_lineNumbers.js
@@ -0,0 +1,52 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that editing a rule will update the line numbers of subsequent
+// rules in the rule view.
+
+const TESTCASE_URI = TEST_URL_ROOT + "doc_ruleLineNumbers.html";
+
+add_task(function*() {
+  yield addTab(TESTCASE_URI);
+  let { inspector, view } = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  let elementRuleEditor = getRuleViewRuleEditor(view, 1);
+
+  let bodyRuleEditor = getRuleViewRuleEditor(view, 3);
+  let value = getRuleViewLinkTextByIndex(view, 2);
+  // Note that this is relative to the <style>.
+  is(value.slice(-2), ":6", "initial rule line number is 6");
+
+  info("Focusing a new property name in the rule-view");
+  let editor = yield focusEditableField(view, elementRuleEditor.closeBrace);
+
+  is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
+    "The new property editor got focused");
+  let input = editor.input;
+
+  info("Entering font-size in the property name editor");
+  input.value = "font-size";
+
+  info("Pressing return to commit and focus the new value field");
+  let onLocationChanged = once(bodyRuleEditor.rule.domRule, "location-changed");
+  let onValueFocus = once(elementRuleEditor.element, "focus", true);
+  EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+  yield onValueFocus;
+  yield elementRuleEditor.rule._applyingModifications;
+
+  // Getting the new value editor after focus
+  editor = inplaceEditor(view.styleDocument.activeElement);
+  info("Entering a value and bluring the field to expect a rule change");
+  editor.input.value = "23px";
+  editor.input.blur();
+  yield elementRuleEditor.rule._applyingModifications;
+
+  yield onLocationChanged;
+
+  let newBodyTitle = getRuleViewLinkTextByIndex(view, 2);
+  // Note that this is relative to the <style>.
+  is(newBodyTitle.slice(-2), ":7", "updated rule line number is 7");
+});
--- a/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_05.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_05.js
@@ -22,15 +22,16 @@ add_task(function*() {
   yield selectNode("#testid", inspector);
   yield testMarkOverridden(inspector, view);
 });
 
 function* testMarkOverridden(inspector, view) {
   let ruleEditor = getRuleViewRuleEditor(view, 1);
 
   yield createNewRuleViewProperty(ruleEditor, "background-color: red;");
+  yield ruleEditor.rule._applyingModifications;
 
   let firstProp = ruleEditor.rule.textProps[0];
   let secondProp = ruleEditor.rule.textProps[1];
 
   ok(firstProp.overridden, "First property should be overridden.");
   ok(!secondProp.overridden, "Second property should not be overridden.");
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_06.js
@@ -0,0 +1,60 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view marks overridden rules correctly after
+// editing the selector.
+
+const TEST_URI = `
+  <style type='text/css'>
+  div {
+    background-color: blue;
+    background-color: chartreuse;
+  }
+  </style>
+  <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+  let elementStyle = view._elementStyle;
+  let rule = elementStyle.rules[1];
+  checkProperties(rule);
+
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+  info("Focusing an existing selector name in the rule-view");
+  let editor = yield focusEditableField(view, ruleEditor.selectorText);
+
+  info("Entering a new selector name and committing");
+  editor.input.value = "div[class]";
+
+  let onRuleViewChanged = once(view, "ruleview-changed");
+  info("Entering the commit key");
+  EventUtils.synthesizeKey("VK_RETURN", {});
+  yield onRuleViewChanged;
+
+  view.searchField.focus();
+  checkProperties(rule);
+}
+
+// A helper to perform a repeated set of checks.
+function checkProperties(rule) {
+  let prop = rule.textProps[0];
+  is(prop.name, "background-color",
+     "First property should be background-color");
+  is(prop.value, "blue", "First property value should be blue");
+  ok(prop.overridden, "prop should be overridden.");
+  prop = rule.textProps[1];
+  is(prop.name, "background-color",
+     "Second property should be background-color");
+  is(prop.value, "chartreuse", "First property value should be chartreuse");
+  ok(!prop.overridden, "prop should not be overridden.");
+}
--- a/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_07.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_07.js
@@ -46,17 +46,18 @@ function* testMarkOverridden(inspector, 
 
   let RESULTS = [
     // We skip the first element
     [],
     [{name: "margin-left", value: "23px", overridden: true}],
     [{name: "margin-right", value: "23px", overridden: false},
      {name: "margin-left", value: "1px", overridden: false}],
     [{name: "font-size", value: "12px", overridden: false}],
-    [{name: "font-size", value: "79px", overridden: true}]
+    [{name: "margin-right", value: "1px", overridden: true},
+     {name: "font-size", value: "79px", overridden: true}]
   ];
 
   for (let i = 1; i < RESULTS.length; ++i) {
     let idRule = elementStyle.rules[i];
 
     for (let propIndex in RESULTS[i]) {
       let expected = RESULTS[i][propIndex];
       let prop = idRule.textProps[propIndex];
--- a/devtools/client/styleinspector/test/browser_ruleview_media-queries.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_media-queries.js
@@ -19,12 +19,11 @@ add_task(function*() {
   let _strings = Services.strings
     .createBundle("chrome://global/locale/devtools/styleinspector.properties");
 
   let inline = _strings.GetStringFromName("rule.sourceInline");
 
   is(elementStyle.rules.length, 3, "Should have 3 rules.");
   is(elementStyle.rules[0].title, inline, "check rule 0 title");
   is(elementStyle.rules[1].title, inline +
-    ":15 @media screen and (min-width: 1px)", "check rule 1 title");
-  is(elementStyle.rules[2].title, inline + ":8", "check rule 2 title");
+    ":9 @media screen and (min-width: 1px)", "check rule 1 title");
+  is(elementStyle.rules[2].title, inline + ":2", "check rule 2 title");
 });
-
--- a/devtools/client/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js
@@ -1,44 +1,32 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test that the rule-view behaves correctly when entering mutliple and/or
+// Test that the rule-view behaves correctly when entering multiple and/or
 // unfinished properties/values in inplace-editors
 
 const TEST_URI = "<div>Test Element</div>";
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   yield selectNode("div", inspector);
   yield testCreateNewMultiUnfinished(inspector, view);
 });
 
-function waitRuleViewChanged(view, n) {
-  let deferred = promise.defer();
-  let count = 0;
-  let listener = function() {
-    if (++count == n) {
-      view.off("ruleview-changed", listener);
-      deferred.resolve();
-    }
-  };
-  view.on("ruleview-changed", listener);
-  return deferred.promise;
-}
 function* testCreateNewMultiUnfinished(inspector, view) {
   let ruleEditor = getRuleViewRuleEditor(view, 0);
   let onMutation = inspector.once("markupmutation");
-  // There is 5 rule-view updates, one for the rule view creation,
-  // one for each new property
-  let onRuleViewChanged = waitRuleViewChanged(view, 5);
+  // There are 2 rule-view updates: one for the preview and one for
+  // the final commit.
+  let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
   yield createNewRuleViewProperty(ruleEditor,
     "color:blue;background : orange   ; text-align:center; border-color: ");
   yield onMutation;
   yield onRuleViewChanged;
 
   is(ruleEditor.rule.textProps.length, 4,
     "Should have created new text properties.");
   is(ruleEditor.propertyList.children.length, 4,
--- a/devtools/client/styleinspector/test/browser_ruleview_pseudo-element_01.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_pseudo-element_01.js
@@ -161,27 +161,27 @@ function* testParagraph(inspector, view)
     firstLetterRulesNb: 1,
     selectionRulesNb: 1
   });
 
   assertGutters(view);
 
   let elementFirstLineRule = rules.firstLineRules[0];
   is(convertTextPropsToString(elementFirstLineRule.textProps),
-     "background: blue none repeat scroll 0% 0%",
+     "background: blue",
      "Paragraph first-line properties are correct");
 
   let elementFirstLetterRule = rules.firstLetterRules[0];
   is(convertTextPropsToString(elementFirstLetterRule.textProps),
      "color: red; font-size: 130%",
      "Paragraph first-letter properties are correct");
 
   let elementSelectionRule = rules.selectionRules[0];
   is(convertTextPropsToString(elementSelectionRule.textProps),
-     "color: white; background: black none repeat scroll 0% 0%",
+     "color: white; background: black",
      "Paragraph first-letter properties are correct");
 }
 
 function* testBody(inspector, view) {
   yield testNode("body", inspector, view);
 
   let gutters = getGutters(view);
   is(gutters.length, 0, "There are no gutter headings");
--- a/devtools/client/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js
@@ -126,30 +126,28 @@ function* testPropertyChange6(inspector,
     "url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
     inspector);
 
   let rule = ruleView._elementStyle.rules[0];
   is(rule.editor.element.querySelectorAll(".ruleview-property").length, 5,
     "Added a property");
   validateTextProp(rule.textProps[4], true, "background",
                    "red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
-                   "shortcut property correctly set",
-                   "#F00 url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%");
+                   "shortcut property correctly set");
 }
 
 function* changeElementStyle(testElement, style, inspector) {
   let onRefreshed = inspector.once("rule-view-refreshed");
   testElement.setAttribute("style", style);
   yield onRefreshed;
 }
 
-function validateTextProp(aProp, aEnabled, aName, aValue, aDesc,
-    valueSpanText) {
+function validateTextProp(aProp, aEnabled, aName, aValue, aDesc) {
   is(aProp.enabled, aEnabled, aDesc + ": enabled.");
   is(aProp.name, aName, aDesc + ": name.");
   is(aProp.value, aValue, aDesc + ": value.");
 
   is(aProp.editor.enable.hasAttribute("checked"), aEnabled,
     aDesc + ": enabled checkbox.");
   is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span.");
   is(aProp.editor.valueSpan.textContent,
-    valueSpanText || aValue, aDesc + ": value span.");
+    aValue, aDesc + ": value span.");
 }
--- a/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_03.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_03.js
@@ -2,22 +2,24 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that the rule view search filter works properly in the computed list
 // for color values.
 
-const SEARCH = "background-color: #F3F3F3";
+// The color format here is chosen to match the default returned by
+// CssColor.toString.
+const SEARCH = "background-color: rgb(243, 243, 243)";
 
 const TEST_URI = `
   <style type="text/css">
     .testclass {
-      background: #F3F3F3 none repeat scroll 0% 0%;
+      background: rgb(243, 243, 243) none repeat scroll 0% 0%;
     }
   </style>
   <div class="testclass">Styled Node</h1>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
--- a/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_04.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_04.js
@@ -38,17 +38,17 @@ function* testModifyPropertyValueFilter(
   is(rule.selectorText, "#testid", "Second rule is #testid.");
   ok(!propEditor.container.classList.contains("ruleview-highlight"),
     "margin text property is not highlighted.");
   ok(rule.textProps[1].editor.container.classList
     .contains("ruleview-highlight"),
     "top text property is correctly highlighted.");
 
   let onBlur = once(editor.input, "blur");
-  let onModification = rule._applyingModifications;
+  let onModification = view.once("ruleview-changed");
   EventUtils.sendString("4px 0px", view.styleWindow);
   EventUtils.synthesizeKey("VK_RETURN", {});
   yield onBlur;
   yield onModification;
 
   ok(propEditor.container.classList.contains("ruleview-highlight"),
     "margin text property is correctly highlighted.");
   ok(!computed.hasAttribute("filter-open"), "margin computed list is closed.");
--- a/devtools/client/styleinspector/test/browser_ruleview_select-and-copy-styles.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_select-and-copy-styles.js
@@ -59,20 +59,20 @@ function* checkCopySelection(view) {
   range.setEnd(values[4], 2);
   win.getSelection().addRange(range);
 
   info("Checking that _Copy() returns the correct clipboard value");
 
   let expectedPattern = "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
                         "    font-family: helvetica,sans-serif;[\\r\\n]+" +
-                        "    color: #AAA;[\\r\\n]+" +
+                        "    color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
-                        "    color: #000;[\\r\\n]*";
+                        "    color: #000000;[\\r\\n]*";
 
   let onPopup = once(view._contextmenu._menupopup, "popupshown");
   EventUtils.synthesizeMouseAtCenter(prop,
     {button: 2, type: "contextmenu"}, win);
   yield onPopup;
 
   ok(!view._contextmenu.menuitemCopy.hidden,
     "Copy menu item is not hidden as expected");
@@ -97,20 +97,20 @@ function* checkSelectAll(view) {
   info("Checking that _SelectAll() then copy returns the correct " +
     "clipboard value");
   view._contextmenu._onSelectAll();
   let expectedPattern = "[\\r\\n]+" +
                         "element {[\\r\\n]+" +
                         "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
                         "    font-family: helvetica,sans-serif;[\\r\\n]+" +
-                        "    color: #AAA;[\\r\\n]+" +
+                        "    color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
-                        "    color: #000;[\\r\\n]+" +
+                        "    color: #000000;[\\r\\n]+" +
                         "}[\\r\\n]*";
 
   let onPopup = once(view._contextmenu._menupopup, "popupshown");
   EventUtils.synthesizeMouseAtCenter(prop,
     {button: 2, type: "contextmenu"}, win);
   yield onPopup;
 
   ok(!view._contextmenu.menuitemCopy.hidden,
--- a/devtools/client/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js
@@ -35,19 +35,21 @@ function* userAgentStylesUneditable(insp
 
   yield selectNode("a", inspector);
   let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable);
 
   for (let rule of uaRules) {
     ok(rule.editor.element.hasAttribute("uneditable"),
       "UA rules have uneditable attribute");
 
-    ok(!rule.textProps[0].editor.nameSpan._editable,
+    let firstProp = rule.textProps.filter(p => !p.invisible)[0];
+
+    ok(!firstProp.editor.nameSpan._editable,
       "nameSpan is not editable");
-    ok(!rule.textProps[0].editor.valueSpan._editable,
+    ok(!firstProp.editor.valueSpan._editable,
       "valueSpan is not editable");
     ok(!rule.editor.closeBrace._editable, "closeBrace is not editable");
 
     let colorswatch = rule.editor.element
       .querySelector(".ruleview-colorswatch");
     if (colorswatch) {
       ok(!view.tooltips.colorPicker.swatches.has(colorswatch),
         "The swatch is not editable");
--- a/devtools/client/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js
+++ b/devtools/client/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -80,17 +80,17 @@ function testIsColorPopupOnNode(view, no
   info("Testing node " + node);
   view.styleDocument.popupNode = node;
   view._contextmenu._colorToCopy = "";
 
   let result = view._contextmenu._isColorPopup();
   let correct = isColorValueNode(node);
 
   is(result, correct, "_isColorPopup returned the expected value " + correct);
-  is(view._contextmenu._colorToCopy, (correct) ? "#123ABC" : "",
+  is(view._contextmenu._colorToCopy, (correct) ? "rgb(18, 58, 188)" : "",
      "_colorToCopy was set to the expected value");
 }
 
 /**
  * Check if a node is part of color value i.e. it has parent with a 'data-color'
  * attribute.
  */
 function isColorValueNode(node) {
--- a/devtools/client/styleinspector/test/browser_styleinspector_refresh_when_active.js
+++ b/devtools/client/styleinspector/test/browser_styleinspector_refresh_when_active.js
@@ -11,17 +11,17 @@ const TEST_URI = `
   <div id="two" style="color:blue;">two</div>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   yield selectNode("#one", inspector);
 
-  is(getRuleViewPropertyValue(view, "element", "color"), "#F00",
+  is(getRuleViewPropertyValue(view, "element", "color"), "red",
     "The rule-view shows the properties for test node one");
 
   let cView = inspector.sidebar.getWindowForTab("computedview")
     .computedview.view;
   let prop = getComputedViewProperty(cView, "color");
   ok(!prop, "The computed-view doesn't show the properties for test node one");
 
   info("Switching to the computed-view");
@@ -33,11 +33,11 @@ add_task(function*() {
     "The computed-view shows the properties for test node one");
 
   info("Selecting test node two");
   yield selectNode("#two", inspector);
 
   ok(getComputedViewPropertyValue(cView, "color"), "#00F",
     "The computed-view shows the properties for test node two");
 
-  is(getRuleViewPropertyValue(view, "element", "color"), "#F00",
+  is(getRuleViewPropertyValue(view, "element", "color"), "red",
     "The rule-view doesn't the properties for test node two");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleinspector/test/doc_ruleLineNumbers.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>simple testcase</title>
+  <style type="text/css">
+  #testid {
+    background-color: seagreen;
+  }
+
+  body {
+    color: chartreuse;
+  }
+  </style>
+</head>
+<body>
+	<div id="testid">simple testcase</div>
+</body>
+</html>
--- a/devtools/client/styleinspector/test/head.js
+++ b/devtools/client/styleinspector/test/head.js
@@ -302,46 +302,66 @@ function openComputedView() {
  * @return a promise that resolves when the inspector is ready and the rule
  * view is visible and ready
  */
 function openRuleView() {
   return openInspectorSideBar("ruleview");
 }
 
 /**
+ * Wait for eventName on target to be delivered a number of times.
+ *
+ * @param {Object} target
+ *        An observable object that either supports on/off or
+ *        addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Number} numTimes
+ *        Number of deliveries to wait for.
+ * @param {Boolean} useCapture
+ *        Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function waitForNEvents(target, eventName, numTimes, useCapture = false) {
+  info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+  let deferred = promise.defer();
+  let count = 0;
+
+  for (let [add, remove] of [
+    ["addEventListener", "removeEventListener"],
+    ["addListener", "removeListener"],
+    ["on", "off"]
+  ]) {
+    if ((add in target) && (remove in target)) {
+      target[add](eventName, function onEvent(...aArgs) {
+        if (++count == numTimes) {
+          target[remove](eventName, onEvent, useCapture);
+          deferred.resolve.apply(deferred, aArgs);
+        }
+      }, useCapture);
+      break;
+    }
+  }
+
+  return deferred.promise;
+}
+
+/**
  * Wait for eventName on target.
  *
  * @param {Object} target
  *        An observable object that either supports on/off or
  *        addEventListener/removeEventListener
  * @param {String} eventName
  * @param {Boolean} useCapture
  *        Optional, for addEventListener/removeEventListener
  * @return A promise that resolves when the event has been handled
  */
 function once(target, eventName, useCapture=false) {
-  info("Waiting for event: '" + eventName + "' on " + target + ".");
-
-  let deferred = promise.defer();
-
-  for (let [add, remove] of [
-    ["addEventListener", "removeEventListener"],
-    ["addListener", "removeListener"],
-    ["on", "off"]
-  ]) {
-    if ((add in target) && (remove in target)) {
-      target[add](eventName, function onEvent(...aArgs) {
-        target[remove](eventName, onEvent, useCapture);
-        deferred.resolve.apply(deferred, aArgs);
-      }, useCapture);
-      break;
-    }
-  }
-
-  return deferred.promise;
+  return waitForNEvents(target, eventName, 1, useCapture);
 }
 
 /**
  * This shouldn't be used in the tests, but is useful when writing new tests or
  * debugging existing tests in order to introduce delays in the test steps
  *
  * @param {Number} ms
  *        The time to wait
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -409,16 +409,30 @@ var AnimationPlayerActor = ActorClass({
    */
   setPlaybackRate: method(function(playbackRate) {
     this.player.playbackRate = playbackRate;
   }, {
     request: {
       currentTime: Arg(0, "number")
     },
     response: {}
+  }),
+
+  /**
+   * Get data about the keyframes of this animation player.
+   * @return {Object} Returns a list of frames, each frame containing the list
+   * animated properties as well as the frame's offset.
+   */
+  getFrames: method(function() {
+    return this.player.effect.getFrames();
+  }, {
+    request: {},
+    response: {
+      frames: RetVal("json")
+    }
   })
 });
 
 var AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
   initialize: function(conn, form, detail, ctx) {
     Front.prototype.initialize.call(this, conn, form, detail, ctx);
 
     this.state = {};
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -9,26 +9,31 @@ const {Cc, Ci, Cu} = require("chrome");
 const promise = require("promise");
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal, types} = protocol;
 const events = require("sdk/event/core");
 const {Class} = require("sdk/core/heritage");
 const {LongStringActor} = require("devtools/server/actors/string");
 const {PSEUDO_ELEMENT_SET} = require("devtools/shared/styleinspector/css-logic");
 
-// This will add the "stylesheet" actor type for protocol.js to recognize
-require("devtools/server/actors/stylesheets");
+// This will also add the "stylesheet" actor type for protocol.js to recognize
+const {UPDATE_PRESERVING_RULES, UPDATE_GENERAL} =
+      require("devtools/server/actors/stylesheets");
 
 loader.lazyGetter(this, "CssLogic", () => {
   return require("devtools/shared/styleinspector/css-logic").CssLogic;
 });
 loader.lazyGetter(this, "DOMUtils", () => {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
+loader.lazyGetter(this, "RuleRewriter", () => {
+  return require("devtools/client/styleinspector/css-parsing-utils").RuleRewriter;
+});
+
 // The PageStyle actor flattens the DOM CSS objects a little bit, merging
 // Rules and their Styles into one actor.  For elements (which have a style
 // but no associated rule) we fake a rule with the following style id.
 const ELEMENT_STYLE = 100;
 exports.ELEMENT_STYLE = ELEMENT_STYLE;
 
 // Not included since these are uneditable by the user.
 // See https://hg.mozilla.org/mozilla-central/file/696a4ad5d011/layout/style/nsCSSPseudoElementList.h#l74
@@ -123,16 +128,23 @@ types.addDictType("fontface", {
 
 /**
  * The PageStyle actor lets the client look at the styles on a page, as
  * they are applied to a given node.
  */
 var PageStyleActor = protocol.ActorClass({
   typeName: "pagestyle",
 
+  events: {
+    "stylesheet-updated": {
+      type: "styleSheetUpdated",
+      styleSheet: Arg(0, "stylesheet")
+    }
+  },
+
   /**
    * Create a PageStyleActor.
    *
    * @param inspector
    *    The InspectorActor that owns this PageStyleActor.
    *
    * @constructor
    */
@@ -146,29 +158,37 @@ var PageStyleActor = protocol.ActorClass
     this.walker = inspector.walker;
     this.cssLogic = new CssLogic();
 
     // Stores the association of DOM objects -> actors
     this.refMap = new Map();
 
     this.onFrameUnload = this.onFrameUnload.bind(this);
     events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
+
+    this._styleApplied = this._styleApplied.bind(this);
+    this._watchedSheets = new Set();
   },
 
-  destroy: function () {
+  destroy: function() {
     if (!this.walker) {
       return;
     }
     protocol.Actor.prototype.destroy.call(this);
     events.off(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
     this.inspector = null;
     this.walker = null;
     this.refMap = null;
     this.cssLogic = null;
     this._styleElement = null;
+
+    for (let sheet of this._watchedSheets) {
+      sheet.off("style-applied", this._styleApplied);
+    }
+    this._watchedSheets.clear();
   },
 
   get conn() {
     return this.inspector.conn;
   },
 
   form: function(detail) {
     if (detail === "actorid") {
@@ -177,46 +197,76 @@ var PageStyleActor = protocol.ActorClass
 
     return {
       actor: this.actorID,
       traits: {
         // Whether the actor has had bug 1103993 fixed, which means that the
         // getApplied method calls cssLogic.highlight(node) to recreate the
         // style cache. Clients requesting getApplied from actors that have not
         // been fixed must make sure cssLogic.highlight(node) was called before.
-        getAppliedCreatesStyleCache: true
+        getAppliedCreatesStyleCache: true,
+        // Whether addNewRule accepts the editAuthored argument.
+        authoredStyles: true
       }
     };
   },
 
   /**
+   * Called when a style sheet is updated.
+   */
+  _styleApplied: function(kind, styleSheet) {
+    if (kind === UPDATE_GENERAL) {
+      events.emit(this, "stylesheet-updated", styleSheet);
+    }
+  },
+
+  /**
    * Return or create a StyleRuleActor for the given item.
    * @param item Either a CSSStyleRule or a DOM element.
    */
   _styleRef: function(item) {
     if (this.refMap.has(item)) {
       return this.refMap.get(item);
     }
     let actor = StyleRuleActor(this, item);
     this.manage(actor);
     this.refMap.set(item, actor);
 
     return actor;
   },
 
   /**
+   * Update the association between a StyleRuleActor and its
+   * corresponding item.  This is used when a StyleRuleActor updates
+   * as style sheet and starts using a new rule.
+   *
+   * @param oldItem The old association; either a CSSStyleRule or a
+   *                DOM element.
+   * @param item Either a CSSStyleRule or a DOM element.
+   * @param actor a StyleRuleActor
+   */
+  updateStyleRef: function(oldItem, item, actor) {
+    this.refMap.delete(oldItem);
+    this.refMap.set(item, actor);
+  },
+
+  /**
    * Return or create a StyleSheetActor for the given nsIDOMCSSStyleSheet.
    * @param  {DOMStyleSheet} sheet
    *         The style sheet to create an actor for.
    * @return {StyleSheetActor}
    *         The actor for this style sheet
    */
   _sheetRef: function(sheet) {
     let tabActor = this.inspector.tabActor;
     let actor = tabActor.createStyleSheetActor(sheet);
+    if (!this._watchedSheets.has(actor)) {
+      this._watchedSheets.add(actor);
+      actor.on("style-applied", this._styleApplied);
+    }
     return actor;
   },
 
   /**
    * Get the computed style for a node.
    *
    * @param NodeActor node
    * @param object options
@@ -519,26 +569,32 @@ var PageStyleActor = protocol.ActorClass
    *   `filter`: A string filter that affects the "matched" handling.
    *     'user': Include properties from user style sheets.
    *     'ua': Include properties from user and user-agent sheets.
    *     Default value is 'ua'
    *   `inherited`: Include styles inherited from parent nodes.
    *   `matchedSelectors`: Include an array of specific selectors that
    *     caused this rule to match its node.
    */
-  getApplied: method(function(node, options) {
+  getApplied: method(Task.async(function*(node, options) {
     if (!node) {
       return {entries: [], rules: [], sheets: []};
     }
 
     this.cssLogic.highlight(node.rawNode);
     let entries = [];
     entries = entries.concat(this._getAllElementRules(node, undefined, options));
-    return this.getAppliedProps(node, entries, options);
-  }, {
+
+    let result = this.getAppliedProps(node, entries, options);
+    for (let rule of result.rules) {
+      // See the comment in |form| to understand this.
+      yield rule.getAuthoredCssText();
+    }
+    return result;
+  }), {
     request: {
       node: Arg(0, "domnode"),
       inherited: Option(1, "boolean"),
       matchedSelectors: Option(1, "boolean"),
       filter: Option(1, "string")
     },
     response: RetVal("appliedStylesReturn")
   }),
@@ -900,22 +956,27 @@ var PageStyleActor = protocol.ActorClass
   getNewAppliedProps: function(node, rule) {
     let ruleActor = this._styleRef(rule);
     return this.getAppliedProps(node, [{ rule: ruleActor }],
       { matchedSelectors: true });
   },
 
   /**
    * Adds a new rule, and returns the new StyleRuleActor.
-   * @param NodeActor node
-   * @param [string] pseudoClasses The list of pseudo classes to append to the
-   * new selector.
-   * @returns StyleRuleActor of the new rule
+   * @param {NodeActor} node
+   * @param {String} pseudoClasses The list of pseudo classes to append to the
+   *        new selector.
+   * @param {Boolean} editAuthored
+   *        True if the selector should be updated by editing the
+   *        authored text; false if the selector should be updated via
+   *        CSSOM.
+   * @returns {StyleRuleActor} the new rule
    */
-  addNewRule: method(function(node, pseudoClasses) {
+  addNewRule: method(Task.async(function*(node, pseudoClasses,
+                                          editAuthored = false) {
     let style = this.styleElement;
     let sheet = style.sheet;
     let cssRules = sheet.cssRules;
     let rawNode = node.rawNode;
 
     let selector;
     if (rawNode.id) {
       selector = "#" + CSS.escape(rawNode.id);
@@ -925,21 +986,32 @@ var PageStyleActor = protocol.ActorClass
       selector = rawNode.tagName.toLowerCase();
     }
 
     if (pseudoClasses && pseudoClasses.length > 0) {
       selector += pseudoClasses.join("");
     }
 
     let index = sheet.insertRule(selector + " {}", cssRules.length);
-    return this.getNewAppliedProps(node, cssRules.item(index));
-  }, {
+
+    // If inserting the rule succeeded, go ahead and edit the source
+    // text if requested.
+    if (editAuthored) {
+      let sheetActor = this._sheetRef(sheet);
+      let {str: authoredText} = yield sheetActor.getText();
+      authoredText += "\n" + selector + " {\n" + "}";
+      yield sheetActor.update(authoredText, false);
+    }
+
+    return this.getNewAppliedProps(node, sheet.cssRules.item(index));
+  }), {
     request: {
       node: Arg(0, "domnode"),
-      pseudoClasses: Arg(1, "nullable:array:string")
+      pseudoClasses: Arg(1, "nullable:array:string"),
+      editAuthored: Arg(2, "boolean")
     },
     response: RetVal("appliedStylesReturn")
   }),
 });
 exports.PageStyleActor = PageStyleActor;
 
 /**
  * Front object for the PageStyleActor
@@ -961,16 +1033,20 @@ var PageStyleFront = protocol.FrontClass
   destroy: function() {
     protocol.Front.prototype.destroy.call(this);
   },
 
   get walker() {
     return this.inspector.walker;
   },
 
+  get supportsAuthoredStyles() {
+    return this._form.traits && this._form.traits.authoredStyles;
+  },
+
   getMatchedSelectors: protocol.custom(function(node, property, options) {
     return this._getMatchedSelectors(node, property, options).then(ret => {
       return ret.matched;
     });
   }, {
     impl: "_getMatchedSelectors"
   }),
 
@@ -984,17 +1060,23 @@ var PageStyleFront = protocol.FrontClass
     }
     let ret = yield this._getApplied(node, options);
     return ret.entries;
   }), {
     impl: "_getApplied"
   }),
 
   addNewRule: protocol.custom(function(node, pseudoClasses) {
-    return this._addNewRule(node, pseudoClasses).then(ret => {
+    let addPromise;
+    if (this.supportsAuthoredStyles) {
+      addPromise = this._addNewRule(node, pseudoClasses, true);
+    } else {
+      addPromise = this._addNewRule(node, pseudoClasses);
+    }
+    return addPromise.then(ret => {
       return ret.entries[0];
     });
   }, {
     impl: "_addNewRule"
   })
 });
 
 /**
@@ -1002,29 +1084,44 @@ var PageStyleFront = protocol.FrontClass
  *
  * We slightly flatten the CSSOM for this actor, it represents
  * both the CSSRule and CSSStyle objects in one actor.  For nodes
  * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor
  * with a special rule type (100).
  */
 var StyleRuleActor = protocol.ActorClass({
   typeName: "domstylerule",
+
+  events: {
+    "location-changed": {
+      type: "locationChanged",
+      line: Arg(0, "number"),
+      column: Arg(1, "number")
+    },
+  },
+
   initialize: function(pageStyle, item) {
     protocol.Actor.prototype.initialize.call(this, null);
     this.pageStyle = pageStyle;
     this.rawStyle = item.style;
+    this._parentSheet = null;
+    this._onStyleApplied = this._onStyleApplied.bind(this);
 
     if (item instanceof (Ci.nsIDOMCSSRule)) {
       this.type = item.type;
       this.rawRule = item;
       if ((this.rawRule instanceof Ci.nsIDOMCSSStyleRule ||
            this.rawRule instanceof Ci.nsIDOMMozCSSKeyframeRule) &&
-           this.rawRule.parentStyleSheet) {
-        this.line = DOMUtils.getRuleLine(this.rawRule);
+          this.rawRule.parentStyleSheet) {
+        this.line = DOMUtils.getRelativeRuleLine(this.rawRule);
         this.column = DOMUtils.getRuleColumn(this.rawRule);
+        this._parentSheet = this.rawRule.parentStyleSheet;
+        this._computeRuleIndex();
+        this.sheetActor = this.pageStyle._sheetRef(this._parentSheet);
+        this.sheetActor.on("style-applied", this._onStyleApplied);
       }
     } else {
       // Fake a rule
       this.type = ELEMENT_STYLE;
       this.rawNode = item;
       this.rawRule = {
         style: item.style,
         toString: function() {
@@ -1042,24 +1139,38 @@ var StyleRuleActor = protocol.ActorClass
     if (!this.rawStyle) {
       return;
     }
     protocol.Actor.prototype.destroy.call(this);
     this.rawStyle = null;
     this.pageStyle = null;
     this.rawNode = null;
     this.rawRule = null;
+    if (this.sheetActor) {
+      this.sheetActor.off("style-applied", this._onStyleApplied);
+    }
   },
 
   // Objects returned by this actor are owned by the PageStyleActor
   // to which this rule belongs.
   get marshallPool() {
     return this.pageStyle;
   },
 
+  // True if this rule supports as-authored styles, meaning that the
+  // rule text can be rewritten using setRuleText.
+  get canSetRuleText() {
+    // Special case about:PreferenceStyleSheet, as it is
+    // generated on the fly and the URI is not registered with the
+    // about: handler.
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
+    return !!(this._parentSheet &&
+              this._parentSheet.href !== "about:PreferenceStyleSheet");
+  },
+
   getDocument: function(sheet) {
     let document;
 
     if (sheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) {
       document = sheet.ownerNode;
     } else {
       document = sheet.ownerNode.ownerDocument;
     }
@@ -1080,16 +1191,19 @@ var StyleRuleActor = protocol.ActorClass
       actor: this.actorID,
       type: this.type,
       line: this.line || undefined,
       column: this.column,
       traits: {
         // Whether the style rule actor implements the modifySelector2 method
         // that allows for unmatched rule to be added
         modifySelectorUnmatched: true,
+        // Whether the style rule actor implements the setRuleText
+        // method.
+        canSetRuleText: this.canSetRuleText,
       }
     };
 
     if (this.rawRule.parentRule) {
       form.parentRule = this.pageStyle._styleRef(this.rawRule.parentRule).actorID;
 
       // CSS rules that we call media rules are STYLE_RULES that are children
       // of MEDIA_RULEs. We need to check the parentRule to check if a rule is
@@ -1097,20 +1211,27 @@ var StyleRuleActor = protocol.ActorClass
       // below.
       if (this.rawRule.parentRule.type === Ci.nsIDOMCSSRule.MEDIA_RULE) {
         form.media = [];
         for (let i = 0, n = this.rawRule.parentRule.media.length; i < n; i++) {
           form.media.push(this.rawRule.parentRule.media.item(i));
         }
       }
     }
-    if (this.rawRule.parentStyleSheet) {
-      form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID;
+    if (this._parentSheet) {
+      form.parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet).actorID;
     }
 
+    // One tricky thing here is that other methods in this actor must
+    // ensure that authoredText has been set before |form| is called.
+    // This has to be treated specially, for now, because we cannot
+    // synchronously compute the authored text, but |form| also cannot
+    // return a promise.  See bug 1205868.
+    form.authoredText = this.authoredText;
+
     switch (this.type) {
       case Ci.nsIDOMCSSRule.STYLE_RULE:
         form.selectors = CssLogic.getSelectors(this.rawRule);
         form.cssText = this.rawStyle.cssText || "";
         break;
       case ELEMENT_STYLE:
         // Elements don't have a parent stylesheet, and therefore
         // don't have an associated URI.  Provide a URI for
@@ -1134,17 +1255,135 @@ var StyleRuleActor = protocol.ActorClass
         form.keyText = this.rawRule.keyText || "";
         break;
     }
 
     return form;
   },
 
   /**
-   * Modify a rule's properties.  Passed an array of modifications:
+   * Send an event notifying that the location of the rule has
+   * changed.
+   *
+   * @param {Number} line the new line number
+   * @param {Number} column the new column number
+   */
+  _notifyLocationChanged: function(line, column) {
+    events.emit(this, "location-changed", line, column);
+  },
+
+  /**
+   * Compute the index of this actor's raw rule in its parent style
+   * sheet.
+   */
+  _computeRuleIndex: function() {
+    let rule = this.rawRule;
+    let cssRules = this._parentSheet.cssRules;
+    this._ruleIndex = -1;
+    for (let i = 0; i < cssRules.length; i++) {
+      if (rule === cssRules.item(i)) {
+        this._ruleIndex = i;
+        break;
+      }
+    }
+  },
+
+  /**
+   * This is attached to the parent style sheet actor's
+   * "style-applied" event.
+   */
+  _onStyleApplied: function(kind) {
+    if (kind === UPDATE_GENERAL) {
+      // A general change means that the rule actors are invalidated,
+      // so stop listening to events now.
+      if (this.sheetActor) {
+        this.sheetActor.off("style-applied", this._onStyleApplied);
+      }
+    } else if (this._ruleIndex >= 0) {
+      // The sheet was updated by this actor, in a way that preserves
+      // the rules.  Now, recompute our new rule from the style sheet,
+      // so that we aren't left with a reference to a dangling rule.
+      let oldRule = this.rawRule;
+      this.rawRule = this._parentSheet.cssRules[this._ruleIndex];
+      // Also tell the page style so that future calls to _styleRef
+      // return the same StyleRuleActor.
+      this.pageStyle.updateStyleRef(oldRule, this.rawRule, this);
+      let line = DOMUtils.getRelativeRuleLine(this.rawRule);
+      let column = DOMUtils.getRuleColumn(this.rawRule);
+      if (line !== this.line || column !== this.column) {
+        this._notifyLocationChanged(line, column);
+      }
+      this.line = line;
+      this.column = column;
+    }
+  },
+
+  /**
+   * Return a promise that resolves to the authored form of a rule's
+   * text, if available.  If the authored form is not available, the
+   * returned promise simply resolves to the empty string.  If the
+   * authored form is available, this also sets |this.authoredText|.
+   * The authored text will include invalid and otherwise ignored
+   * properties.
+   */
+  getAuthoredCssText: function() {
+    if (!this.canSetRuleText ||
+        (this.type !== Ci.nsIDOMCSSRule.STYLE_RULE &&
+         this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) {
+      return promise.resolve("");
+    }
+
+    if (typeof this.authoredText === "string") {
+      return promise.resolve(this.authoredText);
+    }
+
+    let parentStyleSheet =
+        this.pageStyle._sheetRef(this._parentSheet);
+    return parentStyleSheet.getText().then((longStr) => {
+      let cssText = longStr.str;
+      let {text} = getRuleText(cssText, this.line, this.column);
+
+      // Cache the result on the rule actor to avoid parsing again next time
+      this.authoredText = text;
+      return this.authoredText;
+    });
+  },
+
+  /**
+   * Set the contents of the rule.  This rewrites the rule in the
+   * stylesheet and causes it to be re-evaluated.
+   *
+   * @param {String} newText the new text of the rule
+   * @returns the rule with updated properties
+   */
+  setRuleText: method(Task.async(function*(newText) {
+    if (!this.canSetRuleText ||
+        (this.type !== Ci.nsIDOMCSSRule.STYLE_RULE &&
+         this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) {
+      throw new Error("invalid call to setRuleText");
+    }
+
+    let parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet);
+    let {str: cssText} = yield parentStyleSheet.getText();
+
+    let {offset, text} = getRuleText(cssText, this.line, this.column);
+    cssText = cssText.substring(0, offset) + newText +
+      cssText.substring(offset + text.length);
+
+    this.authoredText = newText;
+    yield parentStyleSheet.update(cssText, false, UPDATE_PRESERVING_RULES);
+
+    return this;
+  }), {
+    request: { modification: Arg(0, "string") },
+    response: { rule: RetVal("domstylerule") }
+  }),
+
+  /**
+   * Modify a rule's properties. Passed an array of modifications:
    * {
    *   type: "set",
    *   name: <string>,
    *   value: <string>,
    *   priority: <optional string>
    * }
    *  or
    * {
@@ -1158,17 +1397,17 @@ var StyleRuleActor = protocol.ActorClass
     // Use a fresh element for each call to this function to prevent side
     // effects that pop up based on property values that were already set on the
     // element.
 
     let document;
     if (this.rawNode) {
       document = this.rawNode.ownerDocument;
     } else {
-      let parentStyleSheet = this.rawRule.parentStyleSheet;
+      let parentStyleSheet = this._parentSheet;
       while (parentStyleSheet.ownerRule &&
           parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) {
         parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet;
       }
 
       document = this.getDocument(parentStyleSheet);
     }
 
@@ -1191,136 +1430,178 @@ var StyleRuleActor = protocol.ActorClass
   }),
 
   /**
    * Helper function for modifySelector and modifySelector2, inserts the new
    * rule with the new selector into the parent style sheet and removes the
    * current rule. Returns the newly inserted css rule or null if the rule is
    * unsuccessfully inserted to the parent style sheet.
    *
-   * @param string value
+   * @param {String} value
    *        The new selector value
+   * @param {Boolean} editAuthored
+   *        True if the selector should be updated by editing the
+   *        authored text; false if the selector should be updated via
+   *        CSSOM.
    *
-   * @returns CSSRule
+   * @returns {CSSRule}
    *        The new CSS rule added
    */
-  _addNewSelector: function(value) {
+  _addNewSelector: Task.async(function*(value, editAuthored) {
     let rule = this.rawRule;
-    let parentStyleSheet = rule.parentStyleSheet;
-    let cssRules = parentStyleSheet.cssRules;
-    let cssText = rule.cssText;
-    let selectorText = rule.selectorText;
+    let parentStyleSheet = this._parentSheet;
+
+    // We know the selector modification is ok, so if the client asked
+    // for the authored text to be edited, do it now.
+    if (editAuthored) {
+      let document = this.getDocument(this._parentSheet);
+      try {
+        document.querySelector(value);
+      } catch (e) {
+        return null;
+      }
 
-    for (let i = 0; i < cssRules.length; i++) {
-      if (rule === cssRules.item(i)) {
-        try {
-          // Inserts the new style rule into the current style sheet and
-          // delete the current rule
-          let ruleText = cssText.slice(selectorText.length).trim();
-          parentStyleSheet.insertRule(value + " " + ruleText, i);
-          parentStyleSheet.deleteRule(i + 1);
-          return cssRules.item(i);
-        } catch(e) {
-          // The selector could be invalid, or the rule could fail to insert.
-          // If that happens, the method returns null.
+      let sheetActor = this.pageStyle._sheetRef(parentStyleSheet);
+      let {str: authoredText} = yield sheetActor.getText();
+      let [startOffset, endOffset] = getSelectorOffsets(authoredText, this.line,
+                                                        this.column);
+      authoredText = authoredText.substring(0, startOffset) + value +
+        authoredText.substring(endOffset);
+      yield sheetActor.update(authoredText, false, UPDATE_PRESERVING_RULES);
+    } else {
+      let cssRules = parentStyleSheet.cssRules;
+      let cssText = rule.cssText;
+      let selectorText = rule.selectorText;
+
+      for (let i = 0; i < cssRules.length; i++) {
+        if (rule === cssRules.item(i)) {
+          try {
+            // Inserts the new style rule into the current style sheet and
+            // delete the current rule
+            let ruleText = cssText.slice(selectorText.length).trim();
+            parentStyleSheet.insertRule(value + " " + ruleText, i);
+            parentStyleSheet.deleteRule(i + 1);
+            break;
+          } catch(e) {
+            // The selector could be invalid, or the rule could fail to insert.
+            return null;
+          }
         }
-
-        break;
       }
     }
 
-    return null;
-  },
+    return parentStyleSheet.cssRules[this._ruleIndex];
+  }),
 
   /**
    * Modify the current rule's selector by inserting a new rule with the new
    * selector value and removing the current rule.
    *
    * Note this method was kept for backward compatibility, but unmatched rules
    * support was added in FF41.
    *
    * @param string value
    *        The new selector value
    * @returns boolean
    *        Returns a boolean if the selector in the stylesheet was modified,
    *        and false otherwise
    */
-  modifySelector: method(function(value) {
+  modifySelector: method(Task.async(function*(value) {
     if (this.type === ELEMENT_STYLE) {
       return false;
     }
 
-    let document = this.getDocument(this.rawRule.parentStyleSheet);
+    let document = this.getDocument(this._parentSheet);
     // Extract the selector, and pseudo elements and classes
     let [selector, pseudoProp] = value.split(/(:{1,2}.+$)/);
     let selectorElement;
 
     try {
       selectorElement = document.querySelector(selector);
     } catch (e) {
       return false;
     }
 
     // Check if the selector is valid and not the same as the original
     // selector
     if (selectorElement && this.rawRule.selectorText !== value) {
-      this._addNewSelector(value);
+      yield this._addNewSelector(value, false);
       return true;
     }
     return false;
-  }, {
+  }), {
     request: { selector: Arg(0, "string") },
     response: { isModified: RetVal("boolean") },
   }),
 
   /**
    * Modify the current rule's selector by inserting a new rule with the new
    * selector value and removing the current rule.
    *
    * In contrast with the modifySelector method which was used before FF41,
    * this method also returns information about the new rule and applied style
    * so that consumers can immediately display the new rule, whether or not the
    * selector matches the current element without having to refresh the whole
    * list.
    *
-   * @param DOMNode node
+   * @param {DOMNode} node
    *        The current selected element
-   * @param string value
+   * @param {String} value
    *        The new selector value
-   * @returns Object
+   * @param {Boolean} editAuthored
+   *        True if the selector should be updated by editing the
+   *        authored text; false if the selector should be updated via
+   *        CSSOM.
+   * @returns {Object}
    *        Returns an object that contains the applied style properties of the
    *        new rule and a boolean indicating whether or not the new selector
    *        matches the current selected element
    */
-  modifySelector2: method(function(node, value) {
-    let isMatching = false;
+  modifySelector2: method(function(node, value, editAuthored = false) {
     let ruleProps = null;
 
     if (this.type === ELEMENT_STYLE ||
         this.rawRule.selectorText === value) {
       return { ruleProps, isMatching: true };
     }
 
-    let newCssRule = this._addNewSelector(value);
-    if (newCssRule) {
-      ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
+    let selectorPromise = this._addNewSelector(value, editAuthored);
+
+    if (editAuthored) {
+      selectorPromise = selectorPromise.then((newCssRule) => {
+        if (newCssRule) {
+          let style = this.pageStyle._styleRef(newCssRule);
+          // See the comment in |form| to understand this.
+          return style.getAuthoredCssText().then(() => newCssRule);
+        }
+        return newCssRule;
+      });
     }
 
-    // Determine if the new selector value matches the current selected element
-    try {
-      isMatching = node.rawNode.matches(value);
-    } catch(e) {
-      // This fails when value is an invalid selector.
-    }
+    return selectorPromise.then((newCssRule) => {
+      if (newCssRule) {
+        ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
+      }
 
-    return { ruleProps, isMatching };
+      // Determine if the new selector value matches the current
+      // selected element
+      let isMatching = false;
+      try {
+        isMatching = node.rawNode.matches(value);
+      } catch(e) {
+        // This fails when value is an invalid selector.
+      }
+
+      return { ruleProps, isMatching };
+    });
   }, {
     request: {
       node: Arg(0, "domnode"),
-      value: Arg(1, "string")
+      value: Arg(1, "string"),
+      editAuthored: Arg(2, "boolean")
     },
     response: RetVal("modifiedStylesReturn")
   })
 });
 
 /**
  * Front for the StyleRule actor.
  */
@@ -1341,34 +1622,53 @@ var StyleRuleFront = protocol.FrontClass
     this.actorID = form.actor;
     this._form = form;
     if (this._mediaText) {
       this._mediaText = null;
     }
   },
 
   /**
-   * Return a new RuleModificationList for this node.
+   * Ensure _form is updated when location-changed is emitted.
+   */
+  _locationChangedPre: protocol.preEvent("location-changed", function(line,
+                                                                      column) {
+    this._clearOriginalLocation();
+    this._form.line = line;
+    this._form.column = column;
+  }),
+
+  /**
+   * Return a new RuleModificationList or RuleRewriter for this node.
+   * A RuleRewriter will be returned when the rule's canSetRuleText
+   * trait is true; otherwise a RuleModificationList will be
+   * returned.
    */
   startModifyingProperties: function() {
+    if (this.canSetRuleText) {
+      return new RuleRewriter(this, this.authoredText);
+    }
     return new RuleModificationList(this);
   },
 
   get type() {
     return this._form.type;
   },
   get line() {
     return this._form.line || -1;
   },
   get column() {
     return this._form.column || -1;
   },
   get cssText() {
     return this._form.cssText;
   },
+  get authoredText() {
+    return this._form.authoredText || this._form.cssText;
+  },
   get keyText() {
     return this._form.keyText;
   },
   get name() {
     return this._form.name;
   },
   get selectors() {
     return this._form.selectors;
@@ -1411,25 +1711,33 @@ var StyleRuleFront = protocol.FrontClass
     let sheet = this.parentStyleSheet;
     return sheet ? sheet.nodeHref : "";
   },
 
   get supportsModifySelectorUnmatched() {
     return this._form.traits && this._form.traits.modifySelectorUnmatched;
   },
 
+  get canSetRuleText() {
+    return this._form.traits && this._form.traits.canSetRuleText;
+  },
+
   get location() {
     return {
       source: this.parentStyleSheet,
       href: this.href,
       line: this.line,
       column: this.column
     };
   },
 
+  _clearOriginalLocation: function() {
+    this._originalLocation = null;
+  },
+
   getOriginalLocation: function() {
     if (this._originalLocation) {
       return promise.resolve(this._originalLocation);
     }
     let parentSheet = this.parentStyleSheet;
     if (!parentSheet) {
       // This rule doesn't belong to a stylesheet so it is an inline style.
       // Inline styles do not have any mediaText so we can return early.
@@ -1453,36 +1761,51 @@ var StyleRuleFront = protocol.FrontClass
         return location;
       });
   },
 
   modifySelector: protocol.custom(Task.async(function*(node, value) {
     let response;
     if (this.supportsModifySelectorUnmatched) {
       // If the debugee supports adding unmatched rules (post FF41)
-      response = yield this.modifySelector2(node, value);
+      if (this.canSetRuleText) {
+        response = yield this.modifySelector2(node, value, true);
+      } else {
+        response = yield this.modifySelector2(node, value);
+      }
     } else {
       response = yield this._modifySelector(value);
     }
 
     if (response.ruleProps) {
       response.ruleProps = response.ruleProps.entries[0];
     }
     return response;
   }), {
     impl: "_modifySelector"
+  }),
+
+  setRuleText: protocol.custom(function(newText) {
+    this._form.authoredText = newText;
+    return this._setRuleText(newText);
+  }, {
+    impl: "_setRuleText"
   })
 });
 
 /**
  * Convenience API for building a list of attribute modifications
  * for the `modifyProperties` request.  A RuleModificationList holds a
  * list of modifications that will be applied to a StyleRuleActor.
  * The modifications are processed in the order in which they are
  * added to the RuleModificationList.
+ *
+ * Objects of this type expose the same API as @see RuleRewriter.
+ * This lets the inspector use (mostly) the same code, regardless of
+ * whether the server implements setRuleText.
  */
 var RuleModificationList = Class({
   /**
    * Initialize a RuleModificationList.
    * @param {StyleRuleFront} rule the associated rule
    */
   initialize: function(rule) {
     this.rule = rule;
@@ -1497,41 +1820,105 @@ var RuleModificationList = Class({
    */
   apply: function() {
     return this.rule.modifyProperties(this.modifications);
   },
 
   /**
    * Add a "set" entry to the modification list.
    *
-   * @param {string} name the property's name
-   * @param {string} value the property's value
-   * @param {string} priority the property's priority, either the empty
+   * @param {Number} index index of the property in the rule.
+   *                       This can be -1 in the case where
+   *                       the rule does not support setRuleText;
+   *                       generally for setting properties
+   *                       on an element's style.
+   * @param {String} name the property's name
+   * @param {String} value the property's value
+   * @param {String} priority the property's priority, either the empty
    *                          string or "important"
    */
-  setProperty: function(name, value, priority) {
+  setProperty: function(index, name, value, priority) {
     this.modifications.push({
       type: "set",
       name: name,
       value: value,
       priority: priority
     });
   },
 
   /**
    * Add a "remove" entry to the modification list.
    *
-   * @param {string} name the name of the property to remove
+   * @param {Number} index index of the property in the rule.
+   *                       This can be -1 in the case where
+   *                       the rule does not support setRuleText;
+   *                       generally for setting properties
+   *                       on an element's style.
+   * @param {String} name the name of the property to remove
    */
-  removeProperty: function(name) {
+  removeProperty: function(index, name) {
     this.modifications.push({
       type: "remove",
       name: name
     });
-  }
+  },
+
+  /**
+   * Rename a property.  This implementation acts like
+   * |removeProperty|, because |setRuleText| is not available.
+   *
+   * @param {Number} index index of the property in the rule.
+   *                       This can be -1 in the case where
+   *                       the rule does not support setRuleText;
+   *                       generally for setting properties
+   *                       on an element's style.
+   * @param {String} name current name of the property
+   * @param {String} newName new name of the property
+   */
+  renameProperty: function(index, name, newName) {
+    this.removeProperty(index, name);
+  },
+
+  /**
+   * Enable or disable a property.  This implementation acts like
+   * |removeProperty| when disabling, or a no-op when enabling,
+   * because |setRuleText| is not available.
+   *
+   * @param {Number} index index of the property in the rule.
+   *                       This can be -1 in the case where
+   *                       the rule does not support setRuleText;
+   *                       generally for setting properties
+   *                       on an element's style.
+   * @param {String} name current name of the property
+   * @param {Boolean} isEnabled true if the property should be enabled;
+   *                        false if it should be disabled
+   */
+  setPropertyEnabled: function(index, name, isEnabled) {
+    if (!isEnabled) {
+      this.removeProperty(index, name);
+    }
+  },
+
+  /**
+   * Create a new property.  This implementation does nothing, because
+   * |setRuleText| is not available.
+   *
+   * @param {Number} index index of the property in the rule.
+   *                       This can be -1 in the case where
+   *                       the rule does not support setRuleText;
+   *                       generally for setting properties
+   *                       on an element's style.
+   * @param {String} name name of the new property
+   * @param {String} value value of the new property
+   * @param {String} priority priority of the new property; either
+   *                          the empty string or "important"
+   */
+  createProperty: function(index, name, value, priority) {
+    // Nothing.
+  },
 });
 
 /**
  * Helper function for getting an image preview of the given font.
  *
  * @param font {string}
  *        Name of font to preview
  * @param doc {Document}
@@ -1655,16 +2042,57 @@ function getRuleText(initialText, line, 
   // that cssTokenizer skips them.
   return {offset: textOffset + startOffset,
           text: text.substring(startOffset, endOffset)};
 }
 
 exports.getRuleText = getRuleText;
 
 /**
+ * Compute the start and end offsets of a rule's selector text, given
+ * the CSS text and the line and column at which the rule begins.
+ * @param {String} initialText
+ * @param {Number} line (1-indexed)
+ * @param {Number} column (1-indexed)
+ * @return {array} An array with two elements: [startOffset, endOffset].
+ *                 The elements mark the bounds in |initialText| of
+ *                 the CSS rule's selector.
+ */
+function getSelectorOffsets(initialText, line, column) {
+  if (typeof line === "undefined" || typeof column === "undefined") {
+    throw new Error("Location information is missing");
+  }
+
+  let {offset: textOffset, text} =
+      getTextAtLineColumn(initialText, line, column);
+  let lexer = DOMUtils.getCSSLexer(text);
+
+  // Search forward for the opening brace.
+  let endOffset;
+  while (true) {
+    let token = lexer.nextToken();
+    if (!token) {
+      break;
+    }
+    if (token.tokenType === "symbol" && token.text === "{") {
+      if (endOffset === undefined) {
+        break;
+      }
+      return [textOffset, textOffset + endOffset];
+    }
+    // Preserve comments and whitespace just before the "{".
+    if (token.tokenType !== "comment" && token.tokenType !== "whitespace") {
+      endOffset = token.endOffset;
+    }
+  }
+
+  throw new Error("could not find bounds of rule");
+}
+
+/**
  * Return the offset and substring of |text| that starts at the given
  * line and column.
  * @param {String} text
  * @param {Number} line (1-indexed)
  * @param {Number} column (1-indexed)
  * @return {object} An object of the form {offset: number, text: string},
  *                  where the offset is the offset into the input string
  *                  where the text starts, and where text is the text.
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -18,16 +18,21 @@ const protocol = require("devtools/serve
 const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
 const {fetch} = require("devtools/shared/DevToolsUtils");
 const {listenOnce} = require("devtools/shared/async-utils");
 const {SourceMapConsumer} = require("source-map");
 
 loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/styleinspector/css-logic").CssLogic);
 
+const {
+  getIndentationFromPrefs,
+  getIndentationFromString
+} = require("devtools/shared/shared/indentation");
+
 var TRANSITION_CLASS = "moz-styleeditor-transitioning";
 var TRANSITION_DURATION_MS = 500;
 var TRANSITION_BUFFER_MS = 1000;
 var TRANSITION_RULE_SELECTOR =
 ".moz-styleeditor-transitioning:root, .moz-styleeditor-transitioning:root *";
 var TRANSITION_RULE = TRANSITION_RULE_SELECTOR + " {\
 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
 transition-delay: 0ms !important;\
@@ -35,16 +40,32 @@ transition-timing-function: ease-out !im
 transition-property: all !important;\
 }";
 
 var LOAD_ERROR = "error-load";
 
 types.addActorType("stylesheet");
 types.addActorType("originalsource");
 
+// The possible kinds of style-applied events.
+// UPDATE_PRESERVING_RULES means that the update is guaranteed to
+// preserve the number and order of rules on the style sheet.
+// UPDATE_GENERAL covers any other kind of change to the style sheet.
+const UPDATE_PRESERVING_RULES = 0;
+exports.UPDATE_PRESERVING_RULES = UPDATE_PRESERVING_RULES;
+const UPDATE_GENERAL = 1;
+exports.UPDATE_GENERAL = UPDATE_GENERAL;
+
+// If the user edits a style sheet, we stash a copy of the edited text
+// here, keyed by the style sheet.  This way, if the tools are closed
+// and then reopened, the edited text will be available.  A weak map
+// is used so that navigation by the user will eventually cause the
+// edited text to be collected.
+let modifiedStyleSheets = new WeakMap();
+
 /**
  * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
  * stylesheets of a document.
  */
 var StyleSheetsActor = exports.StyleSheetsActor = protocol.ActorClass({
   typeName: "stylesheets",
 
   /**
@@ -380,17 +401,19 @@ var StyleSheetActor = protocol.ActorClas
 
   events: {
     "property-change" : {
       type: "propertyChange",
       property: Arg(0, "string"),
       value: Arg(1, "json")
     },
     "style-applied" : {
-      type: "styleApplied"
+      type: "styleApplied",
+      kind: Arg(0, "number"),
+      styleSheet: Arg(1, "stylesheet")
     },
     "media-rules-changed" : {
       type: "mediaRulesChanged",
       rules: Arg(0, "array:mediarule")
     }
   },
 
   /* List of original sources that generated this stylesheet */
@@ -592,16 +615,22 @@ var StyleSheetActor = protocol.ActorClas
    * @return {Promise}
    *         Promise that resolves with a string text of the stylesheet.
    */
   _getText: function() {
     if (typeof this.text === "string") {
       return promise.resolve(this.text);
     }
 
+    let cssText = modifiedStyleSheets.get(this.rawSheet);
+    if (cssText !== undefined) {
+      this.text = cssText;
+      return promise.resolve(cssText);
+    }
+
     if (!this.href) {
       // this is an inline <style> sheet
       let content = this.ownerNode.textContent;
       this.text = content;
       return promise.resolve(content);
     }
 
     let options = {
@@ -867,73 +896,76 @@ var StyleSheetActor = protocol.ActorClas
   },
 
   /**
    * Update the style sheet in place with new text.
    *
    * @param  {object} request
    *         'text' - new text
    *         'transition' - whether to do CSS transition for change.
+   *         'kind' - either UPDATE_PRESERVING_RULES or UPDATE_GENERAL
    */
-  update: method(function(text, transition) {
+  update: method(function(text, transition, kind = UPDATE_GENERAL) {
     DOMUtils.parseStyleSheet(this.rawSheet, text);
 
+    modifiedStyleSheets.set(this.rawSheet, text);
+
     this.text = text;
 
     this._notifyPropertyChanged("ruleCount");
 
     if (transition) {
-      this._insertTransistionRule();
+      this._insertTransistionRule(kind);
     }
     else {
-      events.emit(this, "style-applied");
+      events.emit(this, "style-applied", kind, this);
     }
 
     this._getMediaRules().then((rules) => {
       events.emit(this, "media-rules-changed", rules);
     });
   }, {
     request: {
       text: Arg(0, "string"),
       transition: Arg(1, "boolean")
     }
   }),
 
   /**
    * Insert a catch-all transition rule into the document. Set a timeout
    * to remove the rule after a certain time.
    */
-  _insertTransistionRule: function() {
+  _insertTransistionRule: function(kind) {
     this.document.documentElement.classList.add(TRANSITION_CLASS);
 
     // We always add the rule since we've just reset all the rules
     this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
 
     // Set up clean up and commit after transition duration (+buffer)
     // @see _onTransitionEnd
     this.window.clearTimeout(this._transitionTimeout);
-    this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this),
+    this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this, kind),
                               TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS);
   },
 
   /**
    * This cleans up class and rule added for transition effect and then
    * notifies that the style has been applied.
    */
-  _onTransitionEnd: function()
+  _onTransitionEnd: function(kind)
   {
     this.document.documentElement.classList.remove(TRANSITION_CLASS);
 
     let index = this.rawSheet.cssRules.length - 1;
     let rule = this.rawSheet.cssRules[index];
     if (rule.selectorText == TRANSITION_RULE_SELECTOR) {
       this.rawSheet.deleteRule(index);
     }
 
-    events.emit(this, "style-applied");
+    events.emit(this, "style-applied", kind, this);
   }
 })
 
 /**
  * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
  */
 var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
   initialize: function(conn, form) {
@@ -976,16 +1008,39 @@ var StyleSheetFront = protocol.FrontClas
   get isSystem() {
     return this._form.system;
   },
   get styleSheetIndex() {
     return this._form.styleSheetIndex;
   },
   get ruleCount() {
     return this._form.ruleCount;
+  },
+
+  /**
+   * Get the indentation to use for edits to this style sheet.
+   *
+   * @return {Promise} A promise that will resolve to a string that
+   * should be used to indent a block in this style sheet.
+   */
+  guessIndentation: function() {
+    let prefIndent = getIndentationFromPrefs();
+    if (prefIndent) {
+      let {indentUnit, indentWithTabs} = prefIndent;
+      return promise.resolve(indentWithTabs ? "\t" : " ".repeat(indentUnit));
+    }
+
+    return Task.spawn(function*() {
+      let longStr = yield this.getText();
+      let source = yield longStr.string();
+
+      let {indentUnit, indentWithTabs} = getIndentationFromString(source);
+
+      return indentWithTabs ? "\t" : " ".repeat(indentUnit);
+    }.bind(this));
   }
 });
 
 /**
  * Actor representing an original source of a style sheet that was specified
  * in a source map.
  */
 var OriginalSourceActor = protocol.ActorClass({
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -16,32 +16,33 @@ support-files =
   storage-unsecured-iframe.html
   storage-updates.html
   storage-secured-iframe.html
   stylesheets-nested-iframes.html
   timeline-iframe-child.html
   timeline-iframe-parent.html
   director-script-target.html
 
-[browser_animation_actors_01.js]
-[browser_animation_actors_02.js]
-[browser_animation_actors_03.js]
-[browser_animation_actors_04.js]
+[browser_animation_emitMutations.js]
+[browser_animation_getFrames.js]
+[browser_animation_getMultipleStates.js]
+[browser_animation_getPlayers.js]
+[browser_animation_getStateAfterFinished.js]
+[browser_animation_getSubTreeAnimations.js]
+[browser_animation_keepFinished.js]
+[browser_animation_playerState.js]
+[browser_animation_playPauseIframe.js]
+[browser_animation_playPauseSeveral.js]
+[browser_animation_reconstructState.js]
+[browser_animation_refreshTransitions.js]
+[browser_animation_setCurrentTime.js]
+[browser_animation_setPlaybackRate.js]
+[browser_animation_simple.js]
+[browser_animation_updatedState.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
-[browser_animation_actors_06.js]
-[browser_animation_actors_07.js]
-[browser_animation_actors_08.js]
-[browser_animation_actors_09.js]
-[browser_animation_actors_10.js]
-[browser_animation_actors_11.js]
-[browser_animation_actors_12.js]
-[browser_animation_actors_13.js]
-[browser_animation_actors_14.js]
-[browser_animation_actors_15.js]
-[browser_animation_actors_16.js]
 [browser_canvasframe_helper_01.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_canvasframe_helper_02.js]
 [browser_canvasframe_helper_03.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_canvasframe_helper_04.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_canvasframe_helper_05.js]
rename from devtools/server/tests/browser/browser_animation_actors_13.js
rename to devtools/server/tests/browser/browser_animation_emitMutations.js
--- a/devtools/server/tests/browser/browser_animation_actors_13.js
+++ b/devtools/server/tests/browser/browser_animation_emitMutations.js
@@ -2,28 +2,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that the AnimationsActor emits events about changed animations on a
 // node after getAnimationPlayersForNode was called on that node.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let animations = AnimationsFront(client, form);
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   info("Retrieve a non-animated node");
   let node = yield walker.querySelector(walker.rootNode, ".not-animated");
 
   info("Retrieve the animation player for the node");
   let players = yield animations.getAnimationPlayersForNode(node);
   is(players.length, 0, "The node has no animation players");
 
@@ -55,17 +46,17 @@ add_task(function*() {
   yield node.modifyAttributes([
     {attributeName: "class", newValue: "not-animated"}
   ]);
 
   changes = yield onMutations;
 
   ok(true, "The mutations event was emitted");
   is(changes.length, 2, "There are 2 changes in the mutation event");
-  ok(changes.every(({type}) => type === "removed"), "Both changes are removals");
+  ok(changes.every(({type}) => type === "removed"), "Both are removals");
   ok(changes[0].player === p1 || changes[0].player === p2,
     "The first removed player was one of the previously added players");
   ok(changes[1].player === p1 || changes[1].player === p2,
     "The second removed player was one of the previously added players");
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_animation_getFrames.js
@@ -0,0 +1,32 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationPlayerActor exposes a getFrames method that returns
+// the list of keyframes in the animation.
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+add_task(function*() {
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
+
+  info("Get the test node and its animation front");
+  let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+  let [player] = yield animations.getAnimationPlayersForNode(node);
+
+  ok(player.getFrames, "The front has the getFrames method");
+
+  let frames = yield player.getFrames();
+  is(frames.length, 2, "The correct number of keyframes was retrieved");
+  ok(frames[0].transform, "Frame 0 has the transform property");
+  ok(frames[1].transform, "Frame 1 has the transform property");
+  // Note that we don't really test the content of the frame object here on
+  // purpose. This object comes straight out of the web animations API
+  // unmodified.
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
rename from devtools/server/tests/browser/browser_animation_actors_06.js
rename to devtools/server/tests/browser/browser_animation_getMultipleStates.js
--- a/devtools/server/tests/browser/browser_animation_actors_06.js
+++ b/devtools/server/tests/browser/browser_animation_getMultipleStates.js
@@ -2,49 +2,48 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that the duration, iterationCount and delay are retrieved correctly for
 // multiple animations.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
-
-  yield playerHasAnInitialState(walker, front);
+  yield playerHasAnInitialState(walker, animations);
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
-function* playerHasAnInitialState(walker, front) {
-  let state = yield getAnimationStateForNode(walker, front,
+function* playerHasAnInitialState(walker, animations) {
+  let state = yield getAnimationStateForNode(walker, animations,
     ".delayed-multiple-animations", 0);
-  ok(state.duration, 500, "The duration of the first animation is correct");
-  ok(state.iterationCount, 10, "The iterationCount of the first animation is correct");
-  ok(state.delay, 1000, "The delay of the first animation is correct");
+
+  ok(state.duration, 500,
+     "The duration of the first animation is correct");
+  ok(state.iterationCount, 10,
+     "The iterationCount of the first animation is correct");
+  ok(state.delay, 1000,
+     "The delay of the first animation is correct");
 
-  state = yield getAnimationStateForNode(walker, front,
+  state = yield getAnimationStateForNode(walker, animations,
     ".delayed-multiple-animations", 1);
-  ok(state.duration, 1000, "The duration of the secon animation is correct");
-  ok(state.iterationCount, 30, "The iterationCount of the secon animation is correct");
-  ok(state.delay, 750, "The delay of the secon animation is correct");
+
+  ok(state.duration, 1000,
+     "The duration of the secon animation is correct");
+  ok(state.iterationCount, 30,
+     "The iterationCount of the secon animation is correct");
+  ok(state.delay, 750,
+     "The delay of the secon animation is correct");
 }
 
-function* getAnimationStateForNode(walker, front, nodeSelector, playerIndex) {
-  let node = yield walker.querySelector(walker.rootNode, nodeSelector);
-  let players = yield front.getAnimationPlayersForNode(node);
+function* getAnimationStateForNode(walker, animations, selector, playerIndex) {
+  let node = yield walker.querySelector(walker.rootNode, selector);
+  let players = yield animations.getAnimationPlayersForNode(node);
   let player = players[playerIndex];
   yield player.ready();
   let state = yield player.getCurrentState();
   return state;
 }
rename from devtools/server/tests/browser/browser_animation_actors_02.js
rename to devtools/server/tests/browser/browser_animation_getPlayers.js
--- a/devtools/server/tests/browser/browser_animation_actors_02.js
+++ b/devtools/server/tests/browser/browser_animation_getPlayers.js
@@ -1,63 +1,63 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check the output of getAnimationPlayersForNode
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
-
-  yield theRightNumberOfPlayersIsReturned(walker, front);
-  yield playersCanBePausedAndResumed(walker, front);
+  yield theRightNumberOfPlayersIsReturned(walker, animations);
+  yield playersCanBePausedAndResumed(walker, animations);
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
-function* theRightNumberOfPlayersIsReturned(walker, front) {
+function* theRightNumberOfPlayersIsReturned(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".not-animated");
-  let players = yield front.getAnimationPlayersForNode(node);
-  is(players.length, 0, "0 players were returned for the unanimated node");
+  let players = yield animations.getAnimationPlayersForNode(node);
+  is(players.length, 0,
+     "0 players were returned for the unanimated node");
 
   node = yield walker.querySelector(walker.rootNode, ".simple-animation");
-  players = yield front.getAnimationPlayersForNode(node);
-  is(players.length, 1, "One animation player was returned");
+  players = yield animations.getAnimationPlayersForNode(node);
+  is(players.length, 1,
+     "One animation player was returned");
 
   node = yield walker.querySelector(walker.rootNode, ".multiple-animations");
-  players = yield front.getAnimationPlayersForNode(node);
-  is(players.length, 2, "Two animation players were returned");
+  players = yield animations.getAnimationPlayersForNode(node);
+  is(players.length, 2,
+     "Two animation players were returned");
 
   node = yield walker.querySelector(walker.rootNode, ".transition");
-  players = yield front.getAnimationPlayersForNode(node);
-  is(players.length, 1, "One animation player was returned for the transitioned node");
+  players = yield animations.getAnimationPlayersForNode(node);
+  is(players.length, 1,
+     "One animation player was returned for the transitioned node");
 }
 
-function* playersCanBePausedAndResumed(walker, front) {
+function* playersCanBePausedAndResumed(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
-  let [player] = yield front.getAnimationPlayersForNode(node);
+  let [player] = yield animations.getAnimationPlayersForNode(node);
   yield player.ready();
 
-  ok(player.initialState, "The player has an initialState");
-  ok(player.getCurrentState, "The player has the getCurrentState method");
-  is(player.initialState.playState, "running", "The animation is currently running");
+  ok(player.initialState,
+     "The player has an initialState");
+  ok(player.getCurrentState,
+     "The player has the getCurrentState method");
+  is(player.initialState.playState, "running",
+     "The animation is currently running");
 
   yield player.pause();
   let state = yield player.getCurrentState();
-  is(state.playState, "paused", "The animation is now paused");
+  is(state.playState, "paused",
+     "The animation is now paused");
 
   yield player.play();
   state = yield player.getCurrentState();
-  is(state.playState, "running", "The animation is now running again");
+  is(state.playState, "running",
+     "The animation is now running again");
 }
rename from devtools/server/tests/browser/browser_animation_actors_10.js
rename to devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
--- a/devtools/server/tests/browser/browser_animation_actors_10.js
+++ b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
@@ -5,47 +5,38 @@
 "use strict";
 
 // Check that the right duration/iterationCount/delay are retrieved even when
 // the node has multiple animations and one of them already ended before getting
 // the player objects.
 // See devtools/server/actors/animation.js |getPlayerIndex| for more
 // information.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   info("Retrieve a non animated node");
   let node = yield walker.querySelector(walker.rootNode, ".not-animated");
 
   info("Apply the multiple-animations-2 class to start the animations");
   yield node.modifyAttributes([
     {attributeName: "class", newValue: "multiple-animations-2"}
   ]);
 
   info("Get the list of players, by the time this executes, the first, " +
        "short, animation should have ended.");
-  let players = yield front.getAnimationPlayersForNode(node);
+  let players = yield animations.getAnimationPlayersForNode(node);
   if (players.length === 3) {
     info("The short animation hasn't ended yet, wait for a bit.");
     // The animation lasts for 500ms, so 1000ms should do it.
     yield new Promise(resolve => setTimeout(resolve, 1000));
 
     info("And get the list again");
-    players = yield front.getAnimationPlayersForNode(node);
+    players = yield animations.getAnimationPlayersForNode(node);
   }
 
   is(players.length, 2, "2 animations remain on the node");
 
   is(players[0].state.duration, 1000,
      "The duration of the first animation is correct");
   is(players[0].state.delay, 2000,
      "The delay of the first animation is correct");
rename from devtools/server/tests/browser/browser_animation_actors_16.js
rename to devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
--- a/devtools/server/tests/browser/browser_animation_actors_16.js
+++ b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
@@ -2,43 +2,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that the AnimationsActor can retrieve all animations inside a node's
 // subtree (but not going into iframes).
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 const URL = MAIN_DOMAIN + "animation.html";
 
 add_task(function*() {
   info("Creating a test document with 2 iframes containing animated nodes");
-  yield addTab("data:text/html;charset=utf-8," +
-               "<iframe id='iframe' src='" + URL + "'></iframe>");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
+  let {client, walker, animations} = yield initAnimationsFrontForUrl(
+    "data:text/html;charset=utf-8," +
+    "<iframe id='iframe' src='" + URL + "'></iframe>");
 
   info("Try retrieving all animations from the root doc's <body> node");
   let rootBody = yield walker.querySelector(walker.rootNode, "body");
-  let players = yield front.getAnimationPlayersForNode(rootBody);
+  let players = yield animations.getAnimationPlayersForNode(rootBody);
   is(players.length, 0, "The node has no animation players");
 
   info("Retrieve all animations from the iframe's <body> node");
   let iframe = yield walker.querySelector(walker.rootNode, "#iframe");
   let {nodes} = yield walker.children(iframe);
   let frameBody = yield walker.querySelector(nodes[0], "body");
-  players = yield front.getAnimationPlayersForNode(frameBody);
+  players = yield animations.getAnimationPlayersForNode(frameBody);
 
   // Testing for a hard-coded number of animations here would intermittently
   // fail depending on how fast or slow the test is (indeed, the test page
   // contains short transitions, and delayed animations). So just make sure we
   // at least have the infinitely running animations.
   ok(players.length >= 4, "All subtree animations were retrieved");
 
   yield closeDebuggerClient(client);
rename from devtools/server/tests/browser/browser_animation_actors_14.js
rename to devtools/server/tests/browser/browser_animation_keepFinished.js
--- a/devtools/server/tests/browser/browser_animation_actors_14.js
+++ b/devtools/server/tests/browser/browser_animation_keepFinished.js
@@ -4,28 +4,19 @@
 
 "use strict";
 
 // Test that the AnimationsActor doesn't report finished animations as removed.
 // Indeed, animations that only have the "finished" playState can be modified
 // still, so we want the AnimationsActor to preserve the corresponding
 // AnimationPlayerActor.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let animations = AnimationsFront(client, form);
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   info("Retrieve a non-animated node");
   let node = yield walker.querySelector(walker.rootNode, ".not-animated");
 
   info("Retrieve the animation player for the node");
   let players = yield animations.getAnimationPlayersForNode(node);
   is(players.length, 0, "The node has no animation players");
 
rename from devtools/server/tests/browser/browser_animation_actors_09.js
rename to devtools/server/tests/browser/browser_animation_playPauseIframe.js
--- a/devtools/server/tests/browser/browser_animation_actors_09.js
+++ b/devtools/server/tests/browser/browser_animation_playPauseIframe.js
@@ -2,55 +2,47 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that the AnimationsActor can pause/play all animations even those
 // within iframes.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 const URL = MAIN_DOMAIN + "animation.html";
 
 add_task(function*() {
   info("Creating a test document with 2 iframes containing animated nodes");
-  yield addTab("data:text/html;charset=utf-8," +
-                         "<iframe id='i1' src='" + URL + "'></iframe>" +
-                         "<iframe id='i2' src='" + URL + "'></iframe>");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
+  let {client, walker, animations} = yield initAnimationsFrontForUrl(
+    "data:text/html;charset=utf-8," +
+    "<iframe id='i1' src='" + URL + "'></iframe>" +
+    "<iframe id='i2' src='" + URL + "'></iframe>");
 
   info("Getting the 2 iframe container nodes and animated nodes in them");
   let nodeInFrame1 = yield getNodeInFrame(walker, "#i1", ".simple-animation");
   let nodeInFrame2 = yield getNodeInFrame(walker, "#i2", ".simple-animation");
 
   info("Pause all animations in the test document");
-  yield front.pauseAll();
-  yield checkState(front, nodeInFrame1, "paused");
-  yield checkState(front, nodeInFrame2, "paused");
+  yield animations.pauseAll();
+  yield checkState(animations, nodeInFrame1, "paused");
+  yield checkState(animations, nodeInFrame2, "paused");
 
   info("Play all animations in the test document");
-  yield front.playAll();
-  yield checkState(front, nodeInFrame1, "running");
-  yield checkState(front, nodeInFrame2, "running");
+  yield animations.playAll();
+  yield checkState(animations, nodeInFrame1, "running");
+  yield checkState(animations, nodeInFrame2, "running");
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
-function* checkState(front, nodeFront, playState) {
+function* checkState(animations, nodeFront, playState) {
   info("Getting the AnimationPlayerFront for the test node");
-  let [player] = yield front.getAnimationPlayersForNode(nodeFront);
+  let [player] = yield animations.getAnimationPlayersForNode(nodeFront);
   yield player.ready;
   let state = yield player.getCurrentState();
   is(state.playState, playState,
      "The playState of the test node is " + playState);
 }
 
 function* getNodeInFrame(walker, frameSelector, nodeSelector) {
   let iframe = yield walker.querySelector(walker.rootNode, frameSelector);
rename from devtools/server/tests/browser/browser_animation_actors_08.js
rename to devtools/server/tests/browser/browser_animation_playPauseSeveral.js
--- a/devtools/server/tests/browser/browser_animation_actors_08.js
+++ b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js
@@ -2,87 +2,78 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that the AnimationsActor can pause/play all animations at once, and
 // check that it can also pause/play a given list of animations at once.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 // List of selectors that match "all" animated nodes in the test page.
 // This list misses a bunch of animated nodes on purpose. Only the ones that
 // have infinite animations are listed. This is done to avoid intermittents
 // caused when finite animations are already done playing by the time the test
 // runs.
 const ALL_ANIMATED_NODES = [".simple-animation", ".multiple-animations",
                             ".delayed-animation"];
 // List of selectors that match some animated nodes in the test page only.
 const SOME_ANIMATED_NODES = [".simple-animation", ".delayed-animation"];
 
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   info("Pause all animations in the test document");
-  yield front.pauseAll();
-  yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "paused");
+  yield animations.pauseAll();
+  yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused");
 
   info("Play all animations in the test document");
-  yield front.playAll();
-  yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "running");
+  yield animations.playAll();
+  yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running");
 
   info("Pause all animations in the test document using toggleAll");
-  yield front.toggleAll();
-  yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "paused");
+  yield animations.toggleAll();
+  yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused");
 
   info("Play all animations in the test document using toggleAll");
-  yield front.toggleAll();
-  yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "running");
+  yield animations.toggleAll();
+  yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running");
 
   info("Pause a given list of animations only");
   let players = [];
   for (let selector of SOME_ANIMATED_NODES) {
-    let [player] = yield getPlayersFor(walker, front, selector);
+    let [player] = yield getPlayersFor(walker, animations, selector);
     players.push(player);
   }
-  yield front.toggleSeveral(players, true);
-  yield checkAnimationsStates(walker, front, SOME_ANIMATED_NODES, "paused");
-  yield checkAnimationsStates(walker, front, [".multiple-animations"], "running");
+  yield animations.toggleSeveral(players, true);
+  yield checkStates(walker, animations, SOME_ANIMATED_NODES, "paused");
+  yield checkStates(walker, animations, [".multiple-animations"], "running");
 
   info("Play the same list of animations");
-  yield front.toggleSeveral(players, false);
-  yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "running");
+  yield animations.toggleSeveral(players, false);
+  yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running");
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
-function* checkAnimationsStates(walker, front, selectors, playState) {
+function* checkStates(walker, animations, selectors, playState) {
   info("Checking the playState of all the nodes that have infinite running " +
        "animations");
 
   for (let selector of selectors) {
     info("Getting the AnimationPlayerFront for node " + selector);
-    let [player] = yield getPlayersFor(walker, front, selector);
+    let [player] = yield getPlayersFor(walker, animations, selector);
     yield player.ready();
     yield checkPlayState(player, selector, playState);
   }
 }
 
-function* getPlayersFor(walker, front, selector) {
+function* getPlayersFor(walker, animations, selector) {
   let node = yield walker.querySelector(walker.rootNode, selector);
-  return front.getAnimationPlayersForNode(node);
+  return animations.getAnimationPlayersForNode(node);
 }
 
 function* checkPlayState(player, selector, expectedState) {
   let state = yield player.getCurrentState();
   is(state.playState, expectedState,
     "The playState of node " + selector + " is " + expectedState);
 }
rename from devtools/server/tests/browser/browser_animation_actors_03.js
rename to devtools/server/tests/browser/browser_animation_playerState.js
--- a/devtools/server/tests/browser/browser_animation_actors_03.js
+++ b/devtools/server/tests/browser/browser_animation_playerState.js
@@ -1,98 +1,96 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check the animation player's initial state
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
-
-  yield playerHasAnInitialState(walker, front);
-  yield playerStateIsCorrect(walker, front);
+  yield playerHasAnInitialState(walker, animations);
+  yield playerStateIsCorrect(walker, animations);
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
-function* playerHasAnInitialState(walker, front) {
+function* playerHasAnInitialState(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
-  let [player] = yield front.getAnimationPlayersForNode(node);
+  let [player] = yield animations.getAnimationPlayersForNode(node);
 
   ok(player.initialState, "The player front has an initial state");
   ok("startTime" in player.initialState, "Player's state has startTime");
   ok("currentTime" in player.initialState, "Player's state has currentTime");
   ok("playState" in player.initialState, "Player's state has playState");
   ok("playbackRate" in player.initialState, "Player's state has playbackRate");
   ok("name" in player.initialState, "Player's state has name");
   ok("duration" in player.initialState, "Player's state has duration");
   ok("delay" in player.initialState, "Player's state has delay");
-  ok("iterationCount" in player.initialState, "Player's state has iterationCount");
-  ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor");
+  ok("iterationCount" in player.initialState,
+     "Player's state has iterationCount");
+  ok("isRunningOnCompositor" in player.initialState,
+     "Player's state has isRunningOnCompositor");
   ok("type" in player.initialState, "Player's state has type");
-  ok("documentCurrentTime" in player.initialState, "Player's state has documentCurrentTime");
+  ok("documentCurrentTime" in player.initialState,
+     "Player's state has documentCurrentTime");
 }
 
-function* playerStateIsCorrect(walker, front) {
+function* playerStateIsCorrect(walker, animations) {
   info("Checking the state of the simple animation");
 
-  let state = yield getAnimationStateForNode(walker, front, ".simple-animation", 0);
+  let state = yield getAnimationStateForNode(walker, animations,
+                                             ".simple-animation", 0);
   is(state.name, "move", "Name is correct");
   is(state.duration, 2000, "Duration is correct");
   // null = infinite count
   is(state.iterationCount, null, "Iteration count is correct");
   is(state.playState, "running", "PlayState is correct");
   is(state.playbackRate, 1, "PlaybackRate is correct");
   is(state.type, "cssanimation", "Type is correct");
 
   info("Checking the state of the transition");
 
-  state = yield getAnimationStateForNode(walker, front, ".transition", 0);
+  state = yield getAnimationStateForNode(walker, animations, ".transition", 0);
   is(state.name, "width", "Transition name matches transition property");
   is(state.duration, 5000, "Transition duration is correct");
   // transitions run only once
   is(state.iterationCount, 1, "Transition iteration count is correct");
   is(state.playState, "running", "Transition playState is correct");
   is(state.playbackRate, 1, "Transition playbackRate is correct");
   is(state.type, "csstransition", "Transition type is correct");
 
   info("Checking the state of one of multiple animations on a node");
 
   // Checking the 2nd player
-  state = yield getAnimationStateForNode(walker, front, ".multiple-animations", 1);
+  state = yield getAnimationStateForNode(walker, animations,
+                                         ".multiple-animations", 1);
   is(state.name, "glow", "The 2nd animation's name is correct");
   is(state.duration, 1000, "The 2nd animation's duration is correct");
   is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
   is(state.playState, "running", "The 2nd animation's playState is correct");
   is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct");
 
   info("Checking the state of an animation with delay");
 
-  state = yield getAnimationStateForNode(walker, front, ".delayed-animation", 0);
+  state = yield getAnimationStateForNode(walker, animations,
+                                         ".delayed-animation", 0);
   is(state.delay, 5000, "The animation delay is correct");
 
   info("Checking the state of an transition with delay");
 
-  state = yield getAnimationStateForNode(walker, front, ".delayed-transition", 0);
+  state = yield getAnimationStateForNode(walker, animations,
+                                         ".delayed-transition", 0);
   is(state.delay, 3000, "The transition delay is correct");
 }
 
-function* getAnimationStateForNode(walker, front, nodeSelector, playerIndex) {
+function* getAnimationStateForNode(walker, animations, nodeSelector, index) {
   let node = yield walker.querySelector(walker.rootNode, nodeSelector);
-  let players = yield front.getAnimationPlayersForNode(node);
-  let player = players[playerIndex];
+  let players = yield animations.getAnimationPlayersForNode(node);
+  let player = players[index];
   yield player.ready();
   let state = yield player.getCurrentState();
   return state;
 }
rename from devtools/server/tests/browser/browser_animation_actors_07.js
rename to devtools/server/tests/browser/browser_animation_reconstructState.js
--- a/devtools/server/tests/browser/browser_animation_actors_07.js
+++ b/devtools/server/tests/browser/browser_animation_reconstructState.js
@@ -2,38 +2,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that, even though the AnimationPlayerActor only sends the bits of its
 // state that change, the front reconstructs the whole state everytime.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
-
-  yield playerHasCompleteStateAtAllTimes(walker, front);
+  yield playerHasCompleteStateAtAllTimes(walker, animations);
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
-function* playerHasCompleteStateAtAllTimes(walker, front) {
+function* playerHasCompleteStateAtAllTimes(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
-  let [player] = yield front.getAnimationPlayersForNode(node);
+  let [player] = yield animations.getAnimationPlayersForNode(node);
   yield player.ready();
 
   // Get the list of state key names from the initialstate.
   let keys = Object.keys(player.initialState);
 
   // Get the state over and over again and check that the object returned
   // contains all keys.
   // Normally, only the currentTime will have changed in between 2 calls.
rename from devtools/server/tests/browser/browser_animation_actors_15.js
rename to devtools/server/tests/browser/browser_animation_refreshTransitions.js
--- a/devtools/server/tests/browser/browser_animation_actors_15.js
+++ b/devtools/server/tests/browser/browser_animation_refreshTransitions.js
@@ -3,28 +3,19 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // When a transition finishes, no "removed" event is sent because it may still
 // be used, but when it restarts again (transitions back), then a new
 // AnimationPlayerFront should be sent, and the old one should be removed.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let animations = AnimationsFront(client, form);
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   info("Retrieve the test node");
   let node = yield walker.querySelector(walker.rootNode, ".all-transitions");
 
   info("Retrieve the animation players for the node");
   let players = yield animations.getAnimationPlayersForNode(node);
   is(players.length, 0, "The node has no animation players yet");
 
rename from devtools/server/tests/browser/browser_animation_actors_11.js
rename to devtools/server/tests/browser/browser_animation_setCurrentTime.js
--- a/devtools/server/tests/browser/browser_animation_actors_11.js
+++ b/devtools/server/tests/browser/browser_animation_setCurrentTime.js
@@ -2,28 +2,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that a player's currentTime can be changed and that the AnimationsActor
 // allows changing many players' currentTimes at once.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let animations = AnimationsFront(client, form);
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   yield testSetCurrentTime(walker, animations);
   yield testSetCurrentTimes(walker, animations);
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
rename from devtools/server/tests/browser/browser_animation_actors_12.js
rename to devtools/server/tests/browser/browser_animation_setPlaybackRate.js
--- a/devtools/server/tests/browser/browser_animation_actors_12.js
+++ b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js
@@ -1,28 +1,19 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check that a player's playbackRate can be changed.
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  yield addTab(MAIN_DOMAIN + "animation.html");
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let animations = AnimationsFront(client, form);
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   info("Retrieve an animated node");
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
 
   info("Retrieve the animation player for the node");
   let [player] = yield animations.getAnimationPlayersForNode(node);
 
   ok(player.setPlaybackRate, "Player has the setPlaybackRate method");
rename from devtools/server/tests/browser/browser_animation_actors_01.js
rename to devtools/server/tests/browser/browser_animation_simple.js
--- a/devtools/server/tests/browser/browser_animation_actors_01.js
+++ b/devtools/server/tests/browser/browser_animation_simple.js
@@ -1,43 +1,35 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Simple checks for the AnimationsActor
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  let doc = yield addTab("data:text/html;charset=utf-8,<title>test</title><div></div>");
+  let {client, walker, animations} = yield initAnimationsFrontForUrl(
+    "data:text/html;charset=utf-8,<title>test</title><div></div>");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
-
-  ok(front, "The AnimationsFront was created");
-  ok(front.getAnimationPlayersForNode, "The getAnimationPlayersForNode method exists");
-  ok(front.toggleAll, "The toggleAll method exists");
-  ok(front.playAll, "The playAll method exists");
-  ok(front.pauseAll, "The pauseAll method exists");
+  ok(animations, "The AnimationsFront was created");
+  ok(animations.getAnimationPlayersForNode,
+     "The getAnimationPlayersForNode method exists");
+  ok(animations.toggleAll, "The toggleAll method exists");
+  ok(animations.playAll, "The playAll method exists");
+  ok(animations.pauseAll, "The pauseAll method exists");
 
   let didThrow = false;
   try {
-    yield front.getAnimationPlayersForNode(null);
+    yield animations.getAnimationPlayersForNode(null);
   } catch (e) {
     didThrow = true;
   }
   ok(didThrow, "An exception was thrown for a missing NodeActor");
 
   let invalidNode = yield walker.querySelector(walker.rootNode, "title");
-  let players = yield front.getAnimationPlayersForNode(invalidNode);
+  let players = yield animations.getAnimationPlayersForNode(invalidNode);
   ok(Array.isArray(players), "An array of players was returned");
   is(players.length, 0, "0 players have been returned for the invalid node");
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
rename from devtools/server/tests/browser/browser_animation_actors_04.js
rename to devtools/server/tests/browser/browser_animation_updatedState.js
--- a/devtools/server/tests/browser/browser_animation_actors_04.js
+++ b/devtools/server/tests/browser/browser_animation_updatedState.js
@@ -1,56 +1,48 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Check the animation player's updated state
 
-const {AnimationsFront} = require("devtools/server/actors/animation");
-const {InspectorFront} = require("devtools/server/actors/inspector");
-
 add_task(function*() {
-  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+  let {client, walker, animations} =
+    yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let inspector = InspectorFront(client, form);
-  let walker = yield inspector.getWalker();
-  let front = AnimationsFront(client, form);
-
-  yield playStateIsUpdatedDynamically(walker, front);
+  yield playStateIsUpdatedDynamically(walker, animations);
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
 
-function* playStateIsUpdatedDynamically(walker, front) {
+function* playStateIsUpdatedDynamically(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".short-animation");
 
   // Restart the animation to make sure we can get the player (it might already
-  // be finished by now). Do this by toggling the class and forcing a sync reflow
-  // using the CPOW.
+  // be finished by now). Do this by toggling the class and forcing a sync
+  // reflow using the CPOW.
   let cpow = content.document.querySelector(".short-animation");
   cpow.classList.remove("short-animation");
   let reflow = cpow.offsetWidth;
   cpow.classList.add("short-animation");
 
-  let [player] = yield front.getAnimationPlayersForNode(node);
+  let [player] = yield animations.getAnimationPlayersForNode(node);
 
   yield player.ready();
   let state = yield player.getCurrentState();
 
   is(state.playState, "running",
     "The playState is running while the transition is running");
 
   info("Wait until the animation stops (more than 1000ms)");
-  yield wait(1500); // Waiting 1.5sec for good measure
+  // Waiting 1.5sec for good measure
+  yield wait(1500);
 
   state = yield player.getCurrentState();
   is(state.playState, "finished",
     "The animation has ended and the state has been updated");
   ok(state.currentTime > player.initialState.currentTime,
     "The currentTime has been updated");
 }
 
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -40,16 +40,32 @@ var addTab = Task.async(function* (url) 
   yield new Promise(resolve => {
     let isBlank = url == "about:blank";
     waitForFocus(resolve, content, isBlank);
   });
 
   return tab.linkedBrowser.contentWindow.document;
 });
 
+function* initAnimationsFrontForUrl(url) {
+  const {AnimationsFront} = require("devtools/server/actors/animation");
+  const {InspectorFront} = require("devtools/server/actors/inspector");
+
+  yield addTab(url);
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let animations = AnimationsFront(client, form);
+
+  return {inspector, walker, animations, client};
+}
+
 function initDebuggerServer() {
   try {
     // Sometimes debugger server does not get destroyed correctly by previous
     // tests.
     DebuggerServer.destroy();
   } catch (ex) { }
   DebuggerServer.init();
   DebuggerServer.addBrowserActors();
--- a/devtools/server/tests/mochitest/test_styles-modify.html
+++ b/devtools/server/tests/mochitest/test_styles-modify.html
@@ -47,34 +47,34 @@ addTest(function modifyProperties() {
   }).then(applied => {
     elementStyle = applied[0].rule;
     is(elementStyle.cssText, localNode.style.cssText, "Got expected css text");
 
     // Will start with "color:blue"
     let changes = elementStyle.startModifyingProperties();
 
     // Change an existing property...
-    changes.setProperty("color", "black");
+    changes.setProperty(-1, "color", "black");
     // Create a new property
-    changes.setProperty("background-color", "green");
+    changes.setProperty(-1, "background-color", "green");
 
     // Create a new property and then change it immediately.
-    changes.setProperty("border", "1px solid black");
-    changes.setProperty("border", "2px solid black");
+    changes.setProperty(-1, "border", "1px solid black");
+    changes.setProperty(-1, "border", "2px solid black");
 
     return changes.apply();
   }).then(() => {
     is(elementStyle.cssText, "color: black; background-color: green; border: 2px solid black;", "Should have expected cssText");
     is(elementStyle.cssText, localNode.style.cssText, "Local node and style front match.");
 
     // Remove all the properties
     let changes = elementStyle.startModifyingProperties();
-    changes.removeProperty("color");
-    changes.removeProperty("background-color");
-    changes.removeProperty("border");
+    changes.removeProperty(-1, "color");
+    changes.removeProperty(-1, "background-color");
+    changes.removeProperty(-1, "border");
 
     return changes.apply();
   }).then(() => {
     is(elementStyle.cssText, "", "Should have expected cssText");
     is(elementStyle.cssText, localNode.style.cssText, "Local node and style front match.");
   }).then(runNextTest));
 });
 
--- a/mobile/android/base/home/TabMenuStripLayout.java
+++ b/mobile/android/base/home/TabMenuStripLayout.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.home;
 
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
 import android.content.res.ColorStateList;
 import org.mozilla.gecko.R;
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
@@ -37,24 +38,27 @@ class TabMenuStripLayout extends LinearL
     private View fromTab;
     private int fromPosition;
     private int toPosition;
     private float progress;
 
     // This variable is used to predict the direction of scroll.
     private float prevProgress;
     private int tabContentStart;
+    private boolean titlebarFill;
     private int activeTextColor;
     private ColorStateList inactiveTextColor;
 
     TabMenuStripLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip);
         final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1);
+
+        titlebarFill = a.getBoolean(R.styleable.TabMenuStrip_titlebarFill, false);
         tabContentStart = a.getDimensionPixelSize(R.styleable.TabMenuStrip_tabContentStart, 0);
         activeTextColor = a.getColor(R.styleable.TabMenuStrip_activeTextColor, R.color.text_and_tabs_tray_grey);
         inactiveTextColor = a.getColorStateList(R.styleable.TabMenuStrip_inactiveTextColor);
         a.recycle();
 
         if (stripResId != -1) {
             strip = getResources().getDrawable(stripResId);
         }
@@ -62,16 +66,23 @@ class TabMenuStripLayout extends LinearL
         setWillNotDraw(false);
     }
 
     void onAddPagerView(String title) {
         final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false);
         button.setText(title.toUpperCase());
         button.setTextColor(inactiveTextColor);
 
+        // Set titles width to weight, or wrap text width.
+        if (titlebarFill) {
+            button.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f));
+        } else {
+            button.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
+        }
+
         if (getChildCount() == 0) {
             button.setPadding(button.getPaddingLeft() + tabContentStart,
                               button.getPaddingTop(),
                               button.getPaddingRight(),
                               button.getPaddingBottom());
         }
 
         addView(button);
--- a/mobile/android/base/resources/layout/firstrun_pane.xml
+++ b/mobile/android/base/resources/layout/firstrun_pane.xml
@@ -17,13 +17,14 @@
                     android:background="@color/firstrun_pager_background">
 
         <org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
                                              android:layout_height="@dimen/tabs_strip_height"
                                              android:background="@color/firstrun_tabstrip"
                                              android:visibility="visible"
                                              android:layout_gravity="top"
                                              gecko:strip="@drawable/home_tab_menu_strip"
+                                             gecko:titlebarFill="true"
                                              gecko:activeTextColor="@android:color/white"
                                              gecko:inactiveTextColor="@color/divider_light" />
 
     </org.mozilla.gecko.firstrun.FirstrunPager>
 </org.mozilla.gecko.firstrun.FirstrunPane>
--- a/mobile/android/base/resources/layout/tab_menu_strip.xml
+++ b/mobile/android/base/resources/layout/tab_menu_strip.xml
@@ -1,16 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
           android:layout_width="0dp"
           android:layout_height="match_parent"
-          android:layout_weight="1"
           android:minWidth="@dimen/tabs_strip_button_width"
           android:background="@drawable/tabs_strip_indicator"
           android:paddingLeft="@dimen/tabs_strip_button_padding"
           android:paddingRight="@dimen/tabs_strip_button_padding"
           android:gravity="center"
           android:focusable="true"
           style="@style/TextAppearance.Widget.HomePagerTabMenuStrip"/>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -160,16 +160,17 @@
         <attr name="android:verticalSpacing"/>
     </declare-styleable>
 
     <declare-styleable name="TabMenuStrip">
         <attr name="strip" format="reference"/>
         <attr name="tabContentStart" format="dimension" />
         <attr name="activeTextColor" format="color" />
         <attr name="inactiveTextColor" format="color" />
+        <attr name="titlebarFill" format="boolean" />
     </declare-styleable>
 
     <declare-styleable name="TabPanelBackButton">
         <attr name="rightDivider" format="reference"/>
         <attr name="dividerVerticalPadding" format="dimension"/>
     </declare-styleable>
 
     <declare-styleable name="EllipsisTextView">
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -849,17 +849,17 @@ pref("devtools.debugger.force-local", tr
 // Display a prompt when a new connection starts to accept/reject it
 pref("devtools.debugger.prompt-connection", true);
 // Block tools from seeing / interacting with certified apps
 pref("devtools.debugger.forbid-certified-apps", true);
 // List of permissions that a sideloaded app can't ask for
 pref("devtools.apps.forbidden-permissions", "embed-apps,engineering-mode,embed-widgets");
 
 // DevTools default color unit
-pref("devtools.defaultColorUnit", "hex");
+pref("devtools.defaultColorUnit", "authored");
 
 // Used for devtools debugging
 pref("devtools.dump.emit", false);
 
 // Disable device discovery logging
 pref("devtools.discovery.log", false);
 // Whether to scan for DevTools devices via WiFi
 pref("devtools.remote.wifi.scan", true);
--- a/toolkit/components/alerts/moz.build
+++ b/toolkit/components/alerts/moz.build
@@ -7,18 +7,23 @@
 MOCHITEST_MANIFESTS += ['test/mochitest.ini']
 
 XPIDL_SOURCES += [
     'nsIAlertsService.idl',
 ]
 
 XPIDL_MODULE = 'alerts'
 
+EXPORTS += [
+    'nsAlertsUtils.h',
+]
+
 UNIFIED_SOURCES += [
     'nsAlertsService.cpp',
+    'nsAlertsUtils.cpp',
     'nsXULAlerts.cpp',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 
 JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/components/alerts/nsAlertsService.cpp
+++ b/toolkit/components/alerts/nsAlertsService.cpp
@@ -115,17 +115,17 @@ NS_IMETHODIMP nsAlertsService::ShowAlert
     if (aAlertListener)
       aAlertListener->Observe(nullptr, "alertfinished", PromiseFlatString(aAlertCookie).get());
     return NS_OK;
   }
 
   // Use XUL notifications as a fallback if above methods have failed.
   rv = mXULAlerts.ShowAlertNotification(aImageUrl, aAlertTitle, aAlertText, aAlertTextClickable,
                                         aAlertCookie, aAlertListener, aAlertName,
-                                        aBidi, aLang, aInPrivateBrowsing);
+                                        aBidi, aLang, aPrincipal, aInPrivateBrowsing);
   return rv;
 #endif // !MOZ_WIDGET_ANDROID
 }
 
 NS_IMETHODIMP nsAlertsService::CloseAlert(const nsAString& aAlertName,
                                           nsIPrincipal* aPrincipal)
 {
   if (XRE_IsContentProcess()) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsUtils.cpp
@@ -0,0 +1,74 @@
+/* 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/. */
+
+#include "nsAlertsUtils.h"
+
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsIStringBundle.h"
+#include "nsIURI.h"
+#include "nsXPIDLString.h"
+
+#define ALERTS_BUNDLE "chrome://alerts/locale/alert.properties"
+
+/* static */
+bool
+nsAlertsUtils::IsActionablePrincipal(nsIPrincipal* aPrincipal)
+{
+  return aPrincipal &&
+         !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal) &&
+         !aPrincipal->GetIsNullPrincipal();
+}
+
+/* static */
+void
+nsAlertsUtils::GetSource(nsIPrincipal* aPrincipal, nsAString& aSource)
+{
+  nsAutoString hostPort;
+  GetSourceHostPort(aPrincipal, hostPort);
+  if (hostPort.IsEmpty()) {
+    return;
+  }
+  nsCOMPtr<nsIStringBundleService> stringService(
+    mozilla::services::GetStringBundleService());
+  if (!stringService) {
+    return;
+  }
+  nsCOMPtr<nsIStringBundle> alertsBundle;
+  if (NS_WARN_IF(NS_FAILED(stringService->CreateBundle(ALERTS_BUNDLE,
+      getter_AddRefs(alertsBundle))))) {
+    return;
+  }
+  const char16_t* params[1] = { hostPort.get() };
+  nsXPIDLString result;
+  if (NS_WARN_IF(NS_FAILED(
+      alertsBundle->FormatStringFromName(MOZ_UTF16("source.label"), params, 1,
+      getter_Copies(result))))) {
+    return;
+  }
+  aSource = result;
+}
+
+/* static */
+void
+nsAlertsUtils::GetSourceHostPort(nsIPrincipal* aPrincipal,
+                                 nsAString& aHostPort)
+{
+  if (!IsActionablePrincipal(aPrincipal)) {
+    return;
+  }
+  nsCOMPtr<nsIURI> principalURI;
+  if (NS_WARN_IF(NS_FAILED(
+      aPrincipal->GetURI(getter_AddRefs(principalURI))))) {
+    return;
+  }
+  if (!principalURI) {
+    return;
+  }
+  nsAutoCString hostPort;
+  if (NS_WARN_IF(NS_FAILED(principalURI->GetHostPort(hostPort)))) {
+    return;
+  }
+  CopyUTF8toUTF16(hostPort, aHostPort);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsUtils.h
@@ -0,0 +1,39 @@
+/* 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/. */
+
+#ifndef nsAlertsUtils_h
+#define nsAlertsUtils_h
+
+#include "nsIPrincipal.h"
+#include "nsString.h"
+
+class nsAlertsUtils final
+{
+private:
+  nsAlertsUtils() = delete;
+
+public:
+  /**
+   * Indicates whether an alert from |aPrincipal| should include the source
+   * string and action buttons. Returns false if |aPrincipal| is |nullptr|, or
+   * a system, expanded, or null principal.
+   */
+  static bool
+  IsActionablePrincipal(nsIPrincipal* aPrincipal);
+
+  /**
+   * Sets |aSource| to the localized notification source string, or an empty
+   * string if |aPrincipal| is not actionable.
+   */
+  static void
+  GetSource(nsIPrincipal* aPrincipal, nsAString& aSource);
+
+  /**
+   * Sets |aHostPort| to the host and port from |aPrincipal|'s URI, or an
+   * empty string if |aPrincipal| is not actionable.
+   */
+  static void
+  GetSourceHostPort(nsIPrincipal* aPrincipal, nsAString& aHostPort);
+};
+#endif /* nsAlertsUtils_h */
--- a/toolkit/components/alerts/nsXULAlerts.cpp
+++ b/toolkit/components/alerts/nsXULAlerts.cpp
@@ -3,16 +3,17 @@
  * 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/. */
 
 #include "nsXULAlerts.h"
 
 #include "nsAutoPtr.h"
 #include "mozilla/LookAndFeel.h"
 #include "nsIServiceManager.h"
+#include "nsAlertsUtils.h"
 #include "nsISupportsArray.h"
 #include "nsISupportsPrimitives.h"
 #include "nsPIDOMWindow.h"
 #include "nsIWindowWatcher.h"
 
 using namespace mozilla;
 
 #define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xul"
@@ -39,17 +40,18 @@ nsXULAlertObserver::Observe(nsISupports*
   return rv;
 }
 
 nsresult
 nsXULAlerts::ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle,
                                    const nsAString& aAlertText, bool aAlertTextClickable,
                                    const nsAString& aAlertCookie, nsIObserver* aAlertListener,
                                    const nsAString& aAlertName, const nsAString& aBidi,
-                                   const nsAString& aLang, bool aInPrivateBrowsing)
+                                   const nsAString& aLang, nsIPrincipal* aPrincipal,
+                                   bool aInPrivateBrowsing)
 {
   nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID));
 
   nsCOMPtr<nsISupportsArray> argsArray;
   nsresult rv = NS_NewISupportsArray(getter_AddRefs(argsArray));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // create scriptable versions of our strings that we can store in our nsISupportsArray....
@@ -129,16 +131,26 @@ nsXULAlerts::ShowAlertNotification(const
   NS_ENSURE_SUCCESS(rv, rv);
   nsRefPtr<nsXULAlertObserver> alertObserver = new nsXULAlertObserver(this, aAlertName, aAlertListener);
   nsCOMPtr<nsISupports> iSupports(do_QueryInterface(alertObserver));
   ifptr->SetData(iSupports);
   ifptr->SetDataIID(&NS_GET_IID(nsIObserver));
   rv = argsArray->AppendElement(ifptr);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // The source contains the host and port of the site that sent the
+  // notification. It is empty for system alerts.
+  nsCOMPtr<nsISupportsString> scriptableAlertSource (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+  NS_ENSURE_TRUE(scriptableAlertSource, NS_ERROR_FAILURE);
+  nsAutoString source;
+  nsAlertsUtils::GetSource(aPrincipal, source);
+  scriptableAlertSource->SetData(source);
+  rv = argsArray->AppendElement(scriptableAlertSource);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   nsCOMPtr<nsIDOMWindow> newWindow;
   nsAutoCString features("chrome,dialog=yes,titlebar=no,popup=yes");
   if (aInPrivateBrowsing) {
     features.AppendLiteral(",private");
   }
   rv = wwatch->OpenWindow(0, ALERT_CHROME_URL, "_blank", features.get(),
                           argsArray, getter_AddRefs(newWindow));
   NS_ENSURE_SUCCESS(rv, rv);
--- a/toolkit/components/alerts/nsXULAlerts.h
+++ b/toolkit/components/alerts/nsXULAlerts.h
@@ -20,17 +20,18 @@ public:
   }
 
   virtual ~nsXULAlerts() {}
 
   nsresult ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle,
                                  const nsAString& aAlertText, bool aAlertTextClickable,
                                  const nsAString& aAlertCookie, nsIObserver* aAlertListener,
                                  const nsAString& aAlertName, const nsAString& aBidi,
-                                 const nsAString& aLang, bool aInPrivateBrowsing);
+                                 const nsAString& aLang, nsIPrincipal* aPrincipal,
+                                 bool aInPrivateBrowsing);
 
   nsresult CloseAlert(const nsAString& aAlertName);
 protected:
   nsInterfaceHashtable<nsStringHashKey, nsIDOMWindow> mNamedWindows;
 };
 
 /**
  * This class wraps observers for alerts and watches
--- a/toolkit/components/alerts/resources/content/alert.js
+++ b/toolkit/components/alerts/resources/content/alert.js
@@ -27,19 +27,29 @@ function prefillAlertInfo() {
   // arguments[2] --> the alert text
   // arguments[3] --> is the text clickable?
   // arguments[4] --> the alert cookie to be passed back to the listener
   // arguments[5] --> the alert origin reported by the look and feel
   // arguments[6] --> bidi
   // arguments[7] --> lang
   // arguments[8] --> replaced alert window (nsIDOMWindow)
   // arguments[9] --> an optional callback listener (nsIObserver)
+  // arguments[10] -> the localized alert source string
 
   switch (window.arguments.length) {
     default:
+    case 11: {
+      let label = document.getElementById('alertSourceLabel');
+      if (window.arguments[10]) {
+        label.hidden = false;
+        label.setAttribute('value', window.arguments[10]);
+      } else {
+        label.hidden = true;
+      }
+    }
     case 10:
       gAlertListener = window.arguments[9];
     case 9:
       gReplacedWindow = window.arguments[8];
     case 8:
       if (window.arguments[7]) {
         document.getElementById('alertTitleLabel').setAttribute('lang', window.arguments[7]);
         document.getElementById('alertTextLabel').setAttribute('lang', window.arguments[7]);
--- a/toolkit/components/alerts/resources/content/alert.xul
+++ b/toolkit/components/alerts/resources/content/alert.xul
@@ -27,16 +27,18 @@
     <box>
       <hbox id="alertImageBox" class="alertImageBox" align="center" pack="center">
         <image id="alertImage"/>
       </hbox>
 
       <vbox id="alertTextBox" class="alertTextBox">
         <label id="alertTitleLabel" class="alertTitle plain"/>
         <label id="alertTextLabel" class="alertText plain"/>
+        <spacer flex="1"/>
+        <label id="alertSourceLabel" class="alertSource plain"/>
       </vbox>
     </box>
 
     <vbox class="alertCloseBox">
       <toolbarbutton class="alertCloseButton close-icon"
                      tooltiptext="&closeAlert.tooltip;"
                      onclick="event.stopPropagation();"
                      oncommand="close();"/>
--- a/toolkit/components/alerts/test/mochitest.ini
+++ b/toolkit/components/alerts/test/mochitest.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
-skip-if = buildapp == 'b2g'
+skip-if = buildapp == 'b2g' || buildapp == 'mulet'
 
 # Synchronous tests like test_alerts.html must come before
 # asynchronous tests like test_alerts_noobserve.html!
 [test_alerts.html]
 skip-if = toolkit == 'android'
 [test_alerts_noobserve.html]
 skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_multiple_alerts.html]
+[test_principal.html]
+skip-if = toolkit == 'android'
--- a/toolkit/components/alerts/test/test_alerts.html
+++ b/toolkit/components/alerts/test/test_alerts.html
@@ -17,26 +17,26 @@
 <br>If so, the test will finish once the notification disappears.
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 var observer = {
   alertShow: false,
   observe: function (aSubject, aTopic, aData) {
-    if (aTopic == "alertclickcallback") { 
+    is(aData, "foobarcookie", "Checking whether the alert cookie was passed correctly");
+    if (aTopic == "alertclickcallback") {
       todo(false, "Did someone click the notification while running mochitests? (Please don't.)");
     } else if (aTopic == "alertshow") {
       ok(!this.alertShow, "Alert should not be shown more than once");
       this.alertShow = true;
     } else {
       is(aTopic, "alertfinished", "Checking the topic for a finished notification");
       SimpleTest.finish();
     }
-    is(aData, "foobarcookie", "Checking whether the alert cookie was passed correctly");
   }
 };
 
 function runTest() {
   const Cc = SpecialPowers.Cc;
   const Ci = SpecialPowers.Ci;
 
   if (!("@mozilla.org/alerts-service;1" in Cc)) {
@@ -57,19 +57,20 @@ function runTest() {
     return;
   }
 
   try {
     var alertName = "fiorello";
     SimpleTest.waitForExplicitFinish();
     notifier.showAlertNotification(null, "Notification test",
                                    "Surprise! I'm here to test notifications!",
-                                   false, "foobarcookie", observer, alertname);
+                                   false, "foobarcookie", observer, alertName);
     ok(true, "showAlertNotification() succeeded.  Waiting for notification...");
-    if (MAC) {
+
+    if (SpecialPowers.Services.appinfo.OS == "Darwin") {
       // Notifications are native on OS X 10.8 and later, and when they are they
       // persist in the Notification Center. We need to close explicitly to avoid a hang.
       // This also works for XUL notifications when running this test on OS X < 10.8.
       notifier.closeAlert(alertName);
     }
   } catch (ex) {
     todo(false, "showAlertNotification() failed.", ex);
     SimpleTest.finish();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/alerts/test/test_principal.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Bug 1202933</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Services = SpecialPowers.Services;
+
+const notifier = Cc["@mozilla.org/alerts-service;1"]
+                   .getService(Ci.nsIAlertsService);
+
+function notify(alertName, principal) {
+  return new Promise((resolve, reject) => {
+    var source;
+    function observe(subject, topic, data) {
+      if (topic == "alertclickcallback") {
+        reject(new Error("Alerts should not be clicked during test"));
+      } else if (topic == "alertshow") {
+        var alertWindows = Services.wm.getEnumerator("alert:alert");
+        ok(alertWindows.hasMoreElements(), "Should show alert");
+        var alertWindow = alertWindows.getNext();
+        source = alertWindow.document.getElementById("alertSourceLabel").getAttribute("value");
+      } else {
+        is(topic, "alertfinished", "Should hide alert");
+        resolve(source);
+      }
+    }
+    notifier.showAlertNotification(null, "Notification test",
+                                   "Surprise! I'm here to test notifications!",
+                                   false, alertName, observe, alertName,
+                                   null, null, null, principal);
+    if (SpecialPowers.Services.appinfo.OS == "Darwin") {
+      notifier.closeAlert(alertName);
+    }
+  });
+}
+
+function* testNoPrincipal() {
+  var source = yield notify("noPrincipal", null);
+  ok(!source, "Should omit source without principal");
+}
+
+function* testSystemPrincipal() {
+  var principal = Services.scriptSecurityManager.getSystemPrincipal();
+  var source = yield notify("systemPrincipal", principal);
+  ok(!source, "Should omit source for system principal");
+}
+
+function* testExpandedPrincipal() {
+  var principal = Services.scriptSecurityManager.createExpandedPrincipal([], 0);
+  var source = yield notify("expandedPrincipal", principal);
+  ok(!source, "Should omit source for expanded principal");
+}
+
+function* testNullPrincipal() {
+  var principal = Services.scriptSecurityManager.createNullPrincipal({});
+  var source = yield notify("nullPrincipal", principal);
+  ok(!source, "Should omit source for null principal");
+}
+
+function* testNodePrincipal() {
+  var principal = SpecialPowers.wrap(document).nodePrincipal;
+  var source = yield notify("nodePrincipal", principal);
+
+  var stringBundle = Services.strings.createBundle(
+    "chrome://alerts/locale/alert.properties"
+  );
+  var localizedSource = stringBundle.formatStringFromName(
+    "source.label", [principal.URI.hostPort], 1);
+  is(source, localizedSource, "Should include source for node principal");
+}
+
+function runTest() {
+  if (!("@mozilla.org/alerts-service;1" in Cc)) {
+    todo(false, "Alerts service does not exist in this application");
+    return;
+  }
+
+  if ("@mozilla.org/system-alerts-service;1" in Cc) {
+    todo(false, "Native alerts service exists in this application");
+    return;
+  }
+
+  ok(true, "Alerts service exists in this application");
+
+  ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(),
+     "Alerts should not be present at the start of the test.");
+
+  add_task(testNoPrincipal);
+  add_task(testSystemPrincipal);
+  add_task(testExpandedPrincipal);
+  add_task(testNullPrincipal);
+  add_task(testNodePrincipal);
+}
+
+runTest();
+</script>
+</pre>
+</body>
+</html>
--- a/toolkit/locales/en-US/chrome/alerts/alert.properties
+++ b/toolkit/locales/en-US/chrome/alerts/alert.properties
@@ -4,9 +4,13 @@
 
 # LOCALIZATION NOTE(closeButton.title): Used as the close button text for web notifications on OS X.
 # This should ideally match the string that OS X uses for the close button on alert-type
 # notifications. OS X will truncate the value if it's too long.
 closeButton.title = Close
 # LOCALIZATION NOTE(actionButton.label): Used as the button label to provide more actions on OS X notifications. OS X will truncate this if it's too long.
 actionButton.label = …
 webActions.disable.label = Disable notifications from this site
+# LOCALIZATION NOTE(source.label): Used to show the URL of the site that
+# sent the notification (e.g., "via mozilla.org"). "%1$S" is the source host
+# and port.
+source.label=via %1$S
 webActions.settings.label = Notification settings
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -2803,35 +2803,39 @@ this.XPIProvider = {
              getService(Ci.nsIWindowWatcher);
     ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
 
     // Ensure any changes to the add-ons list are flushed to disk
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
                                !XPIDatabase.writeAddonsList());
   },
 
-  updateSystemAddons: Task.async(function XPI_updateSystemAddons() {
+  updateSystemAddons: Task.async(function* XPI_updateSystemAddons() {
+    let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+    if (!systemAddonLocation)
+      return;
+
     // Don't do anything in safe mode
     if (Services.appinfo.inSafeMode)
       return;
 
     // Download the list of system add-ons
     let url = Preferences.get(PREF_SYSTEM_ADDON_UPDATE_URL, null);
     if (!url)
-      return;
+      return systemAddonLocation.cleanDirectories();
 
     url = UpdateUtils.formatUpdateURL(url);
 
     logger.info(`Starting system add-on update check from ${url}.`);
     let addonList = yield ProductAddonChecker.getProductAddonList(url);
 
     // If there was no list then do nothing.
     if (!addonList) {
       logger.info("No system add-ons list was returned.");
-      return;
+      return systemAddonLocation.cleanDirectories();
     }
 
     addonList = new Map([for (spec of addonList) [spec.id, { spec, path: null, addon: null }]]);
 
     let getAddonsInLocation = (location) => {
       return new Promise(resolve => {
         XPIDatabase.getAddonsInLocation(location, resolve);
       });
@@ -2848,32 +2852,30 @@ this.XPIProvider = {
           return false;
         if (wantedInfo.spec.version != addon.version)
           return false;
       }
 
       return true;
     };
 
-    let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
-
     // If this matches the current set in the profile location then do nothing.
     let updatedAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_ADDONS));
     if (setMatches(addonList, updatedAddons)) {
       logger.info("Retaining existing updated system add-ons.");
-      return;
+      return systemAddonLocation.cleanDirectories();
     }
 
     // If this matches the current set in the default location then reset the
     // updated set.
     let defaultAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS));
     if (setMatches(addonList, defaultAddons)) {
       logger.info("Resetting system add-ons.");
       systemAddonLocation.resetAddonSet();
-      return;
+      return systemAddonLocation.cleanDirectories();
     }
 
     // Download all the add-ons
     // Bug 1204158: If we already have some of these locally then just use those
     let downloadAddon = Task.async(function*(item) {
       try {
         let sourceAddon = updatedAddons.get(item.spec.id);
         if (sourceAddon && sourceAddon.version == item.spec.version) {
@@ -2946,16 +2948,18 @@ this.XPIProvider = {
           try {
             yield OS.File.remove(item.path);
           }
           catch (e) {
             logger.warn(`Failed to remove temporary file ${item.path}.`, e);
           }
         }
       }
+
+      yield systemAddonLocation.cleanDirectories();
     }
   }),
 
   /**
    * Verifies that all installed add-ons are still correctly signed.
    */
   verifySignatures: function XPI_verifySignatures() {
     XPIDatabase.getAddonList(a => true, (addons) => {
@@ -7501,16 +7505,17 @@ Object.assign(MutableDirectoryInstallLoc
  *         The nsIFile directory for the install location
  * @param  aScope
  *         The scope of add-ons installed in this location
  * @param  aResetSet
  *         True to throw away the current add-on set
  */
 function SystemAddonInstallLocation(aName, aDirectory, aScope, aResetSet) {
   this._baseDir = aDirectory;
+  this._nextDir = null;
 
   if (aResetSet)
     this.resetAddonSet();
 
   this._addonSet = this._loadAddonSet();
 
   this._directory = null;
   if (this._addonSet.directory) {
@@ -7622,16 +7627,68 @@ Object.assign(SystemAddonInstallLocation
   /**
    * Resets the add-on set so on the next startup the default set will be used.
    */
   resetAddonSet: function() {
     this._saveAddonSet({ schema: 1, addons: {} });
   },
 
   /**
+   * Removes any directories not currently in use or pending use after a
+   * restart. Any errors that happen here don't really matter as we'll attempt
+   * to cleanup again next time.
+   */
+  cleanDirectories: Task.async(function*() {
+    let iterator;
+    try {
+      iterator = new OS.File.DirectoryIterator(this._baseDir.path);
+    }
+    catch (e) {
+      logger.error("Failed to clean updated system add-ons directories.", e);
+      return;
+    }
+
+    try {
+      let entries = [];
+
+      yield iterator.forEach(entry => {
+        // Skip the directory currently in use
+        if (this._directory && this._directory.path == entry.path)
+          return;
+
+        // Skip the next directory
+        if (this._nextDir && this._nextDir.path == entry.path)
+          return;
+
+        entries.push(entry);
+      });
+
+      for (let entry of entries) {
+        if (entry.isDir) {
+          yield OS.File.removeDir(entry.path, {
+            ignoreAbsent: true,
+            ignorePermissions: true,
+          });
+        }
+        else {
+          yield OS.File.remove(entry.path, {
+            ignoreAbsent: true,
+          });
+        }
+      }
+    }
+    catch (e) {
+      logger.error("Failed to clean updated system add-ons directories.", e);
+    }
+    finally {
+      iterator.close();
+    }
+  }),
+
+  /**
    * Installs a new set of system add-ons into the location and updates the
    * add-on set in prefs. We wait to switch state until a restart.
    */
   installAddonSet: Task.async(function(aAddons) {
     // Make sure the base dir exists
     yield OS.File.makeDir(this._baseDir.path, { ignoreExisting: true });
 
     let newDir = this._baseDir.clone();
@@ -7682,16 +7739,17 @@ Object.assign(SystemAddonInstallLocation
     let state = { schema: 1, directory: newDir.leafName, addons: {} };
     for (let addon of aAddons) {
       state.addons[addon.id] = {
         version: addon.version
       }
     }
 
     this._saveAddonSet(state);
+    this._nextDir = newDir;
   }),
 });
 
 #ifdef XP_WIN
 /**
  * An object that identifies a registry install location for add-ons. The location
  * consists of a registry key which contains string values mapping ID to the
  * path where an add-on is installed
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
@@ -8,48 +8,61 @@ const PREF_APP_UPDATE_ENABLED         = 
 Components.utils.import("resource://testing-common/httpd.js");
 const { computeHash } = Components.utils.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
 
 // Enable signature checks for these tests
 Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
 
 BootstrapMonitor.init();
 
-const featureDir = FileUtils.getDir("ProfD", ["features"]);
+const featureDir = FileUtils.getDir("ProfD", ["features"], false);
 
 function getCurrentFeatureDir() {
   let dir = featureDir.clone();
   let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET));
   dir.append(set.directory);
   return dir;
 }
 
-// Build the test sets
-let dir = FileUtils.getDir("ProfD", ["features", "prefilled"], true);
-do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
-do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
+function clearFeatureDir() {
+  // Delete any existing directories
+  if (featureDir.exists())
+    featureDir.remove(true);
+
+  Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
+}
 
-// Mark these in the past so the startup file scan notices when files have changed properly
-FileUtils.getFile("ProfD", ["features", "prefilled", "system2@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
-FileUtils.getFile("ProfD", ["features", "prefilled", "system3@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
+function buildPrefilledFeatureDir() {
+  clearFeatureDir();
+
+  // Build the test set
+  let dir = FileUtils.getDir("ProfD", ["features", "prefilled"], true);
+
+  do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
+  do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
 
-const prefilledSet = {
-  schema: 1,
-  directory: dir.leafName,
-  addons: {
-    "system2@tests.mozilla.org": {
-      version: "2.0"
-    },
-    "system3@tests.mozilla.org": {
-      version: "2.0"
-    },
-  }
-};
+  // Mark these in the past so the startup file scan notices when files have changed properly
+  FileUtils.getFile("ProfD", ["features", "prefilled", "system2@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
+  FileUtils.getFile("ProfD", ["features", "prefilled", "system3@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
 
-dir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden"], true);
+  Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify({
+    schema: 1,
+    directory: dir.leafName,
+    addons: {
+      "system2@tests.mozilla.org": {
+        version: "2.0"
+      },
+      "system3@tests.mozilla.org": {
+        version: "2.0"
+      },
+    }
+  }));
+}
+
+let dir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden"], true);
 do_get_file("data/system_addons/system1_1.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system2_1.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
 
 dir = FileUtils.getDir("ProfD", ["sysfeatures", "prefilled"], true);
 do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
 
 const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"], true);
@@ -197,44 +210,44 @@ function* check_installed(inProfile, ...
  *
  * setup:        A task to setup the profile into the initial state.
  * initialState: The initial expected system add-on state after setup has run.
  */
 const TEST_CONDITIONS = {
   // Runs tests with no updated or default system add-ons initially installed
   blank: {
     setup: function*() {
-      Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
+      clearFeatureDir();
       distroDir.leafName = "empty";
     },
     initialState: [false, null, null, null, null, null],
   },
 
   // Runs tests with default system add-ons installed
   withAppSet: {
     setup: function*() {
-      Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
+      clearFeatureDir();
       distroDir.leafName = "prefilled";
     },
     initialState: [false, null, "2.0", "2.0", null, null],
   },
 
   // Runs tests with updated system add-ons installed
   withProfileSet: {
     setup: function*() {
-      Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(prefilledSet));
+      buildPrefilledFeatureDir();
       distroDir.leafName = "empty";
     },
     initialState: [true, null, "2.0", "2.0", null, null],
   },
 
   // Runs tests with both default and updated system add-ons installed
   withBothSets: {
     setup: function*() {
-      Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(prefilledSet));
+      buildPrefilledFeatureDir();
       distroDir.leafName = "hidden";
     },
     initialState: [true, null, "2.0", "2.0", null, null],
   },
 };
 
 
 /**
@@ -342,16 +355,32 @@ const TESTS = {
 }
 
 add_task(function* setup() {
   // Initialise the profile
   startupManager();
   yield promiseShutdownManager();
 })
 
+function* get_directories() {
+  let subdirs = [];
+
+  if (yield OS.File.exists(featureDir.path)) {
+    let iterator = new OS.File.DirectoryIterator(featureDir.path);
+    yield iterator.forEach(entry => {
+      if (entry.isDir) {
+        subdirs.push(entry);
+      }
+    });
+    iterator.close();
+  }
+
+  return subdirs;
+}
+
 function* setup_conditions(setup) {
   do_print("Clearing existing database.");
   Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
   distroDir.leafName = "empty";
   startupManager(false);
   yield promiseShutdownManager();
 
   do_print("Setting up conditions.");
@@ -359,19 +388,38 @@ function* setup_conditions(setup) {
 
   startupManager(false);
 
   // Make sure the initial state is correct
   do_print("Checking initial state.");
   yield check_installed(...setup.initialState);
 }
 
-function* verify_state(finalState) {
+function* verify_state(initialState, finalState = undefined) {
+  let expectedDirs = 0;
+
+  // If the initial state was using the profile set then that directory will
+  // still exist.
+  if (initialState[0])
+    expectedDirs++;
+
+  if (finalState == undefined) {
+    finalState = initialState;
+  }
+  else {
+    // If the new state is using the profile then that directory will exist.
+    if (finalState[0])
+      expectedDirs++;
+  }
+
   do_print("Checking final state.");
 
+  let dirs = yield get_directories();
+  do_check_eq(dirs.length, expectedDirs);
+
   // Bug 1204156: Currently switching to the new state requires a restart
   // yield check_installed(...finalState);
 
   // Check that the new state is active after a restart
   yield promiseRestartManager();
   yield check_installed(...finalState);
 }
 
@@ -391,17 +439,17 @@ function* exec_test(setup, test) {
     }
   }
   catch (e) {
     if (!test.fails) {
       do_throw(e);
     }
   }
 
-  yield verify_state(test.finalState ? test.finalState : setup.initialState);
+  yield verify_state(setup.initialState, test.finalState);
 
   yield promiseShutdownManager();
 }
 
 for (let setup of Object.keys(TEST_CONDITIONS)) {
   for (let test of Object.keys(TESTS)) {
     add_task(function*() {
       do_print("Running test " + setup + " " + test);
@@ -418,17 +466,18 @@ for (let setup of Object.keys(TEST_CONDI
 add_task(function* test_addon_update() {
   yield setup_conditions(TEST_CONDITIONS.blank);
 
   yield update_all_addons(yield build_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
   ]));
 
-  yield verify_state([true, null, "2.0", "2.0", null, null]);
+  yield verify_state(TEST_CONDITIONS.blank.initialState,
+                     [true, null, "2.0", "2.0", null, null]);
 
   yield promiseShutdownManager();
 });
 
 // Disabling app updates should block system add-on updates
 add_task(function* test_app_update_disabled() {
   yield setup_conditions(TEST_CONDITIONS.blank);
 
@@ -485,17 +534,18 @@ add_task(function* test_match_default_re
 
   yield install_system_addons(yield build_xml([
     { id: "system1@tests.mozilla.org", version: "1.0", path: "system1_1.xpi" },
     { id: "system2@tests.mozilla.org", version: "1.0", path: "system2_1.xpi" }
   ]));
 
   // This should revert to the default set instead of installing new versions
   // into an updated set.
-  yield verify_state([false, "1.0", "1.0", null, null, null]);
+  yield verify_state(TEST_CONDITIONS.withBothSets.initialState,
+                     [false, "1.0", "1.0", null, null, null]);
 
   yield promiseShutdownManager();
 });
 
 // Tests that a set that matches the current set works
 add_task(function* test_match_current() {
   yield setup_conditions(TEST_CONDITIONS.withBothSets);
 
@@ -518,12 +568,54 @@ add_task(function* test_no_download() {
   yield setup_conditions(TEST_CONDITIONS.withBothSets);
 
   // The missing file here is unneeded since there is a local version already
   yield install_system_addons(yield build_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "missing.xpi" },
     { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
   ]));
 
-  yield verify_state([true, null, "2.0", null, "1.0", null]);
+  yield verify_state(TEST_CONDITIONS.withBothSets.initialState,
+                     [true, null, "2.0", null, "1.0", null]);
 
   yield promiseShutdownManager();
 });
+
+// Tests that a second update before a restart works
+add_task(function* test_double_update() {
+  yield setup_conditions(TEST_CONDITIONS.withAppSet);
+
+  yield install_system_addons(yield build_xml([
+    { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+    { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+  ]));
+
+  yield install_system_addons(yield build_xml([
+    { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" },
+    { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
+  ]));
+
+  yield verify_state(TEST_CONDITIONS.withAppSet.initialState,
+                     [true, null, null, "2.0", "1.0", null]);
+
+  yield promiseShutdownManager();
+});
+
+// A second update after a restart will delete the original unused set
+add_task(function* test_update_purges() {
+  yield setup_conditions(TEST_CONDITIONS.withBothSets);
+
+  yield install_system_addons(yield build_xml([
+    { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+    { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+  ]));
+
+  yield verify_state(TEST_CONDITIONS.withBothSets.initialState,
+                     [true, null, "2.0", "1.0", null, null]);
+
+  yield install_system_addons(yield build_xml(null));
+
+  let dirs = yield get_directories();
+  do_check_eq(dirs.length, 1);
+
+  yield promiseShutdownManager();
+});
+
--- a/toolkit/themes/shared/alert-common.css
+++ b/toolkit/themes/shared/alert-common.css
@@ -33,16 +33,20 @@
   background-image: linear-gradient(rgba(255,255,255,0.4), rgba(255,255,255,0.3));
 }
 
 .alertTitle {
   font-weight: bold;
   font-size: 110%;
 }
 
+.alertSource {
+  color: gray;
+}
+
 #alertImage {
   max-width: 48px;
   max-height: 48px;
   list-style-image: url(chrome://global/skin/alerts/notification-48.png);
 }
 
 #alertNotification[clickable="true"] {
   cursor: pointer;
--- a/widget/cocoa/OSXNotificationCenter.mm
+++ b/widget/cocoa/OSXNotificationCenter.mm
@@ -12,16 +12,17 @@
 #include "imgLoader.h"
 #import "nsCocoaUtils.h"
 #include "nsContentUtils.h"
 #include "nsObjCExceptions.h"
 #include "nsString.h"
 #include "nsCOMPtr.h"
 #include "nsIObserver.h"
 #include "nsIContentPolicy.h"
+#include "nsAlertsUtils.h"
 #include "imgRequestProxy.h"
 
 using namespace mozilla;
 
 #if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8)
 @protocol NSUserNotificationCenterDelegate
 @end
 static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName";
@@ -235,26 +236,30 @@ OSXNotificationCenter::ShowAlertNotifica
                                              const nsAString & aData,
                                              nsIPrincipal * aPrincipal,
                                              bool aInPrivateBrowsing)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
   Class unClass = NSClassFromString(@"NSUserNotification");
   id<FakeNSUserNotification> notification = [[unClass alloc] init];
-  notification.title = [NSString stringWithCharacters:(const unichar *)aAlertTitle.BeginReading()
-                                               length:aAlertTitle.Length()];
-  notification.informativeText = [NSString stringWithCharacters:(const unichar *)aAlertText.BeginReading()
-                                                         length:aAlertText.Length()];
+  notification.title = nsCocoaUtils::ToNSString(aAlertTitle);
+
+  nsAutoString hostPort;
+  nsAlertsUtils::GetSourceHostPort(aPrincipal, hostPort);
+  if (!hostPort.IsEmpty()) {
+    notification.subtitle = nsCocoaUtils::ToNSString(hostPort);
+  }
+
+  notification.informativeText = nsCocoaUtils::ToNSString(aAlertText);
   notification.soundName = NSUserNotificationDefaultSoundName;
   notification.hasActionButton = NO;
 
   // If this is not an application/extension alert, show additional actions dealing with permissions.
-  if (aPrincipal && !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal)
-      && !aPrincipal->GetIsNullPrincipal()) {
+  if (nsAlertsUtils::IsActionablePrincipal(aPrincipal)) {
     nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
     nsCOMPtr<nsIStringBundle> bundle;
     nsresult rv = sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
     if (NS_SUCCEEDED(rv)) {
       nsXPIDLString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle;
       bundle->GetStringFromName(NS_LITERAL_STRING("closeButton.title").get(),
                                 getter_Copies(closeButtonTitle));
       bundle->GetStringFromName(NS_LITERAL_STRING("actionButton.label").get(),
@@ -271,17 +276,17 @@ OSXNotificationCenter::ShowAlertNotifica
       [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"];
       [(NSObject*)notification setValue:@[
                                           nsCocoaUtils::ToNSString(disableButtonTitle),
                                           nsCocoaUtils::ToNSString(settingsButtonTitle)
                                           ]
                                forKey:@"_alternateActionButtonTitles"];
     }
   }
-  NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()];
+  NSString *alertName = nsCocoaUtils::ToNSString(aAlertName);
   if (!alertName) {
     return NS_ERROR_FAILURE;
   }
   notification.userInfo = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
                                                       forKeys:[NSArray arrayWithObjects:@"name", nil]];
 
   OSXNotificationInfo *osxni = new OSXNotificationInfo(alertName, aAlertListener, aAlertCookie);
 
@@ -331,17 +336,17 @@ OSXNotificationCenter::ShowAlertNotifica
 }
 
 NS_IMETHODIMP
 OSXNotificationCenter::CloseAlert(const nsAString& aAlertName,
                                   nsIPrincipal* aPrincipal)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
-  NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()];
+  NSString *alertName = nsCocoaUtils::ToNSString(aAlertName);
   CloseAlertCocoaString(alertName);
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 void
 OSXNotificationCenter::CloseAlertCocoaString(NSString *aAlertName)