diff options
Diffstat (limited to 'devtools/client/shared/widgets/VariablesViewController.jsm')
-rw-r--r-- | devtools/client/shared/widgets/VariablesViewController.jsm | 858 |
1 files changed, 858 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/VariablesViewController.jsm b/devtools/client/shared/widgets/VariablesViewController.jsm new file mode 100644 index 000000000..5413ce1bf --- /dev/null +++ b/devtools/client/shared/widgets/VariablesViewController.jsm @@ -0,0 +1,858 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { utils: Cu } = Components; + +var {require} = Cu.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"); + +Object.defineProperty(this, "WebConsoleUtils", { + get: function () { + return require("devtools/client/webconsole/utils").Utils; + }, + configurable: true, + enumerable: true +}); + +XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () => + Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled") +); + +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); + +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: + * - getObjectClient: @see this._setClientGetters + * - getLongStringClient: @see this._setClientGetters + * - getEnvironmentClient: @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: + * - getObjectClient: callback for creating an object grip client + * - getLongStringClient: callback for creating a long string grip client + * - getEnvironmentClient: callback for creating an environment client + * - releaseActor: callback for releasing an actor when it's no longer needed + */ + _setClientGetters: function (aOptions) { + if (aOptions.getObjectClient) { + this._getObjectClient = aOptions.getObjectClient; + } + if (aOptions.getLongStringClient) { + this._getLongStringClient = aOptions.getLongStringClient; + } + if (aOptions.getEnvironmentClient) { + this._getEnvironmentClient = aOptions.getEnvironmentClient; + } + 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: function (aTarget, aGrip) { + let deferred = defer(); + + let from = aGrip.initial.length; + let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH); + + this._getLongStringClient(aGrip).substring(from, to, aResponse => { + // 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 + aResponse.substring); + aTarget.hideArrow(); + + deferred.resolve(); + }); + + return deferred.promise; + }, + + /** + * 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. + let items = Math.ceil(aGrip.count / 4); + let iterator = aGrip.propertyIterator; + let promises = []; + for (let i = 0; i < 4; i++) { + let start = aGrip.start + i * items; + let count = i != 3 ? items : aGrip.count - i * items; + + // Create a new kind of grip, with additional fields to define the slice + let sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: start, + count: count + }; + + // Query the name of the first and last items for this slice + let deferred = defer(); + iterator.names([start, start + count - 1], ({ names }) => { + let label = "[" + names[0] + ELLIPSIS + names[1] + "]"; + let 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 + let deferred = defer(); + 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. + let deferred = defer(); + let objectClient = this._getObjectClient(aGrip); + let isArray = aGrip.preview && aGrip.preview.kind === "ArrayLike"; + if (isArray) { + // First enumerate array items, e.g. properties from `0` to `array.length`. + let options = { + ignoreNonIndexedProperties: true, + query: aQuery + }; + objectClient.enumProperties(options, ({ iterator }) => { + let 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. + let options = { + ignoreIndexedProperties: true, + sort: true, + query: aQuery + }; + objectClient.enumProperties(options, ({ iterator }) => { + let sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: 0, + count: iterator.count + }; + deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip)); + }); + }); + }); + } else { + // For objects, we just enumerate all the properties sorted by name. + objectClient.enumProperties({ sort: true, query: aQuery }, ({ iterator }) => { + let 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") { + let 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") { + this.addExpander( + aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }), + aGrip.proxyTarget); + this.addExpander( + aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }), + aGrip.proxyHandler); + + // Refuse to play the proxy's stupid game and return immediately + let deferred = defer(); + deferred.resolve(); + return deferred.promise; + } + + if (aGrip.class === "Promise" && aGrip.promiseState) { + const { state, value, reason } = aGrip.promiseState; + aTarget.addItem("<state>", { value: state }, { internalItem: true }); + if (state === "fulfilled") { + this.addExpander( + aTarget.addItem("<value>", { value }, { internalItem: true }), + value); + } else if (state === "rejected") { + this.addExpander( + aTarget.addItem("<reason>", { value: reason }, { internalItem: true }), + reason); + } + } else if (["Map", "WeakMap", "Set", "WeakSet"].includes(aGrip.class)) { + let entriesList = aTarget.addItem("<entries>", {}, { 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(() => { + let deferred = defer(); + let objectClient = this._getObjectClient(aGrip); + objectClient.getPrototype(({ prototype }) => { + this._populateObjectPrototype(aTarget, prototype); + deferred.resolve(); + }); + return deferred.promise; + }); + } + + return this._populateProperties(aTarget, aGrip); + }, + + _populateProperties: function (aTarget, aGrip, aOptions) { + let deferred = defer(); + + let objectClient = this._getObjectClient(aGrip); + objectClient.getPrototypeAndProperties(aResponse => { + let ownProperties = aResponse.ownProperties || {}; + let prototype = aResponse.prototype || null; + // 'safeGetterValues' is new and isn't necessary defined on old actors. + let safeGetterValues = aResponse.safeGetterValues || {}; + let sortable = VariablesView.isSortable(aGrip.class); + + // Merge the safe getter values into one object such that we can use it + // in VariablesView. + for (let name of Object.keys(safeGetterValues)) { + if (name in ownProperties) { + let { 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") { + objectClient.getScope(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) { + let objectScopes = []; + let environment = aScope; + let funcScope = aTarget.addItem("<Closure>"); + funcScope.target.setAttribute("scope", ""); + funcScope.showArrow(); + + do { + // Create a scope to contain all the inspected variables. + let label = StackFrameUtils.getScopeLabel(environment); + + // Block scopes may have the same label, so make addItem allow duplicates. + let 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 { + let deferred = defer(); + objectScopes.push(deferred.promise); + this._getEnvironmentClient(environment).getBindings(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) => { + let name = Object.getOwnPropertyNames(arg)[0]; + let 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) { + let objGrip = grip.obj; + let objectClient = this._getObjectClient(objGrip); + + return new promise((resolve, reject) => { + objectClient.enumEntries((response) => { + if (response.error) { + // Older server might not support the enumEntries method + console.warn(response.error + ": " + response.message); + resolve(); + } else { + let 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; + let getter = aTarget.get("get"); + if (getter) { + getter.evaluationMacro = this._getterOrSetterEvalMacro; + } + let 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 (WebConsoleUtils.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, false); + } + + // Register all the actors that this controller now depends on. + for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) { + if (WebConsoleUtils.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.")); + } + + let 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") { + 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: + let 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 (let 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(); + + let scope = this.view.addScope(options.label); + scope.expanded = true; // Expand the scope by default. + scope.locked = true; // Prevent collapsing the scope. + + let 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") { + let 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"); + } + // Otherwise construct the scope name. + else { + 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": + let f = aEnv.function; + label += " [" + + (f.name || f.userDisplayName || f.displayName || "(anonymous)") + + "]"; + break; + } + return label; + } +}; |