diff --git a/devtools/.eslintrc.js b/devtools/.eslintrc.js index 83ee1864cc83..af410087459b 100644 --- a/devtools/.eslintrc.js +++ b/devtools/.eslintrc.js @@ -32,7 +32,6 @@ module.exports = { "client/framework/**", "client/shared/*.jsm", "client/shared/widgets/*.jsm", - "client/storage/VariablesView.jsm", ], "rules": { "consistent-return": "off", @@ -65,8 +64,6 @@ module.exports = { "files": [ "client/framework/**", "client/shared/widgets/*.jsm", - "client/storage/VariablesView.jsm", - "client/storage/VariablesView.jsm", ], "rules": { "no-shadow": "off", diff --git a/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js index d819ef847c07..7c8eab456261 100644 --- a/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js +++ b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js @@ -6,7 +6,7 @@ // Test that VariablesView._doSearch() works even without an attached // VariablesViewController (bug 1196341). const { VariablesView } = ChromeUtils.import( - "resource://devtools/client/storage/VariablesView.jsm" + "resource://devtools/client/shared/widgets/VariablesView.jsm" ); const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); const { globals } = require("devtools/shared/builtin-modules"); diff --git a/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js index 4f42bfe95c95..19453befce42 100644 --- a/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js +++ b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js @@ -4,7 +4,7 @@ "use strict"; const { VariablesView } = ChromeUtils.import( - "resource://devtools/client/storage/VariablesView.jsm" + "resource://devtools/client/shared/widgets/VariablesView.jsm" ); const PENDING = { diff --git a/devtools/client/shared/widgets/VariablesViewController.jsm b/devtools/client/shared/widgets/VariablesViewController.jsm new file mode 100644 index 000000000000..3f1a4f9b3498 --- /dev/null +++ b/devtools/client/shared/widgets/VariablesViewController.jsm @@ -0,0 +1,886 @@ +/* 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"; + +var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); +var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +var { + VariablesView, +} = require("resource://devtools/client/shared/widgets/VariablesView.jsm"); +var Services = require("Services"); +var promise = require("promise"); +var defer = require("devtools/shared/defer"); +var { LocalizationHelper, ELLIPSIS } = require("devtools/shared/l10n"); + +XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () => + Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled") +); + +const MAX_LONG_STRING_LENGTH = 200000; +const MAX_PROPERTY_ITEMS = 2000; +const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties"; + +this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"]; + +/** + * Localization convenience methods. + */ +var L10N = new LocalizationHelper(DBG_STRINGS_URI); + +/** + * Controller for a VariablesView that handles interfacing with the debugger + * protocol. Is able to populate scopes and variables via the protocol as well + * as manage actor lifespans. + * + * @param VariablesView aView + * The view to attach to. + * @param object aOptions [optional] + * Options for configuring the controller. Supported options: + * - getObjectFront: @see this._setClientGetters + * - getLongStringFront: @see this._setClientGetters + * - getEnvironmentFront: @see this._setClientGetters + * - releaseActor: @see this._setClientGetters + * - overrideValueEvalMacro: @see _setEvaluationMacros + * - getterOrSetterEvalMacro: @see _setEvaluationMacros + * - simpleValueEvalMacro: @see _setEvaluationMacros + */ +function VariablesViewController(aView, aOptions = {}) { + this.addExpander = this.addExpander.bind(this); + + this._setClientGetters(aOptions); + this._setEvaluationMacros(aOptions); + + this._actors = new Set(); + this.view = aView; + this.view.controller = this; +} +this.VariablesViewController = VariablesViewController; + +VariablesViewController.prototype = { + /** + * The default getter/setter evaluation macro. + */ + _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro, + + /** + * The default override value evaluation macro. + */ + _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro, + + /** + * The default simple value evaluation macro. + */ + _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro, + + /** + * Set the functions used to retrieve debugger client grips. + * + * @param object aOptions + * Options for getting the client grips. Supported options: + * - getObjectFront: callback for creating an object grip front + * - getLongStringFront: callback for creating a long string front + * - getEnvironmentFront: callback for creating an environment front + * - releaseActor: callback for releasing an actor when it's no longer needed + */ + _setClientGetters: function(aOptions) { + if (aOptions.getObjectFront) { + this._getObjectFront = aOptions.getObjectFront; + } + if (aOptions.getLongStringFront) { + this._getLongStringFront = aOptions.getLongStringFront; + } + if (aOptions.getEnvironmentFront) { + this._getEnvironmentFront = aOptions.getEnvironmentFront; + } + if (aOptions.releaseActor) { + this._releaseActor = aOptions.releaseActor; + } + }, + + /** + * Sets the functions used when evaluating strings in the variables view. + * + * @param object aOptions + * Options for configuring the macros. Supported options: + * - overrideValueEvalMacro: callback for creating an overriding eval macro + * - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro + * - simpleValueEvalMacro: callback for creating a simple value eval macro + */ + _setEvaluationMacros: function(aOptions) { + if (aOptions.overrideValueEvalMacro) { + this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro; + } + if (aOptions.getterOrSetterEvalMacro) { + this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro; + } + if (aOptions.simpleValueEvalMacro) { + this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro; + } + }, + + /** + * Populate a long string into a target using a grip. + * + * @param Variable aTarget + * The target Variable/Property to put the retrieved string into. + * @param LongStringActor aGrip + * The long string grip that use to retrieve the full string. + * @return Promise + * The promise that will be resolved when the string is retrieved. + */ + _populateFromLongString: async function(aTarget, aGrip) { + const from = aGrip.initial.length; + const to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH); + + const response = await this._getLongStringFront(aGrip).substring(from, to); + // Stop tracking the actor because it's no longer needed. + this.releaseActor(aGrip); + + // Replace the preview with the full string and make it non-expandable. + aTarget.onexpand = null; + aTarget.setGrip(aGrip.initial + response); + aTarget.hideArrow(); + }, + + /** + * Adds pseudo items in case there is too many properties to display. + * Each item can expand into property slices. + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aGrip + * The property iterator grip. + */ + _populatePropertySlices: function(aTarget, aGrip) { + if (aGrip.count < MAX_PROPERTY_ITEMS) { + return this._populateFromPropertyIterator(aTarget, aGrip); + } + + // Divide the keys into quarters. + const items = Math.ceil(aGrip.count / 4); + const iterator = aGrip.propertyIterator; + const promises = []; + for (let i = 0; i < 4; i++) { + const start = aGrip.start + i * items; + const count = i != 3 ? items : aGrip.count - i * items; + + // Create a new kind of grip, with additional fields to define the slice + const sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: start, + count: count, + }; + + // Query the name of the first and last items for this slice + const deferred = defer(); + iterator.names([start, start + count - 1], ({ names }) => { + const label = "[" + names[0] + ELLIPSIS + names[1] + "]"; + const item = aTarget.addItem(label, {}, { internalItem: true }); + item.showArrow(); + this.addExpander(item, sliceGrip); + deferred.resolve(); + }); + promises.push(deferred.promise); + } + + return promise.all(promises); + }, + + /** + * Adds a property slice for a Variable in the view using the already + * property iterator + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aGrip + * The property iterator grip. + */ + _populateFromPropertyIterator: function(aTarget, aGrip) { + if (aGrip.count >= MAX_PROPERTY_ITEMS) { + // We already started to split, but there is still too many properties, split again. + return this._populatePropertySlices(aTarget, aGrip); + } + // We started slicing properties, and the slice is now small enough to be displayed + const deferred = defer(); + // eslint-disable-next-line mozilla/use-returnValue + aGrip.propertyIterator.slice( + aGrip.start, + aGrip.count, + ({ ownProperties }) => { + // Add all the variable properties. + if (Object.keys(ownProperties).length > 0) { + aTarget.addItems(ownProperties, { + sorted: true, + // Expansion handlers must be set after the properties are added. + callback: this.addExpander, + }); + } + deferred.resolve(); + } + ); + return deferred.promise; + }, + + /** + * Adds the properties for a Variable in the view using a new feature in FF40+ + * that allows iteration over properties in slices. + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aGrip + * The grip to use to populate the target. + * @param string aQuery [optional] + * The query string used to fetch only a subset of properties + */ + _populateFromObjectWithIterator: function(aTarget, aGrip, aQuery) { + // FF40+ starts exposing `ownPropertyLength` on ObjectActor's grip, + // as well as `enumProperties` request. + const deferred = defer(); + const objectFront = this._getObjectFront(aGrip); + const isArray = aGrip.preview && aGrip.preview.kind === "ArrayLike"; + if (isArray) { + // First enumerate array items, e.g. properties from `0` to `array.length`. + const options = { + ignoreNonIndexedProperties: true, + query: aQuery, + }; + objectFront.enumProperties(options).then(iterator => { + const sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: 0, + count: iterator.count, + }; + this._populatePropertySlices(aTarget, sliceGrip).then(() => { + // Then enumerate the rest of the properties, like length, buffer, etc. + const options = { + ignoreIndexedProperties: true, + sort: true, + query: aQuery, + }; + objectFront.enumProperties(options).then(iterator => { + const sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: 0, + count: iterator.count, + }; + deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip)); + }); + }); + }); + } else { + const options = { sort: true, query: aQuery }; + // For objects, we just enumerate all the properties sorted by name. + objectFront.enumProperties(options).then(iterator => { + const sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: 0, + count: iterator.count, + }; + deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip)); + }); + } + return deferred.promise; + }, + + /** + * Adds the given prototype in the view. + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aProtype + * The prototype grip. + */ + _populateObjectPrototype: function(aTarget, aPrototype) { + // Add the variable's __proto__. + if (aPrototype && aPrototype.type != "null") { + const proto = aTarget.addItem("__proto__", { value: aPrototype }); + this.addExpander(proto, aPrototype); + } + }, + + /** + * Adds properties to a Scope, Variable, or Property in the view. Triggered + * when a scope is expanded or certain variables are hovered. + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aGrip + * The grip to use to populate the target. + */ + _populateFromObject: function(aTarget, aGrip) { + if (aGrip.class === "Proxy") { + // Refuse to play the proxy's stupid game and just expose the target and handler. + const deferred = defer(); + const objectFront = this._getObjectFront(aGrip); + objectFront.getProxySlots().then(aResponse => { + const target = aTarget.addItem( + "", + { value: aResponse.proxyTarget }, + { internalItem: true } + ); + this.addExpander(target, aResponse.proxyTarget); + const handler = aTarget.addItem( + "", + { value: aResponse.proxyHandler }, + { internalItem: true } + ); + this.addExpander(handler, aResponse.proxyHandler); + deferred.resolve(); + }); + return deferred.promise; + } + + if (aGrip.class === "Promise" && aGrip.promiseState) { + const { state, value, reason } = aGrip.promiseState; + aTarget.addItem("", { value: state }, { internalItem: true }); + if (state === "fulfilled") { + this.addExpander( + aTarget.addItem("", { value }, { internalItem: true }), + value + ); + } else if (state === "rejected") { + this.addExpander( + aTarget.addItem( + "", + { value: reason }, + { internalItem: true } + ), + reason + ); + } + } else if (["Map", "WeakMap", "Set", "WeakSet"].includes(aGrip.class)) { + const entriesList = aTarget.addItem( + "", + {}, + { internalItem: true } + ); + entriesList.showArrow(); + this.addExpander(entriesList, { + type: "entries-list", + obj: aGrip, + }); + } + + // Fetch properties by slices if there is too many in order to prevent UI freeze. + if ( + "ownPropertyLength" in aGrip && + aGrip.ownPropertyLength >= MAX_PROPERTY_ITEMS + ) { + return this._populateFromObjectWithIterator(aTarget, aGrip).then(() => { + const deferred = defer(); + const objectFront = this._getObjectFront(aGrip); + objectFront.getPrototype().then(prototype => { + this._populateObjectPrototype(aTarget, prototype); + deferred.resolve(); + }); + return deferred.promise; + }); + } + + return this._populateProperties(aTarget, aGrip); + }, + + _populateProperties: function(aTarget, aGrip, aOptions) { + const deferred = defer(); + + const objectFront = this._getObjectFront(aGrip); + objectFront.getPrototypeAndProperties().then(aResponse => { + const ownProperties = aResponse.ownProperties || {}; + const prototype = aResponse.prototype || null; + // 'safeGetterValues' is new and isn't necessary defined on old actors. + const safeGetterValues = aResponse.safeGetterValues || {}; + const sortable = VariablesView.isSortable(aGrip.class); + + // Merge the safe getter values into one object such that we can use it + // in VariablesView. + for (const name of Object.keys(safeGetterValues)) { + if (name in ownProperties) { + const { getterValue, getterPrototypeLevel } = safeGetterValues[name]; + ownProperties[name].getterValue = getterValue; + ownProperties[name].getterPrototypeLevel = getterPrototypeLevel; + } else { + ownProperties[name] = safeGetterValues[name]; + } + } + + // Add all the variable properties. + aTarget.addItems(ownProperties, { + // Not all variables need to force sorted properties. + sorted: sortable, + // Expansion handlers must be set after the properties are added. + callback: this.addExpander, + }); + + // Add the variable's __proto__. + this._populateObjectPrototype(aTarget, prototype); + + // If the object is a function we need to fetch its scope chain + // to show them as closures for the respective function. + if (aGrip.class == "Function") { + objectFront.getScope().then(aResponse => { + if (aResponse.error) { + // This function is bound to a built-in object or it's not present + // in the current scope chain. Not necessarily an actual error, + // it just means that there's no closure for the function. + console.warn(aResponse.error + ": " + aResponse.message); + return void deferred.resolve(); + } + this._populateWithClosure(aTarget, aResponse.scope).then( + deferred.resolve + ); + }); + } else { + deferred.resolve(); + } + }); + + return deferred.promise; + }, + + /** + * Adds the scope chain elements (closures) of a function variable. + * + * @param Variable aTarget + * The variable where the properties will be placed into. + * @param Scope aScope + * The lexical environment form as specified in the protocol. + */ + _populateWithClosure: function(aTarget, aScope) { + const objectScopes = []; + let environment = aScope; + const funcScope = aTarget.addItem(""); + funcScope.target.setAttribute("scope", ""); + funcScope.showArrow(); + + do { + // Create a scope to contain all the inspected variables. + const label = StackFrameUtils.getScopeLabel(environment); + + // Block scopes may have the same label, so make addItem allow duplicates. + const closure = funcScope.addItem(label, undefined, { relaxed: true }); + closure.target.setAttribute("scope", ""); + closure.showArrow(); + + // Add nodes for every argument and every other variable in scope. + if (environment.bindings) { + this._populateWithEnvironmentBindings(closure, environment.bindings); + } else { + const deferred = defer(); + objectScopes.push(deferred.promise); + this._getEnvironmentFront(environment) + .getBindings() + .then(response => { + this._populateWithEnvironmentBindings(closure, response.bindings); + deferred.resolve(); + }); + } + } while ((environment = environment.parent)); + + return promise.all(objectScopes).then(() => { + // Signal that scopes have been fetched. + this.view.emit("fetched", "scopes", funcScope); + }); + }, + + /** + * Adds nodes for every specified binding to the closure node. + * + * @param Variable aTarget + * The variable where the bindings will be placed into. + * @param object aBindings + * The bindings form as specified in the protocol. + */ + _populateWithEnvironmentBindings: function(aTarget, aBindings) { + // Add nodes for every argument in the scope. + aTarget.addItems( + aBindings.arguments.reduce((accumulator, arg) => { + const name = Object.getOwnPropertyNames(arg)[0]; + const descriptor = arg[name]; + accumulator[name] = descriptor; + return accumulator; + }, {}), + { + // Arguments aren't sorted. + sorted: false, + // Expansion handlers must be set after the properties are added. + callback: this.addExpander, + } + ); + + // Add nodes for every other variable in the scope. + aTarget.addItems(aBindings.variables, { + // Not all variables need to force sorted properties. + sorted: VARIABLES_SORTING_ENABLED, + // Expansion handlers must be set after the properties are added. + callback: this.addExpander, + }); + }, + + _populateFromEntries: function(target, grip) { + const objGrip = grip.obj; + const objectFront = this._getObjectFront(objGrip); + + // eslint-disable-next-line new-cap + return new promise((resolve, reject) => { + objectFront.enumEntries().then(response => { + if (response.error) { + // Older server might not support the enumEntries method + console.warn(response.error + ": " + response.message); + resolve(); + } else { + const sliceGrip = { + type: "property-iterator", + propertyIterator: response.iterator, + start: 0, + count: response.iterator.count, + }; + + resolve(this._populatePropertySlices(target, sliceGrip)); + } + }); + }); + }, + + /** + * Adds an 'onexpand' callback for a variable, lazily handling + * the addition of new properties. + * + * @param Variable aTarget + * The variable where the properties will be placed into. + * @param any aSource + * The source to use to populate the target. + */ + addExpander: function(aTarget, aSource) { + // Attach evaluation macros as necessary. + if (aTarget.getter || aTarget.setter) { + aTarget.evaluationMacro = this._overrideValueEvalMacro; + const getter = aTarget.get("get"); + if (getter) { + getter.evaluationMacro = this._getterOrSetterEvalMacro; + } + const setter = aTarget.get("set"); + if (setter) { + setter.evaluationMacro = this._getterOrSetterEvalMacro; + } + } else { + aTarget.evaluationMacro = this._simpleValueEvalMacro; + } + + // If the source is primitive then an expander is not needed. + if (VariablesView.isPrimitive({ value: aSource })) { + return; + } + + // If the source is a long string then show the arrow. + if (isActorGrip(aSource) && aSource.type == "longString") { + aTarget.showArrow(); + } + + // Make sure that properties are always available on expansion. + aTarget.onexpand = () => this.populate(aTarget, aSource); + + // Some variables are likely to contain a very large number of properties. + // It's a good idea to be prepared in case of an expansion. + if (aTarget.shouldPrefetch) { + aTarget.addEventListener("mouseover", aTarget.onexpand); + } + + // Register all the actors that this controller now depends on. + for (const grip of [aTarget.value, aTarget.getter, aTarget.setter]) { + if (isActorGrip(grip)) { + this._actors.add(grip.actor); + } + } + }, + + /** + * Adds properties to a Scope, Variable, or Property in the view. Triggered + * when a scope is expanded or certain variables are hovered. + * + * This does not expand the target, it only populates it. + * + * @param Scope aTarget + * The Scope to be expanded. + * @param object aSource + * The source to use to populate the target. + * @return Promise + * The promise that is resolved once the target has been expanded. + */ + populate: function(aTarget, aSource) { + // Fetch the variables only once. + if (aTarget._fetched) { + return aTarget._fetched; + } + // Make sure the source grip is available. + if (!aSource) { + return promise.reject( + new Error("No actor grip was given for the variable.") + ); + } + + const deferred = defer(); + aTarget._fetched = deferred.promise; + + if (aSource.type === "property-iterator") { + return this._populateFromPropertyIterator(aTarget, aSource); + } + + if (aSource.type === "entries-list") { + return this._populateFromEntries(aTarget, aSource); + } + + if (aSource.type === "mapEntry" || aSource.type === "storageEntry") { + aTarget.addItems( + { + key: { value: aSource.preview.key }, + value: { value: aSource.preview.value }, + }, + { + callback: this.addExpander, + } + ); + + return promise.resolve(); + } + + // If the target is a Variable or Property then we're fetching properties. + if (VariablesView.isVariable(aTarget)) { + this._populateFromObject(aTarget, aSource).then(() => { + // Signal that properties have been fetched. + this.view.emit("fetched", "properties", aTarget); + // Commit the hierarchy because new items were added. + this.view.commitHierarchy(); + deferred.resolve(); + }); + return deferred.promise; + } + + switch (aSource.type) { + case "longString": + this._populateFromLongString(aTarget, aSource).then(() => { + // Signal that a long string has been fetched. + this.view.emit("fetched", "longString", aTarget); + deferred.resolve(); + }); + break; + case "with": + case "object": + this._populateFromObject(aTarget, aSource.object).then(() => { + // Signal that variables have been fetched. + this.view.emit("fetched", "variables", aTarget); + // Commit the hierarchy because new items were added. + this.view.commitHierarchy(); + deferred.resolve(); + }); + break; + case "block": + case "function": + this._populateWithEnvironmentBindings(aTarget, aSource.bindings); + // No need to signal that variables have been fetched, since + // the scope arguments and variables are already attached to the + // environment bindings, so pausing the active thread is unnecessary. + // Commit the hierarchy because new items were added. + this.view.commitHierarchy(); + deferred.resolve(); + break; + default: + const error = "Unknown Debugger.Environment type: " + aSource.type; + console.error(error); + deferred.reject(error); + } + + return deferred.promise; + }, + + /** + * Indicates to the view if the targeted actor supports properties search + * + * @return boolean True, if the actor supports enumProperty request + */ + supportsSearch: function() { + // FF40+ starts exposing ownPropertyLength on object actor's grip + // as well as enumProperty which allows to query a subset of properties. + return this.objectActor && "ownPropertyLength" in this.objectActor; + }, + + /** + * Try to use the actor to perform an attribute search. + * + * @param Scope aScope + * The Scope instance to populate with properties + * @param string aToken + * The query string + */ + performSearch: function(aScope, aToken) { + this._populateFromObjectWithIterator(aScope, this.objectActor, aToken).then( + () => { + this.view.emit("fetched", "search", aScope); + } + ); + }, + + /** + * Release an actor from the controller. + * + * @param object aActor + * The actor to release. + */ + releaseActor: function(aActor) { + if (this._releaseActor) { + this._releaseActor(aActor); + } + this._actors.delete(aActor); + }, + + /** + * Release all the actors referenced by the controller, optionally filtered. + * + * @param function aFilter [optional] + * Callback to filter which actors are released. + */ + releaseActors: function(aFilter) { + for (const actor of this._actors) { + if (!aFilter || aFilter(actor)) { + this.releaseActor(actor); + } + } + }, + + /** + * Helper function for setting up a single Scope with a single Variable + * contained within it. + * + * This function will empty the variables view. + * + * @param object options + * Options for the contents of the view: + * - objectActor: the grip of the new ObjectActor to show. + * - rawObject: the raw object to show. + * - label: the label for the inspected object. + * @param object configuration + * Additional options for the controller: + * - overrideValueEvalMacro: @see _setEvaluationMacros + * - getterOrSetterEvalMacro: @see _setEvaluationMacros + * - simpleValueEvalMacro: @see _setEvaluationMacros + * @return Object + * - variable: the created Variable. + * - expanded: the Promise that resolves when the variable expands. + */ + setSingleVariable: function(options, configuration = {}) { + this._setEvaluationMacros(configuration); + this.view.empty(); + + const scope = this.view.addScope(options.label); + scope.expanded = true; // Expand the scope by default. + scope.locked = true; // Prevent collapsing the scope. + + const variable = scope.addItem(undefined, { enumerable: true }); + let populated; + + if (options.objectActor) { + // Save objectActor for properties filtering + this.objectActor = options.objectActor; + if (VariablesView.isPrimitive({ value: this.objectActor })) { + populated = promise.resolve(); + } else { + populated = this.populate(variable, options.objectActor); + variable.expand(); + } + } else if (options.rawObject) { + variable.populate(options.rawObject, { expanded: true }); + populated = promise.resolve(); + } + + return { variable: variable, expanded: populated }; + }, +}; + +/** + * Attaches a VariablesViewController to a VariablesView if it doesn't already + * have one. + * + * @param VariablesView aView + * The view to attach to. + * @param object aOptions + * The options to use in creating the controller. + * @return VariablesViewController + */ +VariablesViewController.attach = function(aView, aOptions) { + if (aView.controller) { + return aView.controller; + } + return new VariablesViewController(aView, aOptions); +}; + +/** + * Utility functions for handling stackframes. + */ +var StackFrameUtils = (this.StackFrameUtils = { + /** + * Create a textual representation for the specified stack frame + * to display in the stackframes container. + * + * @param object aFrame + * The stack frame to label. + */ + getFrameTitle: function(aFrame) { + if (aFrame.type == "call") { + const c = aFrame.callee; + return c.name || c.userDisplayName || c.displayName || "(anonymous)"; + } + return "(" + aFrame.type + ")"; + }, + + /** + * Constructs a scope label based on its environment. + * + * @param object aEnv + * The scope's environment. + * @return string + * The scope's label. + */ + getScopeLabel: function(aEnv) { + let name = ""; + + // Name the outermost scope Global. + if (!aEnv.parent) { + name = L10N.getStr("globalScopeLabel"); + } else { + // Otherwise construct the scope name. + name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1); + } + + let label = L10N.getFormatStr("scopeLabel", name); + switch (aEnv.type) { + case "with": + case "object": + label += " [" + aEnv.object.class + "]"; + break; + case "function": + const f = aEnv.function; + label += + " [" + + (f.name || f.userDisplayName || f.displayName || "(anonymous)") + + "]"; + break; + } + return label; + }, +}); + +/** + * Check if the given value is a grip with an actor. + * + * @param mixed grip + * Value you want to check if it is a grip with an actor. + * @return boolean + * True if the given value is a grip with an actor. + */ +function isActorGrip(grip) { + return grip && typeof grip == "object" && grip.actor; +} diff --git a/devtools/client/shared/widgets/moz.build b/devtools/client/shared/widgets/moz.build index 5110009a2ead..cc2c1b638f37 100644 --- a/devtools/client/shared/widgets/moz.build +++ b/devtools/client/shared/widgets/moz.build @@ -23,5 +23,7 @@ DevToolsModules( 'Spectrum.js', 'TableWidget.js', 'TreeWidget.js', + 'VariablesView.jsm', + 'VariablesViewController.jsm', 'view-helpers.js', ) diff --git a/devtools/client/storage/moz.build b/devtools/client/storage/moz.build index 9e0c7dc18536..55534b228628 100644 --- a/devtools/client/storage/moz.build +++ b/devtools/client/storage/moz.build @@ -8,8 +8,7 @@ BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] DevToolsModules( 'panel.js', - 'ui.js', - 'VariablesView.jsm', + 'ui.js' ) with Files('**'): diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js index d7513fb2f5ff..82f2e0812137 100644 --- a/devtools/client/storage/ui.js +++ b/devtools/client/storage/ui.js @@ -32,7 +32,7 @@ loader.lazyRequireGetter( loader.lazyImporter( this, "VariablesView", - "resource://devtools/client/storage/VariablesView.jsm" + "resource://devtools/client/shared/widgets/VariablesView.jsm" ); /** diff --git a/devtools/server/actors/webconsole/utils.js b/devtools/server/actors/webconsole/utils.js index 75c2333e6efc..4b1bcd5c9f03 100644 --- a/devtools/server/actors/webconsole/utils.js +++ b/devtools/server/actors/webconsole/utils.js @@ -13,7 +13,7 @@ if (!isWorker) { loader.lazyImporter( this, "VariablesView", - "resource://devtools/client/storage/VariablesView.jsm" + "resource://devtools/client/shared/widgets/VariablesView.jsm" ); loader.lazyRequireGetter( this, diff --git a/tools/lint/eslint/modules.json b/tools/lint/eslint/modules.json index 89f65ff0b3d0..fdeed3171a94 100644 --- a/tools/lint/eslint/modules.json +++ b/tools/lint/eslint/modules.json @@ -232,6 +232,7 @@ "utils.js": ["encryptPayload", "makeIdentityConfig", "makeFxAccountsInternalMock", "configureFxAccountIdentity", "configureIdentity", "SyncTestingInfrastructure", "waitForZeroTimer", "MockFxaStorageManager", "AccountState", "sumHistogram", "CommonUtils", "CryptoUtils", "TestingUtils", "promiseZeroTimer", "promiseNamedTimer", "getLoginTelemetryScalar", "syncTestLogging"], "Utils.jsm": ["Utils", "Logger", "PivotContext", "PrefCache"], "VariablesView.jsm": ["VariablesView", "escapeHTML"], + "VariablesViewController.jsm": ["VariablesViewController", "StackFrameUtils"], "version.jsm": ["VERSION"], "vtt.jsm": ["WebVTT"], "WebChannel.jsm": ["WebChannel", "WebChannelBroker"],