browser/devtools/shared/widgets/VariablesViewController.jsm

Thu, 15 Jan 2015 15:55:04 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:55:04 +0100
branch
TOR_BUG_9701
changeset 9
a63d609f5ebe
permissions
-rw-r--r--

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);

mercurial