Bug 1179820 - Convert style editor to client-side source maps; r?gl draft
authorTom Tromey <tom@tromey.com>
Fri, 29 Sep 2017 07:36:30 -0600
changeset 680885 ad6cfa8e25b6cee04002d60d1482d6083c3f2eb0
parent 680884 0957e155142ee8379c6153dd3c51de119b2f16f7
child 680886 567c1fb3e861bffbd9a48b9026afb1d457a07328
push id84661
push userbmo:ttromey@mozilla.com
push dateMon, 16 Oct 2017 14:19:39 +0000
reviewersgl
bugs1179820
milestone58.0a1
Bug 1179820 - Convert style editor to client-side source maps; r?gl MozReview-Commit-ID: CV53VKKZz4A
devtools/client/framework/source-map-url-service.js
devtools/client/styleeditor/StyleEditorUI.jsm
devtools/client/styleeditor/moz.build
devtools/client/styleeditor/original-source.js
devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js
devtools/client/styleeditor/test/head.js
devtools/shared/fronts/stylesheets.js
--- a/devtools/client/framework/source-map-url-service.js
+++ b/devtools/client/framework/source-map-url-service.js
@@ -133,17 +133,18 @@ SourceMapURLService.prototype._onSourceU
  *        The new style sheet's actor.
  */
 SourceMapURLService.prototype._onNewStyleSheet = function (sheet) {
   // Maybe we were shut down while waiting.
   if (!this._urls) {
     return;
   }
 
-  let {href: url, sourceMapURL, actor: id} = sheet._form;
+  let {href, nodeHref, sourceMapURL, actorID: id} = sheet;
+  let url = href || nodeHref;
   this._urls.set(url, { id, url, sourceMapURL});
   this._idMap.set(id, url);
 };
 
 /**
  * A callback that is called from the lower-level source map service
  * proxy (see toolbox.js) when some tool has installed a new source
  * map.  This happens when pretty-printing a source.
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -25,16 +25,17 @@ const {
 } = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
 const {SplitView} = require("resource://devtools/client/shared/SplitView.jsm");
 const {StyleSheetEditor} = require("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
 const {PluralForm} = require("devtools/shared/plural-form");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 const csscoverage = require("devtools/shared/fronts/csscoverage");
 const {console} = require("resource://gre/modules/Console.jsm");
 const {KeyCodes} = require("devtools/client/shared/keycodes");
+const {OriginalSource} = require("devtools/client/styleeditor/original-source");
 
 loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager", true);
 
 const LOAD_ERROR = "error-load";
 const STYLE_EDITOR_TEMPLATE = "stylesheet";
 const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
 const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar";
 const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.mediaSidebarWidth";
@@ -328,34 +329,45 @@ StyleEditorUI.prototype = {
     if (this._suppressAdd) {
       return null;
     }
 
     if (!this._seenSheets.has(styleSheet)) {
       let promise = (async () => {
         let editor = await this._addStyleSheetEditor(styleSheet, isNew);
 
-        if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+        let toolbox = gDevTools.getToolbox(this._target);
+        let sourceMapService = toolbox.sourceMapService;
+        if (!sourceMapService) {
           return editor;
         }
 
-        let sources = await styleSheet.getOriginalSources();
+        let {href, nodeHref, actorID: id, sourceMapURL} = styleSheet;
+        let url = href || nodeHref;
+        let sources = await sourceMapService.getOriginalURLs({
+          id,
+          url,
+          sourceMapURL,
+        });
         // A single generated sheet might map to multiple original
         // sheets, so make editors for each of them.
         if (sources && sources.length) {
           let parentEditorName = editor.friendlyName;
           this._removeStyleSheetEditor(editor);
           editor = null;
 
           for (let source of sources) {
+            let generatedId = sourceMapService.generatedToOriginalId(id, source);
+            let original = new OriginalSource(source, generatedId, sourceMapService);
+
             // set so the first sheet will be selected, even if it's a source
-            source.styleSheetIndex = styleSheet.styleSheetIndex;
-            source.relatedStyleSheet = styleSheet;
-            source.relatedEditorName = parentEditorName;
-            await this._addStyleSheetEditor(source);
+            original.styleSheetIndex = styleSheet.styleSheetIndex;
+            original.relatedStyleSheet = styleSheet;
+            original.relatedEditorName = parentEditorName;
+            await this._addStyleSheetEditor(original);
           }
         }
 
         return editor;
       })();
       this._seenSheets.set(styleSheet, promise);
     }
     return this._seenSheets.get(styleSheet);
@@ -915,17 +927,19 @@ StyleEditorUI.prototype = {
 
         let location = {
           line: line,
           column: column,
           source: editor.styleSheet.href,
           styleSheet: parentStyleSheet
         };
         if (editor.styleSheet.isOriginalSource) {
-          location = yield editor.cssSheet.getOriginalLocation(line, column);
+          let styleSheet = editor.cssSheet;
+          location = yield editor.styleSheet.getOriginalLocation(styleSheet, line,
+                                                                 column);
         }
 
         // this @media rule is from a different original source
         if (location.source != editor.styleSheet.href) {
           continue;
         }
         inSource = true;
 
--- a/devtools/client/styleeditor/moz.build
+++ b/devtools/client/styleeditor/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # 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/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 DevToolsModules(
+    'original-source.js',
     'styleeditor-commands.js',
     'styleeditor-panel.js',
     'StyleEditorUI.jsm',
     'StyleEditorUtil.jsm',
     'StyleSheetEditor.jsm',
 )
 
 with Files('**'):
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/original-source.js
@@ -0,0 +1,99 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * An object of this type represents an original source for the style
+ * editor.  An "original" source is one that is mentioned in a source
+ * map.
+ *
+ * @param {String} url
+ *        The URL of the original source.
+ * @param {String} sourceID
+ *        The source ID of the original source, as used by the source
+ *        map service.
+ * @param {SourceMapService} sourceMapService
+ *        The source map service; @see Toolbox.sourceMapService
+ */
+function OriginalSource(url, sourceId, sourceMapService) {
+  this.isOriginalSource = true;
+
+  this._url = url;
+  this._sourceId = sourceId;
+  this._sourceMapService = sourceMapService;
+}
+
+OriginalSource.prototype = {
+  /** Get the original source's URL.  */
+  get url() {
+    return this._url;
+  },
+
+  /** Get the original source's URL.  */
+  get href() {
+    return this._url;
+  },
+
+  /**
+   * Return a promise that will resolve to the original source's full
+   * text.  The return result is actually an object with a single
+   * `string` method; this method will return the source text as a
+   * string.  This is done because the style editor elsewhere expects
+   * a long string actor.
+   */
+  getText: function () {
+    if (!this._sourcePromise) {
+      this._sourcePromise = this._sourceMapService.getOriginalSourceText({
+        id: this._sourceId,
+        url: this._url,
+      }).then(contents => {
+        // Make it look like a long string actor.
+        return {
+          string: () => contents.text,
+        };
+      });
+    }
+    return this._sourcePromise;
+  },
+
+  /**
+   * Given a source-mapped, generated style sheet, a line, and a
+   * column, return the corresponding original location in this style
+   * sheet.
+   *
+   * @param {StyleSheetFront} relatedSheet
+   *        The generated style sheet's actor
+   * @param {Number} line
+   *        Line number.
+   * @param {Number} column
+   *        Column number.
+   * @return {Location}
+   *        The original location, an object with at least
+   *        `sourceUrl`, `source`, `styleSheet`, `line`, and `column`
+   *        properties.
+   */
+  getOriginalLocation: function (relatedSheet, line, column) {
+    let {href, nodeHref, actorID: sourceId} = relatedSheet;
+    let sourceUrl = href || nodeHref;
+    return this._sourceMapService.getOriginalLocation({
+      sourceId,
+      line,
+      column,
+      sourceUrl,
+    }).then(location => {
+      // Add some properties for the style editor.
+      location.source = location.sourceUrl;
+      location.styleSheet = relatedSheet;
+      return location;
+    });
+  },
+
+  // Dummy implementations, as we never emit an event.
+  on: function () { },
+  off: function () { },
+};
+
+exports.OriginalSource = OriginalSource;
--- a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js
@@ -12,19 +12,17 @@ const LABELS = ["screen and (max-width: 
                 "screen and (min-width: 1200px)"];
 const LINE_NOS = [5, 8];
 
 waitForExplicitFinish();
 
 add_task(function* () {
   Services.prefs.setBoolPref(MAP_PREF, true);
 
-  let { ui, onMediaListChanged } = yield openStyleEditorForURL(TESTCASE_URI);
-
-  yield onMediaListChanged;
+  let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
 
   is(ui.editors.length, 1, "correct number of editors");
 
   // Test editor with @media rules
   let mediaEditor = ui.editors[0];
   yield openEditor(mediaEditor);
   testMediaEditor(mediaEditor);
 
--- a/devtools/client/styleeditor/test/head.js
+++ b/devtools/client/styleeditor/test/head.js
@@ -77,25 +77,21 @@ var openStyleEditor = Task.async(functio
   if (!tab) {
     tab = gBrowser.selectedTab;
   }
   let target = TargetFactory.forTab(tab);
   let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
   let panel = toolbox.getPanel("styleeditor");
   let ui = panel.UI;
 
-  // This event is sometimes needed by tests, but may be emitted before the list
-  // animation is done. So we listen to it here so tests don't have to and can't miss it.
-  let onMediaListChanged = ui.once("media-list-changed");
-
   // The stylesheet list appears with an animation. Let this animation finish.
   let animations = ui._root.getAnimations({subtree: true});
   yield Promise.all(animations.map(a => a.finished));
 
-  return { toolbox, panel, ui, onMediaListChanged };
+  return { toolbox, panel, ui };
 });
 
 /**
  * Creates a new tab in specified window navigates it to the given URL and
  * opens style editor in it.
  */
 var openStyleEditorForURL = Task.async(function* (url, win) {
   let tab = yield addTab(url, win);
--- a/devtools/shared/fronts/stylesheets.js
+++ b/devtools/shared/fronts/stylesheets.js
@@ -138,16 +138,19 @@ const StyleSheetFront = FrontClassWithSp
     return this._form.system;
   },
   get styleSheetIndex() {
     return this._form.styleSheetIndex;
   },
   get ruleCount() {
     return this._form.ruleCount;
   },
+  get sourceMapURL() {
+    return this._form.sourceMapURL;
+  },
 
   /**
    * Get the indentation to use for edits to this style sheet.
    *
    * @return {Promise} A promise that will resolve to a string that
    * should be used to indent a block in this style sheet.
    */
   guessIndentation: function () {