diff --git a/browser/devtools/debugger/debugger-controller.js b/browser/devtools/debugger/debugger-controller.js index 18a5823653c4..b589f6ce0a32 100644 --- a/browser/devtools/debugger/debugger-controller.js +++ b/browser/devtools/debugger/debugger-controller.js @@ -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("", { value: this.currentException }); - this._addVarExpander(excRef, this.currentException); + let excRef = aScope.addItem("", { value: this.currentException }); + DebuggerView.Variables.controller.addExpander(excRef, this.currentException); } // Add any returned value. if (this.currentReturnedValue) { - let retRef = aScope.addVar("", { value: this.currentReturnedValue }); - this._addVarExpander(retRef, this.currentReturnedValue); + let retRef = aScope.addItem("", { 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. */ diff --git a/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js b/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js index 2b088728d1b0..9749073d11cb 100644 --- a/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js +++ b/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js @@ -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 diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-03.js b/browser/devtools/debugger/test/browser_dbg_propertyview-03.js index 5a5f5a8628b6..684af5fb7c3c 100644 --- a/browser/devtools/debugger/test/browser_dbg_propertyview-03.js +++ b/browser/devtools/debugger/test/browser_dbg_propertyview-03.js @@ -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, diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-04.js b/browser/devtools/debugger/test/browser_dbg_propertyview-04.js index d77af3470345..47ab452c7d72 100644 --- a/browser/devtools/debugger/test/browser_dbg_propertyview-04.js +++ b/browser/devtools/debugger/test/browser_dbg_propertyview-04.js @@ -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", diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-05.js b/browser/devtools/debugger/test/browser_dbg_propertyview-05.js index 26a15ade549e..42336079a975 100644 --- a/browser/devtools/debugger/test/browser_dbg_propertyview-05.js +++ b/browser/devtools/debugger/test/browser_dbg_propertyview-05.js @@ -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."); diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-06.js b/browser/devtools/debugger/test/browser_dbg_propertyview-06.js index 5624dde63c4a..74dca2359dc9 100644 --- a/browser/devtools/debugger/test/browser_dbg_propertyview-06.js +++ b/browser/devtools/debugger/test/browser_dbg_propertyview-06.js @@ -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."); diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-data.js b/browser/devtools/debugger/test/browser_dbg_propertyview-data.js index cd24c7c667f0..6cf2c0560db4 100644 --- a/browser/devtools/debugger/test/browser_dbg_propertyview-data.js +++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data.js @@ -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); diff --git a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js index 8cf6cab58470..18245e5bb68f 100644 --- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js +++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js @@ -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, diff --git a/browser/devtools/netmonitor/netmonitor-view.js b/browser/devtools/netmonitor/netmonitor-view.js index 34a0d94a018b..c59dd35809a6 100644 --- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -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. diff --git a/browser/devtools/shared/widgets/VariablesView.jsm b/browser/devtools/shared/widgets/VariablesView.jsm index 9a9fad892bdd..f31ddbc25bda 100644 --- a/browser/devtools/shared/widgets/VariablesView.jsm +++ b/browser/devtools/shared/widgets/VariablesView.jsm @@ -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. * diff --git a/browser/devtools/shared/widgets/VariablesViewController.jsm b/browser/devtools/shared/widgets/VariablesViewController.jsm new file mode 100644 index 000000000000..56704b2d2ac7 --- /dev/null +++ b/browser/devtools/shared/widgets/VariablesViewController.jsm @@ -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); +}; diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index 291d8531d166..373b5c67743f 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -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; }