1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/shared/widgets/VariablesViewController.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,578 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 +"use strict"; 1.10 + 1.11 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.12 + 1.13 +Cu.import("resource://gre/modules/Services.jsm"); 1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.15 +let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.16 +Cu.import("resource:///modules/devtools/VariablesView.jsm"); 1.17 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); 1.18 + 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "devtools", 1.20 + "resource://gre/modules/devtools/Loader.jsm"); 1.21 + 1.22 +Object.defineProperty(this, "WebConsoleUtils", { 1.23 + get: function() { 1.24 + return devtools.require("devtools/toolkit/webconsole/utils").Utils; 1.25 + }, 1.26 + configurable: true, 1.27 + enumerable: true 1.28 +}); 1.29 + 1.30 +XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () => 1.31 + Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled") 1.32 +); 1.33 + 1.34 +XPCOMUtils.defineLazyModuleGetter(this, "console", 1.35 + "resource://gre/modules/devtools/Console.jsm"); 1.36 + 1.37 +const MAX_LONG_STRING_LENGTH = 200000; 1.38 +const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; 1.39 + 1.40 +this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"]; 1.41 + 1.42 + 1.43 +/** 1.44 + * Controller for a VariablesView that handles interfacing with the debugger 1.45 + * protocol. Is able to populate scopes and variables via the protocol as well 1.46 + * as manage actor lifespans. 1.47 + * 1.48 + * @param VariablesView aView 1.49 + * The view to attach to. 1.50 + * @param object aOptions [optional] 1.51 + * Options for configuring the controller. Supported options: 1.52 + * - getObjectClient: @see this._setClientGetters 1.53 + * - getLongStringClient: @see this._setClientGetters 1.54 + * - getEnvironmentClient: @see this._setClientGetters 1.55 + * - releaseActor: @see this._setClientGetters 1.56 + * - overrideValueEvalMacro: @see _setEvaluationMacros 1.57 + * - getterOrSetterEvalMacro: @see _setEvaluationMacros 1.58 + * - simpleValueEvalMacro: @see _setEvaluationMacros 1.59 + */ 1.60 +function VariablesViewController(aView, aOptions = {}) { 1.61 + this.addExpander = this.addExpander.bind(this); 1.62 + 1.63 + this._setClientGetters(aOptions); 1.64 + this._setEvaluationMacros(aOptions); 1.65 + 1.66 + this._actors = new Set(); 1.67 + this.view = aView; 1.68 + this.view.controller = this; 1.69 +} 1.70 + 1.71 +VariablesViewController.prototype = { 1.72 + /** 1.73 + * The default getter/setter evaluation macro. 1.74 + */ 1.75 + _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro, 1.76 + 1.77 + /** 1.78 + * The default override value evaluation macro. 1.79 + */ 1.80 + _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro, 1.81 + 1.82 + /** 1.83 + * The default simple value evaluation macro. 1.84 + */ 1.85 + _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro, 1.86 + 1.87 + /** 1.88 + * Set the functions used to retrieve debugger client grips. 1.89 + * 1.90 + * @param object aOptions 1.91 + * Options for getting the client grips. Supported options: 1.92 + * - getObjectClient: callback for creating an object grip client 1.93 + * - getLongStringClient: callback for creating a long string grip client 1.94 + * - getEnvironmentClient: callback for creating an environment client 1.95 + * - releaseActor: callback for releasing an actor when it's no longer needed 1.96 + */ 1.97 + _setClientGetters: function(aOptions) { 1.98 + if (aOptions.getObjectClient) { 1.99 + this._getObjectClient = aOptions.getObjectClient; 1.100 + } 1.101 + if (aOptions.getLongStringClient) { 1.102 + this._getLongStringClient = aOptions.getLongStringClient; 1.103 + } 1.104 + if (aOptions.getEnvironmentClient) { 1.105 + this._getEnvironmentClient = aOptions.getEnvironmentClient; 1.106 + } 1.107 + if (aOptions.releaseActor) { 1.108 + this._releaseActor = aOptions.releaseActor; 1.109 + } 1.110 + }, 1.111 + 1.112 + /** 1.113 + * Sets the functions used when evaluating strings in the variables view. 1.114 + * 1.115 + * @param object aOptions 1.116 + * Options for configuring the macros. Supported options: 1.117 + * - overrideValueEvalMacro: callback for creating an overriding eval macro 1.118 + * - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro 1.119 + * - simpleValueEvalMacro: callback for creating a simple value eval macro 1.120 + */ 1.121 + _setEvaluationMacros: function(aOptions) { 1.122 + if (aOptions.overrideValueEvalMacro) { 1.123 + this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro; 1.124 + } 1.125 + if (aOptions.getterOrSetterEvalMacro) { 1.126 + this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro; 1.127 + } 1.128 + if (aOptions.simpleValueEvalMacro) { 1.129 + this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro; 1.130 + } 1.131 + }, 1.132 + 1.133 + /** 1.134 + * Populate a long string into a target using a grip. 1.135 + * 1.136 + * @param Variable aTarget 1.137 + * The target Variable/Property to put the retrieved string into. 1.138 + * @param LongStringActor aGrip 1.139 + * The long string grip that use to retrieve the full string. 1.140 + * @return Promise 1.141 + * The promise that will be resolved when the string is retrieved. 1.142 + */ 1.143 + _populateFromLongString: function(aTarget, aGrip){ 1.144 + let deferred = promise.defer(); 1.145 + 1.146 + let from = aGrip.initial.length; 1.147 + let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH); 1.148 + 1.149 + this._getLongStringClient(aGrip).substring(from, to, aResponse => { 1.150 + // Stop tracking the actor because it's no longer needed. 1.151 + this.releaseActor(aGrip); 1.152 + 1.153 + // Replace the preview with the full string and make it non-expandable. 1.154 + aTarget.onexpand = null; 1.155 + aTarget.setGrip(aGrip.initial + aResponse.substring); 1.156 + aTarget.hideArrow(); 1.157 + 1.158 + deferred.resolve(); 1.159 + }); 1.160 + 1.161 + return deferred.promise; 1.162 + }, 1.163 + 1.164 + /** 1.165 + * Adds properties to a Scope, Variable, or Property in the view. Triggered 1.166 + * when a scope is expanded or certain variables are hovered. 1.167 + * 1.168 + * @param Scope aTarget 1.169 + * The Scope where the properties will be placed into. 1.170 + * @param object aGrip 1.171 + * The grip to use to populate the target. 1.172 + */ 1.173 + _populateFromObject: function(aTarget, aGrip) { 1.174 + let deferred = promise.defer(); 1.175 + 1.176 + let objectClient = this._getObjectClient(aGrip); 1.177 + objectClient.getPrototypeAndProperties(aResponse => { 1.178 + let { ownProperties, prototype } = aResponse; 1.179 + // 'safeGetterValues' is new and isn't necessary defined on old actors. 1.180 + let safeGetterValues = aResponse.safeGetterValues || {}; 1.181 + let sortable = VariablesView.isSortable(aGrip.class); 1.182 + 1.183 + // Merge the safe getter values into one object such that we can use it 1.184 + // in VariablesView. 1.185 + for (let name of Object.keys(safeGetterValues)) { 1.186 + if (name in ownProperties) { 1.187 + let { getterValue, getterPrototypeLevel } = safeGetterValues[name]; 1.188 + ownProperties[name].getterValue = getterValue; 1.189 + ownProperties[name].getterPrototypeLevel = getterPrototypeLevel; 1.190 + } else { 1.191 + ownProperties[name] = safeGetterValues[name]; 1.192 + } 1.193 + } 1.194 + 1.195 + // Add all the variable properties. 1.196 + if (ownProperties) { 1.197 + aTarget.addItems(ownProperties, { 1.198 + // Not all variables need to force sorted properties. 1.199 + sorted: sortable, 1.200 + // Expansion handlers must be set after the properties are added. 1.201 + callback: this.addExpander 1.202 + }); 1.203 + } 1.204 + 1.205 + // Add the variable's __proto__. 1.206 + if (prototype && prototype.type != "null") { 1.207 + let proto = aTarget.addItem("__proto__", { value: prototype }); 1.208 + // Expansion handlers must be set after the properties are added. 1.209 + this.addExpander(proto, prototype); 1.210 + } 1.211 + 1.212 + // If the object is a function we need to fetch its scope chain 1.213 + // to show them as closures for the respective function. 1.214 + if (aGrip.class == "Function") { 1.215 + objectClient.getScope(aResponse => { 1.216 + if (aResponse.error) { 1.217 + // This function is bound to a built-in object or it's not present 1.218 + // in the current scope chain. Not necessarily an actual error, 1.219 + // it just means that there's no closure for the function. 1.220 + console.warn(aResponse.error + ": " + aResponse.message); 1.221 + return void deferred.resolve(); 1.222 + } 1.223 + this._populateWithClosure(aTarget, aResponse.scope).then(deferred.resolve); 1.224 + }); 1.225 + } else { 1.226 + deferred.resolve(); 1.227 + } 1.228 + }); 1.229 + 1.230 + return deferred.promise; 1.231 + }, 1.232 + 1.233 + /** 1.234 + * Adds the scope chain elements (closures) of a function variable. 1.235 + * 1.236 + * @param Variable aTarget 1.237 + * The variable where the properties will be placed into. 1.238 + * @param Scope aScope 1.239 + * The lexical environment form as specified in the protocol. 1.240 + */ 1.241 + _populateWithClosure: function(aTarget, aScope) { 1.242 + let objectScopes = []; 1.243 + let environment = aScope; 1.244 + let funcScope = aTarget.addItem("<Closure>"); 1.245 + funcScope.target.setAttribute("scope", ""); 1.246 + funcScope.showArrow(); 1.247 + 1.248 + do { 1.249 + // Create a scope to contain all the inspected variables. 1.250 + let label = StackFrameUtils.getScopeLabel(environment); 1.251 + 1.252 + // Block scopes may have the same label, so make addItem allow duplicates. 1.253 + let closure = funcScope.addItem(label, undefined, true); 1.254 + closure.target.setAttribute("scope", ""); 1.255 + closure.showArrow(); 1.256 + 1.257 + // Add nodes for every argument and every other variable in scope. 1.258 + if (environment.bindings) { 1.259 + this._populateWithEnvironmentBindings(closure, environment.bindings); 1.260 + } else { 1.261 + let deferred = promise.defer(); 1.262 + objectScopes.push(deferred.promise); 1.263 + this._getEnvironmentClient(environment).getBindings(response => { 1.264 + this._populateWithEnvironmentBindings(closure, response.bindings); 1.265 + deferred.resolve(); 1.266 + }); 1.267 + } 1.268 + } while ((environment = environment.parent)); 1.269 + 1.270 + return promise.all(objectScopes).then(() => { 1.271 + // Signal that scopes have been fetched. 1.272 + this.view.emit("fetched", "scopes", funcScope); 1.273 + }); 1.274 + }, 1.275 + 1.276 + /** 1.277 + * Adds nodes for every specified binding to the closure node. 1.278 + * 1.279 + * @param Variable aTarget 1.280 + * The variable where the bindings will be placed into. 1.281 + * @param object aBindings 1.282 + * The bindings form as specified in the protocol. 1.283 + */ 1.284 + _populateWithEnvironmentBindings: function(aTarget, aBindings) { 1.285 + // Add nodes for every argument in the scope. 1.286 + aTarget.addItems(aBindings.arguments.reduce((accumulator, arg) => { 1.287 + let name = Object.getOwnPropertyNames(arg)[0]; 1.288 + let descriptor = arg[name]; 1.289 + accumulator[name] = descriptor; 1.290 + return accumulator; 1.291 + }, {}), { 1.292 + // Arguments aren't sorted. 1.293 + sorted: false, 1.294 + // Expansion handlers must be set after the properties are added. 1.295 + callback: this.addExpander 1.296 + }); 1.297 + 1.298 + // Add nodes for every other variable in the scope. 1.299 + aTarget.addItems(aBindings.variables, { 1.300 + // Not all variables need to force sorted properties. 1.301 + sorted: VARIABLES_SORTING_ENABLED, 1.302 + // Expansion handlers must be set after the properties are added. 1.303 + callback: this.addExpander 1.304 + }); 1.305 + }, 1.306 + 1.307 + /** 1.308 + * Adds an 'onexpand' callback for a variable, lazily handling 1.309 + * the addition of new properties. 1.310 + * 1.311 + * @param Variable aTarget 1.312 + * The variable where the properties will be placed into. 1.313 + * @param any aSource 1.314 + * The source to use to populate the target. 1.315 + */ 1.316 + addExpander: function(aTarget, aSource) { 1.317 + // Attach evaluation macros as necessary. 1.318 + if (aTarget.getter || aTarget.setter) { 1.319 + aTarget.evaluationMacro = this._overrideValueEvalMacro; 1.320 + let getter = aTarget.get("get"); 1.321 + if (getter) { 1.322 + getter.evaluationMacro = this._getterOrSetterEvalMacro; 1.323 + } 1.324 + let setter = aTarget.get("set"); 1.325 + if (setter) { 1.326 + setter.evaluationMacro = this._getterOrSetterEvalMacro; 1.327 + } 1.328 + } else { 1.329 + aTarget.evaluationMacro = this._simpleValueEvalMacro; 1.330 + } 1.331 + 1.332 + // If the source is primitive then an expander is not needed. 1.333 + if (VariablesView.isPrimitive({ value: aSource })) { 1.334 + return; 1.335 + } 1.336 + 1.337 + // If the source is a long string then show the arrow. 1.338 + if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") { 1.339 + aTarget.showArrow(); 1.340 + } 1.341 + 1.342 + // Make sure that properties are always available on expansion. 1.343 + aTarget.onexpand = () => this.populate(aTarget, aSource); 1.344 + 1.345 + // Some variables are likely to contain a very large number of properties. 1.346 + // It's a good idea to be prepared in case of an expansion. 1.347 + if (aTarget.shouldPrefetch) { 1.348 + aTarget.addEventListener("mouseover", aTarget.onexpand, false); 1.349 + } 1.350 + 1.351 + // Register all the actors that this controller now depends on. 1.352 + for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) { 1.353 + if (WebConsoleUtils.isActorGrip(grip)) { 1.354 + this._actors.add(grip.actor); 1.355 + } 1.356 + } 1.357 + }, 1.358 + 1.359 + /** 1.360 + * Adds properties to a Scope, Variable, or Property in the view. Triggered 1.361 + * when a scope is expanded or certain variables are hovered. 1.362 + * 1.363 + * This does not expand the target, it only populates it. 1.364 + * 1.365 + * @param Scope aTarget 1.366 + * The Scope to be expanded. 1.367 + * @param object aSource 1.368 + * The source to use to populate the target. 1.369 + * @return Promise 1.370 + * The promise that is resolved once the target has been expanded. 1.371 + */ 1.372 + populate: function(aTarget, aSource) { 1.373 + // Fetch the variables only once. 1.374 + if (aTarget._fetched) { 1.375 + return aTarget._fetched; 1.376 + } 1.377 + // Make sure the source grip is available. 1.378 + if (!aSource) { 1.379 + return promise.reject(new Error("No actor grip was given for the variable.")); 1.380 + } 1.381 + 1.382 + let deferred = promise.defer(); 1.383 + aTarget._fetched = deferred.promise; 1.384 + 1.385 + // If the target is a Variable or Property then we're fetching properties. 1.386 + if (VariablesView.isVariable(aTarget)) { 1.387 + this._populateFromObject(aTarget, aSource).then(() => { 1.388 + // Signal that properties have been fetched. 1.389 + this.view.emit("fetched", "properties", aTarget); 1.390 + // Commit the hierarchy because new items were added. 1.391 + this.view.commitHierarchy(); 1.392 + deferred.resolve(); 1.393 + }); 1.394 + return deferred.promise; 1.395 + } 1.396 + 1.397 + switch (aSource.type) { 1.398 + case "longString": 1.399 + this._populateFromLongString(aTarget, aSource).then(() => { 1.400 + // Signal that a long string has been fetched. 1.401 + this.view.emit("fetched", "longString", aTarget); 1.402 + deferred.resolve(); 1.403 + }); 1.404 + break; 1.405 + case "with": 1.406 + case "object": 1.407 + this._populateFromObject(aTarget, aSource.object).then(() => { 1.408 + // Signal that variables have been fetched. 1.409 + this.view.emit("fetched", "variables", aTarget); 1.410 + // Commit the hierarchy because new items were added. 1.411 + this.view.commitHierarchy(); 1.412 + deferred.resolve(); 1.413 + }); 1.414 + break; 1.415 + case "block": 1.416 + case "function": 1.417 + this._populateWithEnvironmentBindings(aTarget, aSource.bindings); 1.418 + // No need to signal that variables have been fetched, since 1.419 + // the scope arguments and variables are already attached to the 1.420 + // environment bindings, so pausing the active thread is unnecessary. 1.421 + // Commit the hierarchy because new items were added. 1.422 + this.view.commitHierarchy(); 1.423 + deferred.resolve(); 1.424 + break; 1.425 + default: 1.426 + let error = "Unknown Debugger.Environment type: " + aSource.type; 1.427 + Cu.reportError(error); 1.428 + deferred.reject(error); 1.429 + } 1.430 + 1.431 + return deferred.promise; 1.432 + }, 1.433 + 1.434 + /** 1.435 + * Release an actor from the controller. 1.436 + * 1.437 + * @param object aActor 1.438 + * The actor to release. 1.439 + */ 1.440 + releaseActor: function(aActor){ 1.441 + if (this._releaseActor) { 1.442 + this._releaseActor(aActor); 1.443 + } 1.444 + this._actors.delete(aActor); 1.445 + }, 1.446 + 1.447 + /** 1.448 + * Release all the actors referenced by the controller, optionally filtered. 1.449 + * 1.450 + * @param function aFilter [optional] 1.451 + * Callback to filter which actors are released. 1.452 + */ 1.453 + releaseActors: function(aFilter) { 1.454 + for (let actor of this._actors) { 1.455 + if (!aFilter || aFilter(actor)) { 1.456 + this.releaseActor(actor); 1.457 + } 1.458 + } 1.459 + }, 1.460 + 1.461 + /** 1.462 + * Helper function for setting up a single Scope with a single Variable 1.463 + * contained within it. 1.464 + * 1.465 + * This function will empty the variables view. 1.466 + * 1.467 + * @param object aOptions 1.468 + * Options for the contents of the view: 1.469 + * - objectActor: the grip of the new ObjectActor to show. 1.470 + * - rawObject: the raw object to show. 1.471 + * - label: the label for the inspected object. 1.472 + * @param object aConfiguration 1.473 + * Additional options for the controller: 1.474 + * - overrideValueEvalMacro: @see _setEvaluationMacros 1.475 + * - getterOrSetterEvalMacro: @see _setEvaluationMacros 1.476 + * - simpleValueEvalMacro: @see _setEvaluationMacros 1.477 + * @return Object 1.478 + * - variable: the created Variable. 1.479 + * - expanded: the Promise that resolves when the variable expands. 1.480 + */ 1.481 + setSingleVariable: function(aOptions, aConfiguration = {}) { 1.482 + this._setEvaluationMacros(aConfiguration); 1.483 + this.view.empty(); 1.484 + 1.485 + let scope = this.view.addScope(aOptions.label); 1.486 + scope.expanded = true; // Expand the scope by default. 1.487 + scope.locked = true; // Prevent collpasing the scope. 1.488 + 1.489 + let variable = scope.addItem("", { enumerable: true }); 1.490 + let populated; 1.491 + 1.492 + if (aOptions.objectActor) { 1.493 + populated = this.populate(variable, aOptions.objectActor); 1.494 + variable.expand(); 1.495 + } else if (aOptions.rawObject) { 1.496 + variable.populate(aOptions.rawObject, { expanded: true }); 1.497 + populated = promise.resolve(); 1.498 + } 1.499 + 1.500 + return { variable: variable, expanded: populated }; 1.501 + }, 1.502 +}; 1.503 + 1.504 + 1.505 +/** 1.506 + * Attaches a VariablesViewController to a VariablesView if it doesn't already 1.507 + * have one. 1.508 + * 1.509 + * @param VariablesView aView 1.510 + * The view to attach to. 1.511 + * @param object aOptions 1.512 + * The options to use in creating the controller. 1.513 + * @return VariablesViewController 1.514 + */ 1.515 +VariablesViewController.attach = function(aView, aOptions) { 1.516 + if (aView.controller) { 1.517 + return aView.controller; 1.518 + } 1.519 + return new VariablesViewController(aView, aOptions); 1.520 +}; 1.521 + 1.522 +/** 1.523 + * Utility functions for handling stackframes. 1.524 + */ 1.525 +let StackFrameUtils = { 1.526 + /** 1.527 + * Create a textual representation for the specified stack frame 1.528 + * to display in the stackframes container. 1.529 + * 1.530 + * @param object aFrame 1.531 + * The stack frame to label. 1.532 + */ 1.533 + getFrameTitle: function(aFrame) { 1.534 + if (aFrame.type == "call") { 1.535 + let c = aFrame.callee; 1.536 + return (c.name || c.userDisplayName || c.displayName || "(anonymous)"); 1.537 + } 1.538 + return "(" + aFrame.type + ")"; 1.539 + }, 1.540 + 1.541 + /** 1.542 + * Constructs a scope label based on its environment. 1.543 + * 1.544 + * @param object aEnv 1.545 + * The scope's environment. 1.546 + * @return string 1.547 + * The scope's label. 1.548 + */ 1.549 + getScopeLabel: function(aEnv) { 1.550 + let name = ""; 1.551 + 1.552 + // Name the outermost scope Global. 1.553 + if (!aEnv.parent) { 1.554 + name = L10N.getStr("globalScopeLabel"); 1.555 + } 1.556 + // Otherwise construct the scope name. 1.557 + else { 1.558 + name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1); 1.559 + } 1.560 + 1.561 + let label = L10N.getFormatStr("scopeLabel", name); 1.562 + switch (aEnv.type) { 1.563 + case "with": 1.564 + case "object": 1.565 + label += " [" + aEnv.object.class + "]"; 1.566 + break; 1.567 + case "function": 1.568 + let f = aEnv.function; 1.569 + label += " [" + 1.570 + (f.name || f.userDisplayName || f.displayName || "(anonymous)") + 1.571 + "]"; 1.572 + break; 1.573 + } 1.574 + return label; 1.575 + } 1.576 +}; 1.577 + 1.578 +/** 1.579 + * Localization convenience methods. 1.580 + */ 1.581 +let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);