merge fx-team to mozilla-central
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 11 Dec 2013 10:15:44 +0100
changeset 175855 12ea03a702437fb9421c94756436d867f809bf79
parent 175840 d14c74014d9513c4ba1555c7917e3dbbc854087f (current diff)
parent 175854 92d8dc924a05fc87727f624878779e4fbbb7eb9a (diff)
child 175895 76551fba71214339f7bb63347bdfc5f8ae374202
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
first release with
nightly linux32
12ea03a70243 / 29.0a1 / 20131211030206 / files
nightly linux64
12ea03a70243 / 29.0a1 / 20131211030206 / files
nightly mac
12ea03a70243 / 29.0a1 / 20131211030206 / files
nightly win32
12ea03a70243 / 29.0a1 / 20131211030206 / files
nightly win64
12ea03a70243 / 29.0a1 / 20131211030206 / 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
browser/devtools/styleeditor/StyleEditorDebuggee.jsm
browser/devtools/styleeditor/StyleEditorPanel.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1150,17 +1150,17 @@ pref("devtools.tilt.outro_transition", t
 
 // The maximum number of recently-opened files stored.
 // Setting this preference to 0 will not clear any recent files, but rather hide
 // the 'Open Recent'-menu.
 pref("devtools.scratchpad.recentFilesMax", 10);
 
 // Enable the Style Editor.
 pref("devtools.styleeditor.enabled", true);
-pref("devtools.styleeditor.transitions", true);
+pref("devtools.styleeditor.source-maps-enabled", false);
 
 // Enable the Shader Editor.
 pref("devtools.shadereditor.enabled", false);
 
 // Enable tools for Chrome development.
 pref("devtools.chrome.enabled", false);
 
 // Default theme ("dark" or "light")
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -21,17 +21,17 @@ loader.lazyGetter(this, "osString", () =
 
 let events = require("sdk/system/events");
 
 // Panels
 loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel);
 loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel);
 loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel);
-loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
+loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel);
 loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
 loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel"));
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
 
 // Strings
 const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
--- a/browser/devtools/styleeditor/Makefile.in
+++ b/browser/devtools/styleeditor/Makefile.in
@@ -1,8 +1,10 @@
 # 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 $(topsrcdir)/config/rules.mk
 
 libs::
