michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: Cu.import("resource:///modules/devtools/VariablesView.jsm"); michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "devtools", michael@0: "resource://gre/modules/devtools/Loader.jsm"); michael@0: michael@0: Object.defineProperty(this, "WebConsoleUtils", { michael@0: get: function() { michael@0: return devtools.require("devtools/toolkit/webconsole/utils").Utils; michael@0: }, michael@0: configurable: true, michael@0: enumerable: true michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () => michael@0: Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled") michael@0: ); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", michael@0: "resource://gre/modules/devtools/Console.jsm"); michael@0: michael@0: const MAX_LONG_STRING_LENGTH = 200000; michael@0: const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"]; michael@0: michael@0: michael@0: /** michael@0: * Controller for a VariablesView that handles interfacing with the debugger michael@0: * protocol. Is able to populate scopes and variables via the protocol as well michael@0: * as manage actor lifespans. michael@0: * michael@0: * @param VariablesView aView michael@0: * The view to attach to. michael@0: * @param object aOptions [optional] michael@0: * Options for configuring the controller. Supported options: michael@0: * - getObjectClient: @see this._setClientGetters michael@0: * - getLongStringClient: @see this._setClientGetters michael@0: * - getEnvironmentClient: @see this._setClientGetters michael@0: * - releaseActor: @see this._setClientGetters michael@0: * - overrideValueEvalMacro: @see _setEvaluationMacros michael@0: * - getterOrSetterEvalMacro: @see _setEvaluationMacros michael@0: * - simpleValueEvalMacro: @see _setEvaluationMacros michael@0: */ michael@0: function VariablesViewController(aView, aOptions = {}) { michael@0: this.addExpander = this.addExpander.bind(this); michael@0: michael@0: this._setClientGetters(aOptions); michael@0: this._setEvaluationMacros(aOptions); michael@0: michael@0: this._actors = new Set(); michael@0: this.view = aView; michael@0: this.view.controller = this; michael@0: } michael@0: michael@0: VariablesViewController.prototype = { michael@0: /** michael@0: * The default getter/setter evaluation macro. michael@0: */ michael@0: _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro, michael@0: michael@0: /** michael@0: * The default override value evaluation macro. michael@0: */ michael@0: _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro, michael@0: michael@0: /** michael@0: * The default simple value evaluation macro. michael@0: */ michael@0: _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro, michael@0: michael@0: /** michael@0: * Set the functions used to retrieve debugger client grips. michael@0: * michael@0: * @param object aOptions michael@0: * Options for getting the client grips. Supported options: michael@0: * - getObjectClient: callback for creating an object grip client michael@0: * - getLongStringClient: callback for creating a long string grip client michael@0: * - getEnvironmentClient: callback for creating an environment client michael@0: * - releaseActor: callback for releasing an actor when it's no longer needed michael@0: */ michael@0: _setClientGetters: function(aOptions) { michael@0: if (aOptions.getObjectClient) { michael@0: this._getObjectClient = aOptions.getObjectClient; michael@0: } michael@0: if (aOptions.getLongStringClient) { michael@0: this._getLongStringClient = aOptions.getLongStringClient; michael@0: } michael@0: if (aOptions.getEnvironmentClient) { michael@0: this._getEnvironmentClient = aOptions.getEnvironmentClient; michael@0: } michael@0: if (aOptions.releaseActor) { michael@0: this._releaseActor = aOptions.releaseActor; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets the functions used when evaluating strings in the variables view. michael@0: * michael@0: * @param object aOptions michael@0: * Options for configuring the macros. Supported options: michael@0: * - overrideValueEvalMacro: callback for creating an overriding eval macro michael@0: * - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro michael@0: * - simpleValueEvalMacro: callback for creating a simple value eval macro michael@0: */ michael@0: _setEvaluationMacros: function(aOptions) { michael@0: if (aOptions.overrideValueEvalMacro) { michael@0: this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro; michael@0: } michael@0: if (aOptions.getterOrSetterEvalMacro) { michael@0: this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro; michael@0: } michael@0: if (aOptions.simpleValueEvalMacro) { michael@0: this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Populate a long string into a target using a grip. michael@0: * michael@0: * @param Variable aTarget michael@0: * The target Variable/Property to put the retrieved string into. michael@0: * @param LongStringActor aGrip michael@0: * The long string grip that use to retrieve the full string. michael@0: * @return Promise michael@0: * The promise that will be resolved when the string is retrieved. michael@0: */ michael@0: _populateFromLongString: function(aTarget, aGrip){ michael@0: let deferred = promise.defer(); michael@0: michael@0: let from = aGrip.initial.length; michael@0: let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH); michael@0: michael@0: this._getLongStringClient(aGrip).substring(from, to, aResponse => { michael@0: // Stop tracking the actor because it's no longer needed. michael@0: this.releaseActor(aGrip); michael@0: michael@0: // Replace the preview with the full string and make it non-expandable. michael@0: aTarget.onexpand = null; michael@0: aTarget.setGrip(aGrip.initial + aResponse.substring); michael@0: aTarget.hideArrow(); michael@0: michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Adds properties to a Scope, Variable, or Property in the view. Triggered michael@0: * when a scope is expanded or certain variables are hovered. michael@0: * michael@0: * @param Scope aTarget michael@0: * The Scope where the properties will be placed into. michael@0: * @param object aGrip michael@0: * The grip to use to populate the target. michael@0: */ michael@0: _populateFromObject: function(aTarget, aGrip) { michael@0: let deferred = promise.defer(); michael@0: michael@0: let objectClient = this._getObjectClient(aGrip); michael@0: objectClient.getPrototypeAndProperties(aResponse => { michael@0: let { ownProperties, prototype } = aResponse; michael@0: // 'safeGetterValues' is new and isn't necessary defined on old actors. michael@0: let safeGetterValues = aResponse.safeGetterValues || {}; michael@0: let sortable = VariablesView.isSortable(aGrip.class); michael@0: michael@0: // Merge the safe getter values into one object such that we can use it michael@0: // in VariablesView. michael@0: for (let name of Object.keys(safeGetterValues)) { michael@0: if (name in ownProperties) { michael@0: let { getterValue, getterPrototypeLevel } = safeGetterValues[name]; michael@0: ownProperties[name].getterValue = getterValue; michael@0: ownProperties[name].getterPrototypeLevel = getterPrototypeLevel; michael@0: } else { michael@0: ownProperties[name] = safeGetterValues[name]; michael@0: } michael@0: } michael@0: michael@0: // Add all the variable properties. michael@0: if (ownProperties) { michael@0: aTarget.addItems(ownProperties, { michael@0: // Not all variables need to force sorted properties. michael@0: sorted: sortable, michael@0: // Expansion handlers must be set after the properties are added. michael@0: callback: this.addExpander michael@0: }); michael@0: } michael@0: michael@0: // Add the variable's __proto__. michael@0: if (prototype && prototype.type != "null") { michael@0: let proto = aTarget.addItem("__proto__", { value: prototype }); michael@0: // Expansion handlers must be set after the properties are added. michael@0: this.addExpander(proto, prototype); michael@0: } michael@0: michael@0: // If the object is a function we need to fetch its scope chain michael@0: // to show them as closures for the respective function. michael@0: if (aGrip.class == "Function") { michael@0: objectClient.getScope(aResponse => { michael@0: if (aResponse.error) { michael@0: // This function is bound to a built-in object or it's not present michael@0: // in the current scope chain. Not necessarily an actual error, michael@0: // it just means that there's no closure for the function. michael@0: console.warn(aResponse.error + ": " + aResponse.message); michael@0: return void deferred.resolve(); michael@0: } michael@0: this._populateWithClosure(aTarget, aResponse.scope).then(deferred.resolve); michael@0: }); michael@0: } else { michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Adds the scope chain elements (closures) of a function variable. michael@0: * michael@0: * @param Variable aTarget michael@0: * The variable where the properties will be placed into. michael@0: * @param Scope aScope michael@0: * The lexical environment form as specified in the protocol. michael@0: */ michael@0: _populateWithClosure: function(aTarget, aScope) { michael@0: let objectScopes = []; michael@0: let environment = aScope; michael@0: let funcScope = aTarget.addItem(""); michael@0: funcScope.target.setAttribute("scope", ""); michael@0: funcScope.showArrow(); michael@0: michael@0: do { michael@0: // Create a scope to contain all the inspected variables. michael@0: let label = StackFrameUtils.getScopeLabel(environment); michael@0: michael@0: // Block scopes may have the same label, so make addItem allow duplicates. michael@0: let closure = funcScope.addItem(label, undefined, true); michael@0: closure.target.setAttribute("scope", ""); michael@0: closure.showArrow(); michael@0: michael@0: // Add nodes for every argument and every other variable in scope. michael@0: if (environment.bindings) { michael@0: this._populateWithEnvironmentBindings(closure, environment.bindings); michael@0: } else { michael@0: let deferred = promise.defer(); michael@0: objectScopes.push(deferred.promise); michael@0: this._getEnvironmentClient(environment).getBindings(response => { michael@0: this._populateWithEnvironmentBindings(closure, response.bindings); michael@0: deferred.resolve(); michael@0: }); michael@0: } michael@0: } while ((environment = environment.parent)); michael@0: michael@0: return promise.all(objectScopes).then(() => { michael@0: // Signal that scopes have been fetched. michael@0: this.view.emit("fetched", "scopes", funcScope); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Adds nodes for every specified binding to the closure node. michael@0: * michael@0: * @param Variable aTarget michael@0: * The variable where the bindings will be placed into. michael@0: * @param object aBindings michael@0: * The bindings form as specified in the protocol. michael@0: */ michael@0: _populateWithEnvironmentBindings: function(aTarget, aBindings) { michael@0: // Add nodes for every argument in the scope. michael@0: aTarget.addItems(aBindings.arguments.reduce((accumulator, arg) => { michael@0: let name = Object.getOwnPropertyNames(arg)[0]; michael@0: let descriptor = arg[name]; michael@0: accumulator[name] = descriptor; michael@0: return accumulator; michael@0: }, {}), { michael@0: // Arguments aren't sorted. michael@0: sorted: false, michael@0: // Expansion handlers must be set after the properties are added. michael@0: callback: this.addExpander michael@0: }); michael@0: michael@0: // Add nodes for every other variable in the scope. michael@0: aTarget.addItems(aBindings.variables, { michael@0: // Not all variables need to force sorted properties. michael@0: sorted: VARIABLES_SORTING_ENABLED, michael@0: // Expansion handlers must be set after the properties are added. michael@0: callback: this.addExpander michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Adds an 'onexpand' callback for a variable, lazily handling michael@0: * the addition of new properties. michael@0: * michael@0: * @param Variable aTarget michael@0: * The variable where the properties will be placed into. michael@0: * @param any aSource michael@0: * The source to use to populate the target. michael@0: */ michael@0: addExpander: function(aTarget, aSource) { michael@0: // Attach evaluation macros as necessary. michael@0: if (aTarget.getter || aTarget.setter) { michael@0: aTarget.evaluationMacro = this._overrideValueEvalMacro; michael@0: let getter = aTarget.get("get"); michael@0: if (getter) { michael@0: getter.evaluationMacro = this._getterOrSetterEvalMacro; michael@0: } michael@0: let setter = aTarget.get("set"); michael@0: if (setter) { michael@0: setter.evaluationMacro = this._getterOrSetterEvalMacro; michael@0: } michael@0: } else { michael@0: aTarget.evaluationMacro = this._simpleValueEvalMacro; michael@0: } michael@0: michael@0: // If the source is primitive then an expander is not needed. michael@0: if (VariablesView.isPrimitive({ value: aSource })) { michael@0: return; michael@0: } michael@0: michael@0: // If the source is a long string then show the arrow. michael@0: if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") { michael@0: aTarget.showArrow(); michael@0: } michael@0: michael@0: // Make sure that properties are always available on expansion. michael@0: aTarget.onexpand = () => this.populate(aTarget, aSource); michael@0: michael@0: // Some variables are likely to contain a very large number of properties. michael@0: // It's a good idea to be prepared in case of an expansion. michael@0: if (aTarget.shouldPrefetch) { michael@0: aTarget.addEventListener("mouseover", aTarget.onexpand, false); michael@0: } michael@0: michael@0: // Register all the actors that this controller now depends on. michael@0: for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) { michael@0: if (WebConsoleUtils.isActorGrip(grip)) { michael@0: this._actors.add(grip.actor); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds properties to a Scope, Variable, or Property in the view. Triggered michael@0: * when a scope is expanded or certain variables are hovered. michael@0: * michael@0: * This does not expand the target, it only populates it. michael@0: * michael@0: * @param Scope aTarget michael@0: * The Scope to be expanded. michael@0: * @param object aSource michael@0: * The source to use to populate the target. michael@0: * @return Promise michael@0: * The promise that is resolved once the target has been expanded. michael@0: */ michael@0: populate: function(aTarget, aSource) { michael@0: // Fetch the variables only once. michael@0: if (aTarget._fetched) { michael@0: return aTarget._fetched; michael@0: } michael@0: // Make sure the source grip is available. michael@0: if (!aSource) { michael@0: return promise.reject(new Error("No actor grip was given for the variable.")); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: aTarget._fetched = deferred.promise; michael@0: michael@0: // If the target is a Variable or Property then we're fetching properties. michael@0: if (VariablesView.isVariable(aTarget)) { michael@0: this._populateFromObject(aTarget, aSource).then(() => { michael@0: // Signal that properties have been fetched. michael@0: this.view.emit("fetched", "properties", aTarget); michael@0: // Commit the hierarchy because new items were added. michael@0: this.view.commitHierarchy(); michael@0: deferred.resolve(); michael@0: }); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: switch (aSource.type) { michael@0: case "longString": michael@0: this._populateFromLongString(aTarget, aSource).then(() => { michael@0: // Signal that a long string has been fetched. michael@0: this.view.emit("fetched", "longString", aTarget); michael@0: deferred.resolve(); michael@0: }); michael@0: break; michael@0: case "with": michael@0: case "object": michael@0: this._populateFromObject(aTarget, aSource.object).then(() => { michael@0: // Signal that variables have been fetched. michael@0: this.view.emit("fetched", "variables", aTarget); michael@0: // Commit the hierarchy because new items were added. michael@0: this.view.commitHierarchy(); michael@0: deferred.resolve(); michael@0: }); michael@0: break; michael@0: case "block": michael@0: case "function": michael@0: this._populateWithEnvironmentBindings(aTarget, aSource.bindings); michael@0: // No need to signal that variables have been fetched, since michael@0: // the scope arguments and variables are already attached to the michael@0: // environment bindings, so pausing the active thread is unnecessary. michael@0: // Commit the hierarchy because new items were added. michael@0: this.view.commitHierarchy(); michael@0: deferred.resolve(); michael@0: break; michael@0: default: michael@0: let error = "Unknown Debugger.Environment type: " + aSource.type; michael@0: Cu.reportError(error); michael@0: deferred.reject(error); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Release an actor from the controller. michael@0: * michael@0: * @param object aActor michael@0: * The actor to release. michael@0: */ michael@0: releaseActor: function(aActor){ michael@0: if (this._releaseActor) { michael@0: this._releaseActor(aActor); michael@0: } michael@0: this._actors.delete(aActor); michael@0: }, michael@0: michael@0: /** michael@0: * Release all the actors referenced by the controller, optionally filtered. michael@0: * michael@0: * @param function aFilter [optional] michael@0: * Callback to filter which actors are released. michael@0: */ michael@0: releaseActors: function(aFilter) { michael@0: for (let actor of this._actors) { michael@0: if (!aFilter || aFilter(actor)) { michael@0: this.releaseActor(actor); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Helper function for setting up a single Scope with a single Variable michael@0: * contained within it. michael@0: * michael@0: * This function will empty the variables view. michael@0: * michael@0: * @param object aOptions michael@0: * Options for the contents of the view: michael@0: * - objectActor: the grip of the new ObjectActor to show. michael@0: * - rawObject: the raw object to show. michael@0: * - label: the label for the inspected object. michael@0: * @param object aConfiguration michael@0: * Additional options for the controller: michael@0: * - overrideValueEvalMacro: @see _setEvaluationMacros michael@0: * - getterOrSetterEvalMacro: @see _setEvaluationMacros michael@0: * - simpleValueEvalMacro: @see _setEvaluationMacros michael@0: * @return Object michael@0: * - variable: the created Variable. michael@0: * - expanded: the Promise that resolves when the variable expands. michael@0: */ michael@0: setSingleVariable: function(aOptions, aConfiguration = {}) { michael@0: this._setEvaluationMacros(aConfiguration); michael@0: this.view.empty(); michael@0: michael@0: let scope = this.view.addScope(aOptions.label); michael@0: scope.expanded = true; // Expand the scope by default. michael@0: scope.locked = true; // Prevent collpasing the scope. michael@0: michael@0: let variable = scope.addItem("", { enumerable: true }); michael@0: let populated; michael@0: michael@0: if (aOptions.objectActor) { michael@0: populated = this.populate(variable, aOptions.objectActor); michael@0: variable.expand(); michael@0: } else if (aOptions.rawObject) { michael@0: variable.populate(aOptions.rawObject, { expanded: true }); michael@0: populated = promise.resolve(); michael@0: } michael@0: michael@0: return { variable: variable, expanded: populated }; michael@0: }, michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Attaches a VariablesViewController to a VariablesView if it doesn't already michael@0: * have one. michael@0: * michael@0: * @param VariablesView aView michael@0: * The view to attach to. michael@0: * @param object aOptions michael@0: * The options to use in creating the controller. michael@0: * @return VariablesViewController michael@0: */ michael@0: VariablesViewController.attach = function(aView, aOptions) { michael@0: if (aView.controller) { michael@0: return aView.controller; michael@0: } michael@0: return new VariablesViewController(aView, aOptions); michael@0: }; michael@0: michael@0: /** michael@0: * Utility functions for handling stackframes. michael@0: */ michael@0: let StackFrameUtils = { michael@0: /** michael@0: * Create a textual representation for the specified stack frame michael@0: * to display in the stackframes container. michael@0: * michael@0: * @param object aFrame michael@0: * The stack frame to label. michael@0: */ michael@0: getFrameTitle: function(aFrame) { michael@0: if (aFrame.type == "call") { michael@0: let c = aFrame.callee; michael@0: return (c.name || c.userDisplayName || c.displayName || "(anonymous)"); michael@0: } michael@0: return "(" + aFrame.type + ")"; michael@0: }, michael@0: michael@0: /** michael@0: * Constructs a scope label based on its environment. michael@0: * michael@0: * @param object aEnv michael@0: * The scope's environment. michael@0: * @return string michael@0: * The scope's label. michael@0: */ michael@0: getScopeLabel: function(aEnv) { michael@0: let name = ""; michael@0: michael@0: // Name the outermost scope Global. michael@0: if (!aEnv.parent) { michael@0: name = L10N.getStr("globalScopeLabel"); michael@0: } michael@0: // Otherwise construct the scope name. michael@0: else { michael@0: name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1); michael@0: } michael@0: michael@0: let label = L10N.getFormatStr("scopeLabel", name); michael@0: switch (aEnv.type) { michael@0: case "with": michael@0: case "object": michael@0: label += " [" + aEnv.object.class + "]"; michael@0: break; michael@0: case "function": michael@0: let f = aEnv.function; michael@0: label += " [" + michael@0: (f.name || f.userDisplayName || f.displayName || "(anonymous)") + michael@0: "]"; michael@0: break; michael@0: } michael@0: return label; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Localization convenience methods. michael@0: */ michael@0: let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);