Bug 926014 - Support CSS source maps; r=dcamp
authorHeather Arthur <fayearthur@gmail.com>
Fri, 06 Dec 2013 23:52:32 -0800
changeset 159891 4204ff8d023412b5905acc2cc104dbffacd9d78d
parent 159890 ca62c888ae7d303611b1eb8a898083e075001804
child 159892 2095335b9a6592fc45f0b9034fe5022648f06d36
push id37461
push userryanvm@gmail.com
push dateWed, 11 Dec 2013 18:38:20 +0000
treeherdermozilla-inbound@d34815b6879b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs926014
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 926014 - Support CSS source maps; r=dcamp
browser/app/profile/firefox.js
browser/devtools/main.js
browser/devtools/styleeditor/Makefile.in
browser/devtools/styleeditor/StyleEditorDebuggee.jsm
browser/devtools/styleeditor/StyleEditorPanel.jsm
browser/devtools/styleeditor/StyleEditorUI.jsm
browser/devtools/styleeditor/StyleSheetEditor.jsm
browser/devtools/styleeditor/styleeditor-panel.js
browser/devtools/styleeditor/test/browser.ini
browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js
browser/devtools/styleeditor/test/browser_styleeditor_new.js
browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js
browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js
browser/devtools/styleeditor/test/browser_styleeditor_reload.js
browser/devtools/styleeditor/test/browser_styleeditor_sourcemaps.js
browser/devtools/styleeditor/test/head.js
browser/devtools/styleeditor/test/sourcemaps.css
browser/devtools/styleeditor/test/sourcemaps.css.map
browser/devtools/styleeditor/test/sourcemaps.html
browser/devtools/styleeditor/test/sourcemaps.scss
browser/devtools/styleinspector/computed-view.js
browser/devtools/styleinspector/computedview.xhtml
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/style-inspector.js
browser/devtools/styleinspector/test/browser.ini
browser/devtools/styleinspector/test/browser_computedview_original_source_link.js
browser/devtools/styleinspector/test/browser_ruleview_original_source_link.js
browser/devtools/styleinspector/test/head.js
browser/devtools/styleinspector/test/sourcemaps.css
browser/devtools/styleinspector/test/sourcemaps.css.map
browser/devtools/styleinspector/test/sourcemaps.html
browser/devtools/styleinspector/test/sourcemaps.scss
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/actors/styleeditor.js
toolkit/devtools/server/actors/styles.js
toolkit/devtools/server/main.js
--- 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/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.