-	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
+	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools/
+	$(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/styleeditor
+
deleted file mode 100644
--- a/browser/devtools/styleeditor/StyleEditorDebuggee.jsm
+++ /dev/null
@@ -1,346 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["StyleEditorDebuggee", "StyleSheet"];
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/devtools/shared/event-emitter.js");
-
-XPCOMUtils.defineLazyModuleGetter(this, "promise",
-    "resource://gre/modules/commonjs/sdk/core/promise.js", "Promise");
-
-/**
- * A StyleEditorDebuggee represents the document the style editor is debugging.
- * It maintains a list of StyleSheet objects that represent the stylesheets in
- * the target's document. It wraps remote debugging protocol comunications.
- *
- * It emits these events:
- *   'document-load': debuggee's document is loaded, style sheets are argument
- *   'stylesheets-cleared': The debuggee's stylesheets have been reset (e.g. the
- *                          page navigated)
- *
- * @param {Target} target
- *         The target the debuggee is listening to
- */
-let StyleEditorDebuggee = function(target) {
-  EventEmitter.decorate(this);
-
-  this.styleSheets = [];
-
-  this.clear = this.clear.bind(this);
-  this._onNewDocument = this._onNewDocument.bind(this);
-  this._onDocumentLoad = this._onDocumentLoad.bind(this);
-
-  this._target = target;
-  this._actor = this.target.form.styleEditorActor;
-
-  this.client.addListener("documentLoad", this._onDocumentLoad);
-  this._target.on("navigate", this._onNewDocument);
-
-  this._onNewDocument();
-}
-
-StyleEditorDebuggee.prototype = {
-  /**
-   * list of StyleSheet objects for this target
-   */
-  styleSheets: null,
-
-  /**
-   * baseURIObject for the current document
-   */
-  baseURI: null,
-
-  /**
-   * The target we're debugging
-   */
-  get target() {
-    return this._target;
-  },
-
-  /**
-   * Client for communicating with server with remote debug protocol.
-   */
-  get client() {
-    return this._target.client;
-  },
-
-  /**
-   * Get the StyleSheet object with the given href.
-   *
-   * @param  {string} href
-   *         Url of the stylesheet to find
-   * @return {StyleSheet}
-   *         StyleSheet with the matching href
-   */
-  styleSheetFromHref: function(href) {
-    for (let sheet of this.styleSheets) {
-      if (sheet.href == href) {
-        return sheet;
-      }
-    }
-    return null;
-  },
-
-  /**
-   * Clear stylesheets and state.
-   */
-  clear: function() {
-    this.baseURI = null;
-    this.clearStyleSheets();
-  },
-
-  /**
-   * Clear stylesheets.
-   */
-  clearStyleSheets: function() {
-    for (let stylesheet of this.styleSheets) {
-      stylesheet.destroy();
-    }
-    this.styleSheets = [];
-    this.emit("stylesheets-cleared");
-  },
-
-  /**
-   * Called when target is created or has navigated.
-   * Clear previous sheets and request new document's
-   */
-  _onNewDocument: function() {
-    this.clear();
-
-    this._getBaseURI();
-
-    let message = { type: "newDocument" };
-    this._sendRequest(message);
-  },
-
-  /**
-   * request baseURIObject information from the document
-   */
-  _getBaseURI: function() {
-    let message = { type: "getBaseURI" };
-    this._sendRequest(message, (response) => {
-      this.baseURI = Services.io.newURI(response.baseURI, null, null);
-    });
-  },
-
-  /**
-   * Handler for document load, forward event with
-   * all the stylesheets available on load.
-   *
-   * @param  {string} type
-   *         Event type
-   * @param  {object} request
-   *         Object with 'styleSheets' array of actor forms
-   */
-  _onDocumentLoad: function(type, request) {
-    if (this.styleSheets.length > 0) {
-      this.clearStyleSheets();
-    }
-    let sheets = [];
-    for (let form of request.styleSheets) {
-      let sheet = this._addStyleSheet(form);
-      sheets.push(sheet);
-    }
-    this.emit("document-load", sheets);
-  },
-
-  /**
-   * Create a new StyleSheet object from the form
-   * and add to our stylesheet list.
-   *
-   * @param {object} form
-   *        Initial properties of the stylesheet
-   */
-  _addStyleSheet: function(form) {
-    let sheet = new StyleSheet(form, this);
-    this.styleSheets.push(sheet);
-    return sheet;
-  },
-
-  /**
-   * Create a new stylesheet with the given text
-   * and attach it to the document.
-   *
-   * @param {string} text
-   *        Initial text of the stylesheet
-   * @param {function} callback
-   *        Function to call when the stylesheet has been added to the document
-   */
-  createStyleSheet: function(text, callback) {
-    let message = { type: "newStyleSheet", text: text };
-    this._sendRequest(message, (response) => {
-      let sheet = this._addStyleSheet(response.styleSheet);
-      callback(sheet);
-    });
-  },
-
-  /**
-   * Send a request to our actor on the server
-   *
-   * @param {object} message
-   *        Message to send to the actor
-   * @param {function} callback
-   *        Function to call with reponse from actor
-   */
-  _sendRequest: function(message, callback) {
-    message.to = this._actor;
-    this.client.request(message, callback);
-  },
-
-  /**
-   * Clean up and remove listeners
-   */
-  destroy: function() {
-    this.clear();
-
-    this._target.off("navigate", this._onNewDocument);
-  }
-}
-
-/**
- * A StyleSheet object represents a stylesheet on the debuggee. It wraps
- * communication with a complimentary StyleSheetActor on the server.
- *
- * It emits these events:
- *   'source-load' - The full text source of the stylesheet has been fetched
- *   'property-change' - Any property (e.g 'disabled') has changed
- *   'style-applied' - A change has been applied to the live stylesheet on the server
- *   'error' - An error occured when loading or saving stylesheet
- *
- * @param {object} form
- *        Initial properties of the stylesheet
- * @param {StyleEditorDebuggee} debuggee
- *        Owner of the stylesheet
- */
-let StyleSheet = function(form, debuggee) {
-  EventEmitter.decorate(this);
-
-  this.debuggee = debuggee;
-  this._client = debuggee.client;
-  this._actor = form.actor;
-
-  this._onSourceLoad = this._onSourceLoad.bind(this);
-  this._onPropertyChange = this._onPropertyChange.bind(this);
-  this._onStyleApplied = this._onStyleApplied.bind(this);
-
-  this._client.addListener("sourceLoad", this._onSourceLoad);
-  this._client.addListener("propertyChange", this._onPropertyChange);
-  this._client.addListener("styleApplied", this._onStyleApplied);
-
-  // Backwards compatibility
-  this._client.addListener("sourceLoad-" + this._actor, this._onSourceLoad);
-  this._client.addListener("propertyChange-" + this._actor, this._onPropertyChange);
-  this._client.addListener("styleApplied-" + this._actor, this._onStyleApplied);
-
-
-  // set initial property values
-  for (let attr in form) {
-    this[attr] = form[attr];
-  }
-}
-
-StyleSheet.prototype = {
-  /**
-   * Toggle the disabled attribute of the stylesheet
-   */
-  toggleDisabled: function() {
-    let message = { type: "toggleDisabled" };
-    this._sendRequest(message);
-  },
-
-  /**
-   * Request that the source of the stylesheet be fetched.
-   * 'source-load' event will be fired when it's been fetched.
-   */
-  fetchSource: function() {
-    let message = { type: "fetchSource" };
-    this._sendRequest(message);
-  },
-
-  /**
-   * Update the stylesheet in place with the given full source.
-   *
-   * @param {string} sheetText
-   *        Full text to update the stylesheet with
-   */
-  update: function(sheetText) {
-    let message = { type: "update", text: sheetText, transition: true };
-    this._sendRequest(message);
-  },
-
-  /**
-   * Handle source load event from the client.
-   *
-   * @param {string} type
-   *        Event type
-   * @param {object} request
-   *        Event details
-   */
-  _onSourceLoad: function(type, request) {
-    if (request.from == this._actor) {
-      if (request.error) {
-        return this.emit("error", request.error);
-      }
-      this.emit("source-load", request.source);
-    }
-  },
-
-  /**
-   * Handle a property change on the stylesheet
-   *
-   * @param {string} type
-   *        Event type
-   * @param {object} request
-   *        Event details
-   */
-  _onPropertyChange: function(type, request) {
-    if (request.from == this._actor) {
-      this[request.property] = request.value;
-      this.emit("property-change", request.property);
-    }
-  },
-
-  /**
-   * Handle event when update has been successfully applied and propogate it.
-   */
-  _onStyleApplied: function(type, request) {
-    if (request.from == this._actor) {
-      this.emit("style-applied");
-    }
-  },
-
-  /**
-   * Send a request to our actor on the server
-   *
-   * @param {object} message
-   *        Message to send to the actor
-   * @param {function} callback
-   *        Function to call with reponse from actor
-   */
-  _sendRequest: function(message, callback) {
-    message.to = this._actor;
-    this._client.request(message, callback);
-  },
-
-  /**
-   * Clean up and remove event listeners
-   */
-  destroy: function() {
-    this._client.removeListener("sourceLoad", this._onSourceLoad);
-    this._client.removeListener("propertyChange", this._onPropertyChange);
-    this._client.removeListener("styleApplied", this._onStyleApplied);
-
-    this._client.removeListener("sourceLoad-" + this._actor, this._onSourceLoad);
-    this._client.removeListener("propertyChange-" + this._actor, this._onPropertyChange);
-    this._client.removeListener("styleApplied-" + this._actor, this._onStyleApplied);
-  }
-}
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -20,50 +20,59 @@ Cu.import("resource:///modules/devtools/
 Cu.import("resource:///modules/devtools/SplitView.jsm");
 Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm");
 
 
 const LOAD_ERROR = "error-load";
 
 const STYLE_EDITOR_TEMPLATE = "stylesheet";
 
+const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled";
+
 /**
  * StyleEditorUI is controls and builds the UI of the Style Editor, including
  * maintaining a list of editors for each stylesheet on a debuggee.
  *
  * Emits events:
  *   'editor-added': A new editor was added to the UI
  *   'editor-selected': An editor was selected
  *   'error': An error occured
  *
- * @param {StyleEditorDebuggee} debuggee
- *        Debuggee of whose stylesheets should be shown in the UI
+ * @param {StyleEditorFront} debuggee
+ *        Client-side front for interacting with the page's stylesheets
+ * @param {Target} target
+ *        Interface for the page we're debugging
  * @param {Document} panelDoc
  *        Document of the toolbox panel to populate UI in.
  */
-function StyleEditorUI(debuggee, panelDoc) {
+function StyleEditorUI(debuggee, target, panelDoc) {
   EventEmitter.decorate(this);
 
   this._debuggee = debuggee;
+  this._target = target;
   this._panelDoc = panelDoc;
   this._window = this._panelDoc.defaultView;
   this._root = this._panelDoc.getElementById("style-editor-chrome");
 
   this.editors = [];
   this.selectedEditor = null;
 
   this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
-  this._onStyleSheetsCleared = this._onStyleSheetsCleared.bind(this);
-  this._onDocumentLoad = this._onDocumentLoad.bind(this);
+  this._onNewDocument = this._onNewDocument.bind(this);
+  this._clear = this._clear.bind(this);
   this._onError = this._onError.bind(this);
 
-  debuggee.on("document-load", this._onDocumentLoad);
-  debuggee.on("stylesheets-cleared", this._onStyleSheetsCleared);
+  this.createUI();
 
-  this.createUI();
+  this._debuggee.getStyleSheets().then((styleSheets) => {
+    this._resetStyleSheetList(styleSheets);
+
+    this._target.on("will-navigate", this._clear);
+    this._target.on("navigate", this._onNewDocument);
+  })
 }
 
 StyleEditorUI.prototype = {
   /**
    * Get whether any of the editors have unsaved changes.
    *
    * @return boolean
    */
@@ -96,25 +105,131 @@ StyleEditorUI.prototype = {
    * Build the initial UI and wire buttons with event handlers.
    */
   createUI: function() {
     let viewRoot = this._root.parentNode.querySelector(".splitview-root");
 
     this._view = new SplitView(viewRoot);
 
     wire(this._view.rootElement, ".style-editor-newButton", function onNew() {
-      this._debuggee.createStyleSheet(null, this._onStyleSheetCreated);
+      this._debuggee.addStyleSheet(null).then(this._onStyleSheetCreated);
     }.bind(this));
 
     wire(this._view.rootElement, ".style-editor-importButton", function onImport() {
       this._importFromFile(this._mockImportFile || null, this._window);
     }.bind(this));
   },
 
   /**
+   * Refresh editors to reflect the stylesheets in the document.
+   *
+   * @param {string} event
+   *        Event name
+   * @param {StyleSheet} styleSheet
+   *        StyleSheet object for new sheet
+   */
+  _onNewDocument: function() {
+    this._debuggee.getStyleSheets().then((styleSheets) => {
+      this._resetStyleSheetList(styleSheets);
+    })
+  },
+
+  /**
+   * Remove all editors and add loading indicator.
+   */
+  _clear: function() {
+    // remember selected sheet and line number for next load
+    if (this.selectedEditor && this.selectedEditor.sourceEditor) {
+      let href = this.selectedEditor.styleSheet.href;
+      let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
+
+      this._styleSheetToSelect = {
+        href: href,
+        line: line,
+        col: ch
+      };
+    }
+
+    this._clearStyleSheetEditors();
+    this._view.removeAll();
+
+    this.selectedEditor = null;
+
+    this._root.classList.add("loading");
+  },
+
+  /**
+   * Add editors for all the given stylesheets to the UI.
+   *
+   * @param  {array} styleSheets
+   *         Array of StyleSheetFront
+   */
+  _resetStyleSheetList: function(styleSheets) {
+    this._clear();
+
+    for (let sheet of styleSheets) {
+      this._addStyleSheet(sheet);
+    }
+
+    this._root.classList.remove("loading");
+
+    this.emit("stylesheets-reset");
+  },
+
+  /**
+   * Add an editor for this stylesheet. Add editors for its original sources
+   * instead (e.g. Sass sources), if applicable.
+   *
+   * @param  {StyleSheetFront} styleSheet
+   *         Style sheet to add to style editor
+   */
+  _addStyleSheet: function(styleSheet) {
+    let editor = this._addStyleSheetEditor(styleSheet);
+
+    if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+      return;
+    }
+
+    styleSheet.getOriginalSources().then((sources) => {
+      if (sources && sources.length) {
+        this._removeStyleSheetEditor(editor);
+        sources.forEach((source) => {
+          // set so the first sheet will be selected, even if it's a source
+          source.styleSheetIndex = styleSheet.styleSheetIndex;
+
+          this._addStyleSheetEditor(source);
+        });
+      }
+    });
+  },
+
+  /**
+   * Add a new editor to the UI for a source.
+   *
+   * @param {StyleSheet}  styleSheet
+   *        Object representing stylesheet
+   * @param {nsIfile}  file
+   *         Optional file object that sheet was imported from
+   * @param {Boolean} isNew
+   *         Optional if stylesheet is a new sheet created by user
+   */
+  _addStyleSheetEditor: function(styleSheet, file, isNew) {
+    let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew);
+
+    editor.on("property-change", this._summaryChange.bind(this, editor));
+    editor.on("style-applied", this._summaryChange.bind(this, editor));
+    editor.on("error", this._onError);
+
+    this.editors.push(editor);
+
+    editor.fetchSource(this._sourceLoaded.bind(this, editor));
+    return editor;
+  },
+
+  /**
    * Import a style sheet from file and asynchronously create a
    * new stylesheet on the debuggee for it.
    *
    * @param {mixed} file
    *        Optional nsIFile or filename string.
    *        If not set a file picker will be shown.
    * @param {nsIWindow} parentWindow
    *        Optional parent window for the file picker.
@@ -129,132 +244,86 @@ StyleEditorUI.prototype = {
       NetUtil.asyncFetch(file, (stream, status) => {
         if (!Components.isSuccessCode(status)) {
           this.emit("error", LOAD_ERROR);
           return;
         }
         let source = NetUtil.readInputStreamToString(stream, stream.available());
         stream.close();
 
-        this._debuggee.createStyleSheet(source, (styleSheet) => {
+        this._debuggee.addStyleSheet(source).then((styleSheet) => {
           this._onStyleSheetCreated(styleSheet, file);
         });
       });
 
     }.bind(this);
 
     showFilePicker(file, false, parentWindow, onFileSelected);
   },
 
-  /**
-   * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
-   */
-  _onStyleSheetsCleared: function() {
-    // remember selected sheet and line number for next load
-    if (this.selectedEditor && this.selectedEditor.sourceEditor) {
-      let href = this.selectedEditor.styleSheet.href;
-      let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
-      this.selectStyleSheet(href, line, ch);
-    }
-
-    this._clearStyleSheetEditors();
-    this._view.removeAll();
-
-    this.selectedEditor = null;
-
-    this._root.classList.add("loading");
-  },
 
   /**
    * When a new or imported stylesheet has been added to the document.
    * Add an editor for it.
    */
   _onStyleSheetCreated: function(styleSheet, file) {
     this._addStyleSheetEditor(styleSheet, file, true);
   },
 
   /**
-   * Handler for debuggee's 'document-load' event. Add editors
-   * for all style sheets in the document
-   *
-   * @param {string} event
-   *        Event name
-   * @param {StyleSheet} styleSheet
-   *        StyleSheet object for new sheet
-   */
-  _onDocumentLoad: function(event, styleSheets) {
-    if (this._styleSheetToSelect) {
-      // if selected stylesheet from previous load isn't here,
-      // just set first stylesheet to be selected instead
-      let selectedExists = styleSheets.some((sheet) => {
-        return this._styleSheetToSelect.href == sheet.href;
-      })
-      if (!selectedExists) {
-        this._styleSheetToSelect = null;
-      }
-    }
-    for (let sheet of styleSheets) {
-      this._addStyleSheetEditor(sheet);
-    }
-
-    this._root.classList.remove("loading");
-
-    this.emit("document-load");
-  },
-
-  /**
    * Forward any error from a stylesheet.
    *
    * @param  {string} event
    *         Event name
    * @param  {string} errorCode
    *         Code represeting type of error
+   * @param  {string} message
+   *         The full error message
    */
-  _onError: function(event, errorCode) {
-    this.emit("error", errorCode);
+  _onError: function(event, errorCode, message) {
+    this.emit("error", errorCode, message);
   },
 
   /**
-   * Add a new editor to the UI for a stylesheet.
+   * Remove a particular stylesheet editor from the UI
    *
-   * @param {StyleSheet}  styleSheet
-   *        Object representing stylesheet
-   * @param {nsIfile}  file
-   *         Optional file object that sheet was imported from
-   * @param {Boolean} isNew
-   *         Optional if stylesheet is a new sheet created by user
+   * @param {StyleSheetEditor}  editor
+   *        The editor to remove.
    */
-  _addStyleSheetEditor: function(styleSheet, file, isNew) {
-    let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew);
+  _removeStyleSheetEditor: function(editor) {
+    if (editor.summary) {
+      this._view.removeItem(editor.summary);
+    }
+    else {
+      let self = this;
+      this.on("editor-added", function onAdd(event, added) {
+        if (editor == added) {
+          self.off("editor-added", onAdd);
+          self._view.removeItem(editor.summary);
+        }
+      })
+    }
 
-    editor.once("source-load", this._sourceLoaded.bind(this, editor));
-    editor.on("property-change", this._summaryChange.bind(this, editor));
-    editor.on("style-applied", this._summaryChange.bind(this, editor));
-    editor.on("error", this._onError);
-
-    this.editors.push(editor);
-
-    // Queue editor loading. This helps responsivity during loading when
-    // there are many heavy stylesheets.
-    this._window.setTimeout(editor.fetchSource.bind(editor), 0);
+    editor.destroy();
+    this.editors.splice(this.editors.indexOf(editor), 1);
   },
 
   /**
    * Clear all the editors from the UI.
    */
   _clearStyleSheetEditors: function() {
     for (let editor of this.editors) {
       editor.destroy();
     }
     this.editors = [];
   },
 
   /**
-   * Handler for an StyleSheetEditor's 'source-load' event.
-   * Create a summary UI for the editor.
+   * Called when a StyleSheetEditor's source has been fetched. Create a
+   * summary UI for the editor.
    *
    * @param  {StyleSheetEditor} editor
    *         Editor to create UI for.
    */
   _sourceLoaded: function(editor) {
     // add new sidebar item and editor to the UI
     this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
       data: {
@@ -305,29 +374,27 @@ StyleEditorUI.prototype = {
         }
 
         if (this._styleSheetToSelect
             && this._styleSheetToSelect.href == editor.styleSheet.href) {
           this.switchToSelectedSheet();
         }
 
         // If this is the first stylesheet, select it
-        if (this.selectedStyleSheetIndex == -1
-            && !this._styleSheetToSelect
+        if (!this.selectedEditor
             && editor.styleSheet.styleSheetIndex == 0) {
           this._selectEditor(editor);
         }
 
         this.emit("editor-added", editor);
       }.bind(this),
 
       onShow: function(summary, details, data) {
         let editor = data.editor;
         this.selectedEditor = editor;
-        this._styleSheetToSelect = null;
 
         if (!editor.sourceEditor) {
           // only initialize source editor when we switch to this view
           let inputElement = details.querySelector(".stylesheet-editor-input");
           editor.load(inputElement);
         }
         editor.onShow();
 
@@ -340,17 +407,18 @@ StyleEditorUI.prototype = {
    * Switch to the editor that has been marked to be selected.
    */
   switchToSelectedSheet: function() {
     let sheet = this._styleSheetToSelect;
 
     for each (let editor in this.editors) {
       if (editor.styleSheet.href == sheet.href) {
         this._selectEditor(editor, sheet.line, sheet.col);
-        break;
+        this._styleSheetToSelect = null;
+        return;
       }
     }
   },
 
   /**
    * Select an editor in the UI.
    *
    * @param  {StyleSheetEditor} editor
@@ -362,17 +430,16 @@ StyleEditorUI.prototype = {
    */
   _selectEditor: function(editor, line, col) {
     line = line || 0;
     col = col || 0;
 
     editor.getSourceEditor().then(() => {
       editor.sourceEditor.setCursor({line: line, ch: col});
     });
-
     this._view.activeSummary = editor.summary;
   },
 
   /**
    * selects a stylesheet and optionally moves the cursor to a selected line
    *
    * @param {string} [href]
    *        Href of stylesheet that should be selected. If a stylesheet is not passed
@@ -461,13 +528,10 @@ StyleEditorUI.prototype = {
     text(summary, ".stylesheet-title", editor.styleSheet.title || "");
     text(summary, ".stylesheet-rule-count",
       PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount));
     text(summary, ".stylesheet-error-message", editor.errorMessage);
   },
 
   destroy: function() {
     this._clearStyleSheetEditors();
-
-    this._debuggee.off("document-load", this._onDocumentLoad);
-    this._debuggee.off("stylesheets-cleared", this._onStyleSheetsCleared);
   }
 }
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -9,40 +9,42 @@ this.EXPORTED_SYMBOLS = ["StyleSheetEdit
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 const Editor  = require("devtools/sourceeditor/editor");
 const promise = require("sdk/core/promise");
+const {CssLogic} = require("devtools/styleinspector/css-logic");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
 
+const LOAD_ERROR = "error-load";
 const SAVE_ERROR = "error-save";
 
 // max update frequency in ms (avoid potential typing lag and/or flicker)
 // @see StyleEditor.updateStylesheet
 const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
 
 /**
  * StyleSheetEditor controls the editor linked to a particular StyleSheet
  * object.
  *
  * Emits events:
- *   'source-load': The source of the stylesheet has been fetched
  *   'property-change': A property on the underlying stylesheet has changed
  *   'source-editor-load': The source editor for this editor has been loaded
  *   'error': An error has occured
  *
- * @param {StyleSheet}  styleSheet
+ * @param {StyleSheet|OriginalSource}  styleSheet
+ *        Stylesheet or original source to show
  * @param {DOMWindow}  win
  *        panel window for style editor
  * @param {nsIFile}  file
  *        Optional file that the sheet was imported from
  * @param {boolean} isNew
  *        Optional whether the sheet was created by the user
  */
 function StyleSheetEditor(styleSheet, win, file, isNew) {
@@ -52,142 +54,120 @@ function StyleSheetEditor(styleSheet, wi
   this._inputElement = null;
   this._sourceEditor = null;
   this._window = win;
   this._isNew = isNew;
   this.savedFile = file;
 
   this.errorMessage = null;
 
+  let readOnly = false;
+  if (styleSheet.isOriginalSource) {
+    // live-preview won't work with sources that need compilation
+    readOnly = true;
+  }
+
   this._state = {   // state to use when inputElement attaches
     text: "",
     selection: {
       start: {line: 0, ch: 0},
       end: {line: 0, ch: 0}
     },
-    readOnly: false,
+    readOnly: readOnly,
     topIndex: 0,              // the first visible line
   };
 
   this._styleSheetFilePath = null;
   if (styleSheet.href &&
       Services.io.extractScheme(this.styleSheet.href) == "file") {
     this._styleSheetFilePath = this.styleSheet.href;
   }
 
-  this._onSourceLoad = this._onSourceLoad.bind(this);
   this._onPropertyChange = this._onPropertyChange.bind(this);
   this._onError = this._onError.bind(this);
 
   this._focusOnSourceEditorReady = false;
 
-  this.styleSheet.once("source-load", this._onSourceLoad);
   this.styleSheet.on("property-change", this._onPropertyChange);
   this.styleSheet.on("error", this._onError);
 }
 
 StyleSheetEditor.prototype = {
   /**
-   * This editor's source editor
-   */
-  get sourceEditor() {
-    return this._sourceEditor;
-  },
-
-  /**
    * Whether there are unsaved changes in the editor
    */
   get unsaved() {
-    return this._sourceEditor && !this._sourceEditor.isClean();
+    return this.sourceEditor && !this.sourceEditor.isClean();
   },
 
   /**
    * Whether the editor is for a stylesheet created by the user
    * through the style editor UI.
    */
   get isNew() {
     return this._isNew;
   },
 
   /**
    * Get a user-friendly name for the style sheet.
    *
    * @return string
    */
   get friendlyName() {
-    if (this.savedFile) { // reuse the saved filename if any
+    if (this.savedFile) {
       return this.savedFile.leafName;
     }
 
     if (this._isNew) {
-      let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs
+      let index = this.styleSheet.styleSheetIndex + 1;
       return _("newStyleSheet", index);
     }
 
     if (!this.styleSheet.href) {
-      let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs
+      let index = this.styleSheet.styleSheetIndex + 1;
       return _("inlineStyleSheet", index);
     }
 
     if (!this._friendlyName) {
       let sheetURI = this.styleSheet.href;
-      let contentURI = this.styleSheet.debuggee.baseURI;
-      let contentURIScheme = contentURI.scheme;
-      let contentURILeafIndex = contentURI.specIgnoringRef.lastIndexOf("/");
-      contentURI = contentURI.specIgnoringRef;
-
-      // get content base URI without leaf name (if any)
-      if (contentURILeafIndex > contentURIScheme.length) {
-        contentURI = contentURI.substring(0, contentURILeafIndex + 1);
-      }
-
-      // avoid verbose repetition of absolute URI when the style sheet URI
-      // is relative to the content URI
-      this._friendlyName = (sheetURI.indexOf(contentURI) == 0)
-                           ? sheetURI.substring(contentURI.length)
-                           : sheetURI;
+      this._friendlyName = CssLogic.shortSource({ href: sheetURI });
       try {
         this._friendlyName = decodeURI(this._friendlyName);
       } catch (ex) {
       }
     }
     return this._friendlyName;
   },
 
   /**
    * Start fetching the full text source for this editor's sheet.
    */
-  fetchSource: function() {
-    this.styleSheet.fetchSource();
-  },
+  fetchSource: function(callback) {
+    this.styleSheet.getText().then((longStr) => {
+      longStr.string().then((source) => {
+        this._state.text = prettifyCSS(source);
+        this.sourceLoaded = true;
 
-  /**
-   * Handle source fetched event. Forward source-load event.
-   *
-   * @param  {string} event
-   *         Event type
-   * @param  {string} source
-   *         Full-text source of the stylesheet
-   */
-  _onSourceLoad: function(event, source) {
-    this._state.text = prettifyCSS(source);
-    this.sourceLoaded = true;
-    this.emit("source-load");
+        callback(source);
+      });
+    }, e => {
+      this.emit("error", LOAD_ERROR, this.styleSheet.href);
+    })
   },
 
   /**
    * Forward property-change event from stylesheet.
    *
    * @param  {string} event
    *         Event type
    * @param  {string} property
    *         Property that has changed on sheet
    */
-  _onPropertyChange: function(event, property) {
-    this.emit("property-change", property);
+  _onPropertyChange: function(property, value) {
+    this.emit("property-change", property, value);
   },
 
   /**
    * Forward error event from stylesheet.
    *
    * @param  {string} event
    *         Event type
    * @param  {string} errorCode
@@ -215,17 +195,17 @@ StyleSheetEditor.prototype = {
     };
     let sourceEditor = new Editor(config);
 
     sourceEditor.appendTo(inputElement).then(() => {
       sourceEditor.on("change", () => {
         this.updateStyleSheet();
       });
 
-      this._sourceEditor = sourceEditor;
+      this.sourceEditor = sourceEditor;
 
       if (this._focusOnSourceEditorReady) {
         this._focusOnSourceEditorReady = false;
         sourceEditor.focus();
       }
 
       sourceEditor.setFirstVisibleLine(this._state.topIndex);
       sourceEditor.setSelection(this._state.selection.start,
@@ -315,17 +295,17 @@ StyleSheetEditor.prototype = {
                              // (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.styleSheet.update(this._state.text);
+    this.styleSheet.update(this._state.text, true);
   },
 
   /**
    * Save the editor contents into a file and set savedFile property.
    * A file picker UI will open if file is not set and editor is not headless.
    *
    * @param mixed file
    *        Optional nsIFile or string representing the filename to save in the
@@ -369,16 +349,18 @@ StyleSheetEditor.prototype = {
         // remember filename for next save if any
         this._friendlyName = null;
         this.savedFile = returnFile;
 
         if (callback) {
           callback(returnFile);
         }
         this.sourceEditor.setClean();
+
+        this.emit("property-change");
       }.bind(this));
     };
 
     showFilePicker(file || this._styleSheetFilePath, true, this._window, onFile);
   },
 
   /**
     * Retrieve custom key bindings objects as expected by Editor.
@@ -399,17 +381,16 @@ StyleSheetEditor.prototype = {
 
     return bindings;
   },
 
   /**
    * Clean up for this editor.
    */
   destroy: function() {
-    this.styleSheet.off("source-load", this._onSourceLoad);
     this.styleSheet.off("property-change", this._onPropertyChange);
     this.styleSheet.off("error", this._onError);
   }
 }
 
 
 const TAB_CHARS = "\t";
 
rename from browser/devtools/styleeditor/StyleEditorPanel.jsm
rename to browser/devtools/styleeditor/styleeditor-panel.js
--- a/browser/devtools/styleeditor/StyleEditorPanel.jsm
+++ b/browser/devtools/styleeditor/styleeditor-panel.js
@@ -1,42 +1,42 @@
 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-this.EXPORTED_SYMBOLS = ["StyleEditorPanel"];
+const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
-Cu.import("resource:///modules/devtools/shared/event-emitter.js");
-Cu.import("resource:///modules/devtools/StyleEditorDebuggee.jsm");
+
+let promise = require("sdk/core/promise");
+let EventEmitter = require("devtools/shared/event-emitter");
+
 Cu.import("resource:///modules/devtools/StyleEditorUI.jsm");
 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
 
-
-XPCOMUtils.defineLazyModuleGetter(this, "StyleEditorChrome",
-                        "resource:///modules/devtools/StyleEditorChrome.jsm");
+loader.lazyGetter(this, "StyleSheetsFront",
+  () => require("devtools/server/actors/styleeditor").StyleSheetsFront);
 
 this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) {
   EventEmitter.decorate(this);
 
   this._toolbox = toolbox;
   this._target = toolbox.target;
   this._panelWin = panelWin;
   this._panelDoc = panelWin.document;
 
   this.destroy = this.destroy.bind(this);
   this._showError = this._showError.bind(this);
 }
 
+exports.StyleEditorPanel = StyleEditorPanel;
+
 StyleEditorPanel.prototype = {
   get target() this._toolbox.target,
 
   get panelWindow() this._panelWin,
 
   /**
    * open is effectively an asynchronous constructor
    */
@@ -49,19 +49,19 @@ StyleEditorPanel.prototype = {
       targetPromise = this.target.makeRemote();
     } else {
       targetPromise = promise.resolve(this.target);
     }
 
     targetPromise.then(() => {
       this.target.on("close", this.destroy);
 
-      this._debuggee = new StyleEditorDebuggee(this.target);
+      this._debuggee = StyleSheetsFront(this.target.client, this.target.form);
 
-      this.UI = new StyleEditorUI(this._debuggee, this._panelDoc);
+      this.UI = new StyleEditorUI(this._debuggee, this.target, this._panelDoc);
       this.UI.on("error", this._showError);
 
       this.isReady = true;
       deferred.resolve(this);
     })
 
     return deferred.promise;
   },
@@ -94,17 +94,16 @@ StyleEditorPanel.prototype = {
    *        Line number to jump to after selecting. One-indexed
    * @param {number} col
    *        Column number to jump to after selecting. One-indexed
    */
   selectStyleSheet: function(href, line, col) {
     if (!this._debuggee || !this.UI) {
       return;
     }
-    let stylesheet = this._debuggee.styleSheetFromHref(href);
     this.UI.selectStyleSheet(href, line - 1, col ? col - 1 : 0);
   },
 
   /**
    * Destroy the style editor.
    */
   destroy: function() {
     if (!this._destroyed) {
--- a/browser/devtools/styleeditor/test/browser.ini
+++ b/browser/devtools/styleeditor/test/browser.ini
@@ -14,16 +14,20 @@ support-files =
   resources_inpage.jsi
   resources_inpage1.css
   resources_inpage2.css
   simple.css
   simple.css.gz
   simple.css.gz^headers^
   simple.gz.html
   simple.html
+  sourcemaps.css
+  sourcemaps.css.map
+  sourcemaps.scss
+  sourcemaps.html
   test_private.css
   test_private.html
 
 [browser_styleeditor_bug_740541_iframes.js]
 [browser_styleeditor_bug_851132_middle_click.js]
 [browser_styleeditor_bug_870339.js]
 [browser_styleeditor_cmd_edit.js]
 [browser_styleeditor_enabled.js]
@@ -37,8 +41,9 @@ support-files =
 [browser_styleeditor_pretty.js]
 # Disabled because of intermittent failures - See Bug 942473
 skip-if = true
 [browser_styleeditor_private_perwindowpb.js]
 [browser_styleeditor_reload.js]
 [browser_styleeditor_sv_keynav.js]
 [browser_styleeditor_sv_resize.js]
 [browser_styleeditor_selectstylesheet.js]
+[browser_styleeditor_sourcemaps.js]
--- a/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
@@ -8,17 +8,18 @@ let gUI;
 function test() {
   waitForExplicitFinish();
 
   let count = 0;
   addTabAndOpenStyleEditor(function(panel) {
     gUI = panel.UI;
     gUI.on("editor-added", function(event, editor) {
       count++;
-      if (count == 2) {
+      if (count == 4) {
+        info("all editors added");
         runTests();
       }
     })
   });
 
   content.location = TESTCASE_URI;
 }
 
@@ -30,26 +31,30 @@ function runTests() {
   gUI.editors[1].getSourceEditor().then(onEditor1Attach);
 }
 
 function getStylesheetNameLinkFor(aEditor) {
   return aEditor.summary.querySelector(".stylesheet-name");
 }
 
 function onEditor0Attach(aEditor) {
+  info("first editor selected");
+
   waitForFocus(function () {
     // left mouse click should focus editor 1
     EventUtils.synthesizeMouseAtCenter(
       getStylesheetNameLinkFor(gUI.editors[1]),
       {button: 0},
       gPanelWindow);
   }, gPanelWindow);
 }
 
 function onEditor1Attach(aEditor) {
+  info("second editor selected");
+
   ok(aEditor.sourceEditor.hasFocus(),
      "left mouse click has given editor 1 focus");
 
   // right mouse click should not open a new tab
   EventUtils.synthesizeMouseAtCenter(
     getStylesheetNameLinkFor(gUI.editors[2]),
     {button: 1},
     gPanelWindow);
--- a/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js
@@ -14,33 +14,33 @@ function test()
              " </head>",
              " <body>",
              " </body>",
              "</html>"
             ].join("\n"));
 
   waitForExplicitFinish();
   addTabAndOpenStyleEditor(function (aPanel) {
-    let debuggee = aPanel._debuggee;
+    let UI = aPanel.UI;
 
     // Spam the _onNewDocument callback multiple times before the
     // StyleEditorActor has a chance to respond to the first one.
     const SPAM_COUNT = 2;
     for (let i=0; i<SPAM_COUNT; ++i) {
-      debuggee._onNewDocument();
+      UI._onNewDocument();
     }
 
     // Wait for the StyleEditorActor to respond to each "newDocument"
     // message.
     let loadCount = 0;
-    debuggee.on("document-load", function () {
+    UI.on("stylesheets-reset", function () {
       ++loadCount;
       if (loadCount == SPAM_COUNT) {
         // No matter how large SPAM_COUNT is, the number of style
         // sheets should never be more than the number of style sheets
         // in the document.
-        is(debuggee.styleSheets.length, 1, "correct style sheet count");
+        is(UI.editors.length, 1, "correct style sheet count");
         finish();
       }
     });
   });
   content.location = DOCUMENT_WITH_ONE_STYLESHEET;
 }
--- a/browser/devtools/styleeditor/test/browser_styleeditor_new.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_new.js
@@ -53,17 +53,17 @@ function testEditorAdded(aEvent, aEditor
     ok(!content.document.documentElement.classList.contains(TRANSITION_CLASS),
        "StyleEditor's transition class has been removed from content");
 
     if (++checksCompleted == 3) {
       cleanup();
     }
   });
 
-  aEditor.styleSheet.on("property-change", function(event, property) {
+  aEditor.styleSheet.on("property-change", function(property) {
     if (property == "ruleCount") {
       let ruleCount = aEditor.summary.querySelector(".stylesheet-rule-count").textContent;
       is(parseInt(ruleCount), 1,
          "new editor shows 1 rule after modification");
 
       if (++checksCompleted == 3) {
         cleanup();
       }
--- a/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js
@@ -10,17 +10,17 @@ function test()
   waitForExplicitFinish();
 
   // launch Style Editor right when the tab is created (before load)
   // this checks that the Style Editor still launches correctly when it is opened
   // *while* the page is still loading. The Style Editor should not signal that
   // it is loaded until the accompanying content page is loaded.
 
   addTabAndOpenStyleEditor(function(panel) {
-    panel.UI.once("document-load", testDocumentLoad);
+    panel.UI.once("stylesheets-reset", testDocumentLoad);
 
     content.location = TESTCASE_URI;
   });
 }
 
 function testDocumentLoad(event)
 {
   let root = gPanelWindow.document.querySelector(".splitview-root");
--- a/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js
@@ -28,22 +28,17 @@ function test() {
         gUI.on("editor-added", onEditorAdded);
       });
     }, true);
 
     aWindow.gBrowser.selectedBrowser.loadURI(testURI);
   }
 
   function onEditorAdded(aEvent, aEditor) {
-    if (aEditor.sourceLoaded) {
-      checkCache();
-    }
-    else {
-      aEditor.on("source-load", checkCache);
-    }
+    aEditor.getSourceEditor().then(checkCache);
   }
 
   function testOnWindow(options, callback) {
     let win = OpenBrowserWindow(options);
     win.addEventListener("load", function onLoad() {
       win.removeEventListener("load", onLoad, false);
       windowsToClose.push(win);
       executeSoon(function() callback(win));
--- a/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
@@ -17,16 +17,17 @@ function test()
 
   addTabAndOpenStyleEditor(function(panel) {
     gContentWin = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
     gUI = panel.UI;
 
     let count = 0;
     gUI.on("editor-added", function editorAdded(event, editor) {
       if (++count == 2) {
+        info("all editors added to UI");
         gUI.off("editor-added", editorAdded);
         gUI.editors[0].getSourceEditor().then(runTests);
       }
     })
   });
 
   content.location = TESTCASE_URI;
 }
@@ -36,16 +37,17 @@ function runTests()
   let count = 0;
   gUI.once("editor-selected", (event, editor) => {
     editor.getSourceEditor().then(() => {
       info("selected second editor, about to reload page");
       reloadPage();
 
       gUI.on("editor-added", function editorAdded(event, editor) {
         if (++count == 2) {
+          info("all editors added after reload");
           gUI.off("editor-added", editorAdded);
           gUI.editors[1].getSourceEditor().then(testRemembered);
         }
       })
     });
   });
   gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO, COL_NO);
 }
@@ -62,16 +64,17 @@ function testRemembered()
 }
 
 function testNewPage()
 {
   let count = 0;
   gUI.on("editor-added", function editorAdded(event, editor) {
     info("editor added here")
     if (++count == 2) {
+      info("all editors added after navigating page");
       gUI.off("editor-added", editorAdded);
       gUI.editors[0].getSourceEditor().then(testNotRemembered);
     }
   })
 
   info("navigating to a different page");
   navigatePage();
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sourcemaps.js
@@ -0,0 +1,86 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+function test()
+{
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF, true);
+
+  let count = 0;
+  addTabAndOpenStyleEditor(function(panel) {
+    let UI = panel.UI;
+    UI.on("editor-added", (event, editor) => {
+      if (++count >= 3) {
+        // wait for 3 editors - 1 for first style sheet, 1 for the
+        // generated style sheet, and 1 for original source after it
+        // loads and replaces the generated style sheet.
+        runTests(UI);
+      }
+    })
+  });
+
+  content.location = TESTCASE_URI;
+}
+
+function runTests(UI)
+{
+  is(UI.editors.length, 2);
+
+  let firstEditor = UI.editors[0];
+  testFirstEditor(firstEditor);
+
+  let ScssEditor = UI.editors[1];
+
+  let link = getStylesheetNameLinkFor(ScssEditor);
+  link.click();
+
+  ScssEditor.getSourceEditor().then(() => {
+    testScssEditor(ScssEditor);
+
+    finishUp();
+  });
+}
+
+function testFirstEditor(editor) {
+  let name = getStylesheetNameFor(editor);
+  is(name, "simple.css", "First style sheet display name is correct");
+}
+
+function testScssEditor(editor) {
+  let name = getStylesheetNameFor(editor);
+  is(name, "sourcemaps.scss", "Original source display name is correct");
+
+  let text = editor.sourceEditor.getText();
+
+  is(text, "\n\
+$paulrougetpink: #f06;\n\
+\n\
+div {\n\
+  color: $paulrougetpink;\n\
+}\n\
+\n\
+span {\n\
+  background-color: #EEE;\n\
+}", "Original source text is correct");
+}
+
+/* Helpers */
+function getStylesheetNameLinkFor(editor) {
+  return editor.summary.querySelector(".stylesheet-name");
+}
+
+function getStylesheetNameFor(editor) {
+  return editor.summary.querySelector(".stylesheet-name > label")
+         .getAttribute("value")
+}
+
+function finishUp() {
+  Services.prefs.clearUserPref(PREF);
+  finish();
+}
\ No newline at end of file
--- a/browser/devtools/styleeditor/test/head.js
+++ b/browser/devtools/styleeditor/test/head.js
@@ -41,56 +41,20 @@ function addTabAndOpenStyleEditor(callba
 
 function openStyleEditorInWindow(win, callback) {
   let target = TargetFactory.forTab(win.gBrowser.selectedTab);
   win.gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
     let panel = toolbox.getCurrentPanel();
     gPanelWindow = panel._panelWin;
 
     panel.UI._alwaysDisableAnimations = true;
-
-    /*
-    if (aSheet) {
-      panel.selectStyleSheet(aSheet, aLine, aCol);
-    } */
-
     callback(panel);
   });
 }
 
-/*
-function launchStyleEditorChrome(aCallback, aSheet, aLine, aCol)
-{
-  launchStyleEditorChromeFromWindow(window, aCallback, aSheet, aLine, aCol);
-}
-
-function launchStyleEditorChromeFromWindow(aWindow, aCallback, aSheet, aLine, aCol)
-{
-  let target = TargetFactory.forTab(aWindow.gBrowser.selectedTab);
-  gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
-    let panel = toolbox.getCurrentPanel();
-    gPanelWindow = panel._panelWin;
-    gPanelWindow.styleEditorChrome._alwaysDisableAnimations = true;
-    if (aSheet) {
-      panel.selectStyleSheet(aSheet, aLine, aCol);
-    }
-    aCallback(gPanelWindow.styleEditorChrome);
-  });
-}
-
-function addTabAndLaunchStyleEditorChromeWhenLoaded(aCallback, aSheet, aLine, aCol)
-{
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
-    gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
-    launchStyleEditorChrome(aCallback, aSheet, aLine, aCol);
-  }, true);
-}
-*/
-
 function checkDiskCacheFor(host, done)
 {
   let foundPrivateData = false;
 
   Visitor.prototype = {
     onCacheStorageInfo: function(num, consumption)
     {
       info("disk storage contains " + num + " entries");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/sourcemaps.css
@@ -0,0 +1,7 @@
+div {
+  color: #ff0066; }
+
+span {
+  background-color: #EEE; }
+
+/*# sourceMappingURL=sourcemaps.css.map */
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/sourcemaps.css.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI",
+"sources": ["sourcemaps.scss"],
+"names": [],
+"file": "sourcemaps.css"
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/sourcemaps.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+  <title>testcase for testing CSS source maps</title>
+  <link rel="stylesheet" type="text/css" href="simple.css"/>
+  <link rel="stylesheet" type="text/css" href="sourcemaps.css"/>
+</head>
+<body>
+	<div>source maps <span>testcase</span></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/sourcemaps.scss
@@ -0,0 +1,10 @@
+
+$paulrougetpink: #f06;
+
+div {
+  color: $paulrougetpink;
+}
+
+span {
+  background-color: #EEE;
+}
\ No newline at end of file
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -21,16 +21,18 @@ Cu.import("resource://gre/modules/devtoo
 
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
 const FILTER_CHANGED_TIMEOUT = 300;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
+const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled";
+
 /**
  * Helper for long-running processes that should yield occasionally to
  * the mainloop.
  *
  * @param {Window} aWin
  *        Timeouts will be set on this window when appropriate.
  * @param {Generator} aGenerator
  *        Will iterate this generator.
@@ -1091,16 +1093,35 @@ function SelectorView(aTree, aSelectorIn
   this.tree = aTree;
   this.selectorInfo = aSelectorInfo;
   this._cacheStatusNames();
 
   let rule = this.selectorInfo.rule;
   if (rule && rule.parentStyleSheet) {
     this.sheet = rule.parentStyleSheet;
     this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
+
+    let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+    if (showOrig && rule.type != ELEMENT_STYLE) {
+      rule.getOriginalLocation().then(({href, line, column}) => {
+        let newSource = CssLogic.shortSource({href: href}) + ":" + line;
+
+        // Really hacky. Setting the 'source' property won't change the
+        // link's text if the link's already been loaded via template, so we
+        // have to retroactively mutate the DOM.
+        if (newSource != this.source && this.tree.propertyContainer) {
+          let selector = '[sourcelocation="' + this.source + '"]';
+          let link = this.tree.propertyContainer.querySelector(selector);
+          if (link) {
+            link.textContent = newSource;
+          }
+        }
+        this.source = newSource;
+      });
+    }
   } else {
     this.source = CssLogic.l10n("rule.sourceElement");
     this.href = "#";
   }
 }
 
 /**
  * Decode for cssInfo.rule.status
@@ -1212,44 +1233,49 @@ SelectorView.prototype = {
    *   style editor.
    *
    * @param aEvent The click event
    */
   openStyleEditor: function(aEvent)
   {
     let inspector = this.tree.styleInspector.inspector;
     let rule = this.selectorInfo.rule;
-    let line = rule.line || 0;
 
     // The style editor can only display stylesheets coming from content because
     // chrome stylesheets are not listed in the editor's stylesheet selector.
     //
     // If the stylesheet is a content stylesheet we send it to the style
     // editor else we display it in the view source window.
-    //
+    let sheet = rule.parentStyleSheet;
+    if (!sheet || sheet.isSystem) {
+      let contentDoc = null;
+      if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
+        let rawNode = this.tree.viewedElement.rawNode();
+        if (rawNode) {
+          contentDoc = rawNode.ownerDocument;
+        }
+      }
+      let viewSourceUtils = inspector.viewSourceUtils;
+      viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line);
+      return;
+    }
 
-    let href = rule.href;
-    let sheet = rule.parentStyleSheet;
-    if (sheet && href && !sheet.isSystem) {
+    let location = promise.resolve({
+      href: rule.href,
+      line: rule.line
+    });
+    if (rule.href && Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+      location = rule.getOriginalLocation();
+    }
+
+    location.then(({href, line}) => {
       let target = inspector.target;
       if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
         gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
           toolbox.getCurrentPanel().selectStyleSheet(href, line);
         });
       }
-      return;
-    }
-
-    let contentDoc = null;
-    if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
-      let rawNode = this.tree.viewedElement.rawNode();
-      if (rawNode) {
-        contentDoc = rawNode.ownerDocument;
-      }
-    }
-
-    let viewSourceUtils = inspector.viewSourceUtils;
-    viewSourceUtils.viewSource(href, null, contentDoc, line);
+    });
   }
 };
 
 exports.CssHtmlTree = CssHtmlTree;
 exports.PropertyView = PropertyView;
--- a/browser/devtools/styleinspector/computedview.xhtml
+++ b/browser/devtools/styleinspector/computedview.xhtml
@@ -94,16 +94,17 @@
       <div id="templateMatchedSelectors">
         <loop foreach="selector in ${matchedSelectorViews}">
           <p>
             <span class="rule-link">
               <a target="_blank" class="link theme-link"
                   onclick="${selector.openStyleEditor}"
                   onkeydown="${selector.maybeOpenStyleEditor}"
                   title="${selector.href}"
+                  sourcelocation="${selector.source}"
                   tabindex="0">${selector.source}</a>
             </span>
             <span dir="ltr" class="rule-text ${selector.statusClass} theme-fg-color3" title="${selector.statusText}">
               ${selector.sourceText}
               <span class="other-property-value theme-fg-color1">${selector.outputFragment}</span>
             </span>
           </p>
         </loop>
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -17,16 +17,18 @@ const {Tooltip, SwatchColorPickerTooltip
 const {OutputParser} = require("devtools/output-parser");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
+const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled";
+
 /**
  * These regular expressions are adapted from firebug's css.js, and are
  * used to parse CSSStyleDeclaration's cssText attribute.
  */
 
 // Used to split on css line separators
 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
 
@@ -483,16 +485,43 @@ Rule.prototype = {
    * The rule's line within a stylesheet
    */
   get ruleLine()
   {
     return this.domRule ? this.domRule.line : null;
   },
 
   /**
+   * The rule's column within a stylesheet
+   */
+  get ruleColumn()
+  {
+    return this.domRule ? this.domRule.column : null;
+  },
+
+  /**
+   * 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 a string.
+   */
+  getOriginalSourceString: function Rule_getOriginalSourceString()
+  {
+    if (this._originalSourceString) {
+      return promise.resolve(this._originalSourceString);
+    }
+    return this.domRule.getOriginalLocation().then(({href, line}) => {
+      let string = CssLogic.shortSource({href: href}) + ":" + line;
+      this._originalSourceString = string;
+      return string;
+    });
+  },
+
+  /**
    * Returns true if the rule matches the creation options
    * specified.
    *
    * @param {object} aOptions
    *        Creation options.  See the Rule constructor for documentation.
    */
   matches: function Rule_matches(aOptions)
   {
@@ -1581,16 +1610,24 @@ RuleEditor.prototype = {
       this.element.dispatchEvent(evt);
     }.bind(this));
     let sourceLabel = this.doc.createElementNS(XUL_NS, "label");
     sourceLabel.setAttribute("crop", "center");
     sourceLabel.setAttribute("value", this.rule.title);
     sourceLabel.setAttribute("tooltiptext", this.rule.title);
     source.appendChild(sourceLabel);
 
+    let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+    if (showOrig && this.rule.domRule.type != ELEMENT_STYLE) {
+      this.rule.getOriginalSourceString().then((string) => {
+        sourceLabel.setAttribute("value", string);
+        sourceLabel.setAttribute("tooltiptext", string);
+      })
+    }
+
     let code = createChild(this.element, "div", {
       class: "ruleview-code"
     });
 
     let header = createChild(code, "div", {});
 
     this.selectorText = createChild(header, "span", {
       class: "ruleview-selector theme-fg-color3"
--- a/browser/devtools/styleinspector/style-inspector.js
+++ b/browser/devtools/styleinspector/style-inspector.js
@@ -1,26 +1,29 @@
 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const {Cc, Cu, Ci} = require("chrome");
+const promise = require("sdk/core/promise");
 
 let ToolDefinitions = require("main").Tools;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "gDevTools", () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
 loader.lazyGetter(this, "RuleView", () => require("devtools/styleinspector/rule-view"));
 loader.lazyGetter(this, "ComputedView", () => require("devtools/styleinspector/computed-view"));
 loader.lazyGetter(this, "_strings", () => Services.strings
   .createBundle("chrome://global/locale/devtools/styleinspector.properties"));
 
+const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled";
+
 // This module doesn't currently export any symbols directly, it only
 // registers inspector tools.
 
 function RuleViewTool(aInspector, aWindow, aIFrame)
 {
   this.inspector = aInspector;
   this.doc = aWindow.document;
   this.outerIFrame = aIFrame;
@@ -37,39 +40,40 @@ function RuleViewTool(aInspector, aWindo
   this._refreshHandler = () => {
     this.inspector.emit("rule-view-refreshed");
   };
 
   this.view.element.addEventListener("CssRuleViewRefreshed", this._refreshHandler);
 
   this._cssLinkHandler = (aEvent) => {
     let rule = aEvent.detail.rule;
-    let line = rule.line || 0;
+    let sheet = rule.parentStyleSheet;
 
-    // The style editor can only display stylesheets coming from content because
-    // chrome stylesheets are not listed in the editor's stylesheet selector.
-    //
-    // If the stylesheet is a content stylesheet we send it to the style
-    // editor else we display it in the view source window.
-    //
-    let href = rule.href;
-    let sheet = rule.parentStyleSheet;
-    if (sheet && href && !sheet.isSystem) {
+    // Chrome stylesheets are not listed in the style editor, so show
+    // these sheets in the view source window instead.
+    if (!sheet || !rule.href || sheet.isSystem) {
+      let contentDoc = this.inspector.selection.document;
+      let viewSourceUtils = this.inspector.viewSourceUtils;
+      viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line || 0);
+      return;
+    }
+
+    let location = promise.resolve(rule.location);
+    if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+      location = rule.getOriginalLocation();
+    }
+    location.then(({ href, line, column }) => {
       let target = this.inspector.target;
       if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
         gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
-          toolbox.getCurrentPanel().selectStyleSheet(href, line);
+          toolbox.getCurrentPanel().selectStyleSheet(href, line, column);
         });
       }
       return;
-    }
-
-    let contentDoc = this.inspector.selection.document;
-    let viewSourceUtils = this.inspector.viewSourceUtils;
-    viewSourceUtils.viewSource(href, null, contentDoc, line);
+    })
   }
 
   this.view.element.addEventListener("CssRuleViewCSSLinkClicked",
                                      this._cssLinkHandler);
 
   this._onSelect = this.onSelect.bind(this);
   this.inspector.selection.on("detached", this._onSelect);
   this.inspector.selection.on("new-node-front", this._onSelect);
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -49,8 +49,15 @@ support-files =
 [browser_ruleview_bug_902966_revert_value_on_ESC.js]
 [browser_ruleview_pseudoelement.js]
 support-files = browser_ruleview_pseudoelement.html
 [browser_computedview_bug835808_keyboard_nav.js]
 [browser_bug913014_matched_expand.js]
 [browser_bug765105_background_image_tooltip.js]
 [browser_bug889638_rule_view_color_picker.js]
 [browser_bug940500_rule_view_pick_gradient_color.js]
+[browser_ruleview_original_source_link.js]
+support-files =
+  sourcemaps.html
+  sourcemaps.css
+  sourcemaps.css.map
+  sourcemaps.scss
+[browser_computedview_original_source_link.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_computedview_original_source_link.js
@@ -0,0 +1,119 @@
+/* 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/ */
+
+let win;
+let doc;
+let inspector;
+let computedView;
+let toolbox;
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+function test()
+{
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF, true);
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee,
+      true);
+    doc = content.document;
+    waitForFocus(function () { openComputedView(highlightNode); }, content);
+  }, true);
+
+  content.location = TESTCASE_URI;
+}
+
+function highlightNode(aInspector, aComputedView)
+{
+  inspector = aInspector;
+  computedView = aComputedView;
+
+  // Highlight a node.
+  let div = content.document.getElementsByTagName("div")[0];
+  ok(div, "div to select exists")
+
+  inspector.selection.setNode(div);
+  inspector.once("inspector-updated", () => {
+    is(inspector.selection.node, div, "selection matches the div element");
+
+    expandProperty(0, testComputedViewLink);
+  }).then(null, console.error);
+}
+
+function testComputedViewLink() {
+  let link = getLinkByIndex(0);
+  waitForSuccess({
+    name: "link text changed to display original source location",
+    validatorFn: function()
+    {
+      return link.textContent == "sourcemaps.scss:4";
+    },
+    successFn: linkChanged,
+    failureFn: linkChanged,
+  });
+}
+
+function linkChanged() {
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = gDevTools.getToolbox(target);
+
+  toolbox.once("styleeditor-ready", function(id, aToolbox) {
+    let panel = toolbox.getCurrentPanel();
+    panel.UI.on("editor-selected", (event, editor) => {
+      // The style editor selects the first sheet at first load before
+      // selecting the desired sheet.
+      if (editor.styleSheet.href.endsWith("scss")) {
+        info("original source editor selected");
+        editor.getSourceEditor().then(editorSelected);
+      }
+    });
+  });
+
+  let link = getLinkByIndex(0);
+
+  info("clicking rule view link");
+  link.scrollIntoView();
+  link.click();
+}
+
+function editorSelected(editor) {
+  let href = editor.styleSheet.href;
+  ok(href.endsWith("sourcemaps.scss"), "selected stylesheet is correct one");
+
+  let {line, col} = editor.sourceEditor.getCursor();
+  is(line, 3, "cursor is at correct line number in original source");
+
+  finishUp();
+}
+
+/* Helpers */
+function expandProperty(aIndex, aCallback)
+{
+  info("expanding property " + aIndex);
+  let contentDoc = computedView.styleDocument;
+  let expando = contentDoc.querySelectorAll(".expandable")[aIndex];
+
+  expando.click();
+
+  inspector.once("computed-view-property-expanded", aCallback);
+}
+
+function getLinkByIndex(aIndex)
+{
+  let contentDoc = computedView.styleDocument;
+  let links = contentDoc.querySelectorAll(".rule-link .link");
+  return links[aIndex];
+}
+
+function finishUp()
+{
+  gBrowser.removeCurrentTab();
+  doc = inspector = computedView = toolbox = win = null;
+  Services.prefs.clearUserPref(PREF);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_original_source_link.js
@@ -0,0 +1,112 @@
+/* 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/ */
+
+let win;
+let doc;
+let contentWindow;
+let inspector;
+let toolbox;
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html";
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+function test()
+{
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF, true);
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee,
+      true);
+    doc = content.document;
+    waitForFocus(openToolbox, content);
+  }, true);
+
+  content.location = TESTCASE_URI;
+}
+
+function openToolbox() {
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+  gDevTools.showToolbox(target, "inspector").then(function(aToolbox) {
+    toolbox = aToolbox;
+    inspector = toolbox.getCurrentPanel();
+    inspector.sidebar.select("ruleview");
+    highlightNode();
+  });
+}
+
+function highlightNode()
+{
+  // Highlight a node.
+  let div = content.document.getElementsByTagName("div")[0];
+
+  inspector.selection.setNode(div);
+  inspector.once("inspector-updated", () => {
+    is(inspector.selection.node, div, "selection matches the div element");
+    testRuleViewLink();
+  }).then(null, console.error);
+}
+
+function testRuleViewLink() {
+  let label = getLinkByIndex(1).querySelector("label");
+
+  waitForSuccess({
+    name: "link text changed to display original source location",
+    validatorFn: function()
+    {
+      return label.getAttribute("value") == "sourcemaps.scss:4";
+    },
+    successFn: linkChanged,
+    failureFn: linkChanged,
+  });
+}
+
+function linkChanged() {
+  toolbox.once("styleeditor-ready", function(id, aToolbox) {
+    let panel = toolbox.getCurrentPanel();
+    panel.UI.on("editor-selected", (event, editor) => {
+      // The style editor selects the first sheet at first load before
+      // selecting the desired sheet.
+      if (editor.styleSheet.href.endsWith("scss")) {
+        info("original source editor selected");
+        editor.getSourceEditor().then(editorSelected);
+      }
+    });
+  });
+
+  let link = getLinkByIndex(1);
+
+  info("clicking rule view link");
+  link.scrollIntoView();
+  link.click();
+}
+
+function editorSelected(editor) {
+  let href = editor.styleSheet.href;
+  ok(href.endsWith("sourcemaps.scss"), "selected stylesheet is correct one");
+
+  let {line, col} = editor.sourceEditor.getCursor();
+  is(line, 3, "cursor is at correct line number in original source");
+
+  finishUp();
+}
+
+function getLinkByIndex(aIndex)
+{
+  let contentDoc = ruleView().doc;
+  contentWindow = contentDoc.defaultView;
+  let links = contentDoc.querySelectorAll(".ruleview-rule-source");
+  return links[aIndex];
+}
+
+function finishUp()
+{
+  gBrowser.removeCurrentTab();
+  contentWindow = doc = inspector = toolbox = win = null;
+  Services.prefs.clearUserPref(PREF);
+  finish();
+}
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -1,13 +1,16 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
+const TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/styleinspector/test/";
+const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/styleinspector/test/";
+
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 SimpleTest.registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.debugger.log");
 });
 
 let tempScope = {};
 
 Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
@@ -182,12 +185,59 @@ function getComputedPropertyValue(aName)
 
     if (name.textContent === aName) {
       let value = prop.querySelector(".property-value");
       return value.textContent;
     }
   }
 }
 
+/**
+ * Polls a given function waiting for it to become true.
+ *
+ * @param object aOptions
+ *        Options object with the following properties:
+ *        - validatorFn
+ *        A validator function that returns a boolean. This is called every few
+ *        milliseconds to check if the result is true. When it is true, succesFn
+ *        is called and polling stops. If validatorFn never returns true, then
+ *        polling timeouts after several tries and a failure is recorded.
+ *        - successFn
+ *        A function called when the validator function returns true.
+ *        - failureFn
+ *        A function called if the validator function timeouts - fails to return
+ *        true in the given time.
+ *        - name
+ *        Name of test. This is used to generate the success and failure
+ *        messages.
+ *        - timeout
+ *        Timeout for validator function, in milliseconds. Default is 5000.
+ */
+function waitForSuccess(aOptions)
+{
+  let start = Date.now();
+  let timeout = aOptions.timeout || 5000;
+
+  function wait(validatorFn, successFn, failureFn)
+  {
+    if ((Date.now() - start) > timeout) {
+      // Log the failure.
+      ok(false, "Timed out while waiting for: " + aOptions.name);
+      failureFn(aOptions);
+      return;
+    }
+
+    if (validatorFn(aOptions)) {
+      ok(true, aOptions.name);
+      successFn();
+    }
+    else {
+      setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
+    }
+  }
+
+  wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn);
+}
+
 registerCleanupFunction(tearDown);
 
 waitForExplicitFinish();
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/sourcemaps.css
@@ -0,0 +1,7 @@
+div {
+  color: #ff0066; }
+
+span {
+  background-color: #EEE; }
+
+/*# sourceMappingURL=sourcemaps.css.map */
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/sourcemaps.css.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI",
+"sources": ["sourcemaps.scss"],
+"names": [],
+"file": "sourcemaps.css"
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/sourcemaps.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+  <title>testcase for testing CSS source maps</title>
+  <link rel="stylesheet" type="text/css" href="simple.css"/>
+  <link rel="stylesheet" type="text/css" href="sourcemaps.css"/>
+</head>
+<body>
+	<div>source maps <span>testcase</span></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/sourcemaps.scss
@@ -0,0 +1,10 @@
+
+$paulrougetpink: #f06;
+
+div {
+  color: $paulrougetpink;
+}
+
+span {
+  background-color: #EEE;
+}
\ No newline at end of file
--- a/browser/metro/base/content/bindings/circularprogress.xml
+++ b/browser/metro/base/content/bindings/circularprogress.xml
@@ -15,72 +15,80 @@
 
     <content>
       <xul:stack>
         <xul:toolbarbutton anonid="progressButton" class="circularprogressindicator-progressButton"/>
         <html:div anonid="progressTrack" xbl:inherits="progress" class="circularprogressindicator-progressTrack"></html:div>
         <html:canvas anonid="progressRing" xbl:inherits="progress" class="circularprogressindicator-progressRing" width="40" height="40"></html:canvas>
       </xul:stack>
     </content>
+
     <implementation>
       <field name="_progressCanvas">
           document.getAnonymousElementByAttribute(this, "anonid", "progressRing");
       </field>
       <field name="_progressCircleCtx">null</field>
       <field name="_img">null</field>
       <constructor>
         <![CDATA[
           this._progressCircleCtx = this._progressCanvas.getContext('2d');
-          this._img = new Image();
         ]]>
       </constructor>
       <method name="updateProgress">
         <parameter name="percentComplete"/>
         <body>
           <![CDATA[
             const PROGRESS_RING_IMG = "chrome://browser/skin/images/progresscircle.png";
 
             // show ring background even if % is 0.
             this.setAttribute("progress", percentComplete);
 
             let startAngle = 1.5 * Math.PI;
             let endAngle = startAngle + (2 * Math.PI * (percentComplete / 100));
 
-            let ctx = this._progressCircleCtx;
-            ctx.clearRect(0, 0,
-              this._progressCanvas.width, this._progressCanvas.height);
-
-            // Save the state, so we can undo the clipping
-            ctx.save();
-
-            ctx.beginPath();
-            let center = this._progressCanvas.width / 2;
-            ctx.arc(center, center, center, startAngle, endAngle, false);
-            ctx.lineTo(center, center);
-            ctx.closePath();
-            ctx.clip();
-
-            // Draw circle image.
-            if (this._img && this._img.src) {
-              ctx.drawImage(this._img, 0, 0);
-            } else {
-              this._img.onload = function() {
-                ctx.drawImage(this._img, 0, 0);
-              }.bind(this);
+            if (!this._img) {
+              this._img = new Image();
+              this._img.onload = () => {
+                this.updateProgress(this.getAttribute("progress"))
+              }
               this._img.src = PROGRESS_RING_IMG;
             }
+            else if (this._img.complete) {
+              let ctx = this._progressCircleCtx;
+              ctx.clearRect(0, 0,
+                this._progressCanvas.width, this._progressCanvas.height);
 
-            ctx.restore();
+              // Save the state, so we can undo the clipping
+              ctx.save();
+
+              ctx.beginPath();
+              let center = this._progressCanvas.width / 2;
+              ctx.arc(center, center, center, startAngle, endAngle, false);
+              ctx.lineTo(center, center);
+              ctx.closePath();
+              ctx.clip();
+
+              // Draw circle image.
+              ctx.drawImage(this._img, 0, 0);
+
+              ctx.restore();
+            } else {
+              //  Image is still loading
+            }
             return [startAngle, endAngle];
           ]]>
         </body>
       </method>
       <method name="reset">
         <body>
           <![CDATA[
+            if(this._img && !this._img.complete) {
+              // cancel any pending updateProgress
+              this._img.onload = () => {};
+            }
             this._progressCircleCtx.clearRect(0, 0,
               this._progressCanvas.width, this._progressCanvas.height);
             this.removeAttribute("progress");
           ]]>
         </body>
       </method>
     </implementation>
   </binding>
--- a/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js
+++ b/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js
@@ -41,24 +41,26 @@ function setPortraitViewstate() {
   browser.style.borderRight = padding + "px solid gray";
 
   // cap the height to create more even surface for testing on
   if (fullHeight > maxPortraitHeight)
     browser.style.borderBottom = (fullHeight - maxPortraitHeight) + "px solid gray";
 
   ContentAreaObserver._updateViewState("portrait");
   ContentAreaObserver._dispatchBrowserEvent("SizeChanged");
+  yield waitForMessage("Content:SetWindowSize:Complete", browser.messageManager);
 
   // Make sure it renders the new mode properly
   yield waitForMs(0);
 }
 
 function restoreViewstate() {
   ContentAreaObserver._updateViewState("landscape");
   ContentAreaObserver._dispatchBrowserEvent("SizeChanged");
+  yield waitForMessage("Content:SetWindowSize:Complete", Browser.selectedBrowser.messageManager);
 
   ok(isLandscapeMode(), "restoreViewstate should restore landscape mode.");
 
   Browser.selectedBrowser.style.removeProperty("border-right");
   Browser.selectedBrowser.style.removeProperty("border-bottom");
 
   yield waitForMs(0);
 }
--- a/browser/metro/modules/View.jsm
+++ b/browser/metro/modules/View.jsm
@@ -22,28 +22,28 @@ function makeURI(aURL, aOriginCharset, a
 
 
 // --------------------------------
 // View prototype for shared functionality
 
 function View(aSet) {
   this._set = aSet;
   this._set.controller = this;
+  this._window = aSet.ownerDocument.defaultView;
 
-  this.viewStateObserver = {
-    observe: (aSubject, aTopic, aData) => this._adjustDOMforViewState(aData)
-  };
-  Services.obs.addObserver(this.viewStateObserver, "metro_viewstate_changed", false);
+  this.onResize = () => this._adjustDOMforViewState();
+  this._window.addEventListener("resize", this.onResize);
+
   ColorUtils.init();
   this._adjustDOMforViewState();
 }
 
 View.prototype = {
   destruct: function () {
-    Services.obs.removeObserver(this.viewStateObserver, "metro_viewstate_changed");
+    this._window.removeEventListener("resize", this.onResize);
   },
 
   _adjustDOMforViewState: function _adjustDOMforViewState(aState) {
     let grid = this._set;
     if (!grid) {
       return;
     }
     if (!aState) {
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -81,19 +81,19 @@ this.BrowserUITelemetry = {
       }
       parent = current;
       current = current[key];
     }
     return parent;
   },
 
   _countableEvents: {},
-  _countMouseUpEvent: function(aCategory, aAction, aMouseUpEvent) {
+  _countMouseUpEvent: function(aCategory, aAction, aButton) {
     const BUTTONS = ["left", "middle", "right"];
-    let buttonKey = BUTTONS[aMouseUpEvent.button];
+    let buttonKey = BUTTONS[aButton];
     if (buttonKey) {
       let countObject =
         this._ensureObjectChain([aCategory, aAction, buttonKey], 0);
       countObject[buttonKey]++;
     }
   },
 
   _registerWindow: function(aWindow) {
@@ -129,17 +129,17 @@ this.BrowserUITelemetry = {
 
   _handleMouseUp: function(aEvent) {
     let item = aEvent.originalTarget;
     // Perhaps we're seeing one of the default toolbar items
     // being clicked.
     if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) {
       // Base case - we clicked directly on one of our built-in items,
       // and we can go ahead and register that click.
-      this._countMouseUpEvent("click-builtin-item", item.id, aEvent);
+      this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button);
     }
   },
 
   getToolbarMeasures: function() {
     // Grab the most recent non-popup, non-private browser window for us to
     // analyze the toolbars in...
     let win = RecentWindow.getMostRecentBrowserWindow({
       private: false,
--- a/mobile/android/base/GeckoView.java
+++ b/mobile/android/base/GeckoView.java
@@ -32,23 +32,31 @@ public class GeckoView extends LayerView
     implements GeckoEventListener, ContextGetter {
 
     private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GeckoView";
     private static final String LOGTAG = "GeckoView";
 
     private ChromeDelegate mChromeDelegate;
     private ContentDelegate mContentDelegate;
 
+    public GeckoView(Context context) {
+        super(context);
+        init(context, null, true);
+    }
+
     public GeckoView(Context context, AttributeSet attrs) {
         super(context, attrs);
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GeckoView);
         String url = a.getString(R.styleable.GeckoView_url);
         boolean doInit = a.getBoolean(R.styleable.GeckoView_doinit, true);
         a.recycle();
+        init(context, url, doInit);
+    }
 
+    private void init(Context context, String url, boolean doInit) {
         // TODO: Fennec currently takes care of its own initialization, so this
         // flag is a hack used in Fennec to prevent GeckoView initialization.
         // This should go away once Fennec also uses GeckoView for
         // initialization.
         if (!doInit)
             return;
 
         // If running outside of a GeckoActivity (eg, from a library project),
--- a/mobile/android/base/gfx/LayerRenderer.java
+++ b/mobile/android/base/gfx/LayerRenderer.java
@@ -706,13 +706,17 @@ public class LayerRenderer implements Ta
 
     @Override
     public void onTabChanged(final Tab tab, Tabs.TabEvents msg, Object data) {
         // Sets the background of the newly selected tab. This background color
         // gets cleared in endDrawing(). This function runs on the UI thread,
         // but other code that touches the paint state is run on the compositor
         // thread, so this may need to be changed if any problems appear.
         if (msg == Tabs.TabEvents.SELECTED) {
-            mView.getChildAt(0).setBackgroundColor(tab.getBackgroundColor());
-            mView.setPaintState(LayerView.PAINT_START);
+            if (mView != null) {
+                if (mView.getChildAt(0) != null) {
+                    mView.getChildAt(0).setBackgroundColor(tab.getBackgroundColor());
+                }
+                mView.setPaintState(LayerView.PAINT_START);
+            }
         }
     }
 }
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -114,16 +114,32 @@ public class LayerView extends FrameLayo
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
             mOverscroll = new OverscrollEdgeEffect(this);
         } else {
             mOverscroll = null;
         }
         Tabs.registerOnTabsChangedListener(this);
     }
 
+    public LayerView(Context context) {
+        super(context);
+
+        mGLController = GLController.getInstance(this);
+        mPaintState = PAINT_START;
+        mBackgroundColor = Color.WHITE;
+
+        mTouchInterceptors = new ArrayList<TouchEventInterceptor>();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            mOverscroll = new OverscrollEdgeEffect(this);
+        } else {
+            mOverscroll = null;
+        }
+        Tabs.registerOnTabsChangedListener(this);
+    }
+
     public void initializeView(EventDispatcher eventDispatcher) {
         mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
         if (mOverscroll != null) {
             mLayerClient.setOverscrollHandler(mOverscroll);
         }
 
         mPanZoomController = mLayerClient.getPanZoomController();
         mMarginsAnimator = mLayerClient.getLayerMarginsAnimator();
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -717,17 +717,17 @@ var NodeListActor = exports.NodeListActo
   }),
 
   release: method(function() {}, { release: true })
 });
 
 /**
  * Client side of a node list as returned by querySelectorAll()
  */
-var NodeListFront = exports.NodeLIstFront = protocol.FrontClass(NodeListActor, {
+var NodeListFront = exports.NodeListFront = protocol.FrontClass(NodeListActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
   },
 
   destroy: function() {
     protocol.Front.prototype.destroy.call(this);
   },
 
--- a/toolkit/devtools/server/actors/styleeditor.js
+++ b/toolkit/devtools/server/actors/styleeditor.js
@@ -1,191 +1,153 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-let Cc = Components.classes;
-let Ci = Components.interfaces;
-let Cu = Components.utils;
+let { components, Cc, Ci, Cu } = require('chrome');
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
+
+const promise = require("sdk/core/promise");
+const events = require("sdk/event/core");
+const protocol = require("devtools/server/protocol");
+const {Arg, Option, method, RetVal, types} = protocol;
+const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
+
+loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
 
 let TRANSITION_CLASS = "moz-styleeditor-transitioning";
 let TRANSITION_DURATION_MS = 500;
 let TRANSITION_RULE = "\
 :root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\
 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
 transition-delay: 0ms !important;\
 transition-timing-function: ease-out !important;\
 transition-property: all !important;\
 }";
 
 let LOAD_ERROR = "error-load";
 
-/**
- * Creates a StyleEditorActor. StyleEditorActor provides remote access to the
- * built-in style editor module.
- */
-function StyleEditorActor(aConnection, aParentActor)
-{
-  this.conn = aConnection;
-  this._onDocumentLoaded = this._onDocumentLoaded.bind(this);
-  this._onSheetLoaded = this._onSheetLoaded.bind(this);
-  this.parentActor = aParentActor;
+exports.register = function(handle) {
+  handle.addTabActor(StyleSheetsActor, "styleSheetsActor");
+  handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor");
+};
 
-  // keep a map of sheets-to-actors so we don't create two actors for one sheet
-  this._sheets = new Map();
+exports.unregister = function(handle) {
+  handle.removeTabActor(StyleSheetsActor);
+  handle.removeGlobalActor(StyleSheetsActor);
+};
 
-  this._actorPool = new ActorPool(this.conn);
-  this.conn.addActorPool(this._actorPool);
-}
+types.addActorType("stylesheet");
+types.addActorType("originalsource");
 
-StyleEditorActor.prototype = {
-  /**
-   * Actor pool for all of the actors we send to the client.
-   */
-  _actorPool: null,
-
-  /**
-   * The debugger server connection instance.
-   */
-  conn: null,
+/**
+ * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
+ * stylesheets of a document.
+ */
+let StyleSheetsActor = protocol.ActorClass({
+  typeName: "stylesheets",
 
   /**
    * The window we work with, taken from the parent actor.
    */
   get window() this.parentActor.window,
 
   /**
    * The current content document of the window we work with.
    */
   get document() this.window.document,
 
-  actorPrefix: "styleEditor",
-
   form: function()
   {
     return { actor: this.actorID };
   },
 
-  /**
-   * Destroy the current StyleEditorActor instance.
-   */
-  disconnect: function()
-  {
-    if (this._observer) {
-      this._observer.disconnect();
-      delete this._observer;
-    }
+  initialize: function (conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, null);
 
-    this._sheets.clear();
+    this.parentActor = tabActor;
 
-    this.conn.removeActorPool(this._actorPool);
-    this._actorPool = null;
-    this.conn = null;
+    // keep a map of sheets-to-actors so we don't create two actors for one sheet
+    this._sheets = new Map();
   },
 
   /**
-   * Release an actor from our actor pool.
+   * Destroy the current StyleSheetsActor instance.
    */
-  releaseActor: function(actor)
+  destroy: function()
   {
-    if (this._actorPool) {
-      this._actorPool.removeActor(actor.actorID);
-    }
-  },
-
-  /**
-   * Get the BaseURI for the document.
-   *
-   * @return {object} JSON message to with BaseURI
-   */
-  onGetBaseURI: function() {
-    return { baseURI: this.document.baseURIObject.spec };
+    this._sheets.clear();
   },
 
   /**
-   * Called when target navigates to a new document.
-   * Adds load listeners to document.
+   * Protocol method for getting a list of StyleSheetActors representing
+   * all the style sheets in this document.
    */
-  onNewDocument: function() {
-    // delete previous document's actors
-    this._clearStyleSheetActors();
+  getStyleSheets: method(function() {
+    let deferred = promise.defer();
+
+    let window = this.window;
+    var domReady = () => {
+      window.removeEventListener("DOMContentLoaded", domReady, true);
 
-    // Note: listening for load won't be necessary once
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
-    if (this.document.readyState == "complete") {
-      this._onDocumentLoaded();
+      let documents = [this.document];
+      let actors = [];
+      for (let doc of documents) {
+        let sheets = this._addStyleSheets(doc.styleSheets);
+        actors = actors.concat(sheets);
+        // Recursively handle style sheets of the documents in iframes.
+        for (let iframe of doc.getElementsByTagName("iframe")) {
+          documents.push(iframe.contentDocument);
+        }
+      }
+      deferred.resolve(actors);
+    };
+
+    if (window.document.readyState === "loading") {
+      window.addEventListener("DOMContentLoaded", domReady, true);
+    } else {
+      domReady();
     }
-    else {
-      this.window.addEventListener("load", this._onDocumentLoaded, false);
-    }
-    return {};
-  },
+
+    return deferred.promise;
+  }, {
+    request: {},
+    response: { styleSheets: RetVal("array:stylesheet") }
+  }),
 
   /**
-   * Event handler for document loaded event. Add actor for each stylesheet
-   * and send an event notifying of the load
-   */
-  _onDocumentLoaded: function(event) {
-    if (event) {
-      this.window.removeEventListener("load", this._onDocumentLoaded, false);
-    }
-
-    let documents = [this.document];
-    var forms = [];
-    for (let doc of documents) {
-      let sheetForms = this._addStyleSheets(doc.styleSheets);
-      forms = forms.concat(sheetForms);
-      // Recursively handle style sheets of the documents in iframes.
-      for (let iframe of doc.getElementsByTagName("iframe")) {
-        documents.push(iframe.contentDocument);
-      }
-    }
-
-    this.conn.send({
-      from: this.actorID,
-      type: "documentLoad",
-      styleSheets: forms
-    });
-  },
-
-  /**
-   * Add all the stylesheets to the map and create an actor
-   * for each one if not already created. Send event that there
-   * are new stylesheets.
+   * Add all the stylesheets to the map and create an actor for each one
+   * if not already created. Send event that there are new stylesheets.
    *
    * @param {[DOMStyleSheet]} styleSheets
    *        Stylesheets to add
    * @return {[object]}
-   *         Array of forms for each StyleSheetActor created
+   *         Array of actors for each StyleSheetActor created
    */
   _addStyleSheets: function(styleSheets)
   {
     let sheets = [];
     for (let i = 0; i < styleSheets.length; i++) {
       let styleSheet = styleSheets[i];
       sheets.push(styleSheet);
 
       // Get all sheets, including imported ones
       let imports = this._getImported(styleSheet);
       sheets = sheets.concat(imports);
     }
+    let actors = sheets.map(this._createStyleSheetActor.bind(this));
 
-    let forms = sheets.map((sheet) => {
-      let actor = this._createStyleSheetActor(sheet);
-      return actor.form();
-    });
-
-    return forms;
+    return actors;
   },
 
   /**
    * Get all the stylesheets @imported from a stylesheet.
    *
    * @param  {DOMStyleSheet} styleSheet
    *         Style sheet to search
    * @return {array}
@@ -211,462 +173,526 @@ StyleEditorActor.prototype = {
         // @import rules must precede all others except @charset
         break;
       }
     }
     return imported;
   },
 
   /**
-   * Create a new actor for a style sheet, if it hasn't
-   * already been created, and return it.
+   * Create a new actor for a style sheet, if it hasn't already been created.
    *
-   * @param  {DOMStyleSheet} aStyleSheet
+   * @param  {DOMStyleSheet} styleSheet
    *         The style sheet to create an actor for.
    * @return {StyleSheetActor}
    *         The actor for this style sheet
    */
-  _createStyleSheetActor: function(aStyleSheet)
+  _createStyleSheetActor: function(styleSheet)
   {
-    if (this._sheets.has(aStyleSheet)) {
-      return this._sheets.get(aStyleSheet);
+    if (this._sheets.has(styleSheet)) {
+      return this._sheets.get(styleSheet);
     }
-    let actor = new StyleSheetActor(aStyleSheet, this);
-    this._actorPool.addActor(actor);
-    this._sheets.set(aStyleSheet, actor);
+    let actor = new StyleSheetActor(styleSheet, this);
+
+    this.manage(actor);
+    this._sheets.set(styleSheet, actor);
+
     return actor;
   },
 
   /**
    * Clear all the current stylesheet actors in map.
    */
   _clearStyleSheetActors: function() {
     for (let actor in this._sheets) {
-      this.releaseActor(this._sheets[actor]);
+      this.unmanage(this._sheets[actor]);
     }
     this._sheets.clear();
   },
 
   /**
-   * Get the actors of all the stylesheets in the current document.
-   *
-   * @return {object} JSON message with the stylesheet actors' forms
-   */
-  onGetStyleSheets: function() {
-    let forms = this._addStyleSheets(this.document.styleSheets);
-    return { "styleSheets": forms };
-  },
-
-  /**
-   * Handler for style sheet loading event. Add
-   * a new actor for the sheet and notify.
-   *
-   * @param  {Event} event
-   */
-  _onSheetLoaded: function(event) {
-    let style = event.target;
-    style.removeEventListener("load", this._onSheetLoaded, false);
-
-    let actor = this._createStyleSheetActor(style.sheet);
-    this._notifyStyleSheetsAdded([actor.form()]);
-  },
-
-  /**
    * Create a new style sheet in the document with the given text.
    * Return an actor for it.
    *
    * @param  {object} request
    *         Debugging protocol request object, with 'text property'
    * @return {object}
    *         Object with 'styelSheet' property for form on new actor.
    */
-  onNewStyleSheet: function(request) {
+  addStyleSheet: method(function(text) {
     let parent = this.document.documentElement;
     let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
     style.setAttribute("type", "text/css");
 
-    if (request.text) {
-      style.appendChild(this.document.createTextNode(request.text));
+    if (text) {
+      style.appendChild(this.document.createTextNode(text));
     }
     parent.appendChild(style);
 
     let actor = this._createStyleSheetActor(style.sheet);
-    return { styleSheet: actor.form() };
-  }
-};
+    return actor;
+  }, {
+    request: { text: Arg(0, "string") },
+    response: { styleSheet: RetVal("stylesheet") }
+  })
+});
 
 /**
- * The request types this actor can handle.
+ * The corresponding Front object for the StyleSheetsActor.
  */
-StyleEditorActor.prototype.requestTypes = {
-  "getStyleSheets": StyleEditorActor.prototype.onGetStyleSheets,
-  "newStyleSheet": StyleEditorActor.prototype.onNewStyleSheet,
-  "getBaseURI": StyleEditorActor.prototype.onGetBaseURI,
-  "newDocument": StyleEditorActor.prototype.onNewDocument
-};
+let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, {
+  initialize: function(client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.styleSheetsActor;
 
-
-function StyleSheetActor(aStyleSheet, aParentActor) {
-  this.styleSheet = aStyleSheet;
-  this.parentActor = aParentActor;
-
-  // text and index are unknown until source load
-  this.text = null;
-  this._styleSheetIndex = -1;
+    client.addActorPool(this);
+    this.manage(this);
+  }
+});
 
-  this._transitionRefCount = 0;
-
-  this._onSourceLoad = this._onSourceLoad.bind(this);
+/**
+ * A StyleSheetActor represents a stylesheet on the server.
+ */
+let StyleSheetActor = protocol.ActorClass({
+  typeName: "stylesheet",
 
-  // if this sheet has an @import, then it's rules are loaded async
-  let ownerNode = this.styleSheet.ownerNode;
-  if (ownerNode) {
-    let onSheetLoaded = function(event) {
-      ownerNode.removeEventListener("load", onSheetLoaded, false);
-      this._notifyPropertyChanged("ruleCount");
-    }.bind(this);
+  events: {
+    "property-change" : {
+      type: "propertyChange",
+      property: Arg(0, "string"),
+      value: Arg(1, "json")
+    },
+    "style-applied" : {
+      type: "styleApplied"
+    }
+  },
 
-    ownerNode.addEventListener("load", onSheetLoaded, false);
-  }
-}
-
-StyleSheetActor.prototype = {
-  actorPrefix: "stylesheet",
+  /* List of original sources that generated this stylesheet */
+  _originalSources: null,
 
   toString: function() {
     return "[StyleSheetActor " + this.actorID + "]";
   },
 
-  disconnect: function() {
-    this.parentActor.releaseActor(this);
-  },
-
   /**
    * Window of target
    */
-  get window() this.parentActor.window,
+  get window() this._window || this.parentActor.window,
 
   /**
    * Document of target.
    */
   get document() this.window.document,
 
   /**
+   * URL of underlying stylesheet.
+   */
+  get href() this.rawSheet.href,
+
+  /**
    * Retrieve the index (order) of stylesheet in the document.
    *
    * @return number
    */
   get styleSheetIndex()
   {
     if (this._styleSheetIndex == -1) {
       for (let i = 0; i < this.document.styleSheets.length; i++) {
-        if (this.document.styleSheets[i] == this.styleSheet) {
+        if (this.document.styleSheets[i] == this.rawSheet) {
           this._styleSheetIndex = i;
           break;
         }
       }
     }
     return this._styleSheetIndex;
   },
 
+  initialize: function(aStyleSheet, aParentActor, aWindow) {
+    protocol.Actor.prototype.initialize.call(this, null);
+
+    this.rawSheet = aStyleSheet;
+    this.parentActor = aParentActor;
+    this.conn = this.parentActor.conn;
+
+    this._window = aWindow;
+
+    // text and index are unknown until source load
+    this.text = null;
+    this._styleSheetIndex = -1;
+
+    this._transitionRefCount = 0;
+
+    // if this sheet has an @import, then it's rules are loaded async
+    let ownerNode = this.rawSheet.ownerNode;
+    if (ownerNode) {
+      let onSheetLoaded = function(event) {
+        ownerNode.removeEventListener("load", onSheetLoaded, false);
+        this._notifyPropertyChanged("ruleCount");
+      }.bind(this);
+
+      ownerNode.addEventListener("load", onSheetLoaded, false);
+    }
+  },
+
   /**
    * Get the current state of the actor
    *
    * @return {object}
    *         With properties of the underlying stylesheet, plus 'text',
    *        'styleSheetIndex' and 'parentActor' if it's @imported
    */
-  form: function() {
+  form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+
+    let docHref;
+    if (this.rawSheet.ownerNode) {
+      if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) {
+        docHref = this.rawSheet.ownerNode.location.href;
+      }
+      if (this.rawSheet.ownerNode.ownerDocument) {
+        docHref = this.rawSheet.ownerNode.ownerDocument.location.href;
+      }
+    }
+
     let form = {
       actor: this.actorID,  // actorID is set when this actor is added to a pool
-      href: this.styleSheet.href,
-      disabled: this.styleSheet.disabled,
-      title: this.styleSheet.title,
-      styleSheetIndex: this.styleSheetIndex,
-      text: this.text
-    }
-
-    // get parent actor if this sheet was @imported
-    let parent = this.styleSheet.parentStyleSheet;
-    if (parent) {
-      form.parentActor = this.parentActor._sheets.get(parent).form();
+      href: this.href,
+      nodeHref: docHref,
+      disabled: this.rawSheet.disabled,
+      title: this.rawSheet.title,
+      system: !CssLogic.isContentStylesheet(this.rawSheet),
+      styleSheetIndex: this.styleSheetIndex
     }
 
     try {
-      form.ruleCount = this.styleSheet.cssRules.length;
+      form.ruleCount = this.rawSheet.cssRules.length;
     }
     catch(e) {
       // stylesheet had an @import rule that wasn't loaded yet
     }
-
     return form;
   },
 
   /**
    * Toggle the disabled property of the style sheet
    *
    * @return {object}
    *         'disabled' - the disabled state after toggling.
    */
-  onToggleDisabled: function() {
-    this.styleSheet.disabled = !this.styleSheet.disabled;
+  toggleDisabled: method(function() {
+    this.rawSheet.disabled = !this.rawSheet.disabled;
     this._notifyPropertyChanged("disabled");
 
-    return { disabled: this.styleSheet.disabled };
-  },
+    return this.rawSheet.disabled;
+  }, {
+    response: { disabled: RetVal("boolean")}
+  }),
 
   /**
    * Send an event notifying that a property of the stylesheet
    * has changed.
    *
    * @param  {string} property
    *         Name of the changed property
    */
   _notifyPropertyChanged: function(property) {
-    this.conn.send({
-      from: this.actorID,
-      type: "propertyChange",
-      property: property,
-      value: this.form()[property]
+    events.emit(this, "property-change", property, this.form()[property]);
+  },
+
+  /**
+   * Protocol method to get the text of this stylesheet.
+   */
+  getText: method(function() {
+    return this._getText().then((text) => {
+      return new LongStringActor(this.conn, text || "");
+    });
+  }, {
+    response: {
+      text: RetVal("longstring")
+    }
+  }),
+
+  /**
+   * Fetch the text for this stylesheet from the cache or network. Return
+   * cached text if it's already been fetched.
+   *
+   * @return {Promise}
+   *         Promise that resolves with a string text of the stylesheet.
+   */
+  _getText: function() {
+    if (this.text) {
+      return promise.resolve(this.text);
+    }
+
+    if (!this.href) {
+      // this is an inline <style> sheet
+      let content = this.rawSheet.ownerNode.textContent;
+      this.text = content;
+      return promise.resolve(content);
+    }
+
+    let options = {
+      window: this.window,
+      charset: this._getCSSCharset()
+    };
+
+    return fetch(this.href, options).then(({ content }) => {
+      this.text = content;
+      return content;
+    });
+  },
+
+  /**
+   * Protocol method to get the original source (actors) for this
+   * stylesheet if it has uses source maps.
+   */
+  getOriginalSources: method(function() {
+    if (this._originalSources) {
+      return promise.resolve(this._originalSources);
+    }
+    return this._fetchOriginalSources();
+  }, {
+    request: {},
+    response: {
+      originalSources: RetVal("nullable:array:originalsource")
+    }
+  }),
+
+  /**
+   * Fetch the original sources (actors) for this style sheet using its
+   * source map. If they've already been fetched, returns cached array.
+   *
+   * @return {Promise}
+   *         Promise that resolves with an array of OriginalSourceActors
+   */
+  _fetchOriginalSources: function() {
+    this._clearOriginalSources();
+    this._originalSources = [];
+
+    return this.getSourceMap().then((sourceMap) => {
+      if (!sourceMap) {
+        return null;
+      }
+      for (let url of sourceMap.sources) {
+        let actor = new OriginalSourceActor(url, sourceMap, this);
+
+        this.manage(actor);
+        this._originalSources.push(actor);
+      }
+      return this._originalSources;
     })
   },
 
   /**
-   * Handler for event when the style sheet's full text has been
-   * loaded from its source.
+   * Get the SourceMapConsumer for this stylesheet's source map, if
+   * it exists. Saves the consumer for later queries.
    *
-   * @param  {string} error
-   *         Error from source load, null if no error
-   * @param  {string} source
-   *         Text of the style sheet
-   * @param  {[type]} charset
-   *         Optional charset of the source
+   * @return {Promise}
+   *         A promise that resolves with a SourceMapConsumer, or null.
+   */
+  getSourceMap: function() {
+    if (this._sourceMap) {
+      return this._sourceMap;
+    }
+    return this._fetchSourceMap();
+  },
+
+  /**
+   * Fetch the source map for this stylesheet.
+   *
+   * @return {Promise}
+   *         A promise that resolves with a SourceMapConsumer, or null.
    */
-  _onSourceLoad: function(error, source, charset) {
-    let message = {
-      from: this.actorID,
-      type: "sourceLoad",
-    };
+  _fetchSourceMap: function() {
+    let deferred = promise.defer();
+
+    this._getText().then((content) => {
+      let url = this._extractSourceMapUrl(content);
+      if (!url) {
+        // no source map for this stylesheet
+        deferred.resolve(null);
+        return;
+      };
+
+      url = normalize(url, this.href);
+
+      let map = fetch(url, { loadFromCache: false, window: this.window })
+        .then(({content}) => {
+          let map = new SourceMapConsumer(content);
+          this._setSourceMapRoot(map, url, this.href);
+          this._sourceMap = promise.resolve(map);
 
-    if (error) {
-      message.error = error;
+          deferred.resolve(map);
+          return map;
+        }, deferred.reject);
+
+      this._sourceMap = map;
+    }, deferred.reject);
+
+    return deferred.promise;
+  },
+
+  /**
+   * Clear and unmanage the original source actors for this stylesheet.
+   */
+  _clearOriginalSources: function() {
+    for (actor in this._originalSources) {
+      this.unmanage(actor);
     }
-    else {
-      this.text = this._decodeCSSCharset(source, charset || "");
-      message.source = this.text;
-    }
-
-    this.conn.send(message);
+    this._originalSources = null;
   },
 
   /**
-   * Fetch the source of the style sheet from its URL
+   * Sets the source map's sourceRoot to be relative to the source map url.
    */
-  onFetchSource: function() {
-    if (!this.styleSheet.href) {
-      // this is an inline <style> sheet
-      let source = this.styleSheet.ownerNode.textContent;
-      this._onSourceLoad(null, source);
-      return {};
-    }
+  _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) {
+    const base = dirname(
+      aAbsSourceMapURL.indexOf("data:") === 0
+        ? aScriptURL
+        : aAbsSourceMapURL);
+    aSourceMap.sourceRoot = aSourceMap.sourceRoot
+      ? normalize(aSourceMap.sourceRoot, base)
+      : base;
+  },
 
-    let scheme = Services.io.extractScheme(this.styleSheet.href);
-    switch (scheme) {
-      case "file":
-        this._styleSheetFilePath = this.styleSheet.href;
-      case "chrome":
-      case "resource":
-        this._loadSourceFromFile(this.styleSheet.href);
-        break;
-      default:
-        this._loadSourceFromCache(this.styleSheet.href);
-        break;
+  /**
+   * Get the source map url specified in the text of a stylesheet.
+   *
+   * @param  {string} content
+   *         The text of the style sheet.
+   * @return {string}
+   *         Url of source map.
+   */
+  _extractSourceMapUrl: function(content) {
+    var matches = /sourceMappingURL\=([^\s\*]*)/.exec(content);
+    if (matches) {
+      return matches[1];
     }
-    return {};
+    return null;
   },
 
   /**
-   * Decode a CSS source string to unicode according to the character set rules
+   * Protocol method that gets the location in the original source of a
+   * line, column pair in this stylesheet, if its source mapped, otherwise
+   * a promise of the same location.
+   */
+  getOriginalLocation: method(function(line, column) {
+    return this.getSourceMap().then((sourceMap) => {
+      if (sourceMap) {
+        return sourceMap.originalPositionFor({ line: line, column: column });
+      }
+      return {
+        source: this.href,
+        line: line,
+        column: column
+      }
+    });
+  }, {
+    request: {
+      line: Arg(0, "number"),
+      column: Arg(1, "number")
+    },
+    response: RetVal(types.addDictType("originallocationresponse", {
+      source: "string",
+      line: "number",
+      column: "number"
+    }))
+  }),
+
+  /**
+   * Get the charset of the stylesheet according to the character set rules
    * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>.
    *
-   * @param string string
-   *        Source of a CSS stylesheet, loaded from file or cache.
    * @param string channelCharset
    *        Charset of the source string if set by the HTTP channel.
-   * @return string
-   *         The CSS string, in unicode.
    */
-  _decodeCSSCharset: function(string, channelCharset)
+  _getCSSCharset: function(channelCharset)
   {
     // StyleSheet's charset can be specified from multiple sources
-
-    if (channelCharset.length > 0) {
+    if (channelCharset && channelCharset.length > 0) {
       // step 1 of syndata.html: charset given in HTTP header.
-      return this._convertToUnicode(string, channelCharset);
+      return channelCharset;
     }
 
-    let sheet = this.styleSheet;
+    let sheet = this.rawSheet;
     if (sheet) {
       // Do we have a @charset rule in the stylesheet?
       // step 2 of syndata.html (without the BOM check).
       if (sheet.cssRules) {
         let rules = sheet.cssRules;
         if (rules.length
             && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
-          return this._convertToUnicode(string, rules.item(0).encoding);
+          return rules.item(0).encoding;
         }
       }
 
       // step 3: charset attribute of <link> or <style> element, if it exists
       if (sheet.ownerNode && sheet.ownerNode.getAttribute) {
         let linkCharset = sheet.ownerNode.getAttribute("charset");
         if (linkCharset != null) {
-          return this._convertToUnicode(string, linkCharset);
+          return linkCharset;
         }
       }
 
       // step 4 (1 of 2): charset of referring stylesheet.
       let parentSheet = sheet.parentStyleSheet;
       if (parentSheet && parentSheet.cssRules &&
           parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
-        return this._convertToUnicode(string,
-            parentSheet.cssRules[0].encoding);
+        return parentSheet.cssRules[0].encoding;
       }
 
       // step 4 (2 of 2): charset of referring document.
       if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) {
-        return this._convertToUnicode(string,
-            sheet.ownerNode.ownerDocument.characterSet);
+        return sheet.ownerNode.ownerDocument.characterSet;
       }
     }
 
     // step 5: default to utf-8.
-    return this._convertToUnicode(string, "UTF-8");
-  },
-
-  /**
-   * Convert a given string, encoded in a given character set, to unicode.
-   *
-   * @param string string
-   *        A string.
-   * @param string charset
-   *        A character set.
-   * @return string
-   *         A unicode string.
-   */
-  _convertToUnicode: function(string, charset) {
-    // Decoding primitives.
-    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
-        .createInstance(Ci.nsIScriptableUnicodeConverter);
-
-    try {
-      converter.charset = charset;
-      return converter.ConvertToUnicode(string);
-    } catch(e) {
-      return string;
-    }
+    return "UTF-8";
   },
 
   /**
-   * Load source from a file or file-like resource.
-   *
-   * @param string href
-   *        URL for the stylesheet.
-   */
-  _loadSourceFromFile: function(href)
-  {
-    try {
-      NetUtil.asyncFetch(href, (stream, status) => {
-        if (!Components.isSuccessCode(status)) {
-          this._onSourceLoad(LOAD_ERROR);
-          return;
-        }
-        let source = NetUtil.readInputStreamToString(stream, stream.available());
-        stream.close();
-        this._onSourceLoad(null, source);
-      });
-    } catch (ex) {
-      this._onSourceLoad(LOAD_ERROR);
-    }
-  },
-
-  /**
-   * Load source from the HTTP cache.
-   *
-   * @param string href
-   *        URL for the stylesheet.
-   */
-  _loadSourceFromCache: function(href)
-  {
-    let channel = Services.io.newChannel(href, null, null);
-    let chunks = [];
-    let channelCharset = "";
-    let streamListener = { // nsIStreamListener inherits nsIRequestObserver
-      onStartRequest: (aRequest, aContext, aStatusCode) => {
-        if (!Components.isSuccessCode(aStatusCode)) {
-          this._onSourceLoad(LOAD_ERROR);
-        }
-      },
-      onDataAvailable: (aRequest, aContext, aStream, aOffset, aCount) => {
-        let channel = aRequest.QueryInterface(Ci.nsIChannel);
-        if (!channelCharset) {
-          channelCharset = channel.contentCharset;
-        }
-        chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
-      },
-      onStopRequest: (aRequest, aContext, aStatusCode) => {
-        if (!Components.isSuccessCode(aStatusCode)) {
-          this._onSourceLoad(LOAD_ERROR);
-          return;
-        }
-        let source = chunks.join("");
-        this._onSourceLoad(null, source, channelCharset);
-      }
-    };
-
-    channel.loadGroup = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
-                            .getInterface(Ci.nsIWebNavigation)
-                            .QueryInterface(Ci.nsIDocumentLoader)
-                            .loadGroup;
-    channel.loadFlags = channel.LOAD_FROM_CACHE;
-    channel.asyncOpen(streamListener, null);
-  },
-
-  /**
-   * Update the style sheet in place with new text
+   * Update the style sheet in place with new text.
    *
    * @param  {object} request
    *         'text' - new text
    *         'transition' - whether to do CSS transition for change.
    */
-  onUpdate: function(request) {
-    DOMUtils.parseStyleSheet(this.styleSheet, request.text);
+  update: method(function(text, transition) {
+    DOMUtils.parseStyleSheet(this.rawSheet, text);
+
+    this.text = text;
 
     this._notifyPropertyChanged("ruleCount");
 
-    if (request.transition) {
+    if (transition) {
       this._insertTransistionRule();
     }
     else {
       this._notifyStyleApplied();
     }
-
-    return {};
-  },
+  }, {
+    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() {
     // Insert the global transition rule
     // Use a ref count to make sure we do not add it multiple times.. and remove
-    // it only when all pending StyleEditor-generated transitions ended.
+    // it only when all pending StyleSheets-generated transitions ended.
     if (this._transitionRefCount == 0) {
-      this.styleSheet.insertRule(TRANSITION_RULE, this.styleSheet.cssRules.length);
+      this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
       this.document.documentElement.classList.add(TRANSITION_CLASS);
     }
 
     this._transitionRefCount++;
 
     // Set up clean up and commit after transition duration (+10% buffer)
     // @see _onTransitionEnd
     this.window.setTimeout(this._onTransitionEnd.bind(this),
@@ -676,38 +702,285 @@ StyleSheetActor.prototype = {
   /**
     * This cleans up class and rule added for transition effect and then
     * notifies that the style has been applied.
     */
   _onTransitionEnd: function()
   {
     if (--this._transitionRefCount == 0) {
       this.document.documentElement.classList.remove(TRANSITION_CLASS);
-      this.styleSheet.deleteRule(this.styleSheet.cssRules.length - 1);
+      this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1);
     }
 
-    this._notifyStyleApplied();
+    events.emit(this, "style-applied");
+  }
+})
+
+/**
+ * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
+ */
+var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
+  initialize: function(conn, form, ctx, detail) {
+    protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail);
+
+    this._onPropertyChange = this._onPropertyChange.bind(this);
+    events.on(this, "property-change", this._onPropertyChange);
+  },
+
+  destroy: function() {
+    events.off(this, "property-change", this._onPropertyChange);
+
+    protocol.Front.prototype.destroy.call(this);
+  },
+
+  _onPropertyChange: function(property, value) {
+    this._form[property] = value;
+  },
+
+  form: function(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+    this.actorID = form.actor;
+    this._form = form;
+  },
+
+  get href() this._form.href,
+  get nodeHref() this._form.nodeHref,
+  get disabled() !!this._form.disabled,
+  get title() this._form.title,
+  get isSystem() this._form.system,
+  get styleSheetIndex() this._form.styleSheetIndex,
+  get ruleCount() this._form.ruleCount
+});
+
+/**
+ * Actor representing an original source of a style sheet that was specified
+ * in a source map.
+ */
+let OriginalSourceActor = protocol.ActorClass({
+  typeName: "originalsource",
+
+  initialize: function(aUrl, aSourceMap, aParentActor) {
+    protocol.Actor.prototype.initialize.call(this, null);
+
+    this.url = aUrl;
+    this.sourceMap = aSourceMap;
+    this.parentActor = aParentActor;
+    this.conn = this.parentActor.conn;
+
+    this.text = null;
+  },
+
+  form: function() {
+    return {
+      actor: this.actorID, // actorID is set when it's added to a pool
+      url: this.url,
+      parentSource: this.parentActor.actorID
+    };
+  },
+
+  _getText: function() {
+    if (this.text) {
+      return promise.resolve(this.text);
+    }
+    return fetch(this.url, { window: this.window }).then(({content}) => {
+      this.text = content;
+      return content;
+    });
   },
 
   /**
-   * Send and event notifying that the new style has been applied fully.
+   * Protocol method to get the text of this source.
    */
-  _notifyStyleApplied: function()
-  {
-    this.conn.send({
-      from: this.actorID,
-      type: "styleApplied"
-    })
-  }
-}
+  getText: method(function() {
+    return this._getText().then((text) => {
+      return new LongStringActor(this.conn, text || "");
+    });
+  }, {
+    response: {
+      text: RetVal("longstring")
+    }
+  })
+})
 
-StyleSheetActor.prototype.requestTypes = {
-  "toggleDisabled": StyleSheetActor.prototype.onToggleDisabled,
-  "fetchSource": StyleSheetActor.prototype.onFetchSource,
-  "update": StyleSheetActor.prototype.onUpdate
-};
+/**
+ * The client-side counterpart for an OriginalSourceActor.
+ */
+let OriginalSourceFront = protocol.FrontClass(OriginalSourceActor, {
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+
+    this.isOriginalSource = true;
+  },
 
-DebuggerServer.addTabActor(StyleEditorActor, "styleEditorActor");
-DebuggerServer.addGlobalActor(StyleEditorActor, "styleEditorActor");
+  form: function(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+    this.actorID = form.actor;
+    this._form = form;
+  },
+
+  get href() this._form.url,
+  get url() this._form.url
+});
+
 
 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
+
+exports.StyleSheetsActor = StyleSheetsActor;
+exports.StyleSheetsFront = StyleSheetsFront;
+
+exports.StyleSheetActor = StyleSheetActor;
+exports.StyleSheetFront = StyleSheetFront;
+
+
+/**
+ * Performs a request to load the desired URL and returns a promise.
+ *
+ * @param aURL String
+ *        The URL we will request.
+ * @returns Promise
+ *        A promise of the document at that URL, as a string.
+ */
+function fetch(aURL, aOptions={ loadFromCache: true, window: null,
+                                charset: null}) {
+  let deferred = promise.defer();
+  let scheme;
+  let url = aURL.split(" -> ").pop();
+  let charset;
+  let contentType;
+
+  try {
+    scheme = Services.io.extractScheme(url);
+  } catch (e) {
+    // In the xpcshell tests, the script url is the absolute path of the test
+    // file, which will make a malformed URI error be thrown. Add the file
+    // scheme prefix ourselves.
+    url = "file://" + url;
+    scheme = Services.io.extractScheme(url);
+  }
+
+  switch (scheme) {
+    case "file":
+    case "chrome":
+    case "resource":
+      try {
+        NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
+          if (!components.isSuccessCode(aStatus)) {
+            deferred.reject(new Error("Request failed with status code = "
+                                      + aStatus
+                                      + " after NetUtil.asyncFetch for url = "
+                                      + url));
+            return;
+          }
+
+          let source = NetUtil.readInputStreamToString(aStream, aStream.available());
+          contentType = aRequest.contentType;
+          deferred.resolve(source);
+          aStream.close();
+        });
+      } catch (ex) {
+        deferred.reject(ex);
+      }
+      break;
+
+    default:
+      let channel;
+      try {
+        channel = Services.io.newChannel(url, null, null);
+      } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
+        // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
+        // newChannel won't be able to handle it.
+        url = "file:///" + url;
+        channel = Services.io.newChannel(url, null, null);
+      }
+      let chunks = [];
+      let streamListener = {
+        onStartRequest: function(aRequest, aContext, aStatusCode) {
+          if (!components.isSuccessCode(aStatusCode)) {
+            deferred.reject(new Error("Request failed with status code = "
+                                      + aStatusCode
+                                      + " in onStartRequest handler for url = "
+                                      + url));
+          }
+        },
+        onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+          chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
+        },
+        onStopRequest: function(aRequest, aContext, aStatusCode) {
+          if (!components.isSuccessCode(aStatusCode)) {
+            deferred.reject(new Error("Request failed with status code = "
+                                      + aStatusCode
+                                      + " in onStopRequest handler for url = "
+                                      + url));
+            return;
+          }
+
+          charset = channel.contentCharset || charset;
+          contentType = channel.contentType;
+          deferred.resolve(chunks.join(""));
+        }
+      };
+
+      if (aOptions.window) {
+        // respect private browsing
+        channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebNavigation)
+                              .QueryInterface(Ci.nsIDocumentLoader)
+                              .loadGroup;
+      }
+      channel.loadFlags = aOptions.loadFromCache
+        ? channel.LOAD_FROM_CACHE
+        : channel.LOAD_BYPASS_CACHE;
+      channel.asyncOpen(streamListener, null);
+      break;
+  }
+
+  return deferred.promise.then(source => {
+    return {
+      content: convertToUnicode(source, charset),
+      contentType: contentType
+    };
+  });
+}
+
+/**
+ * Convert a given string, encoded in a given character set, to unicode.
+ *
+ * @param string aString
+ *        A string.
+ * @param string aCharset
+ *        A character set.
+ */
+function convertToUnicode(aString, aCharset=null) {
+  // Decoding primitives.
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  try {
+    converter.charset = aCharset || "UTF-8";
+    return converter.ConvertToUnicode(aString);
+  } catch(e) {
+    return aString;
+  }
+}
+
+/**
+ * Normalize multiple relative paths towards the base paths on the right.
+ */
+function normalize(...aURLs) {
+  let base = Services.io.newURI(aURLs.pop(), null, null);
+  let url;
+  while ((url = aURLs.pop())) {
+    base = Services.io.newURI(url, null, base);
+  }
+  return base.spec;
+}
+
+function dirname(aPath) {
+  return Services.io.newURI(
+    ".", null, Services.io.newURI(aPath, null, null)).spec;
+}
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -1,20 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci} = require("chrome");
+const promise = require("sdk/core/promise");
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal, types} = protocol;
 const events = require("sdk/event/core");
 const object = require("sdk/util/object");
 const { Class } = require("sdk/core/heritage");
+const { StyleSheetActor } = require("devtools/server/actors/styleeditor");
 
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
 loader.lazyGetter(this, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils));
 
 // 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.
@@ -99,17 +101,17 @@ var PageStyleActor = protocol.ActorClass
   /**
    * Return or create a StyleSheetActor for the given
    * nsIDOMCSSStyleSheet
    */
   _sheetRef: function(sheet) {
     if (this.refMap.has(sheet)) {
       return this.refMap.get(sheet);
     }
-    let actor = StyleSheetActor(this, sheet);
+    let actor = new StyleSheetActor(sheet, this, this.walker.rootWin);
     this.manage(actor);
     this.refMap.set(sheet, actor);
 
     return actor;
   },
 
   /**
    * Get the computed style for a node.
@@ -248,17 +250,17 @@ var PageStyleActor = protocol.ActorClass
   }, {
     request: {
       node: Arg(0, "domnode"),
       property: Arg(1, "string"),
       filter: Option(2, "string")
     },
     response: RetVal(types.addDictType("matchedselectorresponse", {
       rules: "array:domstylerule",
-      sheets: "array:domsheet",
+      sheets: "array:stylesheet",
       matched: "array:matchedselector"
     }))
   }),
 
   // Get a selector source for a CssSelectorInfo relative to a given
   // node.
   getSelectorSource: function(selectorInfo, relativeTo) {
     let result = selectorInfo.selector.text;
@@ -334,17 +336,17 @@ var PageStyleActor = protocol.ActorClass
       node: Arg(0, "domnode"),
       inherited: Option(1, "boolean"),
       matchedSelectors: Option(1, "boolean"),
       filter: Option(1, "string")
     },
     response: RetVal(types.addDictType("appliedStylesReturn", {
       entries: "array:appliedstyle",
       rules: "array:domstylerule",
-      sheets: "array:domsheet"
+      sheets: "array:stylesheet"
     }))
   }),
 
   _hasInheritedProps: function(style) {
     return Array.prototype.some.call(style, prop => {
       return DOMUtils.isInheritedProperty(prop);
     });
   },
@@ -541,83 +543,16 @@ var PageStyleFront = protocol.FrontClass
     return this._getApplied(node, options).then(ret => {
       return ret.entries;
     });
   }, {
     impl: "_getApplied"
   })
 });
 
-/**
- * Actor representing an nsIDOMCSSStyleSheet.
- */
-var StyleSheetActor = protocol.ActorClass({
-  typeName: "domsheet",
-
-  initialize: function(pageStyle, sheet) {
-    protocol.Front.prototype.initialize.call(this);
-    this.pageStyle = pageStyle;
-    this.rawSheet = sheet;
-  },
-
-  get conn() this.pageStyle.conn,
-
-  form: function(detail) {
-    if (detail === "actorid") {
-      return this.actorID;
-    }
-
-    let href;
-    if (this.rawSheet.ownerNode) {
-      if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument)
-        href = this.rawSheet.ownerNode.location.href;
-      if (this.rawSheet.ownerNode.ownerDocument)
-        href = this.rawSheet.ownerNode.ownerDocument.location.href;
-    }
-
-    return {
-      actor: this.actorID,
-
-      // href stores the uri of the sheet
-      href: this.rawSheet.href,
-
-      // nodeHref stores the URI of the document that
-      // included the sheet.
-      nodeHref: href,
-
-      system: !CssLogic.isContentStylesheet(this.rawSheet),
-      disabled: this.rawSheet.disabled ? true : undefined
-    }
-  }
-});
-
-/**
- * Front for the StyleSheetActor.
- */
-var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
-  initialize: function(conn, form, ctx, detail) {
-    protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail);
-  },
-
-  form: function(form, detail) {
-    if (detail === "actorid") {
-      this.actorID = form;
-      return;
-    }
-    this.actorID = form.actorID;
-    this._form = form;
-  },
-
-  get href() this._form.href,
-  get nodeHref() this._form.nodeHref,
-  get disabled() !!this._form.disabled,
-  get isSystem() this._form.system
-});
-
-
 // Predeclare the domstylerule actor type
 types.addActorType("domstylerule");
 
 /**
  * An actor that represents a CSS style object on the protocol.
  *
  * We slightly flatten the CSSOM for this actor, it represents
  * both the CSSRule and CSSStyle objects in one actor.  For nodes
@@ -631,16 +566,17 @@ var StyleRuleActor = protocol.ActorClass
     this.pageStyle = pageStyle;
     this.rawStyle = item.style;
 
     if (item instanceof (Ci.nsIDOMCSSRule)) {
       this.type = item.type;
       this.rawRule = item;
       if (this.rawRule instanceof Ci.nsIDOMCSSStyleRule && this.rawRule.parentStyleSheet) {
         this.line = DOMUtils.getRuleLine(this.rawRule);
+        this.column = DOMUtils.getRuleColumn(this.rawRule);
       }
     } else {
       // Fake a rule
       this.type = ELEMENT_STYLE;
       this.rawNode = item;
       this.rawRule = {
         style: item.style,
         toString: function() "[element rule " + this.style + "]"
@@ -660,16 +596,17 @@ var StyleRuleActor = protocol.ActorClass
     if (detail === "actorid") {
       return this.actorID;
     }
 
     let form = {
       actor: this.actorID,
       type: this.type,
       line: this.line || undefined,
+      column: this.column
     };
 
     if (this.rawRule.parentRule) {
       form.parentRule = this.pageStyle._styleRef(this.rawRule.parentRule).actorID;
     }
     if (this.rawRule.parentStyleSheet) {
       form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID;
     }
@@ -783,16 +720,17 @@ var StyleRuleFront = protocol.FrontClass
    * Return a new RuleModificationList for this node.
    */
   startModifyingProperties: function() {
   return new RuleModificationList(this);
   },
 
   get type() this._form.type,
   get line() this._form.line || -1,
+  get column() this._form.column || -1,
   get cssText() {
     return this._form.cssText;
   },
   get selectors() {
     return this._form.selectors;
   },
   get media() {
     return this._form.media;
@@ -823,16 +761,50 @@ var StyleRuleFront = protocol.FrontClass
   get href() {
     if (this._form.href) {
       return this._form.href;
     }
     let sheet = this.parentStyleSheet;
     return sheet.href || sheet.nodeHref;
   },
 
+  get location()
+  {
+    return {
+      href: this.href,
+      line: this.line,
+      column: this.column
+    };
+  },
+
+  getOriginalLocation: function()
+  {
+    if (this._originalLocation) {
+      return promise.resolve(this._originalLocation);
+    }
+
+    let parentSheet = this.parentStyleSheet;
+    if (!parentSheet) {
+      return promise.resolve(this.location);
+    }
+    return parentSheet.getOriginalLocation(this.line, this.column)
+      .then(({ source, line, column }) => {
+        let location = {
+          href: source,
+          line: line,
+          column: column
+        }
+        if (!source) {
+          location.href = this.href;
+        }
+        this._originalLocation = location;
+        return location;
+      })
+  },
+
   // Only used for testing, please keep it that way.
   _rawStyle: function() {
     if (!this.conn._transport._serverConnection) {
       console.warn("Tried to use rawNode on a remote connection.");
       return null;
     }
     let actor = this.conn._transport._serverConnection.getActor(this.actorID);
     if (!actor) {
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -347,39 +347,39 @@ var DebuggerServer = {
     this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
     this.addActors("resource://gre/modules/devtools/server/actors/script.js");
     this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger");
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
-    this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     this.registerModule("devtools/server/actors/inspector");
     this.registerModule("devtools/server/actors/webgl");
     this.registerModule("devtools/server/actors/tracer");
     this.registerModule("devtools/server/actors/device");
+    this.registerModule("devtools/server/actors/styleeditor");
   },
 
   /**
    * Install tab actors in documents loaded in content childs
    */
   addChildActors: function () {
     // In case of apps being loaded in parent process, DebuggerServer is already
     // initialized and browser actors are already loaded,
     // but childtab.js hasn't been loaded yet.
     if (!("BrowserTabActor" in this)) {
       this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
       this.addActors("resource://gre/modules/devtools/server/actors/script.js");
       this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
       this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
-      this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
       this.registerModule("devtools/server/actors/inspector");
       this.registerModule("devtools/server/actors/webgl");
+      this.registerModule("devtools/server/actors/styleeditor");
     }
     if (!("ContentAppActor" in DebuggerServer)) {
       this.addActors("resource://gre/modules/devtools/server/actors/childtab.js");
     }
   },
 
   /**
    * Listens on the given port or socket file for remote debugger connections.