fune/devtools/client/shared/widgets/VariablesViewController.jsm
Mark Banner d3608825b8 Bug 1452575 - Automatically fix ESLint issues in shared jsm files in devtools. r=jryans
MozReview-Commit-ID: 422ylOOSZUx

--HG--
extra : rebase_source : 2634cf4588d47274316a2209e3ec34592f7ba4c5
2018-04-09 10:44:03 +01:00

851 lines
28 KiB
JavaScript

/* -*- 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";
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");
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")
);
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);
}
// 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;
}
};