|
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"; |
|
7 |
|
8 const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
9 |
|
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"); |
|
15 |
|
16 XPCOMUtils.defineLazyModuleGetter(this, "devtools", |
|
17 "resource://gre/modules/devtools/Loader.jsm"); |
|
18 |
|
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 }); |
|
26 |
|
27 XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () => |
|
28 Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled") |
|
29 ); |
|
30 |
|
31 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
32 "resource://gre/modules/devtools/Console.jsm"); |
|
33 |
|
34 const MAX_LONG_STRING_LENGTH = 200000; |
|
35 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; |
|
36 |
|
37 this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"]; |
|
38 |
|
39 |
|
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); |
|
59 |
|
60 this._setClientGetters(aOptions); |
|
61 this._setEvaluationMacros(aOptions); |
|
62 |
|
63 this._actors = new Set(); |
|
64 this.view = aView; |
|
65 this.view.controller = this; |
|
66 } |
|
67 |
|
68 VariablesViewController.prototype = { |
|
69 /** |
|
70 * The default getter/setter evaluation macro. |
|
71 */ |
|
72 _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro, |
|
73 |
|
74 /** |
|
75 * The default override value evaluation macro. |
|
76 */ |
|
77 _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro, |
|
78 |
|
79 /** |
|
80 * The default simple value evaluation macro. |
|
81 */ |
|
82 _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro, |
|
83 |
|
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 }, |
|
108 |
|
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 }, |
|
129 |
|
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(); |
|
142 |
|
143 let from = aGrip.initial.length; |
|
144 let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH); |
|
145 |
|
146 this._getLongStringClient(aGrip).substring(from, to, aResponse => { |
|
147 // Stop tracking the actor because it's no longer needed. |
|
148 this.releaseActor(aGrip); |
|
149 |
|
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(); |
|
154 |
|
155 deferred.resolve(); |
|
156 }); |
|
157 |
|
158 return deferred.promise; |
|
159 }, |
|
160 |
|
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(); |
|
172 |
|
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); |
|
179 |
|
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 } |
|
191 |
|
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 } |
|
201 |
|
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 } |
|
208 |
|
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 }); |
|
226 |
|
227 return deferred.promise; |
|
228 }, |
|
229 |
|
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(); |
|
244 |
|
245 do { |
|
246 // Create a scope to contain all the inspected variables. |
|
247 let label = StackFrameUtils.getScopeLabel(environment); |
|
248 |
|
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(); |
|
253 |
|
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)); |
|
266 |
|
267 return promise.all(objectScopes).then(() => { |
|
268 // Signal that scopes have been fetched. |
|
269 this.view.emit("fetched", "scopes", funcScope); |
|
270 }); |
|
271 }, |
|
272 |
|
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 }); |
|
294 |
|
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 }, |
|
303 |
|
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 } |
|
328 |
|
329 // If the source is primitive then an expander is not needed. |
|
330 if (VariablesView.isPrimitive({ value: aSource })) { |
|
331 return; |
|
332 } |
|
333 |
|
334 // If the source is a long string then show the arrow. |
|
335 if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") { |
|
336 aTarget.showArrow(); |
|
337 } |
|
338 |
|
339 // Make sure that properties are always available on expansion. |
|
340 aTarget.onexpand = () => this.populate(aTarget, aSource); |
|
341 |
|
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 } |
|
347 |
|
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 }, |
|
355 |
|
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 } |
|
378 |
|
379 let deferred = promise.defer(); |
|
380 aTarget._fetched = deferred.promise; |
|
381 |
|
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 } |
|
393 |
|
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 } |
|
427 |
|
428 return deferred.promise; |
|
429 }, |
|
430 |
|
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 }, |
|
443 |
|
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 }, |
|
457 |
|
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(); |
|
481 |
|
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. |
|
485 |
|
486 let variable = scope.addItem("", { enumerable: true }); |
|
487 let populated; |
|
488 |
|
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 } |
|
496 |
|
497 return { variable: variable, expanded: populated }; |
|
498 }, |
|
499 }; |
|
500 |
|
501 |
|
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 }; |
|
518 |
|
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 }, |
|
537 |
|
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 = ""; |
|
548 |
|
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 } |
|
557 |
|
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 }; |
|
574 |
|
575 /** |
|
576 * Localization convenience methods. |
|
577 */ |
|
578 let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI); |