Thu, 15 Jan 2015 15:55:04 +0100
Back out 97036ab72558 which inappropriately compared turds to third parties.
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
8 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
10 Cu.import("resource://gre/modules/Services.jsm");
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
13 Cu.import("resource:///modules/devtools/VariablesView.jsm");
14 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
17 "resource://gre/modules/devtools/Loader.jsm");
19 Object.defineProperty(this, "WebConsoleUtils", {
20 get: function() {
21 return devtools.require("devtools/toolkit/webconsole/utils").Utils;
22 },
23 configurable: true,
24 enumerable: true
25 });
27 XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
28 Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
29 );
31 XPCOMUtils.defineLazyModuleGetter(this, "console",
32 "resource://gre/modules/devtools/Console.jsm");
34 const MAX_LONG_STRING_LENGTH = 200000;
35 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
37 this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"];
40 /**
41 * Controller for a VariablesView that handles interfacing with the debugger
42 * protocol. Is able to populate scopes and variables via the protocol as well
43 * as manage actor lifespans.
44 *
45 * @param VariablesView aView
46 * The view to attach to.
47 * @param object aOptions [optional]
48 * Options for configuring the controller. Supported options:
49 * - getObjectClient: @see this._setClientGetters
50 * - getLongStringClient: @see this._setClientGetters
51 * - getEnvironmentClient: @see this._setClientGetters
52 * - releaseActor: @see this._setClientGetters
53 * - overrideValueEvalMacro: @see _setEvaluationMacros
54 * - getterOrSetterEvalMacro: @see _setEvaluationMacros
55 * - simpleValueEvalMacro: @see _setEvaluationMacros
56 */
57 function VariablesViewController(aView, aOptions = {}) {
58 this.addExpander = this.addExpander.bind(this);
60 this._setClientGetters(aOptions);
61 this._setEvaluationMacros(aOptions);
63 this._actors = new Set();
64 this.view = aView;
65 this.view.controller = this;
66 }
68 VariablesViewController.prototype = {
69 /**
70 * The default getter/setter evaluation macro.
71 */
72 _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro,
74 /**
75 * The default override value evaluation macro.
76 */
77 _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro,
79 /**
80 * The default simple value evaluation macro.
81 */
82 _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro,
84 /**
85 * Set the functions used to retrieve debugger client grips.
86 *
87 * @param object aOptions
88 * Options for getting the client grips. Supported options:
89 * - getObjectClient: callback for creating an object grip client
90 * - getLongStringClient: callback for creating a long string grip client
91 * - getEnvironmentClient: callback for creating an environment client
92 * - releaseActor: callback for releasing an actor when it's no longer needed
93 */
94 _setClientGetters: function(aOptions) {
95 if (aOptions.getObjectClient) {
96 this._getObjectClient = aOptions.getObjectClient;
97 }
98 if (aOptions.getLongStringClient) {
99 this._getLongStringClient = aOptions.getLongStringClient;
100 }
101 if (aOptions.getEnvironmentClient) {
102 this._getEnvironmentClient = aOptions.getEnvironmentClient;
103 }
104 if (aOptions.releaseActor) {
105 this._releaseActor = aOptions.releaseActor;
106 }
107 },
109 /**
110 * Sets the functions used when evaluating strings in the variables view.
111 *
112 * @param object aOptions
113 * Options for configuring the macros. Supported options:
114 * - overrideValueEvalMacro: callback for creating an overriding eval macro
115 * - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro
116 * - simpleValueEvalMacro: callback for creating a simple value eval macro
117 */
118 _setEvaluationMacros: function(aOptions) {
119 if (aOptions.overrideValueEvalMacro) {
120 this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
121 }
122 if (aOptions.getterOrSetterEvalMacro) {
123 this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
124 }
125 if (aOptions.simpleValueEvalMacro) {
126 this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
127 }
128 },
130 /**
131 * Populate a long string into a target using a grip.
132 *
133 * @param Variable aTarget
134 * The target Variable/Property to put the retrieved string into.
135 * @param LongStringActor aGrip
136 * The long string grip that use to retrieve the full string.
137 * @return Promise
138 * The promise that will be resolved when the string is retrieved.
139 */
140 _populateFromLongString: function(aTarget, aGrip){
141 let deferred = promise.defer();
143 let from = aGrip.initial.length;
144 let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH);
146 this._getLongStringClient(aGrip).substring(from, to, aResponse => {
147 // Stop tracking the actor because it's no longer needed.
148 this.releaseActor(aGrip);
150 // Replace the preview with the full string and make it non-expandable.
151 aTarget.onexpand = null;
152 aTarget.setGrip(aGrip.initial + aResponse.substring);
153 aTarget.hideArrow();
155 deferred.resolve();
156 });
158 return deferred.promise;
159 },
161 /**
162 * Adds properties to a Scope, Variable, or Property in the view. Triggered
163 * when a scope is expanded or certain variables are hovered.
164 *
165 * @param Scope aTarget
166 * The Scope where the properties will be placed into.
167 * @param object aGrip
168 * The grip to use to populate the target.
169 */
170 _populateFromObject: function(aTarget, aGrip) {
171 let deferred = promise.defer();
173 let objectClient = this._getObjectClient(aGrip);
174 objectClient.getPrototypeAndProperties(aResponse => {
175 let { ownProperties, prototype } = aResponse;
176 // 'safeGetterValues' is new and isn't necessary defined on old actors.
177 let safeGetterValues = aResponse.safeGetterValues || {};
178 let sortable = VariablesView.isSortable(aGrip.class);
180 // Merge the safe getter values into one object such that we can use it
181 // in VariablesView.
182 for (let name of Object.keys(safeGetterValues)) {
183 if (name in ownProperties) {
184 let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
185 ownProperties[name].getterValue = getterValue;
186 ownProperties[name].getterPrototypeLevel = getterPrototypeLevel;
187 } else {
188 ownProperties[name] = safeGetterValues[name];
189 }
190 }
192 // Add all the variable properties.
193 if (ownProperties) {
194 aTarget.addItems(ownProperties, {
195 // Not all variables need to force sorted properties.
196 sorted: sortable,
197 // Expansion handlers must be set after the properties are added.
198 callback: this.addExpander
199 });
200 }
202 // Add the variable's __proto__.
203 if (prototype && prototype.type != "null") {
204 let proto = aTarget.addItem("__proto__", { value: prototype });
205 // Expansion handlers must be set after the properties are added.
206 this.addExpander(proto, prototype);
207 }
209 // If the object is a function we need to fetch its scope chain
210 // to show them as closures for the respective function.
211 if (aGrip.class == "Function") {
212 objectClient.getScope(aResponse => {
213 if (aResponse.error) {
214 // This function is bound to a built-in object or it's not present
215 // in the current scope chain. Not necessarily an actual error,
216 // it just means that there's no closure for the function.
217 console.warn(aResponse.error + ": " + aResponse.message);
218 return void deferred.resolve();
219 }
220 this._populateWithClosure(aTarget, aResponse.scope).then(deferred.resolve);
221 });
222 } else {
223 deferred.resolve();
224 }
225 });
227 return deferred.promise;
228 },
230 /**
231 * Adds the scope chain elements (closures) of a function variable.
232 *
233 * @param Variable aTarget
234 * The variable where the properties will be placed into.
235 * @param Scope aScope
236 * The lexical environment form as specified in the protocol.
237 */
238 _populateWithClosure: function(aTarget, aScope) {
239 let objectScopes = [];
240 let environment = aScope;
241 let funcScope = aTarget.addItem("<Closure>");
242 funcScope.target.setAttribute("scope", "");
243 funcScope.showArrow();
245 do {
246 // Create a scope to contain all the inspected variables.
247 let label = StackFrameUtils.getScopeLabel(environment);
249 // Block scopes may have the same label, so make addItem allow duplicates.
250 let closure = funcScope.addItem(label, undefined, true);
251 closure.target.setAttribute("scope", "");
252 closure.showArrow();
254 // Add nodes for every argument and every other variable in scope.
255 if (environment.bindings) {
256 this._populateWithEnvironmentBindings(closure, environment.bindings);
257 } else {
258 let deferred = promise.defer();
259 objectScopes.push(deferred.promise);
260 this._getEnvironmentClient(environment).getBindings(response => {
261 this._populateWithEnvironmentBindings(closure, response.bindings);
262 deferred.resolve();
263 });
264 }
265 } while ((environment = environment.parent));
267 return promise.all(objectScopes).then(() => {
268 // Signal that scopes have been fetched.
269 this.view.emit("fetched", "scopes", funcScope);
270 });
271 },
273 /**
274 * Adds nodes for every specified binding to the closure node.
275 *
276 * @param Variable aTarget
277 * The variable where the bindings will be placed into.
278 * @param object aBindings
279 * The bindings form as specified in the protocol.
280 */
281 _populateWithEnvironmentBindings: function(aTarget, aBindings) {
282 // Add nodes for every argument in the scope.
283 aTarget.addItems(aBindings.arguments.reduce((accumulator, arg) => {
284 let name = Object.getOwnPropertyNames(arg)[0];
285 let descriptor = arg[name];
286 accumulator[name] = descriptor;
287 return accumulator;
288 }, {}), {
289 // Arguments aren't sorted.
290 sorted: false,
291 // Expansion handlers must be set after the properties are added.
292 callback: this.addExpander
293 });
295 // Add nodes for every other variable in the scope.
296 aTarget.addItems(aBindings.variables, {
297 // Not all variables need to force sorted properties.
298 sorted: VARIABLES_SORTING_ENABLED,
299 // Expansion handlers must be set after the properties are added.
300 callback: this.addExpander
301 });
302 },
304 /**
305 * Adds an 'onexpand' callback for a variable, lazily handling
306 * the addition of new properties.
307 *
308 * @param Variable aTarget
309 * The variable where the properties will be placed into.
310 * @param any aSource
311 * The source to use to populate the target.
312 */
313 addExpander: function(aTarget, aSource) {
314 // Attach evaluation macros as necessary.
315 if (aTarget.getter || aTarget.setter) {
316 aTarget.evaluationMacro = this._overrideValueEvalMacro;
317 let getter = aTarget.get("get");
318 if (getter) {
319 getter.evaluationMacro = this._getterOrSetterEvalMacro;
320 }
321 let setter = aTarget.get("set");
322 if (setter) {
323 setter.evaluationMacro = this._getterOrSetterEvalMacro;
324 }
325 } else {
326 aTarget.evaluationMacro = this._simpleValueEvalMacro;
327 }
329 // If the source is primitive then an expander is not needed.
330 if (VariablesView.isPrimitive({ value: aSource })) {
331 return;
332 }
334 // If the source is a long string then show the arrow.
335 if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
336 aTarget.showArrow();
337 }
339 // Make sure that properties are always available on expansion.
340 aTarget.onexpand = () => this.populate(aTarget, aSource);
342 // Some variables are likely to contain a very large number of properties.
343 // It's a good idea to be prepared in case of an expansion.
344 if (aTarget.shouldPrefetch) {
345 aTarget.addEventListener("mouseover", aTarget.onexpand, false);
346 }
348 // Register all the actors that this controller now depends on.
349 for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
350 if (WebConsoleUtils.isActorGrip(grip)) {
351 this._actors.add(grip.actor);
352 }
353 }
354 },
356 /**
357 * Adds properties to a Scope, Variable, or Property in the view. Triggered
358 * when a scope is expanded or certain variables are hovered.
359 *
360 * This does not expand the target, it only populates it.
361 *
362 * @param Scope aTarget
363 * The Scope to be expanded.
364 * @param object aSource
365 * The source to use to populate the target.
366 * @return Promise
367 * The promise that is resolved once the target has been expanded.
368 */
369 populate: function(aTarget, aSource) {
370 // Fetch the variables only once.
371 if (aTarget._fetched) {
372 return aTarget._fetched;
373 }
374 // Make sure the source grip is available.
375 if (!aSource) {
376 return promise.reject(new Error("No actor grip was given for the variable."));
377 }
379 let deferred = promise.defer();
380 aTarget._fetched = deferred.promise;
382 // If the target is a Variable or Property then we're fetching properties.
383 if (VariablesView.isVariable(aTarget)) {
384 this._populateFromObject(aTarget, aSource).then(() => {
385 // Signal that properties have been fetched.
386 this.view.emit("fetched", "properties", aTarget);
387 // Commit the hierarchy because new items were added.
388 this.view.commitHierarchy();
389 deferred.resolve();
390 });
391 return deferred.promise;
392 }
394 switch (aSource.type) {
395 case "longString":
396 this._populateFromLongString(aTarget, aSource).then(() => {
397 // Signal that a long string has been fetched.
398 this.view.emit("fetched", "longString", aTarget);
399 deferred.resolve();
400 });
401 break;
402 case "with":
403 case "object":
404 this._populateFromObject(aTarget, aSource.object).then(() => {
405 // Signal that variables have been fetched.
406 this.view.emit("fetched", "variables", aTarget);
407 // Commit the hierarchy because new items were added.
408 this.view.commitHierarchy();
409 deferred.resolve();
410 });
411 break;
412 case "block":
413 case "function":
414 this._populateWithEnvironmentBindings(aTarget, aSource.bindings);
415 // No need to signal that variables have been fetched, since
416 // the scope arguments and variables are already attached to the
417 // environment bindings, so pausing the active thread is unnecessary.
418 // Commit the hierarchy because new items were added.
419 this.view.commitHierarchy();
420 deferred.resolve();
421 break;
422 default:
423 let error = "Unknown Debugger.Environment type: " + aSource.type;
424 Cu.reportError(error);
425 deferred.reject(error);
426 }
428 return deferred.promise;
429 },
431 /**
432 * Release an actor from the controller.
433 *
434 * @param object aActor
435 * The actor to release.
436 */
437 releaseActor: function(aActor){
438 if (this._releaseActor) {
439 this._releaseActor(aActor);
440 }
441 this._actors.delete(aActor);
442 },
444 /**
445 * Release all the actors referenced by the controller, optionally filtered.
446 *
447 * @param function aFilter [optional]
448 * Callback to filter which actors are released.
449 */
450 releaseActors: function(aFilter) {
451 for (let actor of this._actors) {
452 if (!aFilter || aFilter(actor)) {
453 this.releaseActor(actor);
454 }
455 }
456 },
458 /**
459 * Helper function for setting up a single Scope with a single Variable
460 * contained within it.
461 *
462 * This function will empty the variables view.
463 *
464 * @param object aOptions
465 * Options for the contents of the view:
466 * - objectActor: the grip of the new ObjectActor to show.
467 * - rawObject: the raw object to show.
468 * - label: the label for the inspected object.
469 * @param object aConfiguration
470 * Additional options for the controller:
471 * - overrideValueEvalMacro: @see _setEvaluationMacros
472 * - getterOrSetterEvalMacro: @see _setEvaluationMacros
473 * - simpleValueEvalMacro: @see _setEvaluationMacros
474 * @return Object
475 * - variable: the created Variable.
476 * - expanded: the Promise that resolves when the variable expands.
477 */
478 setSingleVariable: function(aOptions, aConfiguration = {}) {
479 this._setEvaluationMacros(aConfiguration);
480 this.view.empty();
482 let scope = this.view.addScope(aOptions.label);
483 scope.expanded = true; // Expand the scope by default.
484 scope.locked = true; // Prevent collpasing the scope.
486 let variable = scope.addItem("", { enumerable: true });
487 let populated;
489 if (aOptions.objectActor) {
490 populated = this.populate(variable, aOptions.objectActor);
491 variable.expand();
492 } else if (aOptions.rawObject) {
493 variable.populate(aOptions.rawObject, { expanded: true });
494 populated = promise.resolve();
495 }
497 return { variable: variable, expanded: populated };
498 },
499 };
502 /**
503 * Attaches a VariablesViewController to a VariablesView if it doesn't already
504 * have one.
505 *
506 * @param VariablesView aView
507 * The view to attach to.
508 * @param object aOptions
509 * The options to use in creating the controller.
510 * @return VariablesViewController
511 */
512 VariablesViewController.attach = function(aView, aOptions) {
513 if (aView.controller) {
514 return aView.controller;
515 }
516 return new VariablesViewController(aView, aOptions);
517 };
519 /**
520 * Utility functions for handling stackframes.
521 */
522 let StackFrameUtils = {
523 /**
524 * Create a textual representation for the specified stack frame
525 * to display in the stackframes container.
526 *
527 * @param object aFrame
528 * The stack frame to label.
529 */
530 getFrameTitle: function(aFrame) {
531 if (aFrame.type == "call") {
532 let c = aFrame.callee;
533 return (c.name || c.userDisplayName || c.displayName || "(anonymous)");
534 }
535 return "(" + aFrame.type + ")";
536 },
538 /**
539 * Constructs a scope label based on its environment.
540 *
541 * @param object aEnv
542 * The scope's environment.
543 * @return string
544 * The scope's label.
545 */
546 getScopeLabel: function(aEnv) {
547 let name = "";
549 // Name the outermost scope Global.
550 if (!aEnv.parent) {
551 name = L10N.getStr("globalScopeLabel");
552 }
553 // Otherwise construct the scope name.
554 else {
555 name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1);
556 }
558 let label = L10N.getFormatStr("scopeLabel", name);
559 switch (aEnv.type) {
560 case "with":
561 case "object":
562 label += " [" + aEnv.object.class + "]";
563 break;
564 case "function":
565 let f = aEnv.function;
566 label += " [" +
567 (f.name || f.userDisplayName || f.displayName || "(anonymous)") +
568 "]";
569 break;
570 }
571 return label;
572 }
573 };
575 /**
576 * Localization convenience methods.
577 */
578 let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);