summaryrefslogtreecommitdiffstats
path: root/devtools/shared/fronts
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/fronts')
-rw-r--r--devtools/shared/fronts/actor-registry.js67
-rw-r--r--devtools/shared/fronts/addons.js17
-rw-r--r--devtools/shared/fronts/animation.js140
-rw-r--r--devtools/shared/fronts/call-watcher.js226
-rw-r--r--devtools/shared/fronts/canvas.js91
-rw-r--r--devtools/shared/fronts/css-properties.js323
-rw-r--r--devtools/shared/fronts/csscoverage.js125
-rw-r--r--devtools/shared/fronts/device.js54
-rw-r--r--devtools/shared/fronts/director-manager.js47
-rw-r--r--devtools/shared/fronts/director-registry.js21
-rw-r--r--devtools/shared/fronts/emulation.js24
-rw-r--r--devtools/shared/fronts/eventlooplag.js15
-rw-r--r--devtools/shared/fronts/framerate.js19
-rw-r--r--devtools/shared/fronts/gcli.js40
-rw-r--r--devtools/shared/fronts/highlighters.js34
-rw-r--r--devtools/shared/fronts/inspector.js1007
-rw-r--r--devtools/shared/fronts/layout.js30
-rw-r--r--devtools/shared/fronts/memory.js92
-rw-r--r--devtools/shared/fronts/moz.build41
-rw-r--r--devtools/shared/fronts/performance-entries.js17
-rw-r--r--devtools/shared/fronts/performance-recording.js152
-rw-r--r--devtools/shared/fronts/performance.js148
-rw-r--r--devtools/shared/fronts/preference.js31
-rw-r--r--devtools/shared/fronts/profiler.js80
-rw-r--r--devtools/shared/fronts/promises.js27
-rw-r--r--devtools/shared/fronts/reflow.js29
-rw-r--r--devtools/shared/fronts/settings.js29
-rw-r--r--devtools/shared/fronts/storage.js32
-rw-r--r--devtools/shared/fronts/string.js47
-rw-r--r--devtools/shared/fronts/styleeditor.js113
-rw-r--r--devtools/shared/fronts/styles.js421
-rw-r--r--devtools/shared/fronts/stylesheets.js184
-rw-r--r--devtools/shared/fronts/timeline.js25
-rw-r--r--devtools/shared/fronts/webaudio.js83
-rw-r--r--devtools/shared/fronts/webgl.js45
35 files changed, 3876 insertions, 0 deletions
diff --git a/devtools/shared/fronts/actor-registry.js b/devtools/shared/fronts/actor-registry.js
new file mode 100644
index 000000000..40f87b609
--- /dev/null
+++ b/devtools/shared/fronts/actor-registry.js
@@ -0,0 +1,67 @@
+/* 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 { components } = require("chrome");
+const Services = require("Services");
+const { actorActorSpec, actorRegistrySpec } = require("devtools/shared/specs/actor-registry");
+const protocol = require("devtools/shared/protocol");
+const { custom } = protocol;
+
+loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+
+const ActorActorFront = protocol.FrontClassWithSpec(actorActorSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ }
+});
+
+exports.ActorActorFront = ActorActorFront;
+
+function request(uri) {
+ return new Promise((resolve, reject) => {
+ try {
+ uri = Services.io.newURI(uri, null, null);
+ } catch (e) {
+ reject(e);
+ }
+
+ NetUtil.asyncFetch({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }, (stream, status, req) => {
+ if (!components.isSuccessCode(status)) {
+ reject(new Error("Request failed with status code = "
+ + status
+ + " after NetUtil.asyncFetch for url = "
+ + uri));
+ return;
+ }
+
+ let source = NetUtil.readInputStreamToString(stream, stream.available());
+ stream.close();
+ resolve(source);
+ });
+ });
+}
+
+const ActorRegistryFront = protocol.FrontClassWithSpec(actorRegistrySpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client,
+ { actor: form.actorRegistryActor });
+
+ this.manage(this);
+ },
+
+ registerActor: custom(function (uri, options) {
+ return request(uri, options)
+ .then(sourceText => {
+ return this._registerActor(sourceText, uri, options);
+ });
+ }, {
+ impl: "_registerActor"
+ })
+});
+
+exports.ActorRegistryFront = ActorRegistryFront;
diff --git a/devtools/shared/fronts/addons.js b/devtools/shared/fronts/addons.js
new file mode 100644
index 000000000..780cc9e03
--- /dev/null
+++ b/devtools/shared/fronts/addons.js
@@ -0,0 +1,17 @@
+/* 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 {addonsSpec} = require("devtools/shared/specs/addons");
+const protocol = require("devtools/shared/protocol");
+
+const AddonsFront = protocol.FrontClassWithSpec(addonsSpec, {
+ initialize: function (client, {addonsActor}) {
+ protocol.Front.prototype.initialize.call(this, client);
+ this.actorID = addonsActor;
+ this.manage(this);
+ }
+});
+
+exports.AddonsFront = AddonsFront;
diff --git a/devtools/shared/fronts/animation.js b/devtools/shared/fronts/animation.js
new file mode 100644
index 000000000..01c9f0bfa
--- /dev/null
+++ b/devtools/shared/fronts/animation.js
@@ -0,0 +1,140 @@
+/* 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 {
+ Front,
+ FrontClassWithSpec,
+ custom,
+ preEvent
+} = require("devtools/shared/protocol");
+const {
+ animationPlayerSpec,
+ animationsSpec
+} = require("devtools/shared/specs/animation");
+const { Task } = require("devtools/shared/task");
+
+const AnimationPlayerFront = FrontClassWithSpec(animationPlayerSpec, {
+ initialize: function (conn, form, detail, ctx) {
+ Front.prototype.initialize.call(this, conn, form, detail, ctx);
+
+ this.state = {};
+ },
+
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this._form = form;
+ this.state = this.initialState;
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ /**
+ * If the AnimationsActor was given a reference to the WalkerActor previously
+ * then calling this getter will return the animation target NodeFront.
+ */
+ get animationTargetNodeFront() {
+ if (!this._form.animationTargetNodeActorID) {
+ return null;
+ }
+
+ return this.conn.getActor(this._form.animationTargetNodeActorID);
+ },
+
+ /**
+ * Getter for the initial state of the player. Up to date states can be
+ * retrieved by calling the getCurrentState method.
+ */
+ get initialState() {
+ return {
+ type: this._form.type,
+ startTime: this._form.startTime,
+ previousStartTime: this._form.previousStartTime,
+ currentTime: this._form.currentTime,
+ playState: this._form.playState,
+ playbackRate: this._form.playbackRate,
+ name: this._form.name,
+ duration: this._form.duration,
+ delay: this._form.delay,
+ endDelay: this._form.endDelay,
+ iterationCount: this._form.iterationCount,
+ iterationStart: this._form.iterationStart,
+ easing: this._form.easing,
+ fill: this._form.fill,
+ direction: this._form.direction,
+ isRunningOnCompositor: this._form.isRunningOnCompositor,
+ propertyState: this._form.propertyState,
+ documentCurrentTime: this._form.documentCurrentTime
+ };
+ },
+
+ /**
+ * Executed when the AnimationPlayerActor emits a "changed" event. Used to
+ * update the local knowledge of the state.
+ */
+ onChanged: preEvent("changed", function (partialState) {
+ let {state} = this.reconstructState(partialState);
+ this.state = state;
+ }),
+
+ /**
+ * Refresh the current state of this animation on the client from information
+ * found on the server. Doesn't return anything, just stores the new state.
+ */
+ refreshState: Task.async(function* () {
+ let data = yield this.getCurrentState();
+ if (this.currentStateHasChanged) {
+ this.state = data;
+ }
+ }),
+
+ /**
+ * getCurrentState interceptor re-constructs incomplete states since the actor
+ * only sends the values that have changed.
+ */
+ getCurrentState: custom(function () {
+ this.currentStateHasChanged = false;
+ return this._getCurrentState().then(partialData => {
+ let {state, hasChanged} = this.reconstructState(partialData);
+ this.currentStateHasChanged = hasChanged;
+ return state;
+ });
+ }, {
+ impl: "_getCurrentState"
+ }),
+
+ reconstructState: function (data) {
+ let hasChanged = false;
+
+ for (let key in this.state) {
+ if (typeof data[key] === "undefined") {
+ data[key] = this.state[key];
+ } else if (data[key] !== this.state[key]) {
+ hasChanged = true;
+ }
+ }
+
+ return {state: data, hasChanged};
+ }
+});
+
+exports.AnimationPlayerFront = AnimationPlayerFront;
+
+const AnimationsFront = FrontClassWithSpec(animationsSpec, {
+ initialize: function (client, {animationsActor}) {
+ Front.prototype.initialize.call(this, client, {actor: animationsActor});
+ this.manage(this);
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ }
+});
+
+exports.AnimationsFront = AnimationsFront;
diff --git a/devtools/shared/fronts/call-watcher.js b/devtools/shared/fronts/call-watcher.js
new file mode 100644
index 000000000..5f41c2fbd
--- /dev/null
+++ b/devtools/shared/fronts/call-watcher.js
@@ -0,0 +1,226 @@
+/* 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 { functionCallSpec, callWatcherSpec } = require("devtools/shared/specs/call-watcher");
+const protocol = require("devtools/shared/protocol");
+
+/**
+ * The corresponding Front object for the FunctionCallActor.
+ */
+const FunctionCallFront = protocol.FrontClassWithSpec(functionCallSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ },
+
+ /**
+ * Adds some generic information directly to this instance,
+ * to avoid extra roundtrips.
+ */
+ form: function (form) {
+ this.actorID = form.actor;
+ this.type = form.type;
+ this.name = form.name;
+ this.file = form.file;
+ this.line = form.line;
+ this.timestamp = form.timestamp;
+ this.callerPreview = form.callerPreview;
+ this.argsPreview = form.argsPreview;
+ this.resultPreview = form.resultPreview;
+ }
+});
+
+exports.FunctionCallFront = FunctionCallFront;
+
+/**
+ * The corresponding Front object for the CallWatcherActor.
+ */
+var CallWatcherFront =
+exports.CallWatcherFront =
+protocol.FrontClassWithSpec(callWatcherSpec, {
+ initialize: function (client, { callWatcherActor }) {
+ protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
+ this.manage(this);
+ }
+});
+
+/**
+ * Constants.
+ */
+CallWatcherFront.METHOD_FUNCTION = 0;
+CallWatcherFront.GETTER_FUNCTION = 1;
+CallWatcherFront.SETTER_FUNCTION = 2;
+
+CallWatcherFront.KNOWN_METHODS = {};
+
+CallWatcherFront.KNOWN_METHODS.CanvasRenderingContext2D = {
+ asyncDrawXULElement: {
+ enums: new Set([6]),
+ },
+ drawWindow: {
+ enums: new Set([6])
+ },
+};
+
+CallWatcherFront.KNOWN_METHODS.WebGLRenderingContext = {
+ activeTexture: {
+ enums: new Set([0]),
+ },
+ bindBuffer: {
+ enums: new Set([0]),
+ },
+ bindFramebuffer: {
+ enums: new Set([0]),
+ },
+ bindRenderbuffer: {
+ enums: new Set([0]),
+ },
+ bindTexture: {
+ enums: new Set([0]),
+ },
+ blendEquation: {
+ enums: new Set([0]),
+ },
+ blendEquationSeparate: {
+ enums: new Set([0, 1]),
+ },
+ blendFunc: {
+ enums: new Set([0, 1]),
+ },
+ blendFuncSeparate: {
+ enums: new Set([0, 1, 2, 3]),
+ },
+ bufferData: {
+ enums: new Set([0, 1, 2]),
+ },
+ bufferSubData: {
+ enums: new Set([0, 1]),
+ },
+ checkFramebufferStatus: {
+ enums: new Set([0]),
+ },
+ clear: {
+ enums: new Set([0]),
+ },
+ compressedTexImage2D: {
+ enums: new Set([0, 2]),
+ },
+ compressedTexSubImage2D: {
+ enums: new Set([0, 6]),
+ },
+ copyTexImage2D: {
+ enums: new Set([0, 2]),
+ },
+ copyTexSubImage2D: {
+ enums: new Set([0]),
+ },
+ createShader: {
+ enums: new Set([0]),
+ },
+ cullFace: {
+ enums: new Set([0]),
+ },
+ depthFunc: {
+ enums: new Set([0]),
+ },
+ disable: {
+ enums: new Set([0]),
+ },
+ drawArrays: {
+ enums: new Set([0]),
+ },
+ drawElements: {
+ enums: new Set([0, 2]),
+ },
+ enable: {
+ enums: new Set([0]),
+ },
+ framebufferRenderbuffer: {
+ enums: new Set([0, 1, 2]),
+ },
+ framebufferTexture2D: {
+ enums: new Set([0, 1, 2]),
+ },
+ frontFace: {
+ enums: new Set([0]),
+ },
+ generateMipmap: {
+ enums: new Set([0]),
+ },
+ getBufferParameter: {
+ enums: new Set([0, 1]),
+ },
+ getParameter: {
+ enums: new Set([0]),
+ },
+ getFramebufferAttachmentParameter: {
+ enums: new Set([0, 1, 2]),
+ },
+ getProgramParameter: {
+ enums: new Set([1]),
+ },
+ getRenderbufferParameter: {
+ enums: new Set([0, 1]),
+ },
+ getShaderParameter: {
+ enums: new Set([1]),
+ },
+ getShaderPrecisionFormat: {
+ enums: new Set([0, 1]),
+ },
+ getTexParameter: {
+ enums: new Set([0, 1]),
+ },
+ getVertexAttrib: {
+ enums: new Set([1]),
+ },
+ getVertexAttribOffset: {
+ enums: new Set([1]),
+ },
+ hint: {
+ enums: new Set([0, 1]),
+ },
+ isEnabled: {
+ enums: new Set([0]),
+ },
+ pixelStorei: {
+ enums: new Set([0]),
+ },
+ readPixels: {
+ enums: new Set([4, 5]),
+ },
+ renderbufferStorage: {
+ enums: new Set([0, 1]),
+ },
+ stencilFunc: {
+ enums: new Set([0]),
+ },
+ stencilFuncSeparate: {
+ enums: new Set([0, 1]),
+ },
+ stencilMaskSeparate: {
+ enums: new Set([0]),
+ },
+ stencilOp: {
+ enums: new Set([0, 1, 2]),
+ },
+ stencilOpSeparate: {
+ enums: new Set([0, 1, 2, 3]),
+ },
+ texImage2D: {
+ enums: args => args.length > 6 ? new Set([0, 2, 6, 7]) : new Set([0, 2, 3, 4]),
+ },
+ texParameterf: {
+ enums: new Set([0, 1]),
+ },
+ texParameteri: {
+ enums: new Set([0, 1, 2]),
+ },
+ texSubImage2D: {
+ enums: args => args.length === 9 ? new Set([0, 6, 7]) : new Set([0, 4, 5]),
+ },
+ vertexAttribPointer: {
+ enums: new Set([2])
+ },
+};
diff --git a/devtools/shared/fronts/canvas.js b/devtools/shared/fronts/canvas.js
new file mode 100644
index 000000000..f3a1a6075
--- /dev/null
+++ b/devtools/shared/fronts/canvas.js
@@ -0,0 +1,91 @@
+/* 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 {
+ frameSnapshotSpec,
+ canvasSpec,
+ CANVAS_CONTEXTS,
+ ANIMATION_GENERATORS,
+ LOOP_GENERATORS,
+ DRAW_CALLS,
+ INTERESTING_CALLS,
+} = require("devtools/shared/specs/canvas");
+const protocol = require("devtools/shared/protocol");
+const promise = require("promise");
+
+/**
+ * The corresponding Front object for the FrameSnapshotActor.
+ */
+const FrameSnapshotFront = protocol.FrontClassWithSpec(frameSnapshotSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ this._animationFrameEndScreenshot = null;
+ this._cachedScreenshots = new WeakMap();
+ },
+
+ /**
+ * This implementation caches the animation frame end screenshot to optimize
+ * frontend requests to `generateScreenshotFor`.
+ */
+ getOverview: protocol.custom(function () {
+ return this._getOverview().then(data => {
+ this._animationFrameEndScreenshot = data.screenshot;
+ return data;
+ });
+ }, {
+ impl: "_getOverview"
+ }),
+
+ /**
+ * This implementation saves a roundtrip to the backend if the screenshot
+ * was already generated and retrieved once.
+ */
+ generateScreenshotFor: protocol.custom(function (functionCall) {
+ if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name) ||
+ CanvasFront.LOOP_GENERATORS.has(functionCall.name)) {
+ return promise.resolve(this._animationFrameEndScreenshot);
+ }
+ let cachedScreenshot = this._cachedScreenshots.get(functionCall);
+ if (cachedScreenshot) {
+ return cachedScreenshot;
+ }
+ let screenshot = this._generateScreenshotFor(functionCall);
+ this._cachedScreenshots.set(functionCall, screenshot);
+ return screenshot;
+ }, {
+ impl: "_generateScreenshotFor"
+ })
+});
+
+exports.FrameSnapshotFront = FrameSnapshotFront;
+
+/**
+ * The corresponding Front object for the CanvasActor.
+ */
+const CanvasFront = protocol.FrontClassWithSpec(canvasSpec, {
+ initialize: function (client, { canvasActor }) {
+ protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor });
+ this.manage(this);
+ }
+});
+
+/**
+ * Constants.
+ */
+CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS);
+CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS);
+CanvasFront.LOOP_GENERATORS = new Set(LOOP_GENERATORS);
+CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS);
+CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS);
+CanvasFront.THUMBNAIL_SIZE = 50;
+CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT = 256;
+CanvasFront.INVALID_SNAPSHOT_IMAGE = {
+ index: -1,
+ width: 0,
+ height: 0,
+ pixels: []
+};
+
+exports.CanvasFront = CanvasFront;
diff --git a/devtools/shared/fronts/css-properties.js b/devtools/shared/fronts/css-properties.js
new file mode 100644
index 000000000..9b3172a22
--- /dev/null
+++ b/devtools/shared/fronts/css-properties.js
@@ -0,0 +1,323 @@
+/* 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 { FrontClassWithSpec, Front } = require("devtools/shared/protocol");
+const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties");
+const { Task } = require("devtools/shared/task");
+const { CSS_PROPERTIES_DB } = require("devtools/shared/css/properties-db");
+const { cssColors } = require("devtools/shared/css/color-db");
+
+/**
+ * Build up a regular expression that matches a CSS variable token. This is an
+ * ident token that starts with two dashes "--".
+ *
+ * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ */
+var NON_ASCII = "[^\\x00-\\x7F]";
+var ESCAPE = "\\\\[^\n\r]";
+var FIRST_CHAR = ["[_a-z]", NON_ASCII, ESCAPE].join("|");
+var TRAILING_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
+var IS_VARIABLE_TOKEN = new RegExp(`^--(${FIRST_CHAR})(${TRAILING_CHAR})*$`,
+ "i");
+/**
+ * Check that this is a CSS variable.
+ *
+ * @param {String} input
+ * @return {Boolean}
+ */
+function isCssVariable(input) {
+ return !!input.match(IS_VARIABLE_TOKEN);
+}
+
+var cachedCssProperties = new WeakMap();
+
+/**
+ * The CssProperties front provides a mechanism to have a one-time asynchronous
+ * load of a CSS properties database. This is then fed into the CssProperties
+ * interface that provides synchronous methods for finding out what CSS
+ * properties the current server supports.
+ */
+const CssPropertiesFront = FrontClassWithSpec(cssPropertiesSpec, {
+ initialize: function (client, { cssPropertiesActor }) {
+ Front.prototype.initialize.call(this, client, {actor: cssPropertiesActor});
+ this.manage(this);
+ }
+});
+
+/**
+ * Ask questions to a CSS database. This class does not care how the database
+ * gets loaded in, only the questions that you can ask to it.
+ * Prototype functions are bound to 'this' so they can be passed around as helper
+ * functions.
+ *
+ * @param {Object} db
+ * A database of CSS properties
+ * @param {Object} inheritedList
+ * The key is the property name, the value is whether or not
+ * that property is inherited.
+ */
+function CssProperties(db) {
+ this.properties = db.properties;
+ this.pseudoElements = db.pseudoElements;
+
+ this.isKnown = this.isKnown.bind(this);
+ this.isInherited = this.isInherited.bind(this);
+ this.supportsType = this.supportsType.bind(this);
+ this.isValidOnClient = this.isValidOnClient.bind(this);
+
+ // A weakly held dummy HTMLDivElement to test CSS properties on the client.
+ this._dummyElements = new WeakMap();
+}
+
+CssProperties.prototype = {
+ /**
+ * Checks to see if the property is known by the browser. This function has
+ * `this` already bound so that it can be passed around by reference.
+ *
+ * @param {String} property The property name to be checked.
+ * @return {Boolean}
+ */
+ isKnown(property) {
+ return !!this.properties[property] || isCssVariable(property);
+ },
+
+ /**
+ * Quickly check if a CSS name/value combo is valid on the client.
+ *
+ * @param {String} Property name.
+ * @param {String} Property value.
+ * @param {Document} The client's document object.
+ * @return {Boolean}
+ */
+ isValidOnClient(name, value, doc) {
+ let dummyElement = this._dummyElements.get(doc);
+ if (!dummyElement) {
+ dummyElement = doc.createElement("div");
+ this._dummyElements.set(doc, dummyElement);
+ }
+
+ // `!important` is not a valid value when setting a style declaration in the
+ // CSS Object Model.
+ const sanitizedValue = ("" + value).replace(/!\s*important\s*$/, "");
+
+ // Test the style on the element.
+ dummyElement.style[name] = sanitizedValue;
+ const isValid = !!dummyElement.style[name];
+
+ // Reset the state of the dummy element;
+ dummyElement.style[name] = "";
+ return isValid;
+ },
+
+ /**
+ * Get a function that will check the validity of css name/values for a given document.
+ * Useful for injecting isValidOnClient into components when needed.
+ *
+ * @param {Document} The client's document object.
+ * @return {Function} this.isValidOnClient with the document pre-set.
+ */
+ getValidityChecker(doc) {
+ return (name, value) => this.isValidOnClient(name, value, doc);
+ },
+
+ /**
+ * Checks to see if the property is an inherited one.
+ *
+ * @param {String} property The property name to be checked.
+ * @return {Boolean}
+ */
+ isInherited(property) {
+ return this.properties[property] && this.properties[property].isInherited;
+ },
+
+ /**
+ * Checks if the property supports the given CSS type.
+ * CSS types should come from devtools/shared/css/properties-db.js' CSS_TYPES.
+ *
+ * @param {String} property The property to be checked.
+ * @param {Number} type One of the type values from CSS_TYPES.
+ * @return {Boolean}
+ */
+ supportsType(property, type) {
+ return this.properties[property] && this.properties[property].supports.includes(type);
+ },
+
+ /**
+ * Gets the CSS values for a given property name.
+ *
+ * @param {String} property The property to use.
+ * @return {Array} An array of strings.
+ */
+ getValues(property) {
+ return this.properties[property] ? this.properties[property].values : [];
+ },
+
+ /**
+ * Gets the CSS property names.
+ *
+ * @return {Array} An array of strings.
+ */
+ getNames(property) {
+ return Object.keys(this.properties);
+ },
+
+ /**
+ * Return a list of subproperties for the given property. If |name|
+ * does not name a valid property, an empty array is returned. If
+ * the property is not a shorthand property, then array containing
+ * just the property itself is returned.
+ *
+ * @param {String} name The property to query
+ * @return {Array} An array of subproperty names.
+ */
+ getSubproperties(name) {
+ if (this.isKnown(name)) {
+ if (this.properties[name] && this.properties[name].subproperties) {
+ return this.properties[name].subproperties;
+ }
+ return [name];
+ }
+ return [];
+ },
+};
+
+/**
+ * Create a CssProperties object with a fully loaded CSS database. The
+ * CssProperties interface can be queried synchronously, but the initialization
+ * is potentially async and should be handled up-front when the tool is created.
+ *
+ * The front is returned only with this function so that it can be destroyed
+ * once the toolbox is destroyed.
+ *
+ * @param {Toolbox} The current toolbox.
+ * @returns {Promise} Resolves to {cssProperties, cssPropertiesFront}.
+ */
+const initCssProperties = Task.async(function* (toolbox) {
+ const client = toolbox.target.client;
+ if (cachedCssProperties.has(client)) {
+ return cachedCssProperties.get(client);
+ }
+
+ let db, front;
+
+ // Get the list dynamically if the cssProperties actor exists.
+ if (toolbox.target.hasActor("cssProperties")) {
+ front = CssPropertiesFront(client, toolbox.target.form);
+ const serverDB = yield front.getCSSDatabase();
+
+ // Ensure the database was returned in a format that is understood.
+ // Older versions of the protocol could return a blank database.
+ if (!serverDB.properties && !serverDB.margin) {
+ db = CSS_PROPERTIES_DB;
+ } else {
+ db = serverDB;
+ }
+ } else {
+ // The target does not support this actor, so require a static list of supported
+ // properties.
+ db = CSS_PROPERTIES_DB;
+ }
+
+ const cssProperties = new CssProperties(normalizeCssData(db));
+ cachedCssProperties.set(client, {cssProperties, front});
+ return {cssProperties, front};
+});
+
+/**
+ * Synchronously get a cached and initialized CssProperties.
+ *
+ * @param {Toolbox} The current toolbox.
+ * @returns {CssProperties}
+ */
+function getCssProperties(toolbox) {
+ if (!cachedCssProperties.has(toolbox.target.client)) {
+ throw new Error("The CSS database has not been initialized, please make " +
+ "sure initCssDatabase was called once before for this " +
+ "toolbox.");
+ }
+ return cachedCssProperties.get(toolbox.target.client).cssProperties;
+}
+
+/**
+ * Get a client-side CssProperties. This is useful for dependencies in tests, or parts
+ * of the codebase that don't particularly need to match every known CSS property on
+ * the target.
+ * @return {CssProperties}
+ */
+function getClientCssProperties() {
+ return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB));
+}
+
+/**
+ * Even if the target has the cssProperties actor, the returned data may not be in the
+ * same shape or have all of the data we need. This normalizes the data and fills in
+ * any missing information like color values.
+ *
+ * @return {Object} The normalized CSS database.
+ */
+function normalizeCssData(db) {
+ if (db !== CSS_PROPERTIES_DB) {
+ // Firefox 49's getCSSDatabase() just returned the properties object, but
+ // now it returns an object with multiple types of CSS information.
+ if (!db.properties) {
+ db = { properties: db };
+ }
+
+ // Fill in any missing DB information from the static database.
+ db = Object.assign({}, CSS_PROPERTIES_DB, db);
+
+ for (let name in db.properties) {
+ // Skip the current property if we can't find it in CSS_PROPERTIES_DB.
+ if (typeof CSS_PROPERTIES_DB.properties[name] !== "object") {
+ continue;
+ }
+
+ // Add "supports" information to the css properties if it's missing.
+ if (!db.properties.color.supports) {
+ db.properties[name].supports = CSS_PROPERTIES_DB.properties[name].supports;
+ }
+ // Add "values" information to the css properties if it's missing.
+ if (!db.properties.color.values) {
+ db.properties[name].values = CSS_PROPERTIES_DB.properties[name].values;
+ }
+ // Add "subproperties" information to the css properties if it's missing.
+ if (!db.properties.background.subproperties) {
+ db.properties[name].subproperties =
+ CSS_PROPERTIES_DB.properties[name].subproperties;
+ }
+ }
+ }
+
+ reattachCssColorValues(db);
+
+ return db;
+}
+
+/**
+ * Color values are omitted to save on space. Add them back here.
+ * @param {Object} The CSS database.
+ */
+function reattachCssColorValues(db) {
+ if (db.properties.color.values[0] === "COLOR") {
+ const colors = Object.keys(cssColors);
+
+ for (let name in db.properties) {
+ const property = db.properties[name];
+ // "values" can be undefined if {name} was not found in CSS_PROPERTIES_DB.
+ if (property.values && property.values[0] === "COLOR") {
+ property.values.shift();
+ property.values = property.values.concat(colors).sort();
+ }
+ }
+ }
+}
+
+module.exports = {
+ CssPropertiesFront,
+ CssProperties,
+ getCssProperties,
+ getClientCssProperties,
+ initCssProperties
+};
diff --git a/devtools/shared/fronts/csscoverage.js b/devtools/shared/fronts/csscoverage.js
new file mode 100644
index 000000000..28ab399c5
--- /dev/null
+++ b/devtools/shared/fronts/csscoverage.js
@@ -0,0 +1,125 @@
+/* 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 {cssUsageSpec} = require("devtools/shared/specs/csscoverage");
+const protocol = require("devtools/shared/protocol");
+const {custom} = protocol;
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/shared/locales/csscoverage.properties");
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+
+/**
+ * Allow: let foo = l10n.lookup("csscoverageFoo");
+ */
+const l10n = exports.l10n = {
+ lookup: (msg) => L10N.getStr(msg)
+};
+
+/**
+ * Running more than one usage report at a time is probably bad for performance
+ * and it isn't particularly useful, and it's confusing from a notification POV
+ * so we only allow one.
+ */
+var isRunning = false;
+var notification;
+var target;
+var chromeWindow;
+
+/**
+ * Front for CSSUsageActor
+ */
+const CSSUsageFront = protocol.FrontClassWithSpec(cssUsageSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ this.actorID = form.cssUsageActor;
+ this.manage(this);
+ },
+
+ _onStateChange: protocol.preEvent("state-change", function (ev) {
+ isRunning = ev.isRunning;
+ ev.target = target;
+
+ if (isRunning) {
+ let gnb = chromeWindow.document.getElementById("global-notificationbox");
+ notification = gnb.getNotificationWithValue("csscoverage-running");
+
+ if (notification == null) {
+ let notifyStop = reason => {
+ if (reason == "removed") {
+ this.stop();
+ }
+ };
+
+ let msg = l10n.lookup("csscoverageRunningReply");
+ notification = gnb.appendNotification(msg, "csscoverage-running",
+ "",
+ gnb.PRIORITY_INFO_HIGH,
+ null,
+ notifyStop);
+ }
+ } else {
+ if (notification) {
+ notification.remove();
+ notification = undefined;
+ }
+
+ gDevTools.showToolbox(target, "styleeditor");
+ target = undefined;
+ }
+ }),
+
+ /**
+ * Server-side start is above. Client-side start adds a notification box
+ */
+ start: custom(function (newChromeWindow, newTarget, noreload = false) {
+ target = newTarget;
+ chromeWindow = newChromeWindow;
+
+ return this._start(noreload);
+ }, {
+ impl: "_start"
+ }),
+
+ /**
+ * Server-side start is above. Client-side start adds a notification box
+ */
+ toggle: custom(function (newChromeWindow, newTarget) {
+ target = newTarget;
+ chromeWindow = newChromeWindow;
+
+ return this._toggle();
+ }, {
+ impl: "_toggle"
+ }),
+
+ /**
+ * We count STARTING and STOPPING as 'running'
+ */
+ isRunning: function () {
+ return isRunning;
+ }
+});
+
+exports.CSSUsageFront = CSSUsageFront;
+
+const knownFronts = new WeakMap();
+
+/**
+ * Create a CSSUsageFront only when needed (returns a promise)
+ * For notes on target.makeRemote(), see
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1016330#c7
+ */
+exports.getUsage = function (trgt) {
+ return trgt.makeRemote().then(() => {
+ let front = knownFronts.get(trgt.client);
+ if (front == null && trgt.form.cssUsageActor != null) {
+ front = new CSSUsageFront(trgt.client, trgt.form);
+ knownFronts.set(trgt.client, front);
+ }
+ return front;
+ });
+};
diff --git a/devtools/shared/fronts/device.js b/devtools/shared/fronts/device.js
new file mode 100644
index 000000000..28f7a096a
--- /dev/null
+++ b/devtools/shared/fronts/device.js
@@ -0,0 +1,54 @@
+/* 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, Cu} = require("chrome");
+const {deviceSpec} = require("devtools/shared/specs/device");
+const protocol = require("devtools/shared/protocol");
+const defer = require("devtools/shared/defer");
+
+const DeviceFront = protocol.FrontClassWithSpec(deviceSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client);
+ this.actorID = form.deviceActor;
+ this.manage(this);
+ },
+
+ screenshotToBlob: function () {
+ return this.screenshotToDataURL().then(longstr => {
+ return longstr.string().then(dataURL => {
+ let deferred = defer();
+ longstr.release().then(null, Cu.reportError);
+ let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ req.open("GET", dataURL, true);
+ req.responseType = "blob";
+ req.onload = () => {
+ deferred.resolve(req.response);
+ };
+ req.onerror = () => {
+ deferred.reject(req.status);
+ };
+ req.send();
+ return deferred.promise;
+ });
+ });
+ },
+});
+
+const _knownDeviceFronts = new WeakMap();
+
+exports.getDeviceFront = function (client, form) {
+ if (!form.deviceActor) {
+ return null;
+ }
+
+ if (_knownDeviceFronts.has(client)) {
+ return _knownDeviceFronts.get(client);
+ }
+
+ let front = new DeviceFront(client, form);
+ _knownDeviceFronts.set(client, front);
+ return front;
+};
diff --git a/devtools/shared/fronts/director-manager.js b/devtools/shared/fronts/director-manager.js
new file mode 100644
index 000000000..afef42395
--- /dev/null
+++ b/devtools/shared/fronts/director-manager.js
@@ -0,0 +1,47 @@
+/* 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 {
+ messagePortSpec,
+ directorScriptSpec,
+ directorManagerSpec,
+} = require("devtools/shared/specs/director-manager");
+const protocol = require("devtools/shared/protocol");
+
+/**
+ * The corresponding Front object for the MessagePortActor.
+ */
+const MessagePortFront = protocol.FrontClassWithSpec(messagePortSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ }
+});
+
+exports.MessagePortFront = MessagePortFront;
+
+/**
+ * The corresponding Front object for the DirectorScriptActor.
+ */
+const DirectorScriptFront = protocol.FrontClassWithSpec(directorScriptSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ }
+});
+
+exports.DirectorScriptFront = DirectorScriptFront;
+
+/**
+ * The corresponding Front object for the DirectorManagerActor.
+ */
+const DirectorManagerFront = protocol.FrontClassWithSpec(directorManagerSpec, {
+ initialize: function (client, { directorManagerActor }) {
+ protocol.Front.prototype.initialize.call(this, client, {
+ actor: directorManagerActor
+ });
+ this.manage(this);
+ }
+});
+
+exports.DirectorManagerFront = DirectorManagerFront;
diff --git a/devtools/shared/fronts/director-registry.js b/devtools/shared/fronts/director-registry.js
new file mode 100644
index 000000000..559fe2052
--- /dev/null
+++ b/devtools/shared/fronts/director-registry.js
@@ -0,0 +1,21 @@
+/* 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 {directorRegistrySpec} = require("devtools/shared/specs/director-registry");
+const protocol = require("devtools/shared/protocol");
+
+/**
+ * The corresponding Front object for the DirectorRegistryActor.
+ */
+const DirectorRegistryFront = protocol.FrontClassWithSpec(directorRegistrySpec, {
+ initialize: function (client, { directorRegistryActor }) {
+ protocol.Front.prototype.initialize.call(this, client, {
+ actor: directorRegistryActor
+ });
+ this.manage(this);
+ }
+});
+
+exports.DirectorRegistryFront = DirectorRegistryFront;
diff --git a/devtools/shared/fronts/emulation.js b/devtools/shared/fronts/emulation.js
new file mode 100644
index 000000000..99dbf565b
--- /dev/null
+++ b/devtools/shared/fronts/emulation.js
@@ -0,0 +1,24 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const { emulationSpec } = require("devtools/shared/specs/emulation");
+
+/**
+ * The corresponding Front object for the EmulationActor.
+ */
+const EmulationFront = FrontClassWithSpec(emulationSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = form.emulationActor;
+ this.manage(this);
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+});
+
+exports.EmulationFront = EmulationFront;
diff --git a/devtools/shared/fronts/eventlooplag.js b/devtools/shared/fronts/eventlooplag.js
new file mode 100644
index 000000000..7c130e621
--- /dev/null
+++ b/devtools/shared/fronts/eventlooplag.js
@@ -0,0 +1,15 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const { eventLoopLagSpec } = require("devtools/shared/specs/eventlooplag");
+
+exports.EventLoopLagFront = FrontClassWithSpec(eventLoopLagSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = form.eventLoopLagActor;
+ this.manage(this);
+ },
+});
diff --git a/devtools/shared/fronts/framerate.js b/devtools/shared/fronts/framerate.js
new file mode 100644
index 000000000..2aa678a0d
--- /dev/null
+++ b/devtools/shared/fronts/framerate.js
@@ -0,0 +1,19 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const { framerateSpec } = require("devtools/shared/specs/framerate");
+
+/**
+ * The corresponding Front object for the FramerateActor.
+ */
+var FramerateFront = exports.FramerateFront = FrontClassWithSpec(framerateSpec, {
+ initialize: function (client, { framerateActor }) {
+ Front.prototype.initialize.call(this, client, { actor: framerateActor });
+ this.manage(this);
+ }
+});
+
+exports.FramerateFront = FramerateFront;
diff --git a/devtools/shared/fronts/gcli.js b/devtools/shared/fronts/gcli.js
new file mode 100644
index 000000000..28ae1138b
--- /dev/null
+++ b/devtools/shared/fronts/gcli.js
@@ -0,0 +1,40 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const { gcliSpec } = require("devtools/shared/specs/gcli");
+
+/**
+ *
+ */
+const GcliFront = exports.GcliFront = FrontClassWithSpec(gcliSpec, {
+ initialize: function (client, tabForm) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = tabForm.gcliActor;
+
+ // XXX: This is the first actor type in its hierarchy to use the protocol
+ // library, so we're going to self-own on the client side for now.
+ this.manage(this);
+ },
+});
+
+// A cache of created fronts: WeakMap<Client, Front>
+const knownFronts = new WeakMap();
+
+/**
+ * Create a GcliFront only when needed (returns a promise)
+ * For notes on target.makeRemote(), see
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1016330#c7
+ */
+exports.GcliFront.create = function (target) {
+ return target.makeRemote().then(() => {
+ let front = knownFronts.get(target.client);
+ if (front == null && target.form.gcliActor != null) {
+ front = new GcliFront(target.client, target.form);
+ knownFronts.set(target.client, front);
+ }
+ return front;
+ });
+};
diff --git a/devtools/shared/fronts/highlighters.js b/devtools/shared/fronts/highlighters.js
new file mode 100644
index 000000000..ca39ed526
--- /dev/null
+++ b/devtools/shared/fronts/highlighters.js
@@ -0,0 +1,34 @@
+/* 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 { FrontClassWithSpec, custom } = require("devtools/shared/protocol");
+const {
+ customHighlighterSpec,
+ highlighterSpec
+} = require("devtools/shared/specs/highlighters");
+
+const HighlighterFront = FrontClassWithSpec(highlighterSpec, {
+ // Update the object given a form representation off the wire.
+ form: function (json) {
+ this.actorID = json.actor;
+ // FF42+ HighlighterActors starts exposing custom form, with traits object
+ this.traits = json.traits || {};
+ },
+
+ pick: custom(function (doFocus) {
+ if (doFocus && this.pickAndFocus) {
+ return this.pickAndFocus();
+ }
+ return this._pick();
+ }, {
+ impl: "_pick"
+ })
+});
+
+exports.HighlighterFront = HighlighterFront;
+
+const CustomHighlighterFront = FrontClassWithSpec(customHighlighterSpec, {});
+
+exports.CustomHighlighterFront = CustomHighlighterFront;
diff --git a/devtools/shared/fronts/inspector.js b/devtools/shared/fronts/inspector.js
new file mode 100644
index 000000000..c76b41fe7
--- /dev/null
+++ b/devtools/shared/fronts/inspector.js
@@ -0,0 +1,1007 @@
+/* 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";
+
+require("devtools/shared/fronts/styles");
+require("devtools/shared/fronts/highlighters");
+require("devtools/shared/fronts/layout");
+const { SimpleStringFront } = require("devtools/shared/fronts/string");
+const {
+ Front,
+ FrontClassWithSpec,
+ custom,
+ preEvent,
+ types
+} = require("devtools/shared/protocol.js");
+const {
+ inspectorSpec,
+ nodeSpec,
+ nodeListSpec,
+ walkerSpec
+} = require("devtools/shared/specs/inspector");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const { Task } = require("devtools/shared/task");
+const { Class } = require("sdk/core/heritage");
+const events = require("sdk/event/core");
+const object = require("sdk/util/object");
+const nodeConstants = require("devtools/shared/dom-node-constants.js");
+loader.lazyRequireGetter(this, "CommandUtils",
+ "devtools/client/shared/developer-toolbar", true);
+
+const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyAttributes` request.
+ */
+const AttributeModificationList = Class({
+ initialize: function (node) {
+ this.node = node;
+ this.modifications = [];
+ },
+
+ apply: function () {
+ let ret = this.node.modifyAttributes(this.modifications);
+ return ret;
+ },
+
+ destroy: function () {
+ this.node = null;
+ this.modification = null;
+ },
+
+ setAttributeNS: function (ns, name, value) {
+ this.modifications.push({
+ attributeNamespace: ns,
+ attributeName: name,
+ newValue: value
+ });
+ },
+
+ setAttribute: function (name, value) {
+ this.setAttributeNS(undefined, name, value);
+ },
+
+ removeAttributeNS: function (ns, name) {
+ this.setAttributeNS(ns, name, undefined);
+ },
+
+ removeAttribute: function (name) {
+ this.setAttributeNS(undefined, name, undefined);
+ }
+});
+
+/**
+ * Client side of the node actor.
+ *
+ * Node fronts are strored in a tree that mirrors the DOM tree on the
+ * server, but with a few key differences:
+ * - Not all children will be necessary loaded for each node.
+ * - The order of children isn't guaranteed to be the same as the DOM.
+ * Children are stored in a doubly-linked list, to make addition/removal
+ * and traversal quick.
+ *
+ * Due to the order/incompleteness of the child list, it is safe to use
+ * the parent node from clients, but the `children` request should be used
+ * to traverse children.
+ */
+const NodeFront = FrontClassWithSpec(nodeSpec, {
+ initialize: function (conn, form, detail, ctx) {
+ // The parent node
+ this._parent = null;
+ // The first child of this node.
+ this._child = null;
+ // The next sibling of this node.
+ this._next = null;
+ // The previous sibling of this node.
+ this._prev = null;
+ Front.prototype.initialize.call(this, conn, form, detail, ctx);
+ },
+
+ /**
+ * Destroy a node front. The node must have been removed from the
+ * ownership tree before this is called, unless the whole walker front
+ * is being destroyed.
+ */
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ // Update the object given a form representation off the wire.
+ form: function (form, detail, ctx) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+
+ // backward-compatibility: shortValue indicates we are connected to old server
+ if (form.shortValue) {
+ // If the value is not complete, set nodeValue to null, it will be fetched
+ // when calling getNodeValue()
+ form.nodeValue = form.incompleteValue ? null : form.shortValue;
+ }
+
+ // Shallow copy of the form. We could just store a reference, but
+ // eventually we'll want to update some of the data.
+ this._form = object.merge(form);
+ this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
+
+ if (form.parent) {
+ // Get the owner actor for this actor (the walker), and find the
+ // parent node of this actor from it, creating a standin node if
+ // necessary.
+ let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
+ this.reparent(parentNodeFront);
+ }
+
+ if (form.inlineTextChild) {
+ this.inlineTextChild =
+ types.getType("domnode").read(form.inlineTextChild, ctx);
+ } else {
+ this.inlineTextChild = undefined;
+ }
+ },
+
+ /**
+ * Returns the parent NodeFront for this NodeFront.
+ */
+ parentNode: function () {
+ return this._parent;
+ },
+
+ /**
+ * Process a mutation entry as returned from the walker's `getMutations`
+ * request. Only tries to handle changes of the node's contents
+ * themselves (character data and attribute changes), the walker itself
+ * will keep the ownership tree up to date.
+ */
+ updateMutation: function (change) {
+ if (change.type === "attributes") {
+ // We'll need to lazily reparse the attributes after this change.
+ this._attrMap = undefined;
+
+ // Update any already-existing attributes.
+ let found = false;
+ for (let i = 0; i < this.attributes.length; i++) {
+ let attr = this.attributes[i];
+ if (attr.name == change.attributeName &&
+ attr.namespace == change.attributeNamespace) {
+ if (change.newValue !== null) {
+ attr.value = change.newValue;
+ } else {
+ this.attributes.splice(i, 1);
+ }
+ found = true;
+ break;
+ }
+ }
+ // This is a new attribute. The null check is because of Bug 1192270,
+ // in the case of a newly added then removed attribute
+ if (!found && change.newValue !== null) {
+ this.attributes.push({
+ name: change.attributeName,
+ namespace: change.attributeNamespace,
+ value: change.newValue
+ });
+ }
+ } else if (change.type === "characterData") {
+ this._form.nodeValue = change.newValue;
+ } else if (change.type === "pseudoClassLock") {
+ this._form.pseudoClassLocks = change.pseudoClassLocks;
+ } else if (change.type === "events") {
+ this._form.hasEventListeners = change.hasEventListeners;
+ }
+ },
+
+ // Some accessors to make NodeFront feel more like an nsIDOMNode
+
+ get id() {
+ return this.getAttribute("id");
+ },
+
+ get nodeType() {
+ return this._form.nodeType;
+ },
+ get namespaceURI() {
+ return this._form.namespaceURI;
+ },
+ get nodeName() {
+ return this._form.nodeName;
+ },
+ get displayName() {
+ let {displayName, nodeName} = this._form;
+
+ // Keep `nodeName.toLowerCase()` for backward compatibility
+ return displayName || nodeName.toLowerCase();
+ },
+ get doctypeString() {
+ return "<!DOCTYPE " + this._form.name +
+ (this._form.publicId ? " PUBLIC \"" + this._form.publicId + "\"" : "") +
+ (this._form.systemId ? " \"" + this._form.systemId + "\"" : "") +
+ ">";
+ },
+
+ get baseURI() {
+ return this._form.baseURI;
+ },
+
+ get className() {
+ return this.getAttribute("class") || "";
+ },
+
+ get hasChildren() {
+ return this._form.numChildren > 0;
+ },
+ get numChildren() {
+ return this._form.numChildren;
+ },
+ get hasEventListeners() {
+ return this._form.hasEventListeners;
+ },
+
+ get isBeforePseudoElement() {
+ return this._form.isBeforePseudoElement;
+ },
+ get isAfterPseudoElement() {
+ return this._form.isAfterPseudoElement;
+ },
+ get isPseudoElement() {
+ return this.isBeforePseudoElement || this.isAfterPseudoElement;
+ },
+ get isAnonymous() {
+ return this._form.isAnonymous;
+ },
+ get isInHTMLDocument() {
+ return this._form.isInHTMLDocument;
+ },
+ get tagName() {
+ return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null;
+ },
+
+ get isDocumentElement() {
+ return !!this._form.isDocumentElement;
+ },
+
+ // doctype properties
+ get name() {
+ return this._form.name;
+ },
+ get publicId() {
+ return this._form.publicId;
+ },
+ get systemId() {
+ return this._form.systemId;
+ },
+
+ getAttribute: function (name) {
+ let attr = this._getAttribute(name);
+ return attr ? attr.value : null;
+ },
+ hasAttribute: function (name) {
+ this._cacheAttributes();
+ return (name in this._attrMap);
+ },
+
+ get hidden() {
+ let cls = this.getAttribute("class");
+ return cls && cls.indexOf(HIDDEN_CLASS) > -1;
+ },
+
+ get attributes() {
+ return this._form.attrs;
+ },
+
+ get pseudoClassLocks() {
+ return this._form.pseudoClassLocks || [];
+ },
+ hasPseudoClassLock: function (pseudo) {
+ return this.pseudoClassLocks.some(locked => locked === pseudo);
+ },
+
+ get isDisplayed() {
+ // The NodeActor's form contains the isDisplayed information as a boolean
+ // starting from FF32. Before that, the property is missing
+ return "isDisplayed" in this._form ? this._form.isDisplayed : true;
+ },
+
+ get isTreeDisplayed() {
+ let parent = this;
+ while (parent) {
+ if (!parent.isDisplayed) {
+ return false;
+ }
+ parent = parent.parentNode();
+ }
+ return true;
+ },
+
+ getNodeValue: custom(function () {
+ // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
+ // value of the node needs to be fetched on the server.
+ if (this._form.nodeValue === null && this._form.shortValue) {
+ return this._getNodeValue();
+ }
+
+ let str = this._form.nodeValue || "";
+ return promise.resolve(new SimpleStringFront(str));
+ }, {
+ impl: "_getNodeValue"
+ }),
+
+ // Accessors for custom form properties.
+
+ getFormProperty: function (name) {
+ return this._form.props ? this._form.props[name] : null;
+ },
+
+ hasFormProperty: function (name) {
+ return this._form.props ? (name in this._form.props) : null;
+ },
+
+ get formProperties() {
+ return this._form.props;
+ },
+
+ /**
+ * Return a new AttributeModificationList for this node.
+ */
+ startModifyingAttributes: function () {
+ return AttributeModificationList(this);
+ },
+
+ _cacheAttributes: function () {
+ if (typeof this._attrMap != "undefined") {
+ return;
+ }
+ this._attrMap = {};
+ for (let attr of this.attributes) {
+ this._attrMap[attr.name] = attr;
+ }
+ },
+
+ _getAttribute: function (name) {
+ this._cacheAttributes();
+ return this._attrMap[name] || undefined;
+ },
+
+ /**
+ * Set this node's parent. Note that the children saved in
+ * this tree are unordered and incomplete, so shouldn't be used
+ * instead of a `children` request.
+ */
+ reparent: function (parent) {
+ if (this._parent === parent) {
+ return;
+ }
+
+ if (this._parent && this._parent._child === this) {
+ this._parent._child = this._next;
+ }
+ if (this._prev) {
+ this._prev._next = this._next;
+ }
+ if (this._next) {
+ this._next._prev = this._prev;
+ }
+ this._next = null;
+ this._prev = null;
+ this._parent = parent;
+ if (!parent) {
+ // Subtree is disconnected, we're done
+ return;
+ }
+ this._next = parent._child;
+ if (this._next) {
+ this._next._prev = this;
+ }
+ parent._child = this;
+ },
+
+ /**
+ * Return all the known children of this node.
+ */
+ treeChildren: function () {
+ let ret = [];
+ for (let child = this._child; child != null; child = child._next) {
+ ret.push(child);
+ }
+ return ret;
+ },
+
+ /**
+ * Do we use a local target?
+ * Useful to know if a rawNode is available or not.
+ *
+ * This will, one day, be removed. External code should
+ * not need to know if the target is remote or not.
+ */
+ isLocalToBeDeprecated: function () {
+ return !!this.conn._transport._serverConnection;
+ },
+
+ /**
+ * Get an nsIDOMNode for the given node front. This only works locally,
+ * and is only intended as a stopgap during the transition to the remote
+ * protocol. If you depend on this you're likely to break soon.
+ */
+ rawNode: function (rawNode) {
+ if (!this.isLocalToBeDeprecated()) {
+ console.warn("Tried to use rawNode on a remote connection.");
+ return null;
+ }
+ const { DebuggerServer } = require("devtools/server/main");
+ let actor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
+ if (!actor) {
+ // Can happen if we try to get the raw node for an already-expired
+ // actor.
+ return null;
+ }
+ return actor.rawNode;
+ }
+});
+
+exports.NodeFront = NodeFront;
+
+/**
+ * Client side of a node list as returned by querySelectorAll()
+ */
+const NodeListFront = FrontClassWithSpec(nodeListSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ marshallPool: function () {
+ return this.parent();
+ },
+
+ // Update the object given a form representation off the wire.
+ form: function (json) {
+ this.length = json.length;
+ },
+
+ item: custom(function (index) {
+ return this._item(index).then(response => {
+ return response.node;
+ });
+ }, {
+ impl: "_item"
+ }),
+
+ items: custom(function (start, end) {
+ return this._items(start, end).then(response => {
+ return response.nodes;
+ });
+ }, {
+ impl: "_items"
+ })
+});
+
+exports.NodeListFront = NodeListFront;
+
+/**
+ * Client side of the DOM walker.
+ */
+const WalkerFront = FrontClassWithSpec(walkerSpec, {
+ // Set to true if cleanup should be requested after every mutation list.
+ autoCleanup: true,
+
+ /**
+ * This is kept for backward-compatibility reasons with older remote target.
+ * Targets previous to bug 916443
+ */
+ pick: custom(function () {
+ return this._pick().then(response => {
+ return response.node;
+ });
+ }, {impl: "_pick"}),
+
+ initialize: function (client, form) {
+ this._createRootNodePromise();
+ Front.prototype.initialize.call(this, client, form);
+ this._orphaned = new Set();
+ this._retainedOrphans = new Set();
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ // Update the object given a form representation off the wire.
+ form: function (json) {
+ this.actorID = json.actor;
+ this.rootNode = types.getType("domnode").read(json.root, this);
+ this._rootNodeDeferred.resolve(this.rootNode);
+ // FF42+ the actor starts exposing traits
+ this.traits = json.traits || {};
+ },
+
+ /**
+ * Clients can use walker.rootNode to get the current root node of the
+ * walker, but during a reload the root node might be null. This
+ * method returns a promise that will resolve to the root node when it is
+ * set.
+ */
+ getRootNode: function () {
+ return this._rootNodeDeferred.promise;
+ },
+
+ /**
+ * Create the root node promise, triggering the "new-root" notification
+ * on resolution.
+ */
+ _createRootNodePromise: function () {
+ this._rootNodeDeferred = defer();
+ this._rootNodeDeferred.promise.then(() => {
+ events.emit(this, "new-root");
+ });
+ },
+
+ /**
+ * When reading an actor form off the wire, we want to hook it up to its
+ * parent front. The protocol guarantees that the parent will be seen
+ * by the client in either a previous or the current request.
+ * So if we've already seen this parent return it, otherwise create
+ * a bare-bones stand-in node. The stand-in node will be updated
+ * with a real form by the end of the deserialization.
+ */
+ ensureParentFront: function (id) {
+ let front = this.get(id);
+ if (front) {
+ return front;
+ }
+
+ return types.getType("domnode").read({ actor: id }, this, "standin");
+ },
+
+ /**
+ * See the documentation for WalkerActor.prototype.retainNode for
+ * information on retained nodes.
+ *
+ * From the client's perspective, `retainNode` can fail if the node in
+ * question is removed from the ownership tree before the `retainNode`
+ * request reaches the server. This can only happen if the client has
+ * asked the server to release nodes but hasn't gotten a response
+ * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
+ * set is outstanding.
+ *
+ * If either of those requests is outstanding AND releases the retained
+ * node, this request will fail with noSuchActor, but the ownership tree
+ * will stay in a consistent state.
+ *
+ * Because the protocol guarantees that requests will be processed and
+ * responses received in the order they were sent, we get the right
+ * semantics by setting our local retained flag on the node only AFTER
+ * a SUCCESSFUL retainNode call.
+ */
+ retainNode: custom(function (node) {
+ return this._retainNode(node).then(() => {
+ node.retained = true;
+ });
+ }, {
+ impl: "_retainNode",
+ }),
+
+ unretainNode: custom(function (node) {
+ return this._unretainNode(node).then(() => {
+ node.retained = false;
+ if (this._retainedOrphans.has(node)) {
+ this._retainedOrphans.delete(node);
+ this._releaseFront(node);
+ }
+ });
+ }, {
+ impl: "_unretainNode"
+ }),
+
+ releaseNode: custom(function (node, options = {}) {
+ // NodeFront.destroy will destroy children in the ownership tree too,
+ // mimicking what the server will do here.
+ let actorID = node.actorID;
+ this._releaseFront(node, !!options.force);
+ return this._releaseNode({ actorID: actorID });
+ }, {
+ impl: "_releaseNode"
+ }),
+
+ findInspectingNode: custom(function () {
+ return this._findInspectingNode().then(response => {
+ return response.node;
+ });
+ }, {
+ impl: "_findInspectingNode"
+ }),
+
+ querySelector: custom(function (queryNode, selector) {
+ return this._querySelector(queryNode, selector).then(response => {
+ return response.node;
+ });
+ }, {
+ impl: "_querySelector"
+ }),
+
+ getNodeActorFromObjectActor: custom(function (objectActorID) {
+ return this._getNodeActorFromObjectActor(objectActorID).then(response => {
+ return response ? response.node : null;
+ });
+ }, {
+ impl: "_getNodeActorFromObjectActor"
+ }),
+
+ getStyleSheetOwnerNode: custom(function (styleSheetActorID) {
+ return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => {
+ return response ? response.node : null;
+ });
+ }, {
+ impl: "_getStyleSheetOwnerNode"
+ }),
+
+ getNodeFromActor: custom(function (actorID, path) {
+ return this._getNodeFromActor(actorID, path).then(response => {
+ return response ? response.node : null;
+ });
+ }, {
+ impl: "_getNodeFromActor"
+ }),
+
+ /*
+ * Incrementally search the document for a given string.
+ * For modern servers, results will be searched with using the WalkerActor
+ * `search` function (includes tag names, attributes, and text contents).
+ * Only 1 result is sent back, and calling the method again with the same
+ * query will send the next result. When there are no more results to be sent
+ * back, null is sent.
+ * @param {String} query
+ * @param {Object} options
+ * - "reverse": search backwards
+ * - "selectorOnly": treat input as a selector string (don't search text
+ * tags, attributes, etc)
+ */
+ search: custom(Task.async(function* (query, options = { }) {
+ let nodeList;
+ let searchType;
+ let searchData = this.searchData = this.searchData || { };
+ let selectorOnly = !!options.selectorOnly;
+
+ // Backwards compat. Use selector only search if the new
+ // search functionality isn't implemented, or if the caller (tests)
+ // want it.
+ if (selectorOnly || !this.traits.textSearch) {
+ searchType = "selector";
+ if (this.traits.multiFrameQuerySelectorAll) {
+ nodeList = yield this.multiFrameQuerySelectorAll(query);
+ } else {
+ nodeList = yield this.querySelectorAll(this.rootNode, query);
+ }
+ } else {
+ searchType = "search";
+ let result = yield this._search(query, options);
+ nodeList = result.list;
+ }
+
+ // If this is a new search, start at the beginning.
+ if (searchData.query !== query ||
+ searchData.selectorOnly !== selectorOnly) {
+ searchData.selectorOnly = selectorOnly;
+ searchData.query = query;
+ searchData.index = -1;
+ }
+
+ if (!nodeList.length) {
+ return null;
+ }
+
+ // Move search result cursor and cycle if necessary.
+ searchData.index = options.reverse ? searchData.index - 1 :
+ searchData.index + 1;
+ if (searchData.index >= nodeList.length) {
+ searchData.index = 0;
+ }
+ if (searchData.index < 0) {
+ searchData.index = nodeList.length - 1;
+ }
+
+ // Send back the single node, along with any relevant search data
+ let node = yield nodeList.item(searchData.index);
+ return {
+ type: searchType,
+ node: node,
+ resultsLength: nodeList.length,
+ resultsIndex: searchData.index,
+ };
+ }), {
+ impl: "_search"
+ }),
+
+ _releaseFront: function (node, force) {
+ if (node.retained && !force) {
+ node.reparent(null);
+ this._retainedOrphans.add(node);
+ return;
+ }
+
+ if (node.retained) {
+ // Forcing a removal.
+ this._retainedOrphans.delete(node);
+ }
+
+ // Release any children
+ for (let child of node.treeChildren()) {
+ this._releaseFront(child, force);
+ }
+
+ // All children will have been removed from the node by this point.
+ node.reparent(null);
+ node.destroy();
+ },
+
+ /**
+ * Get any unprocessed mutation records and process them.
+ */
+ getMutations: custom(function (options = {}) {
+ return this._getMutations(options).then(mutations => {
+ let emitMutations = [];
+ for (let change of mutations) {
+ // The target is only an actorID, get the associated front.
+ let targetID;
+ let targetFront;
+
+ if (change.type === "newRoot") {
+ // We may receive a new root without receiving any documentUnload
+ // beforehand. Like when opening tools in middle of a document load.
+ if (this.rootNode) {
+ this._createRootNodePromise();
+ }
+ this.rootNode = types.getType("domnode").read(change.target, this);
+ this._rootNodeDeferred.resolve(this.rootNode);
+ targetID = this.rootNode.actorID;
+ targetFront = this.rootNode;
+ } else {
+ targetID = change.target;
+ targetFront = this.get(targetID);
+ }
+
+ if (!targetFront) {
+ console.trace("Got a mutation for an unexpected actor: " + targetID +
+ ", please file a bug on bugzilla.mozilla.org!");
+ continue;
+ }
+
+ let emittedMutation = object.merge(change, { target: targetFront });
+
+ if (change.type === "childList" ||
+ change.type === "nativeAnonymousChildList") {
+ // Update the ownership tree according to the mutation record.
+ let addedFronts = [];
+ let removedFronts = [];
+ for (let removed of change.removed) {
+ let removedFront = this.get(removed);
+ if (!removedFront) {
+ console.error("Got a removal of an actor we didn't know about: " +
+ removed);
+ continue;
+ }
+ // Remove from the ownership tree
+ removedFront.reparent(null);
+
+ // This node is orphaned unless we get it in the 'added' list
+ // eventually.
+ this._orphaned.add(removedFront);
+ removedFronts.push(removedFront);
+ }
+ for (let added of change.added) {
+ let addedFront = this.get(added);
+ if (!addedFront) {
+ console.error("Got an addition of an actor we didn't know " +
+ "about: " + added);
+ continue;
+ }
+ addedFront.reparent(targetFront);
+
+ // The actor is reconnected to the ownership tree, unorphan
+ // it.
+ this._orphaned.delete(addedFront);
+ addedFronts.push(addedFront);
+ }
+
+ // Before passing to users, replace the added and removed actor
+ // ids with front in the mutation record.
+ emittedMutation.added = addedFronts;
+ emittedMutation.removed = removedFronts;
+
+ // If this is coming from a DOM mutation, the actor's numChildren
+ // was passed in. Otherwise, it is simulated from a frame load or
+ // unload, so don't change the front's form.
+ if ("numChildren" in change) {
+ targetFront._form.numChildren = change.numChildren;
+ }
+ } else if (change.type === "frameLoad") {
+ // Nothing we need to do here, except verify that we don't have any
+ // document children, because we should have gotten a documentUnload
+ // first.
+ for (let child of targetFront.treeChildren()) {
+ if (child.nodeType === nodeConstants.DOCUMENT_NODE) {
+ console.trace("Got an unexpected frameLoad in the inspector, " +
+ "please file a bug on bugzilla.mozilla.org!");
+ }
+ }
+ } else if (change.type === "documentUnload") {
+ if (targetFront === this.rootNode) {
+ this._createRootNodePromise();
+ }
+
+ // We try to give fronts instead of actorIDs, but these fronts need
+ // to be destroyed now.
+ emittedMutation.target = targetFront.actorID;
+ emittedMutation.targetParent = targetFront.parentNode();
+
+ // Release the document node and all of its children, even retained.
+ this._releaseFront(targetFront, true);
+ } else if (change.type === "unretained") {
+ // Retained orphans were force-released without the intervention of
+ // client (probably a navigated frame).
+ for (let released of change.nodes) {
+ let releasedFront = this.get(released);
+ this._retainedOrphans.delete(released);
+ this._releaseFront(releasedFront, true);
+ }
+ } else {
+ targetFront.updateMutation(change);
+ }
+
+ // Update the inlineTextChild property of the target for a selected list of
+ // mutation types.
+ if (change.type === "inlineTextChild" ||
+ change.type === "childList" ||
+ change.type === "nativeAnonymousChildList") {
+ if (change.inlineTextChild) {
+ targetFront.inlineTextChild =
+ types.getType("domnode").read(change.inlineTextChild, this);
+ } else {
+ targetFront.inlineTextChild = undefined;
+ }
+ }
+
+ emitMutations.push(emittedMutation);
+ }
+
+ if (options.cleanup) {
+ for (let node of this._orphaned) {
+ // This will move retained nodes to this._retainedOrphans.
+ this._releaseFront(node);
+ }
+ this._orphaned = new Set();
+ }
+
+ events.emit(this, "mutations", emitMutations);
+ });
+ }, {
+ impl: "_getMutations"
+ }),
+
+ /**
+ * Handle the `new-mutations` notification by fetching the
+ * available mutation records.
+ */
+ onMutations: preEvent("new-mutations", function () {
+ // Fetch and process the mutations.
+ this.getMutations({cleanup: this.autoCleanup}).catch(() => {});
+ }),
+
+ isLocal: function () {
+ return !!this.conn._transport._serverConnection;
+ },
+
+ // XXX hack during transition to remote inspector: get a proper NodeFront
+ // for a given local node. Only works locally.
+ frontForRawNode: function (rawNode) {
+ if (!this.isLocal()) {
+ console.warn("Tried to use frontForRawNode on a remote connection.");
+ return null;
+ }
+ const { DebuggerServer } = require("devtools/server/main");
+ let walkerActor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
+ if (!walkerActor) {
+ throw Error("Could not find client side for actor " + this.actorID);
+ }
+ let nodeActor = walkerActor._ref(rawNode);
+
+ // Pass the node through a read/write pair to create the client side actor.
+ let nodeType = types.getType("domnode");
+ let returnNode = nodeType.read(
+ nodeType.write(nodeActor, walkerActor), this);
+ let top = returnNode;
+ let extras = walkerActor.parents(nodeActor, {sameTypeRootTreeItem: true});
+ for (let extraActor of extras) {
+ top = nodeType.read(nodeType.write(extraActor, walkerActor), this);
+ }
+
+ if (top !== this.rootNode) {
+ // Imported an already-orphaned node.
+ this._orphaned.add(top);
+ walkerActor._orphaned
+ .add(DebuggerServer._searchAllConnectionsForActor(top.actorID));
+ }
+ return returnNode;
+ },
+
+ removeNode: custom(Task.async(function* (node) {
+ let previousSibling = yield this.previousSibling(node);
+ let nextSibling = yield this._removeNode(node);
+ return {
+ previousSibling: previousSibling,
+ nextSibling: nextSibling,
+ };
+ }), {
+ impl: "_removeNode"
+ }),
+});
+
+exports.WalkerFront = WalkerFront;
+
+/**
+ * Client side of the inspector actor, which is used to create
+ * inspector-related actors, including the walker.
+ */
+var InspectorFront = FrontClassWithSpec(inspectorSpec, {
+ initialize: function (client, tabForm) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = tabForm.inspectorActor;
+
+ // XXX: This is the first actor type in its hierarchy to use the protocol
+ // library, so we're going to self-own on the client side for now.
+ this.manage(this);
+ },
+
+ destroy: function () {
+ delete this.walker;
+ Front.prototype.destroy.call(this);
+ },
+
+ getWalker: custom(function (options = {}) {
+ return this._getWalker(options).then(walker => {
+ this.walker = walker;
+ return walker;
+ });
+ }, {
+ impl: "_getWalker"
+ }),
+
+ getPageStyle: custom(function () {
+ return this._getPageStyle().then(pageStyle => {
+ // We need a walker to understand node references from the
+ // node style.
+ if (this.walker) {
+ return pageStyle;
+ }
+ return this.getWalker().then(() => {
+ return pageStyle;
+ });
+ });
+ }, {
+ impl: "_getPageStyle"
+ }),
+
+ pickColorFromPage: custom(Task.async(function* (toolbox, options) {
+ if (toolbox) {
+ // If the eyedropper was already started using the gcli command, hide it so we don't
+ // end up with 2 instances of the eyedropper on the page.
+ let {target} = toolbox;
+ let requisition = yield CommandUtils.createRequisition(target, {
+ environment: CommandUtils.createEnvironment({target})
+ });
+ yield requisition.updateExec("eyedropper --hide");
+ }
+
+ yield this._pickColorFromPage(options);
+ }), {
+ impl: "_pickColorFromPage"
+ })
+});
+
+exports.InspectorFront = InspectorFront;
diff --git a/devtools/shared/fronts/layout.js b/devtools/shared/fronts/layout.js
new file mode 100644
index 000000000..5a1a6185d
--- /dev/null
+++ b/devtools/shared/fronts/layout.js
@@ -0,0 +1,30 @@
+/* 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 { FrontClassWithSpec } = require("devtools/shared/protocol");
+const { gridSpec, layoutSpec } = require("devtools/shared/specs/layout");
+
+const GridFront = FrontClassWithSpec(gridSpec, {
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this._form = form;
+ },
+
+ /**
+ * Getter for the grid fragments data.
+ */
+ get gridFragments() {
+ return this._form.gridFragments;
+ }
+});
+
+const LayoutFront = FrontClassWithSpec(layoutSpec, {});
+
+exports.GridFront = GridFront;
+exports.LayoutFront = LayoutFront;
diff --git a/devtools/shared/fronts/memory.js b/devtools/shared/fronts/memory.js
new file mode 100644
index 000000000..d7a6a3108
--- /dev/null
+++ b/devtools/shared/fronts/memory.js
@@ -0,0 +1,92 @@
+/* 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 { memorySpec } = require("devtools/shared/specs/memory");
+const { Task } = require("devtools/shared/task");
+const protocol = require("devtools/shared/protocol");
+
+loader.lazyRequireGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm", true);
+loader.lazyRequireGetter(this, "HeapSnapshotFileUtils",
+ "devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
+
+const MemoryFront = protocol.FrontClassWithSpec(memorySpec, {
+ initialize: function (client, form, rootForm = null) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ this._client = client;
+ this.actorID = form.memoryActor;
+ this.heapSnapshotFileActorID = rootForm
+ ? rootForm.heapSnapshotFileActor
+ : null;
+ this.manage(this);
+ },
+
+ /**
+ * Save a heap snapshot, transfer it from the server to the client if the
+ * server and client do not share a file system, and return the local file
+ * path to the heap snapshot.
+ *
+ * Note that this is safe to call for actors inside sandoxed child processes,
+ * as we jump through the correct IPDL hoops.
+ *
+ * @params Boolean options.forceCopy
+ * Always force a bulk data copy of the saved heap snapshot, even when
+ * the server and client share a file system.
+ *
+ * @params {Object|undefined} options.boundaries
+ * The boundaries for the heap snapshot. See
+ * ThreadSafeChromeUtils.webidl for more details.
+ *
+ * @returns Promise<String>
+ */
+ saveHeapSnapshot: protocol.custom(Task.async(function* (options = {}) {
+ const snapshotId = yield this._saveHeapSnapshotImpl(options.boundaries);
+
+ if (!options.forceCopy &&
+ (yield HeapSnapshotFileUtils.haveHeapSnapshotTempFile(snapshotId))) {
+ return HeapSnapshotFileUtils.getHeapSnapshotTempFilePath(snapshotId);
+ }
+
+ return yield this.transferHeapSnapshot(snapshotId);
+ }), {
+ impl: "_saveHeapSnapshotImpl"
+ }),
+
+ /**
+ * Given that we have taken a heap snapshot with the given id, transfer the
+ * heap snapshot file to the client. The path to the client's local file is
+ * returned.
+ *
+ * @param {String} snapshotId
+ *
+ * @returns Promise<String>
+ */
+ transferHeapSnapshot: protocol.custom(function (snapshotId) {
+ if (!this.heapSnapshotFileActorID) {
+ throw new Error("MemoryFront initialized without a rootForm");
+ }
+
+ const request = this._client.request({
+ to: this.heapSnapshotFileActorID,
+ type: "transferHeapSnapshot",
+ snapshotId
+ });
+
+ return new Promise((resolve, reject) => {
+ const outFilePath =
+ HeapSnapshotFileUtils.getNewUniqueHeapSnapshotTempFilePath();
+ const outFile = new FileUtils.File(outFilePath);
+
+ const outFileStream = FileUtils.openSafeFileOutputStream(outFile);
+ request.on("bulk-reply", Task.async(function* ({ copyTo }) {
+ yield copyTo(outFileStream);
+ FileUtils.closeSafeFileOutputStream(outFileStream);
+ resolve(outFilePath);
+ }));
+ });
+ })
+});
+
+exports.MemoryFront = MemoryFront;
diff --git a/devtools/shared/fronts/moz.build b/devtools/shared/fronts/moz.build
new file mode 100644
index 000000000..8a38d6b5d
--- /dev/null
+++ b/devtools/shared/fronts/moz.build
@@ -0,0 +1,41 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+DevToolsModules(
+ 'actor-registry.js',
+ 'addons.js',
+ 'animation.js',
+ 'call-watcher.js',
+ 'canvas.js',
+ 'css-properties.js',
+ 'csscoverage.js',
+ 'device.js',
+ 'director-manager.js',
+ 'director-registry.js',
+ 'emulation.js',
+ 'eventlooplag.js',
+ 'framerate.js',
+ 'gcli.js',
+ 'highlighters.js',
+ 'inspector.js',
+ 'layout.js',
+ 'memory.js',
+ 'performance-entries.js',
+ 'performance-recording.js',
+ 'performance.js',
+ 'preference.js',
+ 'profiler.js',
+ 'promises.js',
+ 'reflow.js',
+ 'settings.js',
+ 'storage.js',
+ 'string.js',
+ 'styles.js',
+ 'stylesheets.js',
+ 'timeline.js',
+ 'webaudio.js',
+ 'webgl.js'
+)
diff --git a/devtools/shared/fronts/performance-entries.js b/devtools/shared/fronts/performance-entries.js
new file mode 100644
index 000000000..5f3236530
--- /dev/null
+++ b/devtools/shared/fronts/performance-entries.js
@@ -0,0 +1,17 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const performanceSpec = require("devtools/shared/specs/performance-entries");
+
+var PerformanceEntriesFront = FrontClassWithSpec(performanceSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = form.performanceEntriesActor;
+ this.manage(this);
+ },
+});
+
+exports.PerformanceEntriesFront = PerformanceEntriesFront;
diff --git a/devtools/shared/fronts/performance-recording.js b/devtools/shared/fronts/performance-recording.js
new file mode 100644
index 000000000..09c61d4ee
--- /dev/null
+++ b/devtools/shared/fronts/performance-recording.js
@@ -0,0 +1,152 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const { performanceRecordingSpec } = require("devtools/shared/specs/performance-recording");
+
+loader.lazyRequireGetter(this, "PerformanceIO",
+ "devtools/client/performance/modules/io");
+loader.lazyRequireGetter(this, "PerformanceRecordingCommon",
+ "devtools/shared/performance/recording-common", true);
+loader.lazyRequireGetter(this, "RecordingUtils",
+ "devtools/shared/performance/recording-utils");
+loader.lazyRequireGetter(this, "merge", "sdk/util/object", true);
+
+/**
+ * This can be used on older Profiler implementations, but the methods cannot
+ * be changed -- you must introduce a new method, and detect the server.
+ */
+const PerformanceRecordingFront = FrontClassWithSpec(performanceRecordingSpec, merge({
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this.actorID = form.actor;
+ this._form = form;
+ this._configuration = form.configuration;
+ this._startingBufferStatus = form.startingBufferStatus;
+ this._console = form.console;
+ this._label = form.label;
+ this._startTime = form.startTime;
+ this._localStartTime = form.localStartTime;
+ this._recording = form.recording;
+ this._completed = form.completed;
+ this._duration = form.duration;
+
+ if (form.finalizedData) {
+ this._profile = form.profile;
+ this._systemHost = form.systemHost;
+ this._systemClient = form.systemClient;
+ }
+
+ // Sort again on the client side if we're using realtime markers and the recording
+ // just finished. This is because GC/Compositing markers can come into the array out
+ // of order with the other markers, leading to strange collapsing in waterfall view.
+ if (this._completed && !this._markersSorted) {
+ this._markers = this._markers.sort((a, b) => (a.start > b.start));
+ this._markersSorted = true;
+ }
+ },
+
+ initialize: function (client, form, config) {
+ Front.prototype.initialize.call(this, client, form);
+ this._markers = [];
+ this._frames = [];
+ this._memory = [];
+ this._ticks = [];
+ this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] };
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ /**
+ * Saves the current recording to a file.
+ *
+ * @param nsILocalFile file
+ * The file to stream the data into.
+ */
+ exportRecording: function (file) {
+ let recordingData = this.getAllData();
+ return PerformanceIO.saveRecordingToFile(recordingData, file);
+ },
+
+ /**
+ * Fired whenever the PerformanceFront emits markers, memory or ticks.
+ */
+ _addTimelineData: function (eventName, data) {
+ let config = this.getConfiguration();
+
+ switch (eventName) {
+ // Accumulate timeline markers into an array. Furthermore, the timestamps
+ // do not have a zero epoch, so offset all of them by the start time.
+ case "markers": {
+ if (!config.withMarkers) {
+ break;
+ }
+ let { markers } = data;
+ RecordingUtils.offsetMarkerTimes(markers, this._startTime);
+ RecordingUtils.pushAll(this._markers, markers);
+ break;
+ }
+ // Accumulate stack frames into an array.
+ case "frames": {
+ if (!config.withMarkers) {
+ break;
+ }
+ let { frames } = data;
+ RecordingUtils.pushAll(this._frames, frames);
+ break;
+ }
+ // Accumulate memory measurements into an array. Furthermore, the timestamp
+ // does not have a zero epoch, so offset it by the actor's start time.
+ case "memory": {
+ if (!config.withMemory) {
+ break;
+ }
+ let { delta, measurement } = data;
+ this._memory.push({
+ delta: delta - this._startTime,
+ value: measurement.total / 1024 / 1024
+ });
+ break;
+ }
+ // Save the accumulated refresh driver ticks.
+ case "ticks": {
+ if (!config.withTicks) {
+ break;
+ }
+ let { timestamps } = data;
+ this._ticks = timestamps;
+ break;
+ }
+ // Accumulate allocation sites into an array.
+ case "allocations": {
+ if (!config.withAllocations) {
+ break;
+ }
+ let {
+ allocations: sites,
+ allocationsTimestamps: timestamps,
+ allocationSizes: sizes,
+ frames,
+ } = data;
+
+ RecordingUtils.offsetAndScaleTimestamps(timestamps, this._startTime);
+ RecordingUtils.pushAll(this._allocations.sites, sites);
+ RecordingUtils.pushAll(this._allocations.timestamps, timestamps);
+ RecordingUtils.pushAll(this._allocations.frames, frames);
+ RecordingUtils.pushAll(this._allocations.sizes, sizes);
+ break;
+ }
+ }
+ },
+
+ toString: () => "[object PerformanceRecordingFront]"
+}, PerformanceRecordingCommon));
+
+exports.PerformanceRecordingFront = PerformanceRecordingFront;
diff --git a/devtools/shared/fronts/performance.js b/devtools/shared/fronts/performance.js
new file mode 100644
index 000000000..da5a9ffb0
--- /dev/null
+++ b/devtools/shared/fronts/performance.js
@@ -0,0 +1,148 @@
+/* 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 { Cu } = require("chrome");
+const { Front, FrontClassWithSpec, custom, preEvent } = require("devtools/shared/protocol");
+const { PerformanceRecordingFront } = require("devtools/shared/fronts/performance-recording");
+const { performanceSpec } = require("devtools/shared/specs/performance");
+const { Task } = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "PerformanceIO",
+ "devtools/client/performance/modules/io");
+loader.lazyRequireGetter(this, "LegacyPerformanceFront",
+ "devtools/client/performance/legacy/front", true);
+loader.lazyRequireGetter(this, "getSystemInfo",
+ "devtools/shared/system", true);
+
+const PerformanceFront = FrontClassWithSpec(performanceSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+ this.actorID = form.performanceActor;
+ this.manage(this);
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ /**
+ * Conenct to the server, and handle once-off tasks like storing traits
+ * or system info.
+ */
+ connect: custom(Task.async(function* () {
+ let systemClient = yield getSystemInfo();
+ let { traits } = yield this._connect({ systemClient });
+ this._traits = traits;
+
+ return this._traits;
+ }), {
+ impl: "_connect"
+ }),
+
+ get traits() {
+ if (!this._traits) {
+ Cu.reportError("Cannot access traits of PerformanceFront before " +
+ "calling `connect()`.");
+ }
+ return this._traits;
+ },
+
+ /**
+ * Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much
+ * of this recording's lifetime remains without being overwritten.
+ *
+ * @param {PerformanceRecording} recording
+ * @return {number?}
+ */
+ getBufferUsageForRecording: function (recording) {
+ if (!recording.isRecording()) {
+ return void 0;
+ }
+ let {
+ position: currentPosition,
+ totalSize,
+ generation: currentGeneration
+ } = this._currentBufferStatus;
+ let {
+ position: origPosition,
+ generation: origGeneration
+ } = recording.getStartingBufferStatus();
+
+ let normalizedCurrent = (totalSize * (currentGeneration - origGeneration)) +
+ currentPosition;
+ let percent = (normalizedCurrent - origPosition) / totalSize;
+
+ // Clamp between 0 and 1; can get negative percentage values when a new
+ // recording starts and the currentBufferStatus has not yet been updated. Rather
+ // than fetching another status update, just clamp to 0, and this will be updated
+ // on the next profiler-status event.
+ if (percent < 0) {
+ return 0;
+ } else if (percent > 1) {
+ return 1;
+ }
+
+ return percent;
+ },
+
+ /**
+ * Loads a recording from a file.
+ *
+ * @param {nsILocalFile} file
+ * The file to import the data from.
+ * @return {Promise<PerformanceRecordingFront>}
+ */
+ importRecording: function (file) {
+ return PerformanceIO.loadRecordingFromFile(file).then(recordingData => {
+ let model = new PerformanceRecordingFront();
+ model._imported = true;
+ model._label = recordingData.label || "";
+ model._duration = recordingData.duration;
+ model._markers = recordingData.markers;
+ model._frames = recordingData.frames;
+ model._memory = recordingData.memory;
+ model._ticks = recordingData.ticks;
+ model._allocations = recordingData.allocations;
+ model._profile = recordingData.profile;
+ model._configuration = recordingData.configuration || {};
+ model._systemHost = recordingData.systemHost;
+ model._systemClient = recordingData.systemClient;
+ return model;
+ });
+ },
+
+ /**
+ * Store profiler status when the position has been update so we can
+ * calculate recording's buffer percentage usage after emitting the event.
+ */
+ _onProfilerStatus: preEvent("profiler-status", function (data) {
+ this._currentBufferStatus = data;
+ }),
+
+ /**
+ * For all PerformanceRecordings that are recording, and needing realtime markers,
+ * apply the timeline data to the front PerformanceRecording (so we only have one event
+ * for each timeline data chunk as they could be shared amongst several recordings).
+ */
+ _onTimelineEvent: preEvent("timeline-data", function (type, data, recordings) {
+ for (let recording of recordings) {
+ recording._addTimelineData(type, data);
+ }
+ }),
+});
+
+exports.PerformanceFront = PerformanceFront;
+
+exports.createPerformanceFront = function createPerformanceFront(target) {
+ // If we force legacy mode, or the server does not have a performance actor (< Fx42),
+ // use our LegacyPerformanceFront which will handle
+ // the communication over RDP to other underlying actors.
+ if (target.TEST_PERFORMANCE_LEGACY_FRONT || !target.form.performanceActor) {
+ return new LegacyPerformanceFront(target);
+ }
+ // If our server has a PerformanceActor implementation, set this
+ // up like a normal front.
+ return new PerformanceFront(target.client, target.form);
+};
diff --git a/devtools/shared/fronts/preference.js b/devtools/shared/fronts/preference.js
new file mode 100644
index 000000000..22b3f912f
--- /dev/null
+++ b/devtools/shared/fronts/preference.js
@@ -0,0 +1,31 @@
+/* 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 {preferenceSpec} = require("devtools/shared/specs/preference");
+const protocol = require("devtools/shared/protocol");
+
+const PreferenceFront = protocol.FrontClassWithSpec(preferenceSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client);
+ this.actorID = form.preferenceActor;
+ this.manage(this);
+ },
+});
+
+const _knownPreferenceFronts = new WeakMap();
+
+exports.getPreferenceFront = function (client, form) {
+ if (!form.preferenceActor) {
+ return null;
+ }
+
+ if (_knownPreferenceFronts.has(client)) {
+ return _knownPreferenceFronts.get(client);
+ }
+
+ let front = new PreferenceFront(client, form);
+ _knownPreferenceFronts.set(client, front);
+ return front;
+};
diff --git a/devtools/shared/fronts/profiler.js b/devtools/shared/fronts/profiler.js
new file mode 100644
index 000000000..f564513e3
--- /dev/null
+++ b/devtools/shared/fronts/profiler.js
@@ -0,0 +1,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/. */
+"use strict";
+
+const { Cu } = require("chrome");
+const {
+ Front,
+ FrontClassWithSpec,
+ custom
+} = require("devtools/shared/protocol");
+const { profilerSpec } = require("devtools/shared/specs/profiler");
+
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+loader.lazyRequireGetter(this, "extend", "sdk/util/object", true);
+
+/**
+ * This can be used on older Profiler implementations, but the methods cannot
+ * be changed -- you must introduce a new method, and detect the server.
+ */
+exports.ProfilerFront = FrontClassWithSpec(profilerSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+ this.actorID = form.profilerActor;
+ this.manage(this);
+
+ this._onProfilerEvent = this._onProfilerEvent.bind(this);
+ events.on(this, "*", this._onProfilerEvent);
+ },
+
+ destroy: function () {
+ events.off(this, "*", this._onProfilerEvent);
+ Front.prototype.destroy.call(this);
+ },
+
+ /**
+ * If using the protocol.js Fronts, then make stringify default,
+ * since the read/write mechanisms will expose it as an object anyway, but
+ * this lets other consumers who connect directly (xpcshell tests, Gecko Profiler) to
+ * have unchanged behaviour.
+ */
+ getProfile: custom(function (options) {
+ return this._getProfile(extend({ stringify: true }, options));
+ }, {
+ impl: "_getProfile"
+ }),
+
+ /**
+ * Also emit an old `eventNotification` for older consumers of the profiler.
+ */
+ _onProfilerEvent: function (eventName, data) {
+ // If this event already passed through once, don't repropagate
+ if (data.relayed) {
+ return;
+ }
+ data.relayed = true;
+
+ if (eventName === "eventNotification") {
+ // If this is `eventNotification`, this is coming from an older Gecko (<Fx42)
+ // that doesn't use protocol.js style events. Massage it to emit a protocol.js
+ // style event as well.
+ events.emit(this, data.topic, data);
+ } else {
+ // Otherwise if a modern protocol.js event, emit it also as `eventNotification`
+ // for compatibility reasons on the client (like for any add-ons/Gecko Profiler
+ // using this event) and log a deprecation message if there is a listener.
+ this.conn.emit("eventNotification", {
+ subject: data.subject,
+ topic: data.topic,
+ data: data.data,
+ details: data.details
+ });
+ if (this.conn._getListeners("eventNotification").length) {
+ Cu.reportError(`
+ ProfilerActor's "eventNotification" on the DebuggerClient has been deprecated.
+ Use the ProfilerFront found in "devtools/server/actors/profiler".`);
+ }
+ }
+ },
+});
diff --git a/devtools/shared/fronts/promises.js b/devtools/shared/fronts/promises.js
new file mode 100644
index 000000000..72896455d
--- /dev/null
+++ b/devtools/shared/fronts/promises.js
@@ -0,0 +1,27 @@
+/* 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 {
+ Front,
+ FrontClassWithSpec,
+} = require("devtools/shared/protocol");
+const { promisesSpec } = require("devtools/shared/specs/promises");
+
+/**
+ * PromisesFront, the front for the PromisesActor.
+ */
+const PromisesFront = FrontClassWithSpec(promisesSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+ this.actorID = form.promisesActor;
+ this.manage(this);
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ }
+});
+
+exports.PromisesFront = PromisesFront;
diff --git a/devtools/shared/fronts/reflow.js b/devtools/shared/fronts/reflow.js
new file mode 100644
index 000000000..69c513270
--- /dev/null
+++ b/devtools/shared/fronts/reflow.js
@@ -0,0 +1,29 @@
+/* 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 {reflowSpec} = require("devtools/shared/specs/reflow");
+const protocol = require("devtools/shared/protocol");
+
+/**
+ * Usage example of the reflow front:
+ *
+ * let front = ReflowFront(toolbox.target.client, toolbox.target.form);
+ * front.on("reflows", this._onReflows);
+ * front.start();
+ * // now wait for events to come
+ */
+const ReflowFront = protocol.FrontClassWithSpec(reflowSpec, {
+ initialize: function (client, {reflowActor}) {
+ protocol.Front.prototype.initialize.call(this, client, {actor: reflowActor});
+ this.manage(this);
+ },
+
+ destroy: function () {
+ protocol.Front.prototype.destroy.call(this);
+ },
+});
+
+exports.ReflowFront = ReflowFront;
diff --git a/devtools/shared/fronts/settings.js b/devtools/shared/fronts/settings.js
new file mode 100644
index 000000000..158425364
--- /dev/null
+++ b/devtools/shared/fronts/settings.js
@@ -0,0 +1,29 @@
+/* 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 {settingsSpec} = require("devtools/shared/specs/settings");
+const protocol = require("devtools/shared/protocol");
+
+const SettingsFront = protocol.FrontClassWithSpec(settingsSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client);
+ this.actorID = form.settingsActor;
+ this.manage(this);
+ },
+});
+
+const _knownSettingsFronts = new WeakMap();
+
+exports.getSettingsFront = function (client, form) {
+ if (!form.settingsActor) {
+ return null;
+ }
+ if (_knownSettingsFronts.has(client)) {
+ return _knownSettingsFronts.get(client);
+ }
+ let front = new SettingsFront(client, form);
+ _knownSettingsFronts.set(client, front);
+ return front;
+};
diff --git a/devtools/shared/fronts/storage.js b/devtools/shared/fronts/storage.js
new file mode 100644
index 000000000..304e57c6f
--- /dev/null
+++ b/devtools/shared/fronts/storage.js
@@ -0,0 +1,32 @@
+/* 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 protocol = require("devtools/shared/protocol");
+const specs = require("devtools/shared/specs/storage");
+
+for (let childSpec of Object.values(specs.childSpecs)) {
+ protocol.FrontClassWithSpec(childSpec, {
+ form(form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return null;
+ }
+
+ this.actorID = form.actor;
+ this.hosts = form.hosts;
+ return null;
+ }
+ });
+}
+
+const StorageFront = protocol.FrontClassWithSpec(specs.storageSpec, {
+ initialize(client, tabForm) {
+ protocol.Front.prototype.initialize.call(this, client);
+ this.actorID = tabForm.storageActor;
+ this.manage(this);
+ }
+});
+
+exports.StorageFront = StorageFront;
diff --git a/devtools/shared/fronts/string.js b/devtools/shared/fronts/string.js
new file mode 100644
index 000000000..12036afe4
--- /dev/null
+++ b/devtools/shared/fronts/string.js
@@ -0,0 +1,47 @@
+/* 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 {DebuggerServer} = require("devtools/server/main");
+const promise = require("promise");
+const {longStringSpec, SimpleStringFront} = require("devtools/shared/specs/string");
+const protocol = require("devtools/shared/protocol");
+
+const LongStringFront = protocol.FrontClassWithSpec(longStringSpec, {
+ initialize: function (client) {
+ protocol.Front.prototype.initialize.call(this, client);
+ },
+
+ destroy: function () {
+ this.initial = null;
+ this.length = null;
+ this.strPromise = null;
+ protocol.Front.prototype.destroy.call(this);
+ },
+
+ form: function (form) {
+ this.actorID = form.actor;
+ this.initial = form.initial;
+ this.length = form.length;
+ },
+
+ string: function () {
+ if (!this.strPromise) {
+ let promiseRest = (thusFar) => {
+ if (thusFar.length === this.length) {
+ return promise.resolve(thusFar);
+ }
+ return this.substring(thusFar.length,
+ thusFar.length + DebuggerServer.LONG_STRING_READ_LENGTH)
+ .then((next) => promiseRest(thusFar + next));
+ };
+
+ this.strPromise = promiseRest(this.initial);
+ }
+ return this.strPromise;
+ }
+});
+
+exports.LongStringFront = LongStringFront;
+exports.SimpleStringFront = SimpleStringFront;
diff --git a/devtools/shared/fronts/styleeditor.js b/devtools/shared/fronts/styleeditor.js
new file mode 100644
index 000000000..5feb6df1b
--- /dev/null
+++ b/devtools/shared/fronts/styleeditor.js
@@ -0,0 +1,113 @@
+/* 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 { SimpleStringFront } = require("devtools/shared/fronts/string");
+const { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const {
+ oldStyleSheetSpec,
+ styleEditorSpec
+} = require("devtools/shared/specs/styleeditor");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const events = require("sdk/event/core");
+
+/**
+ * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
+ */
+const OldStyleSheetFront = FrontClassWithSpec(oldStyleSheetSpec, {
+ initialize: function (conn, form, ctx, detail) {
+ 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);
+
+ 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;
+ },
+
+ getText: function () {
+ let deferred = defer();
+
+ events.once(this, "source-load", (source) => {
+ let longStr = new SimpleStringFront(source);
+ deferred.resolve(longStr);
+ });
+ this.fetchSource();
+
+ return deferred.promise;
+ },
+
+ getOriginalSources: function () {
+ return promise.resolve([]);
+ },
+
+ get href() {
+ return this._form.href;
+ },
+ get nodeHref() {
+ return this._form.nodeHref;
+ },
+ get disabled() {
+ return !!this._form.disabled;
+ },
+ get title() {
+ return this._form.title;
+ },
+ get isSystem() {
+ return this._form.system;
+ },
+ get styleSheetIndex() {
+ return this._form.styleSheetIndex;
+ },
+ get ruleCount() {
+ return this._form.ruleCount;
+ }
+});
+
+exports.OldStyleSheetFront = OldStyleSheetFront;
+
+/**
+ * The corresponding Front object for the StyleEditorActor.
+ */
+const StyleEditorFront = FrontClassWithSpec(styleEditorSpec, {
+ initialize: function (client, tabForm) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = tabForm.styleEditorActor;
+ this.manage(this);
+ },
+
+ getStyleSheets: function () {
+ let deferred = defer();
+
+ events.once(this, "document-load", (styleSheets) => {
+ deferred.resolve(styleSheets);
+ });
+ this.newDocument();
+
+ return deferred.promise;
+ },
+
+ addStyleSheet: function (text) {
+ return this.newStyleSheet(text);
+ }
+});
+
+exports.StyleEditorFront = StyleEditorFront;
diff --git a/devtools/shared/fronts/styles.js b/devtools/shared/fronts/styles.js
new file mode 100644
index 000000000..116bb1f75
--- /dev/null
+++ b/devtools/shared/fronts/styles.js
@@ -0,0 +1,421 @@
+/* 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";
+
+require("devtools/shared/fronts/stylesheets");
+const {
+ Front,
+ FrontClassWithSpec,
+ custom,
+ preEvent
+} = require("devtools/shared/protocol");
+const {
+ pageStyleSpec,
+ styleRuleSpec
+} = require("devtools/shared/specs/styles");
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const { Class } = require("sdk/core/heritage");
+const { RuleRewriter } = require("devtools/shared/css/parsing-utils");
+
+/**
+ * PageStyleFront, the front object for the PageStyleActor
+ */
+const PageStyleFront = FrontClassWithSpec(pageStyleSpec, {
+ initialize: function (conn, form, ctx, detail) {
+ Front.prototype.initialize.call(this, conn, form, ctx, detail);
+ this.inspector = this.parent();
+ },
+
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this._form = form;
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ get walker() {
+ return this.inspector.walker;
+ },
+
+ get supportsAuthoredStyles() {
+ return this._form.traits && this._form.traits.authoredStyles;
+ },
+
+ getMatchedSelectors: custom(function (node, property, options) {
+ return this._getMatchedSelectors(node, property, options).then(ret => {
+ return ret.matched;
+ });
+ }, {
+ impl: "_getMatchedSelectors"
+ }),
+
+ getApplied: custom(Task.async(function* (node, options = {}) {
+ // If the getApplied method doesn't recreate the style cache itself, this
+ // means a call to cssLogic.highlight is required before trying to access
+ // the applied rules. Issue a request to getLayout if this is the case.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1103993#c16.
+ if (!this._form.traits || !this._form.traits.getAppliedCreatesStyleCache) {
+ yield this.getLayout(node);
+ }
+ let ret = yield this._getApplied(node, options);
+ return ret.entries;
+ }), {
+ impl: "_getApplied"
+ }),
+
+ addNewRule: custom(function (node, pseudoClasses) {
+ let addPromise;
+ if (this.supportsAuthoredStyles) {
+ addPromise = this._addNewRule(node, pseudoClasses, true);
+ } else {
+ addPromise = this._addNewRule(node, pseudoClasses);
+ }
+ return addPromise.then(ret => {
+ return ret.entries[0];
+ });
+ }, {
+ impl: "_addNewRule"
+ })
+});
+
+exports.PageStyleFront = PageStyleFront;
+
+/**
+ * StyleRuleFront, the front for the StyleRule actor.
+ */
+const StyleRuleFront = FrontClassWithSpec(styleRuleSpec, {
+ initialize: function (client, form, ctx, detail) {
+ Front.prototype.initialize.call(this, client, form, ctx, detail);
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this.actorID = form.actor;
+ this._form = form;
+ if (this._mediaText) {
+ this._mediaText = null;
+ }
+ },
+
+ /**
+ * Ensure _form is updated when location-changed is emitted.
+ */
+ _locationChangedPre: preEvent("location-changed", function (line, column) {
+ this._clearOriginalLocation();
+ this._form.line = line;
+ this._form.column = column;
+ }),
+
+ /**
+ * Return a new RuleModificationList or RuleRewriter for this node.
+ * A RuleRewriter will be returned when the rule's canSetRuleText
+ * trait is true; otherwise a RuleModificationList will be
+ * returned.
+ *
+ * @param {CssPropertiesFront} cssProperties
+ * This is needed by the RuleRewriter.
+ * @return {RuleModificationList}
+ */
+ startModifyingProperties: function (cssProperties) {
+ if (this.canSetRuleText) {
+ return new RuleRewriter(cssProperties.isKnown, this, this.authoredText);
+ }
+ return new RuleModificationList(this);
+ },
+
+ get type() {
+ return this._form.type;
+ },
+ get line() {
+ return this._form.line || -1;
+ },
+ get column() {
+ return this._form.column || -1;
+ },
+ get cssText() {
+ return this._form.cssText;
+ },
+ get authoredText() {
+ return this._form.authoredText || this._form.cssText;
+ },
+ get declarations() {
+ return this._form.declarations || [];
+ },
+ get keyText() {
+ return this._form.keyText;
+ },
+ get name() {
+ return this._form.name;
+ },
+ get selectors() {
+ return this._form.selectors;
+ },
+ get media() {
+ return this._form.media;
+ },
+ get mediaText() {
+ if (!this._form.media) {
+ return null;
+ }
+ if (this._mediaText) {
+ return this._mediaText;
+ }
+ this._mediaText = this.media.join(", ");
+ return this._mediaText;
+ },
+
+ get parentRule() {
+ return this.conn.getActor(this._form.parentRule);
+ },
+
+ get parentStyleSheet() {
+ return this.conn.getActor(this._form.parentStyleSheet);
+ },
+
+ get element() {
+ return this.conn.getActor(this._form.element);
+ },
+
+ get href() {
+ if (this._form.href) {
+ return this._form.href;
+ }
+ let sheet = this.parentStyleSheet;
+ return sheet ? sheet.href : "";
+ },
+
+ get nodeHref() {
+ let sheet = this.parentStyleSheet;
+ return sheet ? sheet.nodeHref : "";
+ },
+
+ get supportsModifySelectorUnmatched() {
+ return this._form.traits && this._form.traits.modifySelectorUnmatched;
+ },
+
+ get canSetRuleText() {
+ return this._form.traits && this._form.traits.canSetRuleText;
+ },
+
+ get location() {
+ return {
+ source: this.parentStyleSheet,
+ href: this.href,
+ line: this.line,
+ column: this.column
+ };
+ },
+
+ _clearOriginalLocation: function () {
+ this._originalLocation = null;
+ },
+
+ getOriginalLocation: function () {
+ if (this._originalLocation) {
+ return promise.resolve(this._originalLocation);
+ }
+ let parentSheet = this.parentStyleSheet;
+ if (!parentSheet) {
+ // This rule doesn't belong to a stylesheet so it is an inline style.
+ // Inline styles do not have any mediaText so we can return early.
+ return promise.resolve(this.location);
+ }
+ return parentSheet.getOriginalLocation(this.line, this.column)
+ .then(({ fromSourceMap, source, line, column }) => {
+ let location = {
+ href: source,
+ line: line,
+ column: column,
+ mediaText: this.mediaText
+ };
+ if (fromSourceMap === false) {
+ location.source = this.parentStyleSheet;
+ }
+ if (!source) {
+ location.href = this.href;
+ }
+ this._originalLocation = location;
+ return location;
+ });
+ },
+
+ modifySelector: custom(Task.async(function* (node, value) {
+ let response;
+ if (this.supportsModifySelectorUnmatched) {
+ // If the debugee supports adding unmatched rules (post FF41)
+ if (this.canSetRuleText) {
+ response = yield this.modifySelector2(node, value, true);
+ } else {
+ response = yield this.modifySelector2(node, value);
+ }
+ } else {
+ response = yield this._modifySelector(value);
+ }
+
+ if (response.ruleProps) {
+ response.ruleProps = response.ruleProps.entries[0];
+ }
+ return response;
+ }), {
+ impl: "_modifySelector"
+ }),
+
+ setRuleText: custom(function (newText) {
+ this._form.authoredText = newText;
+ return this._setRuleText(newText);
+ }, {
+ impl: "_setRuleText"
+ })
+});
+
+exports.StyleRuleFront = StyleRuleFront;
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyProperties` request. A RuleModificationList holds a
+ * list of modifications that will be applied to a StyleRuleActor.
+ * The modifications are processed in the order in which they are
+ * added to the RuleModificationList.
+ *
+ * Objects of this type expose the same API as @see RuleRewriter.
+ * This lets the inspector use (mostly) the same code, regardless of
+ * whether the server implements setRuleText.
+ */
+var RuleModificationList = Class({
+ /**
+ * Initialize a RuleModificationList.
+ * @param {StyleRuleFront} rule the associated rule
+ */
+ initialize: function (rule) {
+ this.rule = rule;
+ this.modifications = [];
+ },
+
+ /**
+ * Apply the modifications in this object to the associated rule.
+ *
+ * @return {Promise} A promise which will be resolved when the modifications
+ * are complete; @see StyleRuleActor.modifyProperties.
+ */
+ apply: function () {
+ return this.rule.modifyProperties(this.modifications);
+ },
+
+ /**
+ * Add a "set" entry to the modification list.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the property's name
+ * @param {String} value the property's value
+ * @param {String} priority the property's priority, either the empty
+ * string or "important"
+ */
+ setProperty: function (index, name, value, priority) {
+ this.modifications.push({
+ type: "set",
+ name: name,
+ value: value,
+ priority: priority
+ });
+ },
+
+ /**
+ * Add a "remove" entry to the modification list.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the name of the property to remove
+ */
+ removeProperty: function (index, name) {
+ this.modifications.push({
+ type: "remove",
+ name: name
+ });
+ },
+
+ /**
+ * Rename a property. This implementation acts like
+ * |removeProperty|, because |setRuleText| is not available.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name current name of the property
+ *
+ * This parameter is also passed, but as it is not used in this
+ * implementation, it is omitted. It is documented here as this
+ * code also defined the interface implemented by @see RuleRewriter.
+ * @param {String} newName new name of the property
+ */
+ renameProperty: function (index, name) {
+ this.removeProperty(index, name);
+ },
+
+ /**
+ * Enable or disable a property. This implementation acts like
+ * |removeProperty| when disabling, or a no-op when enabling,
+ * because |setRuleText| is not available.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name current name of the property
+ * @param {Boolean} isEnabled true if the property should be enabled;
+ * false if it should be disabled
+ */
+ setPropertyEnabled: function (index, name, isEnabled) {
+ if (!isEnabled) {
+ this.removeProperty(index, name);
+ }
+ },
+
+ /**
+ * Create a new property. This implementation does nothing, because
+ * |setRuleText| is not available.
+ *
+ * These parameters are passed, but as they are not used in this
+ * implementation, they are omitted. They are documented here as
+ * this code also defined the interface implemented by @see
+ * RuleRewriter.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name name of the new property
+ * @param {String} value value of the new property
+ * @param {String} priority priority of the new property; either
+ * the empty string or "important"
+ * @param {Boolean} enabled True if the new property should be
+ * enabled, false if disabled
+ */
+ createProperty: function () {
+ // Nothing.
+ },
+});
diff --git a/devtools/shared/fronts/stylesheets.js b/devtools/shared/fronts/stylesheets.js
new file mode 100644
index 000000000..6df8a9bd4
--- /dev/null
+++ b/devtools/shared/fronts/stylesheets.js
@@ -0,0 +1,184 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const {
+ getIndentationFromPrefs,
+ getIndentationFromString
+} = require("devtools/shared/indentation");
+const {
+ originalSourceSpec,
+ mediaRuleSpec,
+ styleSheetSpec,
+ styleSheetsSpec
+} = require("devtools/shared/specs/stylesheets");
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const events = require("sdk/event/core");
+
+/**
+ * The client-side counterpart for an OriginalSourceActor.
+ */
+const OriginalSourceFront = FrontClassWithSpec(originalSourceSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+
+ this.isOriginalSource = true;
+ },
+
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this.actorID = form.actor;
+ this._form = form;
+ },
+
+ get href() {
+ return this._form.url;
+ },
+ get url() {
+ return this._form.url;
+ }
+});
+
+exports.OriginalSourceFront = OriginalSourceFront;
+
+/**
+ * Corresponding client-side front for a MediaRuleActor.
+ */
+const MediaRuleFront = FrontClassWithSpec(mediaRuleSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+
+ this._onMatchesChange = this._onMatchesChange.bind(this);
+ events.on(this, "matches-change", this._onMatchesChange);
+ },
+
+ _onMatchesChange: function (matches) {
+ this._form.matches = matches;
+ },
+
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this.actorID = form.actor;
+ this._form = form;
+ },
+
+ get mediaText() {
+ return this._form.mediaText;
+ },
+ get conditionText() {
+ return this._form.conditionText;
+ },
+ get matches() {
+ return this._form.matches;
+ },
+ get line() {
+ return this._form.line || -1;
+ },
+ get column() {
+ return this._form.column || -1;
+ },
+ get parentStyleSheet() {
+ return this.conn.getActor(this._form.parentStyleSheet);
+ }
+});
+
+exports.MediaRuleFront = MediaRuleFront;
+
+/**
+ * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
+ */
+const StyleSheetFront = FrontClassWithSpec(styleSheetSpec, {
+ initialize: function (conn, form) {
+ Front.prototype.initialize.call(this, conn, form);
+
+ this._onPropertyChange = this._onPropertyChange.bind(this);
+ events.on(this, "property-change", this._onPropertyChange);
+ },
+
+ destroy: function () {
+ events.off(this, "property-change", this._onPropertyChange);
+ 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() {
+ return this._form.href;
+ },
+ get nodeHref() {
+ return this._form.nodeHref;
+ },
+ get disabled() {
+ return !!this._form.disabled;
+ },
+ get title() {
+ return this._form.title;
+ },
+ get isSystem() {
+ return this._form.system;
+ },
+ get styleSheetIndex() {
+ return this._form.styleSheetIndex;
+ },
+ get ruleCount() {
+ return this._form.ruleCount;
+ },
+
+ /**
+ * Get the indentation to use for edits to this style sheet.
+ *
+ * @return {Promise} A promise that will resolve to a string that
+ * should be used to indent a block in this style sheet.
+ */
+ guessIndentation: function () {
+ let prefIndent = getIndentationFromPrefs();
+ if (prefIndent) {
+ let {indentUnit, indentWithTabs} = prefIndent;
+ return promise.resolve(indentWithTabs ? "\t" : " ".repeat(indentUnit));
+ }
+
+ return Task.spawn(function* () {
+ let longStr = yield this.getText();
+ let source = yield longStr.string();
+
+ let {indentUnit, indentWithTabs} = getIndentationFromString(source);
+
+ return indentWithTabs ? "\t" : " ".repeat(indentUnit);
+ }.bind(this));
+ }
+});
+
+exports.StyleSheetFront = StyleSheetFront;
+
+/**
+ * The corresponding Front object for the StyleSheetsActor.
+ */
+const StyleSheetsFront = FrontClassWithSpec(styleSheetsSpec, {
+ initialize: function (client, tabForm) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = tabForm.styleSheetsActor;
+ this.manage(this);
+ }
+});
+
+exports.StyleSheetsFront = StyleSheetsFront;
diff --git a/devtools/shared/fronts/timeline.js b/devtools/shared/fronts/timeline.js
new file mode 100644
index 000000000..cd6bc54e8
--- /dev/null
+++ b/devtools/shared/fronts/timeline.js
@@ -0,0 +1,25 @@
+/* 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 {
+ Front,
+ FrontClassWithSpec,
+} = require("devtools/shared/protocol");
+const { timelineSpec } = require("devtools/shared/specs/timeline");
+
+/**
+ * TimelineFront, the front for the TimelineActor.
+ */
+const TimelineFront = FrontClassWithSpec(timelineSpec, {
+ initialize: function (client, { timelineActor }) {
+ Front.prototype.initialize.call(this, client, { actor: timelineActor });
+ this.manage(this);
+ },
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+});
+
+exports.TimelineFront = TimelineFront;
diff --git a/devtools/shared/fronts/webaudio.js b/devtools/shared/fronts/webaudio.js
new file mode 100644
index 000000000..73594c513
--- /dev/null
+++ b/devtools/shared/fronts/webaudio.js
@@ -0,0 +1,83 @@
+/* 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 {
+ audionodeSpec,
+ webAudioSpec,
+ AUTOMATION_METHODS,
+ NODE_CREATION_METHODS,
+ NODE_ROUTING_METHODS,
+} = require("devtools/shared/specs/webaudio");
+const protocol = require("devtools/shared/protocol");
+const AUDIO_NODE_DEFINITION = require("devtools/server/actors/utils/audionodes.json");
+
+/**
+ * The corresponding Front object for the AudioNodeActor.
+ *
+ * @attribute {String} type
+ * The type of audio node, like "OscillatorNode", "MediaElementAudioSourceNode"
+ * @attribute {Boolean} source
+ * Boolean indicating if the node is a source node, like BufferSourceNode,
+ * MediaElementAudioSourceNode, OscillatorNode, etc.
+ * @attribute {Boolean} bypassable
+ * Boolean indicating if the audio node is bypassable (splitter,
+ * merger and destination nodes, for example, are not)
+ */
+const AudioNodeFront = protocol.FrontClassWithSpec(audionodeSpec, {
+ form: function (form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+
+ this.actorID = form.actor;
+ this.type = form.type;
+ this.source = form.source;
+ this.bypassable = form.bypassable;
+ },
+
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ // if we were manually passed a form, this was created manually and
+ // needs to own itself for now.
+ if (form) {
+ this.manage(this);
+ }
+ }
+});
+
+exports.AudioNodeFront = AudioNodeFront;
+
+/**
+ * The corresponding Front object for the WebAudioActor.
+ */
+const WebAudioFront = protocol.FrontClassWithSpec(webAudioSpec, {
+ initialize: function (client, { webaudioActor }) {
+ protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor });
+ this.manage(this);
+ },
+
+ /**
+ * If connecting to older geckos (<Fx43), where audio node actor's do not
+ * contain `type`, `source` and `bypassable` properties, fetch
+ * them manually here.
+ */
+ _onCreateNode: protocol.preEvent("create-node", function (audionode) {
+ if (!audionode.type) {
+ return audionode.getType().then(type => {
+ audionode.type = type;
+ audionode.source = !!AUDIO_NODE_DEFINITION[type].source;
+ audionode.bypassable = !AUDIO_NODE_DEFINITION[type].unbypassable;
+ });
+ }
+ return null;
+ }),
+});
+
+WebAudioFront.AUTOMATION_METHODS = new Set(AUTOMATION_METHODS);
+WebAudioFront.NODE_CREATION_METHODS = new Set(NODE_CREATION_METHODS);
+WebAudioFront.NODE_ROUTING_METHODS = new Set(NODE_ROUTING_METHODS);
+
+exports.WebAudioFront = WebAudioFront;
diff --git a/devtools/shared/fronts/webgl.js b/devtools/shared/fronts/webgl.js
new file mode 100644
index 000000000..b5f7afac8
--- /dev/null
+++ b/devtools/shared/fronts/webgl.js
@@ -0,0 +1,45 @@
+/* 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 {
+ shaderSpec,
+ programSpec,
+ webGLSpec,
+} = require("devtools/shared/specs/webgl");
+const protocol = require("devtools/shared/protocol");
+
+/**
+ * The corresponding Front object for the ShaderActor.
+ */
+const ShaderFront = protocol.FrontClassWithSpec(shaderSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ }
+});
+
+exports.ShaderFront = ShaderFront;
+
+/**
+ * The corresponding Front object for the ProgramActor.
+ */
+const ProgramFront = protocol.FrontClassWithSpec(programSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client, form);
+ }
+});
+
+exports.ProgramFront = ProgramFront;
+
+/**
+ * The corresponding Front object for the WebGLActor.
+ */
+const WebGLFront = protocol.FrontClassWithSpec(webGLSpec, {
+ initialize: function (client, { webglActor }) {
+ protocol.Front.prototype.initialize.call(this, client, { actor: webglActor });
+ this.manage(this);
+ }
+});
+
+exports.WebGLFront = WebGLFront;