Bug 926014 - Support CSS source maps; r=dcamp

--HG--
rename : browser/devtools/styleeditor/StyleEditorPanel.jsm => browser/devtools/styleeditor/styleeditor-panel.js
This commit is contained in:
Heather Arthur 2013-12-06 23:52:32 -08:00
parent 0460c33036
commit f025be6927
36 changed files with 1474 additions and 1041 deletions

View File

@ -1155,7 +1155,7 @@ 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);

View File

@ -26,7 +26,7 @@ loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbo
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);

View File

@ -5,4 +5,6 @@
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

View File

@ -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);
}
}

View File

@ -25,6 +25,8 @@ 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.
@ -34,15 +36,18 @@ const STYLE_EDITOR_TEMPLATE = "stylesheet";
* '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");
@ -51,14 +56,18 @@ function StyleEditorUI(debuggee, panelDoc) {
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._debuggee.getStyleSheets().then((styleSheets) => {
this._resetStyleSheetList(styleSheets);
this._target.on("will-navigate", this._clear);
this._target.on("navigate", this._onNewDocument);
})
}
StyleEditorUI.prototype = {
@ -101,7 +110,7 @@ StyleEditorUI.prototype = {
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() {
@ -109,6 +118,112 @@ StyleEditorUI.prototype = {
}.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.
@ -134,7 +249,7 @@ StyleEditorUI.prototype = {
let source = NetUtil.readInputStreamToString(stream, stream.available());
stream.close();
this._debuggee.createStyleSheet(source, (styleSheet) => {
this._debuggee.addStyleSheet(source).then((styleSheet) => {
this._onStyleSheetCreated(styleSheet, file);
});
});
@ -144,24 +259,6 @@ StyleEditorUI.prototype = {
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.
@ -171,35 +268,6 @@ StyleEditorUI.prototype = {
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.
*
@ -207,34 +275,35 @@ StyleEditorUI.prototype = {
* 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);
},
/**
@ -248,8 +317,8 @@ StyleEditorUI.prototype = {
},
/**
* 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.
@ -310,8 +379,7 @@ StyleEditorUI.prototype = {
}
// If this is the first stylesheet, select it
if (this.selectedStyleSheetIndex == -1
&& !this._styleSheetToSelect
if (!this.selectedEditor
&& editor.styleSheet.styleSheetIndex == 0) {
this._selectEditor(editor);
}
@ -322,7 +390,6 @@ StyleEditorUI.prototype = {
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
@ -345,7 +412,8 @@ StyleEditorUI.prototype = {
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;
}
}
},
@ -367,7 +435,6 @@ StyleEditorUI.prototype = {
editor.getSourceEditor().then(() => {
editor.sourceEditor.setCursor({line: line, ch: col});
});
this._view.activeSummary = editor.summary;
},
@ -466,8 +533,5 @@ StyleEditorUI.prototype = {
destroy: function() {
this._clearStyleSheetEditors();
this._debuggee.off("document-load", this._onDocumentLoad);
this._debuggee.off("stylesheets-cleared", this._onStyleSheetsCleared);
}
}

View File

@ -14,6 +14,7 @@ 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");
@ -21,6 +22,7 @@ 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)
@ -32,12 +34,12 @@ const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
* 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
@ -57,13 +59,19 @@ function StyleSheetEditor(styleSheet, win, file, isNew) {
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
};
@ -73,30 +81,21 @@ function StyleSheetEditor(styleSheet, win, file, isNew) {
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();
},
/**
@ -113,37 +112,23 @@ StyleSheetEditor.prototype = {
* @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) {
@ -155,22 +140,17 @@ StyleSheetEditor.prototype = {
/**
* 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);
})
},
/**
@ -181,8 +161,8 @@ StyleSheetEditor.prototype = {
* @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);
},
/**
@ -220,7 +200,7 @@ StyleSheetEditor.prototype = {
this.updateStyleSheet();
});
this._sourceEditor = sourceEditor;
this.sourceEditor = sourceEditor;
if (this._focusOnSourceEditorReady) {
this._focusOnSourceEditorReady = false;
@ -320,7 +300,7 @@ StyleSheetEditor.prototype = {
this._state.text = this.sourceEditor.getText();
}
this.styleSheet.update(this._state.text);
this.styleSheet.update(this._state.text, true);
},
/**
@ -374,6 +354,8 @@ StyleSheetEditor.prototype = {
callback(returnFile);
}
this.sourceEditor.setClean();
this.emit("property-change");
}.bind(this));
};
@ -404,7 +386,6 @@ StyleSheetEditor.prototype = {
* 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);
}

View File

@ -4,21 +4,19 @@
* 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);
@ -32,6 +30,8 @@ this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) {
this._showError = this._showError.bind(this);
}
exports.StyleEditorPanel = StyleEditorPanel;
StyleEditorPanel.prototype = {
get target() this._toolbox.target,
@ -54,9 +54,9 @@ StyleEditorPanel.prototype = {
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;
@ -99,7 +99,6 @@ StyleEditorPanel.prototype = {
if (!this._debuggee || !this.UI) {
return;
}
let stylesheet = this._debuggee.styleSheetFromHref(href);
this.UI.selectStyleSheet(href, line - 1, col ? col - 1 : 0);
},

View File

@ -19,6 +19,10 @@ support-files =
simple.css.gz^headers^
simple.gz.html
simple.html
sourcemaps.css
sourcemaps.css.map
sourcemaps.scss
sourcemaps.html
test_private.css
test_private.html
@ -42,3 +46,4 @@ skip-if = true
[browser_styleeditor_sv_keynav.js]
[browser_styleeditor_sv_resize.js]
[browser_styleeditor_selectstylesheet.js]
[browser_styleeditor_sourcemaps.js]

View File

@ -13,7 +13,8 @@ function test() {
gUI = panel.UI;
gUI.on("editor-added", function(event, editor) {
count++;
if (count == 2) {
if (count == 4) {
info("all editors added");
runTests();
}
})
@ -35,6 +36,8 @@ function getStylesheetNameLinkFor(aEditor) {
}
function onEditor0Attach(aEditor) {
info("first editor selected");
waitForFocus(function () {
// left mouse click should focus editor 1
EventUtils.synthesizeMouseAtCenter(
@ -45,6 +48,8 @@ function onEditor0Attach(aEditor) {
}
function onEditor1Attach(aEditor) {
info("second editor selected");
ok(aEditor.sourceEditor.hasFocus(),
"left mouse click has given editor 1 focus");

View File

@ -19,25 +19,25 @@ function test()
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();
}
});

View File

@ -58,7 +58,7 @@ function testEditorAdded(aEvent, aEditor)
}
});
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,

View File

@ -15,7 +15,7 @@ function test()
// 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;
});

View File

@ -33,12 +33,7 @@ function test() {
}
function onEditorAdded(aEvent, aEditor) {
if (aEditor.sourceLoaded) {
checkCache();
}
else {
aEditor.on("source-load", checkCache);
}
aEditor.getSourceEditor().then(checkCache);
}
function testOnWindow(options, callback) {

View File

@ -22,6 +22,7 @@ function test()
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);
}
@ -41,6 +42,7 @@ function runTests()
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);
}
@ -67,6 +69,7 @@ function testNewPage()
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);
}

View File

@ -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();
}

View File

@ -46,46 +46,10 @@ function openStyleEditorInWindow(win, callback) {
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;

View File

@ -0,0 +1,7 @@
div {
color: #ff0066; }
span {
background-color: #EEE; }
/*# sourceMappingURL=sourcemaps.css.map */

View File

@ -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"
}

View File

@ -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>

View File

@ -0,0 +1,10 @@
$paulrougetpink: #f06;
div {
color: $paulrougetpink;
}
span {
background-color: #EEE;
}

View File

@ -26,6 +26,8 @@ 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.
@ -1096,6 +1098,25 @@ function SelectorView(aTree, aSelectorInfo)
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 = "#";
@ -1217,37 +1238,42 @@ SelectorView.prototype = {
{
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 href = rule.href;
let sheet = rule.parentStyleSheet;
if (sheet && href && !sheet.isSystem) {
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 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);
});
}
};

View File

@ -99,6 +99,7 @@
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}">

View File

@ -22,6 +22,8 @@ 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.
@ -487,6 +489,33 @@ Rule.prototype = {
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.
@ -1586,6 +1615,14 @@ RuleEditor.prototype = {
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"
});

View File

@ -5,6 +5,7 @@
* 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;
@ -16,6 +17,8 @@ loader.lazyGetter(this, "ComputedView", () => require("devtools/styleinspector/c
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.
@ -42,29 +45,30 @@ function RuleViewTool(aInspector, aWindow, aIFrame)
this._cssLinkHandler = (aEvent) => {
let rule = aEvent.detail.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 href = rule.href;
let sheet = rule.parentStyleSheet;
if (sheet && href && !sheet.isSystem) {
let target = this.inspector.target;
if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
toolbox.getCurrentPanel().selectStyleSheet(href, line);
});
}
// 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 contentDoc = this.inspector.selection.document;
let viewSourceUtils = this.inspector.viewSourceUtils;
viewSourceUtils.viewSource(href, null, contentDoc, line);
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, column);
});
}
return;
})
}
this.view.element.addEventListener("CssRuleViewCSSLinkClicked",

View File

@ -54,3 +54,10 @@ support-files = browser_ruleview_pseudoelement.html
[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]

View File

@ -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();
}

