Bug 828680 - Variables view needs a controller of its own, r=vporof, msucan

This commit is contained in:
Brandon Benvie 2013-06-14 10:42:52 -07:00
parent d886ea39e6
commit 97d0060a1b
12 changed files with 677 additions and 659 deletions

View File

@ -24,6 +24,7 @@ Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/VariablesView.jsm");
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Parser",
@ -73,6 +74,24 @@ let DebuggerController = {
DebuggerView.initialize(() => {
DebuggerView._isInitialized = true;
VariablesViewController.attach(DebuggerView.Variables, {
getGripClient: aObject => {
return this.activeThread.pauseGrip(aObject);
}
});
// Relay events from the VariablesView.
DebuggerView.Variables.on("fetched", (aEvent, aType) => {
switch (aType) {
case "variables":
window.dispatchEvent(document, "Debugger:FetchedVariables");
break;
case "properties":
window.dispatchEvent(document, "Debugger:FetchedProperties");
break;
}
});
// Chrome debugging needs to initiate the connection by itself.
if (window._isChromeDebugger) {
this.connect().then(deferred.resolve);
@ -403,6 +422,7 @@ ThreadState.prototype = {
}
};
/**
* Keeps the stack frame list up-to-date, using the thread client's
* stack frame cache.
@ -413,9 +433,6 @@ function StackFrames() {
this._onFrames = this._onFrames.bind(this);
this._onFramesCleared = this._onFramesCleared.bind(this);
this._afterFramesCleared = this._afterFramesCleared.bind(this);
this._fetchScopeVariables = this._fetchScopeVariables.bind(this);
this._fetchVarProperties = this._fetchVarProperties.bind(this);
this._addVarExpander = this._addVarExpander.bind(this);
this.evaluate = this.evaluate.bind(this);
}
@ -588,7 +605,12 @@ StackFrames.prototype = {
DebuggerView.StackFrames.empty();
for (let frame of this.activeThread.cachedFrames) {
this._addFrame(frame);
let depth = frame.depth;
let { url, line } = frame.where;
let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
let frameTitle = StackFrameUtils.getFrameTitle(frame);
DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
}
if (this.currentFrame == null) {
DebuggerView.StackFrames.selectedDepth = 0;
@ -661,6 +683,7 @@ StackFrames.prototype = {
// Clear existing scopes and create each one dynamically.
DebuggerView.Variables.empty();
// If watch expressions evaluation results are available, create a scope
// to contain all the values.
if (this.syncedWatchExpressions && watchExpressionsEvaluation) {
@ -684,18 +707,20 @@ StackFrames.prototype = {
// Create a scope to contain all the inspected variables.
let label = StackFrameUtils.getScopeLabel(environment);
let scope = DebuggerView.Variables.addScope(label);
let innermost = environment == frame.environment;
// Handle additions to the innermost scope.
if (environment == frame.environment) {
// Handle special additions to the innermost scope.
if (innermost) {
this._insertScopeFrameReferences(scope, frame);
this._addScopeExpander(scope, environment);
// Always expand the innermost scope by default.
scope.expand();
}
// Lazily add nodes for every other environment scope.
else {
this._addScopeExpander(scope, environment);
this.autoScopeExpand && scope.expand();
DebuggerView.Variables.controller.addExpander(scope, environment);
// The innermost scope is always automatically expanded, because it
// contains the variables in the current stack frame which are likely to
// be inspected.
if (innermost || this.autoScopeExpand) {
scope.expand();
}
} while ((environment = environment.parent));
@ -704,49 +729,6 @@ StackFrames.prototype = {
DebuggerView.Variables.commitHierarchy();
},
/**
* Adds an 'onexpand' callback for a scope, lazily handling
* the addition of new variables.
*
* @param Scope aScope
* The scope where the variables will be placed into.
* @param object aEnv
* The scope's environment.
*/
_addScopeExpander: function(aScope, aEnv) {
aScope._sourceEnvironment = aEnv;
// It's a good idea to be prepared in case of an expansion.
aScope.addEventListener("mouseover", this._fetchScopeVariables, false);
// Make sure that variables are always available on expansion.
aScope.onexpand = this._fetchScopeVariables;
},
/**
* Adds an 'onexpand' callback for a variable, lazily handling
* the addition of new properties.
*
* @param Variable aVar
* The variable where the properties will be placed into.
* @param any aGrip
* The grip of the variable.
*/
_addVarExpander: function(aVar, aGrip) {
// No need for expansion for primitive values.
if (VariablesView.isPrimitive({ value: aGrip })) {
return;
}
aVar._sourceGrip = aGrip;
// Some variables are likely to contain a very large number of properties.
// It's a good idea to be prepared in case of an expansion.
if (aVar.name == "window" || aVar.name == "this") {
aVar.addEventListener("mouseover", this._fetchVarProperties, false);
}
// Make sure that properties are always available on expansion.
aVar.onexpand = this._fetchVarProperties;
},
/**
* Adds the watch expressions evaluation results to a scope in the view.
*
@ -770,8 +752,8 @@ StackFrames.prototype = {
for (let i = 0; i < totalExpressions; i++) {
let name = DebuggerView.WatchExpressions.getExpression(i);
let expVal = ownProperties[i].value;
let expRef = aScope.addVar(name, ownProperties[i]);
this._addVarExpander(expRef, expVal);
let expRef = aScope.addItem(name, ownProperties[i]);
DebuggerView.Variables.controller.addExpander(expRef, expVal);
// Revert some of the custom watch expressions scope presentation flags.
expRef.switch = null;
@ -786,51 +768,6 @@ StackFrames.prototype = {
});
},
/**
* Adds variables to a scope in the view. Triggered when a scope is
* expanded or is hovered. It does not expand the scope.
*
* @param Scope aScope
* The scope where the variables will be placed into.
*/
_fetchScopeVariables: function(aScope) {
// Fetch the variables only once.
if (aScope._fetched) {
return;
}
aScope._fetched = true;
let env = aScope._sourceEnvironment;
switch (env.type) {
case "with":
case "object":
// Add nodes for every variable in scope.
this.activeThread.pauseGrip(env.object).getPrototypeAndProperties((aResponse) => {
let { ownProperties, safeGetterValues } = aResponse;
this._mergeSafeGetterValues(ownProperties, safeGetterValues);
this._insertScopeVariables(ownProperties, aScope);
// Signal that variables have been fetched.
window.dispatchEvent(document, "Debugger:FetchedVariables");
DebuggerView.Variables.commitHierarchy();
});
break;
case "block":
case "function":
// Add nodes for every argument and every other variable in scope.
this._insertScopeArguments(env.bindings.arguments, aScope);
this._insertScopeVariables(env.bindings.variables, aScope);
// No need to signal that variables have been fetched, since
// the scope arguments and variables are already attached to the
// environment bindings, so pausing the active thread is unnecessary.
break;
default:
Cu.reportError("Unknown Debugger.Environment type: " + env.type);
break;
}
},
/**
* Add nodes for special frame references in the innermost scope.
*
@ -842,154 +779,21 @@ StackFrames.prototype = {
_insertScopeFrameReferences: function(aScope, aFrame) {
// Add any thrown exception.
if (this.currentException) {
let excRef = aScope.addVar("<exception>", { value: this.currentException });
this._addVarExpander(excRef, this.currentException);
let excRef = aScope.addItem("<exception>", { value: this.currentException });
DebuggerView.Variables.controller.addExpander(excRef, this.currentException);
}
// Add any returned value.
if (this.currentReturnedValue) {
let retRef = aScope.addVar("<return>", { value: this.currentReturnedValue });
this._addVarExpander(retRef, this.currentReturnedValue);
let retRef = aScope.addItem("<return>", { value: this.currentReturnedValue });
DebuggerView.Variables.controller.addExpander(retRef, this.currentReturnedValue);
}
// Add "this".
if (aFrame.this) {
let thisRef = aScope.addVar("this", { value: aFrame.this });
this._addVarExpander(thisRef, aFrame.this);
let thisRef = aScope.addItem("this", { value: aFrame.this });
DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
}
},
/**
* Add nodes for every argument in scope.
*
* @param object aArguments
* The map of names to arguments, as specified in the protocol.
* @param Scope aScope
* The scope where the nodes will be placed into.
*/
_insertScopeArguments: function(aArguments, aScope) {
if (!aArguments) {
return;
}
for (let argument of aArguments) {
let name = Object.getOwnPropertyNames(argument)[0];
let argRef = aScope.addVar(name, argument[name]);
let argVal = argument[name].value;
this._addVarExpander(argRef, argVal);
}
},
/**
* Add nodes for every variable in scope.
*
* @param object aVariables
* The map of names to variables, as specified in the protocol.
* @param Scope aScope
* The scope where the nodes will be placed into.
*/
_insertScopeVariables: function(aVariables, aScope) {
if (!aVariables) {
return;
}
let variableNames = Object.keys(aVariables);
// Sort all of the variables before adding them, if preferred.
if (Prefs.variablesSortingEnabled) {
variableNames.sort();
}
// Add the variables to the specified scope.
for (let name of variableNames) {
let varRef = aScope.addVar(name, aVariables[name]);
let varVal = aVariables[name].value;
this._addVarExpander(varRef, varVal);
}
},
/**
* Adds properties to a variable in the view. Triggered when a variable is
* expanded or certain variables are hovered. It does not expand the variable.
*
* @param Variable aVar
* The variable where the properties will be placed into.
*/
_fetchVarProperties: function(aVar) {
// Fetch the properties only once.
if (aVar._fetched) {
return;
}
aVar._fetched = true;
let grip = aVar._sourceGrip;
this.activeThread.pauseGrip(grip).getPrototypeAndProperties((aResponse) => {
let { ownProperties, prototype, safeGetterValues } = aResponse;
let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
this._mergeSafeGetterValues(ownProperties, safeGetterValues);
// Add all the variable properties.
if (ownProperties) {
aVar.addProperties(ownProperties, {
// Not all variables need to force sorted properties.
sorted: sortable,
// Expansion handlers must be set after the properties are added.
callback: this._addVarExpander
});
}
// Add the variable's __proto__.
if (prototype && prototype.type != "null") {
aVar.addProperty("__proto__", { value: prototype });
// Expansion handlers must be set after the properties are added.
this._addVarExpander(aVar.get("__proto__"), prototype);
}
// Mark the variable as having retrieved all its properties.
aVar._retrieved = true;
// Signal that properties have been fetched.
window.dispatchEvent(document, "Debugger:FetchedProperties");
DebuggerView.Variables.commitHierarchy();
});
},
/**
* Merge the safe getter values descriptors into the "own properties" object
* that comes from a "prototypeAndProperties" response packet. This is needed
* for Variables View.
*
* @private
* @param object aOwnProperties
* The |ownProperties| object that will get the new safe getter values.
* @param object aSafeGetterValues
* The |safeGetterValues| object.
*/
_mergeSafeGetterValues: function(aOwnProperties, aSafeGetterValues) {
// Merge the safe getter values into one object such that we can use it
// in VariablesView.
for (let name of Object.keys(aSafeGetterValues)) {
if (name in aOwnProperties) {
aOwnProperties[name].getterValue = aSafeGetterValues[name].getterValue;
aOwnProperties[name].getterPrototypeLevel =
aSafeGetterValues[name].getterPrototypeLevel;
} else {
aOwnProperties[name] = aSafeGetterValues[name];
}
}
},
/**
* Adds the specified stack frame to the list.
*
* @param object aFrame
* The new frame to add.
*/
_addFrame: function(aFrame) {
let depth = aFrame.depth;
let { url, line } = aFrame.where;
let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
let frameTitle = StackFrameUtils.getFrameTitle(aFrame);
DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
},
/**
* Loads more stack frames from the debugger server cache.
*/

View File

@ -24,9 +24,9 @@ function testNonEnumProperties() {
Services.tm.currentThread.dispatch({ run: function() {
let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
let testVar = testScope.addVar("foo");
let testVar = testScope.addItem("foo");
testVar.addProperties({
testVar.addItems({
foo: {
value: "bar",
enumerable: true

View File

@ -24,8 +24,8 @@ function testSimpleCall() {
Services.tm.currentThread.dispatch({ run: function() {
let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
let testVar = testScope.addVar("something");
let duplVar = testScope.addVar("something");
let testVar = testScope.addItem("something");
let duplVar = testScope.addItem("something");
info("Scope id: " + testScope.target.id);
info("Scope name: " + testScope.target.name);
@ -61,8 +61,8 @@ function testSimpleCall() {
"Any new variable should have a details container with no child nodes.");
let properties = testVar.addProperties({ "child": { "value": { "type": "object",
"class": "Object" } } });
let properties = testVar.addItems({ "child": { "value": { "type": "object",
"class": "Object" } } });
ok(!testVar.expanded,

View File

@ -24,9 +24,9 @@ function testSimpleCall() {
Services.tm.currentThread.dispatch({ run: function() {
let testScope = gDebugger.DebuggerView.Variables.addScope("test");
let testVar = testScope.addVar("something");
let testVar = testScope.addItem("something");
let properties = testVar.addProperties({
let properties = testVar.addItems({
"child": {
"value": {
"type": "object",
@ -43,7 +43,7 @@ function testSimpleCall() {
"The added detail property should be accessible from the variable.");
let properties2 = testVar.get("child").addProperties({
let properties2 = testVar.get("child").addItems({
"grandchild": {
"value": {
"type": "object",

View File

@ -24,7 +24,7 @@ function testSimpleCall() {
Services.tm.currentThread.dispatch({ run: function() {
let testScope = gDebugger.DebuggerView.Variables.addScope("test");
let testVar = testScope.addVar("something");
let testVar = testScope.addItem("something");
testVar.setGrip(1.618);
@ -44,32 +44,32 @@ function testSimpleCall() {
"The information for the variable wasn't set correctly.");
testVar.addProperties({ "helloWorld": { "value": "hello world", "enumerable": true } });
testVar.addItems({ "helloWorld": { "value": "hello world", "enumerable": true } });
is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
"A new detail node should have been added in the variable tree.");
testVar.addProperties({ "helloWorld": { "value": "hello jupiter", "enumerable": true } });
testVar.addItems({ "helloWorld": { "value": "hello jupiter", "enumerable": true } });
is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
"Shouldn't be able to duplicate nodes added in the variable tree.");
testVar.addProperties({ "someProp0": { "value": "random string", "enumerable": true },
"someProp1": { "value": "another string", "enumerable": true } });
testVar.addItems({ "someProp0": { "value": "random string", "enumerable": true },
"someProp1": { "value": "another string", "enumerable": true } });
is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 3,
"Two new detail nodes should have been added in the variable tree.");
testVar.addProperties({ "someProp2": { "value": { "type": "null" }, "enumerable": true },
"someProp3": { "value": { "type": "undefined" }, "enumerable": true },
"someProp4": {
"value": { "type": "object", "class": "Object" },
"enumerable": true
}
});
testVar.addItems({ "someProp2": { "value": { "type": "null" }, "enumerable": true },
"someProp3": { "value": { "type": "undefined" }, "enumerable": true },
"someProp4": {
"value": { "type": "object", "class": "Object" },
"enumerable": true
}
});
is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 6,
"Three new detail nodes should have been added in the variable tree.");

View File

@ -26,14 +26,14 @@ function testSimpleCall() {
let globalScope = gDebugger.DebuggerView.Variables.addScope("Test-Global");
let localScope = gDebugger.DebuggerView.Variables.addScope("Test-Local");
let windowVar = globalScope.addVar("window");
let documentVar = globalScope.addVar("document");
let localVar0 = localScope.addVar("localVariable");
let localVar1 = localScope.addVar("localVar1");
let localVar2 = localScope.addVar("localVar2");
let localVar3 = localScope.addVar("localVar3");
let localVar4 = localScope.addVar("localVar4");
let localVar5 = localScope.addVar("localVar5");
let windowVar = globalScope.addItem("window");
let documentVar = globalScope.addItem("document");
let localVar0 = localScope.addItem("localVariable");
let localVar1 = localScope.addItem("localVar1");
let localVar2 = localScope.addItem("localVar2");
let localVar3 = localScope.addItem("localVar3");
let localVar4 = localScope.addItem("localVar4");
let localVar5 = localScope.addItem("localVar5");
localVar0.setGrip(42);
localVar1.setGrip(true);
@ -43,36 +43,36 @@ function testSimpleCall() {
localVar4.setGrip({ "type": "null" });
localVar5.setGrip({ "type": "object", "class": "Object" });
localVar5.addProperties({ "someProp0": { "value": 42, "enumerable": true },
"someProp1": { "value": true , "enumerable": true},
"someProp2": { "value": "nasu", "enumerable": true},
"someProp3": { "value": { "type": "undefined" }, "enumerable": true},
"someProp4": { "value": { "type": "null" }, "enumerable": true },
"someProp5": {
"value": { "type": "object", "class": "Object" },
"enumerable": true
}
});
localVar5.addItems({ "someProp0": { "value": 42, "enumerable": true },
"someProp1": { "value": true , "enumerable": true},
"someProp2": { "value": "nasu", "enumerable": true},
"someProp3": { "value": { "type": "undefined" }, "enumerable": true},
"someProp4": { "value": { "type": "null" }, "enumerable": true },
"someProp5": {
"value": { "type": "object", "class": "Object" },
"enumerable": true
}
});
localVar5.get("someProp5").addProperties({ "someProp0": { "value": 42, "enumerable": true },
"someProp1": { "value": true, "enumerable": true },
"someProp2": { "value": "nasu", "enumerable": true },
"someProp3": { "value": { "type": "undefined" }, "enumerable": true },
"someProp4": { "value": { "type": "null" }, "enumerable": true },
"someAccessor": { "get": { "type": "object", "class": "Function" },
"set": { "type": "undefined" },
"enumerable": true } });
localVar5.get("someProp5").addItems({ "someProp0": { "value": 42, "enumerable": true },
"someProp1": { "value": true, "enumerable": true },
"someProp2": { "value": "nasu", "enumerable": true },
"someProp3": { "value": { "type": "undefined" }, "enumerable": true },
"someProp4": { "value": { "type": "null" }, "enumerable": true },
"someAccessor": { "get": { "type": "object", "class": "Function" },
"set": { "type": "undefined" }, "enumerable": true }
});
windowVar.setGrip({ "type": "object", "class": "Window" });
windowVar.addProperties({ "helloWorld": { "value": "hello world" } });
windowVar.addItems({ "helloWorld": { "value": "hello world" } });
documentVar.setGrip({ "type": "object", "class": "HTMLDocument" });
documentVar.addProperties({ "onload": { "value": { "type": "null" } },
"onunload": { "value": { "type": "null" } },
"onfocus": { "value": { "type": "null" } },
"onblur": { "value": { "type": "null" } },
"onclick": { "value": { "type": "null" } },
"onkeypress": { "value": { "type": "null" } } });
documentVar.addItems({ "onload": { "value": { "type": "null" } },
"onunload": { "value": { "type": "null" } },
"onfocus": { "value": { "type": "null" } },
"onblur": { "value": { "type": "null" } },
"onclick": { "value": { "type": "null" } },
"onkeypress": { "value": { "type": "null" } } });
ok(windowVar, "The windowVar hasn't been created correctly.");

View File

@ -75,11 +75,11 @@ function testVariablesView()
testIntegrity(arr, obj);
let fooScope = gVariablesView.addScope("foo");
let anonymousVar = fooScope.addVar();
let anonymousVar = fooScope.addItem();
let anonymousScope = gVariablesView.addScope();
let barVar = anonymousScope.addVar("bar");
let bazProperty = barVar.addProperty("baz");
let barVar = anonymousScope.addItem("bar");
let bazProperty = barVar.addItem("baz");
testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);

View File

@ -110,8 +110,9 @@ function testVariablesFiltering()
is(gSearchBox.value, "*",
"Searchbox value is incorrect after 3 backspaces");
is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
"There should be 3 variables displayed in the inner scope");
// variable count includes `__proto__` for object scopes
is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
"There should be 4 variables displayed in the inner scope");
isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
"There should be some variables displayed in the math scope");
isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
@ -140,8 +141,9 @@ function testVariablesFiltering()
is(gSearchBox.value, "",
"Searchbox value is incorrect after 1 backspace");
is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
"There should be 3 variables displayed in the inner scope");
// variable count includes `__proto__` for object scopes
is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
"There should be 4 variables displayed in the inner scope");
isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
"There should be some variables displayed in the math scope");
isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,

View File

@ -1446,7 +1446,7 @@ NetworkDetailsView.prototype = {
headersScope.expanded = true;
for (let header of aResponse.headers) {
let headerVar = headersScope.addVar(header.name, { null: true }, true);
let headerVar = headersScope.addItem(header.name, { null: true }, true);
gNetwork.getString(header.value).then((aString) => headerVar.setGrip(aString));
}
},
@ -1489,7 +1489,7 @@ NetworkDetailsView.prototype = {
cookiesScope.expanded = true;
for (let cookie of aResponse.cookies) {
let cookieVar = cookiesScope.addVar(cookie.name, { null: true }, true);
let cookieVar = cookiesScope.addItem(cookie.name, { null: true }, true);
gNetwork.getString(cookie.value).then((aString) => cookieVar.setGrip(aString));
// By default the cookie name and value are shown. If this is the only
@ -1591,7 +1591,7 @@ NetworkDetailsView.prototype = {
paramsScope.expanded = true;
for (let param of paramsArray) {
let headerVar = paramsScope.addVar(param.name, { null: true }, true);
let headerVar = paramsScope.addItem(param.name, { null: true }, true);
headerVar.setGrip(param.value);
}
},
@ -1634,7 +1634,7 @@ NetworkDetailsView.prototype = {
: L10N.getStr("jsonScopeName");
let jsonScope = this._json.addScope(jsonScopeName);
jsonScope.addVar().populate(jsonObject, { expanded: true });
jsonScope.addItem().populate(jsonObject, { expanded: true });
jsonScope.expanded = true;
}
// Malformed JSON.

View File

@ -20,6 +20,8 @@ const SEARCH_ACTION_MAX_DELAY = 300; // ms
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
"resource://gre/modules/devtools/NetworkHelper.jsm");
@ -74,6 +76,8 @@ this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
for (let name in aFlags) {
this[name] = aFlags[name];
}
EventEmitter.decorate(this);
};
VariablesView.prototype = {
@ -86,7 +90,7 @@ VariablesView.prototype = {
*/
set rawObject(aObject) {
this.empty();
this.addScope().addVar().populate(aObject);
this.addScope().addItem().populate(aObject);
},
/**
@ -180,6 +184,11 @@ VariablesView.prototype = {
}, aTimeout);
},
/**
* The controller for this VariablesView, if it has one.
*/
controller: null,
/**
* The amount of time (in milliseconds) it takes to empty this view lazily.
*/
@ -587,7 +596,8 @@ VariablesView.prototype = {
*/
getScopeForNode: function(aNode) {
let item = this._itemsByElement.get(aNode);
if (item && !(item instanceof Variable) && !(item instanceof Property)) {
// Match only Scopes, not Variables or Properties.
if (item && !(item instanceof Variable)) {
return item;
}
return null;
@ -790,9 +800,8 @@ VariablesView.prototype = {
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
// Start editing the value or name of the variable or property.
if (item instanceof Variable ||
item instanceof Property) {
// Start editing the value or name of the Variable or Property.
if (item instanceof Variable) {
if (e.metaKey || e.altKey || e.shiftKey) {
item._activateNameInput();
} else {
@ -803,9 +812,8 @@ VariablesView.prototype = {
case e.DOM_VK_DELETE:
case e.DOM_VK_BACK_SPACE:
// Delete the variable or property if allowed.
if (item instanceof Variable ||
item instanceof Property) {
// Delete the Variable or Property if allowed.
if (item instanceof Variable) {
item._onDelete(e);
}
return;
@ -902,6 +910,7 @@ VariablesView.NON_SORTABLE_CLASSES = [
"Array",
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
@ -910,6 +919,16 @@ VariablesView.NON_SORTABLE_CLASSES = [
"Float64Array"
];
/**
* Determine whether an object's properties should be sorted based on its class.
*
* @param string aClassName
* The class of the object.
*/
VariablesView.isSortable = function(aClassName) {
return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1;
};
/**
* Generates the string evaluated when performing simple value changes.
*
@ -917,11 +936,13 @@ VariablesView.NON_SORTABLE_CLASSES = [
* The current variable or property.
* @param string aCurrentString
* The trimmed user inputted string.
* @param string aPrefix [optional]
* Prefix for the symbolic name.
* @return string
* The string to be evaluated.
*/
VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString) {
return aItem._symbolicName + "=" + aCurrentString;
VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
return aPrefix + aItem._symbolicName + "=" + aCurrentString;
};
/**
@ -932,12 +953,14 @@ VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString) {
* The current getter or setter property.
* @param string aCurrentString
* The trimmed user inputted string.
* @param string aPrefix [optional]
* Prefix for the symbolic name.
* @return string
* The string to be evaluated.
*/
VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString) {
VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
let property = "\"" + aItem._nameString + "\"";
let parent = aItem.ownerView._symbolicName || "this";
let parent = aPrefix + aItem.ownerView._symbolicName || "this";
return "Object.defineProperty(" + parent + "," + property + "," +
"{ value: " + aCurrentString +
@ -954,15 +977,17 @@ VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString) {
* The current getter or setter property.
* @param string aCurrentString
* The trimmed user inputted string.
* @param string aPrefix [optional]
* Prefix for the symbolic name.
* @return string
* The string to be evaluated.
*/
VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString) {
VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
let type = aItem._nameString;
let propertyObject = aItem.ownerView;
let parentObject = propertyObject.ownerView;
let property = "\"" + propertyObject._nameString + "\"";
let parent = parentObject._symbolicName || "this";
let parent = aPrefix + parentObject._symbolicName || "this";
switch (aCurrentString) {
case "":
@ -976,7 +1001,7 @@ VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString) {
if ((type == "set" && propertyObject.getter.type == "undefined") ||
(type == "get" && propertyObject.setter.type == "undefined")) {
// Make sure the right getter/setter to value override macro is applied to the target object.
return propertyObject.evaluationMacro(propertyObject, "undefined");
return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix);
}
// Construct and return the getter/setter removal evaluation string.
@ -995,16 +1020,16 @@ VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString) {
default:
// Wrap statements inside a function declaration if not already wrapped.
if (aCurrentString.indexOf("function") != 0) {
if (!aCurrentString.startsWith("function")) {
let header = "function(" + (type == "set" ? "value" : "") + ")";
let body = "";
// If there's a return statement explicitly written, always use the
// standard function definition syntax
if (aCurrentString.indexOf("return ") != -1) {
if (aCurrentString.contains("return ")) {
body = "{" + aCurrentString + "}";
}
// If block syntax is used, use the whole string as the function body.
else if (aCurrentString.indexOf("{") == 0) {
else if (aCurrentString.startsWith("{")) {
body = aCurrentString;
}
// Prefer an expression closure.
@ -1042,6 +1067,7 @@ VariablesView.getterOrSetterDeleteCallback = function(aItem) {
return true; // Don't hide the element.
};
/**
* A Scope is an object holding Variable instances.
* Iterable via "for (let [name, variable] in instance) { }".
@ -1083,12 +1109,31 @@ function Scope(aView, aName, aFlags = {}) {
Scope.prototype = {
/**
* Adds a variable to contain any inspected properties.
* Whether this Scope should be prefetched when it is remoted.
*/
shouldPrefetch: true,
/**
* Create a new Variable that is a child of this Scope.
*
* @param string aName
* The variable's name.
* The name of the new Property.
* @param object aDescriptor
* Specifies the value and/or type & class of the variable,
* The variable's descriptor.
* @return Variable
* The newly created child Variable.
*/
_createChild: function(aName, aDescriptor) {
return new Variable(this, aName, aDescriptor);
},
/**
* Adds a child to contain any inspected properties.
*
* @param string aName
* The child's name.
* @param object aDescriptor
* Specifies the value and/or type & class of the child,
* or 'get' & 'set' accessor properties. If the type is implicit,
* it will be inferred from the value.
* e.g. - { value: 42 }
@ -1104,17 +1149,56 @@ Scope.prototype = {
* @return Variable
* The newly created Variable instance, null if it already exists.
*/
addVar: function(aName = "", aDescriptor = {}, aRelaxed = false) {
addItem: function(aName = "", aDescriptor = {}, aRelaxed = false) {
if (this._store.has(aName) && !aRelaxed) {
return null;
}
let variable = new Variable(this, aName, aDescriptor);
this._store.set(aName, variable);
this._variablesView._itemsByElement.set(variable._target, variable);
this._variablesView._currHierarchy.set(variable._absoluteName, variable);
variable.header = !!aName;
return variable;
let child = this._createChild(aName, aDescriptor);
this._store.set(aName, child);
this._variablesView._itemsByElement.set(child._target, child);
this._variablesView._currHierarchy.set(child._absoluteName, child);
child.header = !!aName;
return child;
},
/**
* Adds items for this variable.
*
* @param object aItems
* An object containing some { name: descriptor } data properties,
* specifying the value and/or type & class of the variable,
* or 'get' & 'set' accessor properties. If the type is implicit,
* it will be inferred from the value.
* e.g. - { someProp0: { value: 42 },
* someProp1: { value: true },
* someProp2: { value: "nasu" },
* someProp3: { value: { type: "undefined" } },
* someProp4: { value: { type: "null" } },
* someProp5: { value: { type: "object", class: "Object" } },
* someProp6: { get: { type: "object", class: "Function" },
* set: { type: "undefined" } } }
* @param object aOptions [optional]
* Additional options for adding the properties. Supported options:
* - sorted: true to sort all the properties before adding them
* - callback: function invoked after each item is added
*/
addItems: function(aItems, aOptions = {}) {
let names = Object.keys(aItems);
// Sort all of the properties before adding them, if preferred.
if (aOptions.sorted) {
names.sort();
}
// Add the properties to the current scope.
for (let name of names) {
let descriptor = aItems[name];
let item = this.addItem(name, descriptor);
if (aOptions.callback) {
aOptions.callback(item, descriptor.value);
}
}
},
/**
@ -1179,11 +1263,13 @@ Scope.prototype = {
if (this.isChildOf(aParent)) {
return true;
}
if (this.ownerView instanceof Scope ||
this.ownerView instanceof Variable ||
this.ownerView instanceof Property) {
// Recurse to parent if it is a Scope, Variable, or Property.
if (this.ownerView instanceof Scope) {
return this.ownerView.isDescendantOf(aParent);
}
return false;
},
/**
@ -1405,10 +1491,9 @@ Scope.prototype = {
}
// Check if all parent objects are expanded.
let item = this;
while ((item = item.ownerView) && /* Parent object exists. */
(item instanceof Scope ||
item instanceof Variable ||
item instanceof Property)) {
// Recurse while parent is a Scope, Variable, or Property
while ((item = item.ownerView) && item instanceof Scope) {
if (!item._isExpanded) {
return false;
}
@ -1722,14 +1807,11 @@ Scope.prototype = {
variable._wasToggled = true;
}
// If the variable is contained in another scope (variable or property),
// If the variable is contained in another Scope, Variable, or Property,
// the parent may not be a match, thus hidden. It should be visible
// ("expand upwards").
while ((variable = variable.ownerView) && /* Parent object exists. */
(variable instanceof Scope ||
variable instanceof Variable ||
variable instanceof Property)) {
variable instanceof Scope) {
// Show and expand the parent, as it is certainly accessible.
variable._matched = true;
@ -1971,79 +2053,24 @@ function Variable(aScope, aName, aDescriptor) {
ViewHelpers.create({ constructor: Variable, proto: Scope.prototype }, {
/**
* Adds a property for this variable.
*
* @param string aName
* The property's name.
* @param object aDescriptor
* Specifies the value and/or type & class of the property,
* or 'get' & 'set' accessor properties. If the type is implicit,
* it will be inferred from the value.
* e.g. - { value: 42 }
* - { value: true }
* - { value: "nasu" }
* - { value: { type: "undefined" } }
* - { value: { type: "null" } }
* - { value: { type: "object", class: "Object" } }
* - { get: { type: "object", class: "Function" },
* set: { type: "undefined" } }
* - { get: { type "object", class: "Function" },
* getterValue: "foo", getterPrototypeLevel: 2 }
* @param boolean aRelaxed
* True if name duplicates should be allowed.
* @return Property
* The newly created Property instance, null if it already exists.
* Whether this Scope should be prefetched when it is remoted.
*/
addProperty: function(aName = "", aDescriptor = {}, aRelaxed = false) {
if (this._store.has(aName) && !aRelaxed) {
return null;
}
let property = new Property(this, aName, aDescriptor);
this._store.set(aName, property);
this._variablesView._itemsByElement.set(property._target, property);
this._variablesView._currHierarchy.set(property._absoluteName, property);
property.header = !!aName;
return property;
get shouldPrefetch(){
return this.name == "window" || this.name == "this";
},
/**
* Adds properties for this variable.
* Create a new Property that is a child of Variable.
*
* @param object aProperties
* An object containing some { name: descriptor } data properties,
* specifying the value and/or type & class of the variable,
* or 'get' & 'set' accessor properties. If the type is implicit,
* it will be inferred from the value.
* e.g. - { someProp0: { value: 42 },
* someProp1: { value: true },
* someProp2: { value: "nasu" },
* someProp3: { value: { type: "undefined" } },
* someProp4: { value: { type: "null" } },
* someProp5: { value: { type: "object", class: "Object" } },
* someProp6: { get: { type: "object", class: "Function" },
* set: { type: "undefined" } } }
* @param object aOptions [optional]
* Additional options for adding the properties. Supported options:
* - sorted: true to sort all the properties before adding them
* - callback: function invoked after each property is added
* @param string aName
* The name of the new Property.
* @param object aDescriptor
* The property's descriptor.
* @return Property
* The newly created child Property.
*/
addProperties: function(aProperties, aOptions = {}) {
let propertyNames = Object.keys(aProperties);
// Sort all of the properties before adding them, if preferred.
if (aOptions.sorted) {
propertyNames.sort();
}
// Add the properties to the current scope.
for (let name of propertyNames) {
let descriptor = aProperties[name];
let property = this.addProperty(name, descriptor);
if (aOptions.callback) {
aOptions.callback(property, descriptor.value);
}
}
_createChild: function(aName, aDescriptor) {
return new Property(this, aName, aDescriptor);
},
/**
@ -2122,7 +2149,7 @@ ViewHelpers.create({ constructor: Variable, proto: Scope.prototype }, {
let descriptor = Object.create(aDescriptor);
descriptor.value = VariablesView.getGrip(aValue);
let propertyItem = this.addProperty(aName, descriptor);
let propertyItem = this.addItem(aName, descriptor);
propertyItem._sourceValue = aValue;
// Add an 'onexpand' callback for the property, lazily handling
@ -2149,7 +2176,7 @@ ViewHelpers.create({ constructor: Variable, proto: Scope.prototype }, {
descriptor.get = VariablesView.getGrip(aDescriptor.get);
descriptor.set = VariablesView.getGrip(aDescriptor.set);
return this.addProperty(aName, descriptor);
return this.addItem(aName, descriptor);
},
/**
@ -2311,8 +2338,8 @@ ViewHelpers.create({ constructor: Variable, proto: Scope.prototype }, {
this.evaluationMacro = null;
}
let getter = this.addProperty("get", { value: descriptor.get });
let setter = this.addProperty("set", { value: descriptor.set });
let getter = this.addItem("get", { value: descriptor.get });
let setter = this.addItem("set", { value: descriptor.set });
getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
@ -2852,9 +2879,8 @@ VariablesView.prototype.commitHierarchy = function() {
if (prevVariable) {
expanded = prevVariable._isExpanded;
// Only analyze variables and properties for displayed value changes.
if (currVariable instanceof Variable ||
currVariable instanceof Property) {
// Only analyze Variables and Properties for displayed value changes.
if (currVariable instanceof Variable) {
changed = prevVariable._valueString != currVariable._valueString;
}
}
@ -2974,6 +3000,16 @@ VariablesView.isFalsy = function(aDescriptor) {
return false;
};
/**
* Returns true if the value is an instance of Variable or Property.
*
* @param any aValue
* The value to test.
*/
VariablesView.isVariable = function(aValue) {
return aValue instanceof Variable;
};
/**
* Returns a standard grip for a value.
*

View File

@ -0,0 +1,350 @@
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource:///modules/devtools/VariablesView.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
);
const MAX_LONG_STRING_LENGTH = 200000;
this.EXPORTED_SYMBOLS = ["VariablesViewController"];
/**
* Controller for a VariablesView that handles interfacing with the debugger
* protocol. Is able to populate scopes and variables via the protocol as well
* as manage actor lifespans.
*
* @param VariablesView aView
* The view to attach to.
* @param object aOptions
* Options for configuring the controller. Supported options:
* - getGripClient: callback for creating an object grip client
* - getLongStringClient: callback for creating a long string grip client
* - releaseActor: callback for releasing an actor when it's no longer needed
* - overrideValueEvalMacro: callback for creating an overriding eval macro
* - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro
* - simpleValueEvalMacro: callback for creating a simple value eval macro
*/
function VariablesViewController(aView, aOptions) {
this.addExpander = this.addExpander.bind(this);
this._getGripClient = aOptions.getGripClient;
this._getLongStringClient = aOptions.getLongStringClient;
this._releaseActor = aOptions.releaseActor;
if (aOptions.overrideValueEvalMacro) {
this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
}
if (aOptions.getterOrSetterEvalMacro) {
this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
}
if (aOptions.simpleValueEvalMacro) {
this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
}
this._actors = new Set();
this.view = aView;
this.view.controller = this;
}
VariablesViewController.prototype = {
/**
* The default getter/setter evaluation macro.
*/
_getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro,
/**
* The default override value evaluation macro.
*/
_overrideValueEvalMacro: VariablesView.overrideValueEvalMacro,
/**
* The default simple value evaluation macro.
*/
_simpleValueEvalMacro: VariablesView.simpleValueEvalMacro,
/**
* Populate a long string into a target using a grip.
*
* @param Variable aTarget
* The target Variable/Property to put the retrieved string into.
* @param LongStringActor aGrip
* The long string grip that use to retrieve the full string.
* @return Promise
* The promise that will be resolved when the string is retrieved.
*/
_populateFromLongString: function(aTarget, aGrip){
let deferred = Promise.defer();
let from = aGrip.initial.length;
let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH);
this._getLongStringClient(aGrip).substring(from, to, aResponse => {
// Stop tracking the actor because it's no longer needed.
this.releaseActor(aGrip);
// Replace the preview with the full string and make it non-expandable.
aTarget.onexpand = null;
aTarget.setGrip(aGrip.initial + aResponse.substring);
aTarget.hideArrow();
// Mark the string as having retrieved.
aTarget._retrieved = true;
deferred.resolve();
});
return deferred.promise;
},
/**
* Adds properties to a Scope, Variable, or Property in the view. Triggered
* when a scope is expanded or certain variables are hovered.
*
* @param Scope aTarget
* The Scope where the properties will be placed into.
* @param object aGrip
* The grip to use to populate the target.
*/
_populateFromObject: function(aTarget, aGrip) {
let deferred = Promise.defer();
this._getGripClient(aGrip).getPrototypeAndProperties(aResponse => {
let { ownProperties, prototype, safeGetterValues } = aResponse;
let sortable = VariablesView.isSortable(aGrip.class);
// Merge the safe getter values into one object such that we can use it
// in VariablesView.
for (let name of Object.keys(safeGetterValues)) {
if (name in ownProperties) {
ownProperties[name].getterValue = safeGetterValues[name].getterValue;
ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
.getterPrototypeLevel;
} else {
ownProperties[name] = safeGetterValues[name];
}
}
// Add all the variable properties.
if (ownProperties) {
aTarget.addItems(ownProperties, {
// Not all variables need to force sorted properties.
sorted: sortable,
// Expansion handlers must be set after the properties are added.
callback: this.addExpander
});
}
// Add the variable's __proto__.
if (prototype && prototype.type != "null") {
let proto = aTarget.addItem("__proto__", { value: prototype });
// Expansion handlers must be set after the properties are added.
this.addExpander(proto, prototype);
}
// Mark the variable as having retrieved all its properties.
aTarget._retrieved = true;
this.view.commitHierarchy();
deferred.resolve();
});
return deferred.promise;
},
/**
* Adds an 'onexpand' callback for a variable, lazily handling
* the addition of new properties.
*
* @param Variable aVar
* The variable where the properties will be placed into.
* @param any aSource
* The source to use to populate the target.
*/
addExpander: function(aTarget, aSource) {
// Attach evaluation macros as necessary.
if (aTarget.getter || aTarget.setter) {
aTarget.evaluationMacro = this._overrideValueEvalMacro;
let getter = aTarget.get("get");
if (getter) {
getter.evaluationMacro = this._getterOrSetterEvalMacro;
}
let setter = aTarget.get("set");
if (setter) {
setter.evaluationMacro = this._getterOrSetterEvalMacro;
}
} else {
aTarget.evaluationMacro = this._simpleValueEvalMacro;
}
// If the source is primitive then an expander is not needed.
if (VariablesView.isPrimitive({ value: aSource })) {
return;
}
// If the source is a long string then show the arrow.
if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
aTarget.showArrow();
}
// Make sure that properties are always available on expansion.
aTarget.onexpand = () => this.expand(aTarget, aSource);
// Some variables are likely to contain a very large number of properties.
// It's a good idea to be prepared in case of an expansion.
if (aTarget.shouldPrefetch) {
aTarget.addEventListener("mouseover", aTarget.onexpand, false);
}
// Register all the actors that this controller now depends on.
for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
if (WebConsoleUtils.isActorGrip(grip)) {
this._actors.add(grip.actor);
}
}
},
/**
* Adds properties to a Scope, Variable, or Property in the view. Triggered
* when a scope is expanded or certain variables are hovered.
*
* @param Scope aTarget
* The Scope to be expanded.
* @param object aSource
* The source to use to populate the target.
* @return Promise
* The promise that is resolved once the target has been expanded.
*/
expand: function(aTarget, aSource) {
// Fetch the variables only once.
if (aTarget._fetched) {
return aTarget._fetched;
}
let deferred = Promise.defer();
aTarget._fetched = deferred.promise;
if (!aSource) {
throw new Error("No actor grip was given for the variable.");
}
// If the target a Variable or Property then we're fetching properties
if (VariablesView.isVariable(aTarget)) {
this._populateFromObject(aTarget, aSource).then(() => {
deferred.resolve();
// Signal that properties have been fetched.
this.view.emit("fetched", "properties", aTarget);
});
return deferred.promise;
}
switch (aSource.type) {
case "longString":
this._populateFromLongString(aTarget, aSource).then(() => {
deferred.resolve();
// Signal that a long string has been fetched.
this.view.emit("fetched", "longString", aTarget);
});
break;
case "with":
case "object":
this._populateFromObject(aTarget, aSource.object).then(() => {
deferred.resolve();
// Signal that variables have been fetched.
this.view.emit("fetched", "variables", aTarget);
});
break;
case "block":
case "function":
// Add nodes for every argument and every other variable in scope.
let args = aSource.bindings.arguments;
if (args) {
for (let arg of args) {
let name = Object.getOwnPropertyNames(arg)[0];
let ref = aTarget.addItem(name, arg[name]);
let val = arg[name].value;
this.addExpander(ref, val);
}
}
aTarget.addItems(aSource.bindings.variables, {
// Not all variables need to force sorted properties.
sorted: VARIABLES_SORTING_ENABLED,
// Expansion handlers must be set after the properties are added.
callback: this.addExpander
});
// No need to signal that variables have been fetched, since
// the scope arguments and variables are already attached to the
// environment bindings, so pausing the active thread is unnecessary.
deferred.resolve();
break;
default:
let error = "Unknown Debugger.Environment type: " + aSource.type;
Cu.reportError(error);
deferred.reject(error);
}
return deferred.promise;
},
/**
* Release an actor from the controller.
*
* @param object aActor
* The actor to release.
*/
releaseActor: function(aActor){
if (this._releaseActor) {
this._releaseActor(aActor);
}
this._actors.delete(aActor);
},
/**
* Release all the actors referenced by the controller, optionally filtered.
*
* @param function aFilter [optional]
* Callback to filter which actors are released.
*/
releaseActors: function(aFilter) {
for (let actor of this._actors) {
if (!aFilter || aFilter(actor)) {
this.releaseActor(actor);
}
}
},
};
/**
* Attaches a VariablesViewController to a VariablesView if it doesn't already
* have one.
*
* @param VariablesView aView
* The view to attach to.
* @param object aOptions
* The options to use in creating the controller.
* @return VariablesViewController
*/
VariablesViewController.attach = function(aView, aOptions) {
if (aView.controller) {
return aView.controller;
}
return new VariablesViewController(aView, aOptions);
};

View File

@ -37,6 +37,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "Promise",
XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
"resource:///modules/devtools/VariablesView.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
"resource:///modules/devtools/VariablesViewController.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource:///modules/devtools/shared/event-emitter.js");
@ -2104,13 +2107,9 @@ WebConsoleFrame.prototype = {
}
else if (aNode.classList.contains("webconsole-msg-inspector")) {
let view = aNode._variablesView;
let actors = view ?
this.jsterm._objectActorsInVariablesViews.get(view) :
new Set();
for (let actor of actors) {
this._releaseObject(actor);
if (view) {
view.controller.releaseActors();
}
actors.clear();
aNode._variablesView = null;
}
@ -2743,6 +2742,35 @@ WebConsoleFrame.prototype = {
},
};
/**
* @see VariablesView.simpleValueEvalMacro
*/
function simpleValueEvalMacro(aItem, aCurrentString)
{
return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self");
};
/**
* @see VariablesView.overrideValueEvalMacro
*/
function overrideValueEvalMacro(aItem, aCurrentString)
{
return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self");
};
/**
* @see VariablesView.getterOrSetterEvalMacro
*/
function getterOrSetterEvalMacro(aItem, aCurrentString)
{
return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self");
}
/**
* Create a JSTerminal (a JavaScript command line). This is attached to an
* existing HeadsUpDisplay (a Web Console instance). This code is responsible
@ -2771,8 +2799,6 @@ function JSTerm(aWebConsoleFrame)
this._keyPress = this.keyPress.bind(this);
this._inputEventHandler = this.inputEventHandler.bind(this);
this._fetchVarProperties = this._fetchVarProperties.bind(this);
this._fetchVarLongString = this._fetchVarLongString.bind(this);
this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this);
EventEmitter.decorate(this);
@ -3282,7 +3308,27 @@ JSTerm.prototype = {
view.searchEnabled = !aOptions.hideFilterInput;
view.lazyEmpty = this._lazyVariablesView;
view.lazyAppend = this._lazyVariablesView;
this._objectActorsInVariablesViews.set(view, new Set());
VariablesViewController.attach(view, {
getGripClient: aGrip => {
return new GripClient(this.hud.proxy.client, aGrip);
},
getLongStringClient: aGrip => {
return this.webConsoleClient.longString(aGrip);
},
releaseActor: aActor => {
this.hud._releaseObject(aActor);
},
simpleValueEvalMacro: simpleValueEvalMacro,
overrideValueEvalMacro: overrideValueEvalMacro,
getterOrSetterEvalMacro: getterOrSetterEvalMacro,
});
// Relay events from the VariablesView.
view.on("fetched", (aEvent, aType, aVar) => {
this.emit("variablesview-fetched", aVar);
});
return view;
},
@ -3304,16 +3350,11 @@ JSTerm.prototype = {
view.createHierarchy();
view.empty();
let actors = this._objectActorsInVariablesViews.get(view);
for (let actor of actors) {
// We need to avoid pruning the object inspection starting point.
// That one is pruned when the console message is removed.
if (view._consoleLastObjectActor != actor) {
this.hud._releaseObject(actor);
}
}
actors.clear();
// We need to avoid pruning the object inspection starting point.
// That one is pruned when the console message is removed.
view.controller.releaseActors(aActor => {
return view._consoleLastObjectActor != aActor;
});
if (aOptions.objectActor) {
// Make sure eval works in the correct context.
@ -3331,11 +3372,11 @@ JSTerm.prototype = {
scope.expanded = true;
scope.locked = true;
let container = scope.addVar();
container.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
let container = scope.addItem();
container.evaluationMacro = simpleValueEvalMacro;
if (aOptions.objectActor) {
this._fetchVarProperties(container, aOptions.objectActor);
view.controller.expand(container, aOptions.objectActor);
view._consoleLastObjectActor = aOptions.objectActor.actor;
}
else if (aOptions.rawObject) {
@ -3374,80 +3415,6 @@ JSTerm.prototype = {
this.requestEvaluation(aString, evalOptions).then(onEval, onEval);
},
/**
* Generates the string evaluated when performing simple value changes in the
* variables view.
*
* @private
* @param Variable | Property aItem
* The current variable or property.
* @param string aCurrentString
* The trimmed user inputted string.
* @return string
* The string to be evaluated.
*/
_variablesViewSimpleValueEvalMacro:
function JST__variablesViewSimpleValueEvalMacro(aItem, aCurrentString)
{
return "_self" + aItem.symbolicName + "=" + aCurrentString;
},
/**
* Generates the string evaluated when overriding getters and setters with
* plain values in the variables view.
*
* @private
* @param Property aItem
* The current getter or setter property.
* @param string aCurrentString
* The trimmed user inputted string.
* @return string
* The string to be evaluated.
*/
_variablesViewOverrideValueEvalMacro:
function JST__variablesViewOverrideValueEvalMacro(aItem, aCurrentString)
{
let parent = aItem.ownerView;
let symbolicName = parent.symbolicName;
if (symbolicName.indexOf("_self") != 0) {
parent._symbolicName = "_self" + symbolicName;
}
let result = VariablesView.overrideValueEvalMacro.apply(this, arguments);
parent._symbolicName = symbolicName;
return result;
},
/**
* Generates the string evaluated when performing getters and setters changes
* in the variables view.
*
* @private
* @param Property aItem
* The current getter or setter property.
* @param string aCurrentString
* The trimmed user inputted string.
* @return string
* The string to be evaluated.
*/
_variablesViewGetterOrSetterEvalMacro:
function JST__variablesViewGetterOrSetterEvalMacro(aItem, aCurrentString)
{
let propertyObject = aItem.ownerView;
let parentObject = propertyObject.ownerView;
let parent = parentObject.symbolicName;
parentObject._symbolicName = "_self" + parent;
let result = VariablesView.getterOrSetterEvalMacro.apply(this, arguments);
parentObject._symbolicName = parent;
return result;
},
/**
* The property deletion function used by the variables view when a property
* is deleted.
@ -3556,144 +3523,7 @@ JSTerm.prototype = {
aCallback && aCallback(aResponse);
},
/**
* Adds properties to a variable in the view. Triggered when a variable is
* expanded. It does not expand the variable.
*
* @param object aVar
* The VariablseView Variable instance where the properties get added.
* @param object [aGrip]
* Optional, the object actor grip of the variable. If the grip is not
* provided, then the aVar.value is used as the object actor grip.
*/
_fetchVarProperties: function JST__fetchVarProperties(aVar, aGrip)
{
// Retrieve the properties only once.
if (aVar._fetched) {
return;
}
aVar._fetched = true;
let grip = aGrip || aVar.value;
if (!grip) {
throw new Error("No object actor grip was given for the variable.");
}
let view = aVar._variablesView;
let actors = this._objectActorsInVariablesViews.get(view);
function addActorForDescriptor(aGrip) {
if (WebConsoleUtils.isActorGrip(aGrip)) {
actors.add(aGrip.actor);
}
}
let onNewProperty = (aProperty) => {
if (aProperty.getter || aProperty.setter) {
aProperty.evaluationMacro = this._variablesViewOverrideValueEvalMacro;
let getter = aProperty.get("get");
let setter = aProperty.get("set");
if (getter) {
getter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
}
if (setter) {
setter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
}
}
else {
aProperty.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
}
let grips = [aProperty.value, aProperty.getter, aProperty.setter];
grips.forEach(addActorForDescriptor);
let inspectable = !VariablesView.isPrimitive({ value: aProperty.value });
let longString = WebConsoleUtils.isActorGrip(aProperty.value) &&
aProperty.value.type == "longString";
if (inspectable) {
aProperty.onexpand = this._fetchVarProperties;
}
else if (longString) {
aProperty.onexpand = this._fetchVarLongString;
aProperty.showArrow();
}
};
let client = new GripClient(this.hud.proxy.client, grip);
client.getPrototypeAndProperties((aResponse) => {
let { ownProperties, prototype, safeGetterValues } = aResponse;
let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
// Merge the safe getter values into one object such that we can use it
// in VariablesView.
for (let name of Object.keys(safeGetterValues)) {
if (name in ownProperties) {
ownProperties[name].getterValue = safeGetterValues[name].getterValue;
ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
.getterPrototypeLevel;
}
else {
ownProperties[name] = safeGetterValues[name];
}
}
// Add all the variable properties.
if (ownProperties) {
aVar.addProperties(ownProperties, {
sorted: sortable,
callback: onNewProperty,
});
}
// Add the variable's __proto__.
if (prototype && prototype.type != "null") {
let proto = aVar.addProperty("__proto__", { value: prototype });
onNewProperty(proto);
}
aVar._retrieved = true;
view.commitHierarchy();
this.emit("variablesview-fetched", aVar);
});
},
/**
* Fetch the full string for a given variable that displays a long string.
*
* @param object aVar
* The VariablesView Variable instance where the properties get added.
*/
_fetchVarLongString: function JST__fetchVarLongString(aVar)
{
if (aVar._fetched) {
return;
}
aVar._fetched = true;
let grip = aVar.value;
if (!grip) {
throw new Error("No long string actor grip was given for the variable.");
}
let client = this.webConsoleClient.longString(grip);
let toIndex = Math.min(grip.length, MAX_LONG_STRING_LENGTH);
client.substring(grip.initial.length, toIndex, (aResponse) => {
if (aResponse.error) {
Cu.reportError("JST__fetchVarLongString substring failure: " +
aResponse.error + ": " + aResponse.message);
return;
}
aVar.onexpand = null;
aVar.setGrip(grip.initial + aResponse.substring);
aVar.hideArrow();
aVar._retrieved = true;
if (toIndex != grip.length) {
this.hud.logWarningAboutStringTooLong();
}
});
},
/**
* Writes a JS object to the JSTerm outputNode.
@ -4358,11 +4188,7 @@ JSTerm.prototype = {
_sidebarDestroy: function JST__sidebarDestroy()
{
if (this._variablesView) {
let actors = this._objectActorsInVariablesViews.get(this._variablesView);
for (let actor of actors) {
this.hud._releaseObject(actor);
}
actors.clear();
this._variablesView.controller.releaseActors();
this._variablesView = null;
}