View File

@ -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();
}

View File

@ -3,6 +3,9 @@
* 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");
@ -187,6 +190,53 @@ function getComputedPropertyValue(aName)
}
}
/**
* 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();

View File

@ -0,0 +1,7 @@
div {
color: #ff0066; }
span {
background-color: #EEE; }
/*# sourceMappingURL=sourcemaps.css.map */

View File

@ -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"
}

View File

@ -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>

View File

@ -0,0 +1,10 @@
$paulrougetpink: #f06;
div {
color: $paulrougetpink;
}
span {
background-color: #EEE;
}

View File

@ -722,7 +722,7 @@ var NodeListActor = exports.NodeListActor = protocol.ActorClass({
/**
* 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);
},

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,13 @@
"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);
@ -104,7 +106,7 @@ var PageStyleActor = protocol.ActorClass({
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);
@ -253,7 +255,7 @@ var PageStyleActor = protocol.ActorClass({
},
response: RetVal(types.addDictType("matchedselectorresponse", {
rules: "array:domstylerule",
sheets: "array:domsheet",
sheets: "array:stylesheet",
matched: "array:matchedselector"
}))
}),
@ -339,7 +341,7 @@ var PageStyleActor = protocol.ActorClass({
response: RetVal(types.addDictType("appliedStylesReturn", {
entries: "array:appliedstyle",
rules: "array:domstylerule",
sheets: "array:domsheet"
sheets: "array:stylesheet"
}))
}),
@ -546,73 +548,6 @@ var PageStyleFront = protocol.FrontClass(PageStyleActor, {
})
});
/**
* 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");
@ -636,6 +571,7 @@ var StyleRuleActor = protocol.ActorClass({
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
@ -665,6 +601,7 @@ var StyleRuleActor = protocol.ActorClass({
actor: this.actorID,
type: this.type,
line: this.line || undefined,
column: this.column
};
if (this.rawRule.parentRule) {
@ -788,6 +725,7 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
get type() this._form.type,
get line() this._form.line || -1,
get column() this._form.column || -1,
get cssText() {
return this._form.cssText;
},
@ -828,6 +766,40 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
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) {

View File

@ -352,12 +352,12 @@ var DebuggerServer = {
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");
},
/**
@ -372,9 +372,9 @@ var DebuggerServer = {
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");