Bug 812814 - Add a way to edit or remove watch expressions while the debugger is paused, r=past

This commit is contained in:
Victor Porof 2012-11-27 18:19:23 +02:00
parent 1080556613
commit 878954fbc8
12 changed files with 837 additions and 76 deletions

View File

@ -509,7 +509,7 @@ StackFrames.prototype = {
// If an error was thrown during the evaluation of the watch expressions,
// then at least one expression evaluation could not be performed.
if (this.currentEvaluation.throw) {
DebuggerView.WatchExpressions.removeExpression(0);
DebuggerView.WatchExpressions.removeExpressionAt(0);
DebuggerController.StackFrames.syncWatchExpressions();
return;
}
@ -600,11 +600,15 @@ StackFrames.prototype = {
// If watch expressions evaluation results are available, create a scope
// to contain all the values.
if (watchExpressionsEvaluation) {
if (this.syncedWatchExpressions && watchExpressionsEvaluation) {
let label = L10N.getStr("watchExpressionsScopeLabel");
let arrow = L10N.getStr("watchExpressionsSeparatorLabel");
let scope = DebuggerView.Variables.addScope(label);
scope.separator = arrow;
scope.allowNameInput = true;
scope.allowDeletion = true;
scope.switch = DebuggerView.WatchExpressions.switchExpression;
scope.delete = DebuggerView.WatchExpressions.deleteExpression;
// The evaluation hasn't thrown, so display the returned results and
// always expand the watch expressions scope by default.
@ -939,6 +943,7 @@ StackFrames.prototype = {
this.syncedWatchExpressions =
this.currentWatchExpressions = null;
}
this.currentFrame = null;
this._onFrames();
},

View File

@ -945,6 +945,8 @@ create({ constructor: BreakpointsView, proto: MenuContainer.prototype }, {
function WatchExpressionsView() {
dumpn("WatchExpressionsView was instantiated");
MenuContainer.call(this);
this.switchExpression = this.switchExpression.bind(this);
this.deleteExpression = this.deleteExpression.bind(this);
this._createItemView = this._createItemView.bind(this);
this._onClick = this._onClick.bind(this);
this._onClose = this._onClose.bind(this);
@ -1028,11 +1030,54 @@ create({ constructor: WatchExpressionsView, proto: MenuContainer.prototype }, {
* @param number aIndex
* The index used to identify the watch expression.
*/
removeExpression: function DVWE_removeExpression(aIndex) {
removeExpressionAt: function DVWE_removeExpressionAt(aIndex) {
this.remove(this._cache[aIndex]);
this._cache.splice(aIndex, 1);
},
/**
* Changes the watch expression corresponding to the specified variable item.
*
* @param Variable aVar
* The variable representing the watch expression evaluation.
* @param string aExpression
* The new watch expression text.
*/
switchExpression: function DVWE_switchExpression(aVar, aExpression) {
let expressionItem =
[i for (i of this._cache) if (i.attachment.expression == aVar.name)][0];
// Remove the watch expression if it's going to be a duplicate.
if (!aExpression || this.getExpressions().indexOf(aExpression) != -1) {
this.deleteExpression(aVar);
return;
}
// Save the watch expression code string.
expressionItem.attachment.expression = aExpression;
expressionItem.target.inputNode.value = aExpression;
// Synchronize with the controller's watch expressions store.
DebuggerController.StackFrames.syncWatchExpressions();
},
/**
* Removes the watch expression corresponding to the specified variable item.
*
* @param Variable aVar
* The variable representing the watch expression evaluation.
*/
deleteExpression: function DVWE_deleteExpression(aVar) {
let expressionItem =
[i for (i of this._cache) if (i.attachment.expression == aVar.name)][0];
// Remove the watch expression at its respective index.
this.removeExpressionAt(this._cache.indexOf(expressionItem));
// Synchronize with the controller's watch expressions store.
DebuggerController.StackFrames.syncWatchExpressions();
},
/**
* Gets the watch expression code string for an item in this container.
*
@ -1101,7 +1146,7 @@ create({ constructor: WatchExpressionsView, proto: MenuContainer.prototype }, {
*/
_onClose: function DVWE__onClose(e) {
let expressionItem = this.getItemForElement(e.target);
this.removeExpression(this._cache.indexOf(expressionItem));
this.removeExpressionAt(this._cache.indexOf(expressionItem));
// Synchronize with the controller's watch expressions store.
DebuggerController.StackFrames.syncWatchExpressions();
@ -1116,15 +1161,15 @@ create({ constructor: WatchExpressionsView, proto: MenuContainer.prototype }, {
_onBlur: function DVWE__onBlur({ target: textbox }) {
let expressionItem = this.getItemForElement(textbox);
let oldExpression = expressionItem.attachment.expression;
let newExpression = textbox.value;
let newExpression = textbox.value.trim();
// Remove the watch expression if it's empty.
if (!newExpression) {
this.removeExpression(this._cache.indexOf(expressionItem));
this.removeExpressionAt(this._cache.indexOf(expressionItem));
}
// Remove the watch expression if it's a duplicate.
else if (!oldExpression && this.getExpressions().indexOf(newExpression) != -1) {
this.removeExpression(this._cache.indexOf(expressionItem));
this.removeExpressionAt(this._cache.indexOf(expressionItem));
}
// Expression is eligible.
else {

View File

@ -33,6 +33,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_dbg_propertyview-09.js \
browser_dbg_propertyview-10.js \
browser_dbg_propertyview-edit.js \
browser_dbg_propertyview-edit-watch.js \
browser_dbg_propertyview-data.js \
browser_dbg_propertyview-filter-01.js \
browser_dbg_propertyview-filter-02.js \

View File

@ -52,6 +52,20 @@ function test()
is(gWatch.getExpressions().length, 1,
"Duplicate watch expressions are automatically removed");
addAndCheckExpressions(2, 0, "a\t", true);
addAndCheckExpressions(2, 0, "a\r", true);
addAndCheckExpressions(2, 0, "a\n", true);
gDebugger.editor.focus();
is(gWatch.getExpressions().length, 1,
"Duplicate watch expressions are automatically removed");
addAndCheckExpressions(2, 0, "\ta", true);
addAndCheckExpressions(2, 0, "\ra", true);
addAndCheckExpressions(2, 0, "\na", true);
gDebugger.editor.focus();
is(gWatch.getExpressions().length, 1,
"Duplicate watch expressions are automatically removed");
addAndCheckCustomExpression(2, 0, "bazΩΩka");
addAndCheckCustomExpression(3, 0, "bambøøcha");
@ -194,7 +208,7 @@ function test()
}
function removeAndCheckExpression(total, index, string) {
gWatch.removeExpression(index);
gWatch.removeExpressionAt(index);
is(gWatch.getExpressions().length, total,
"There should be " + total + " watch expressions available (1)");

View File

@ -0,0 +1,502 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
/**
* Make sure that the editing or removing watch expressions works properly.
*/
const TAB_URL = EXAMPLE_URL + "browser_dbg_watch-expressions.html";
var gPane = null;
var gTab = null;
var gDebuggee = null;
var gDebugger = null;
var gWatch = null;
var gVars = null;
requestLongerTimeout(2);
function test() {
debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gDebuggee = aDebuggee;
gPane = aPane;
gDebugger = gPane.contentWindow;
gWatch = gDebugger.DebuggerView.WatchExpressions;
gVars = gDebugger.DebuggerView.Variables;
gDebugger.DebuggerController.StackFrames.autoScopeExpand = true;
gDebugger.DebuggerView.Variables.nonEnumVisible = false;
testFrameEval();
});
}
function testFrameEval() {
gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function test() {
gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", test, false);
Services.tm.currentThread.dispatch({ run: function() {
is(gDebugger.DebuggerController.activeThread.state, "paused",
"Should only be getting stack frames while paused.");
var localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".scope")[1],
localNodes = localScope.querySelector(".details").childNodes,
aArg = localNodes[1],
varT = localNodes[3];
is(aArg.querySelector(".name").getAttribute("value"), "aArg",
"Should have the right name for 'aArg'.");
is(varT.querySelector(".name").getAttribute("value"), "t",
"Should have the right name for 't'.");
is(aArg.querySelector(".value").getAttribute("value"), "undefined",
"Should have the right initial value for 'aArg'.");
is(varT.querySelector(".value").getAttribute("value"), "\"Browser Debugger Watch Expressions Test\"",
"Should have the right initial value for 't'.");
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
"There should be 5 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 5, "There should be 5 evaluations availalble");
is(scope.get("this")._isShown, true,
"Should have the right visibility state for 'this'.");
is(scope.get("this").target.querySelectorAll(".dbg-variables-delete").length, 1,
"Should have the one close button visible for 'this'.");
is(scope.get("this").name, "this",
"Should have the right name for 'this'.");
is(scope.get("this").value.type, "object",
"Should have the right value type for 'this'.");
is(scope.get("this").value.class, "Proxy",
"Should have the right value type for 'this'.");
is(scope.get("ermahgerd")._isShown, true,
"Should have the right visibility state for 'ermahgerd'.");
is(scope.get("ermahgerd").target.querySelectorAll(".dbg-variables-delete").length, 1,
"Should have the one close button visible for 'ermahgerd'.");
is(scope.get("ermahgerd").name, "ermahgerd",
"Should have the right name for 'ermahgerd'.");
is(scope.get("ermahgerd").value.type, "object",
"Should have the right value type for 'ermahgerd'.");
is(scope.get("ermahgerd").value.class, "Function",
"Should have the right value type for 'ermahgerd'.");
is(scope.get("aArg")._isShown, true,
"Should have the right visibility state for 'aArg'.");
is(scope.get("aArg").target.querySelectorAll(".dbg-variables-delete").length, 1,
"Should have the one close button visible for 'aArg'.");
is(scope.get("aArg").name, "aArg",
"Should have the right name for 'aArg'.");
is(scope.get("aArg").value, undefined,
"Should have the right value for 'aArg'.");
is(scope.get("document.title")._isShown, true,
"Should have the right visibility state for 'document.title'.");
is(scope.get("document.title").target.querySelectorAll(".dbg-variables-delete").length, 1,
"Should have the one close button visible for 'document.title'.");
is(scope.get("document.title").name, "document.title",
"Should have the right name for 'document.title'.");
is(scope.get("document.title").value, "42",
"Should have the right value for 'document.title'.");
is(typeof scope.get("document.title").value, "string",
"Should have the right value type for 'document.title'.");
is(scope.get("document.title = 42")._isShown, true,
"Should have the right visibility state for 'document.title = 42'.");
is(scope.get("document.title = 42").target.querySelectorAll(".dbg-variables-delete").length, 1,
"Should have the one close button visible for 'document.title = 42'.");
is(scope.get("document.title = 42").name, "document.title = 42",
"Should have the right name for 'document.title = 42'.");
is(scope.get("document.title = 42").value, 42,
"Should have the right value for 'document.title = 42'.");
is(typeof scope.get("document.title = 42").value, "number",
"Should have the right value type for 'document.title = 42'.");
testModification(scope.get("document.title = 42").target, test1, function(scope) {
testModification(scope.get("aArg").target, test2, function(scope) {
testModification(scope.get("aArg = 44").target, test3, function(scope) {
testModification(scope.get("document.title = 43").target, test4, function(scope) {
testModification(scope.get("document.title").target, test5, function(scope) {
testExprDeletion(scope.get("this").target, test6, function(scope) {
testExprDeletion(scope.get("ermahgerd").target, test7, function(scope) {
resumeAndFinish();
}, 44, 0, true);
}, 44);
}, " \t\r\n", "\"43\"", 44, 1, true);
}, " \t\r\ndocument.title \t\r\n", "\"43\"", 44);
}, " \t\r\ndocument.title \t\r\n", "\"43\"", 44);
}, "aArg = 44", 44, 44);
}, "document.title = 43", 43, "undefined");
}}, 0);
}, false);
addWatchExpression("this");
addWatchExpression("ermahgerd");
addWatchExpression("aArg");
addWatchExpression("document.title");
addWatchExpression("document.title = 42");
executeSoon(function() {
gDebuggee.ermahgerd(); // ermahgerd!!
});
}
function testModification(aVar, aTest, aCallback, aNewValue, aNewResult, aArgResult,
aLocalScopeIndex = 1, aDeletionFlag = null)
{
function makeChangesAndExitInputMode() {
EventUtils.sendString(aNewValue);
EventUtils.sendKey("RETURN");
}
EventUtils.sendMouseEvent({ type: "dblclick" },
aVar.querySelector(".name"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-name-input"),
"There should be an input element created.");
let testContinued = false;
let fetchedVariables = false;
let fetchedExpressions = false;
let countV = 0;
gDebugger.addEventListener("Debugger:FetchedVariables", function testV() {
// We expect 2 Debugger:FetchedVariables events, one from the global
// object scope and the regular one.
if (++countV < 2) {
info("Number of received Debugger:FetchedVariables events: " + countV);
return;
}
gDebugger.removeEventListener("Debugger:FetchedVariables", testV, false);
fetchedVariables = true;
executeSoon(continueTest);
}, false);
let countE = 0;
gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function testE() {
// We expect only one Debugger:FetchedWatchExpressions event, since all
// expressions are evaluated at the same time.
if (++countE < 1) {
info("Number of received Debugger:FetchedWatchExpressions events: " + countE);
return;
}
gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", testE, false);
fetchedExpressions = true;
executeSoon(continueTest);
}, false);
function continueTest() {
if (testContinued || !fetchedVariables || !fetchedExpressions) {
return;
}
testContinued = true;
// Get the variable reference anew, since the old ones were discarded when
// we resumed.
var localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".scope")[aLocalScopeIndex],
localNodes = localScope.querySelector(".details").childNodes,
aArg = localNodes[1];
is(aArg.querySelector(".value").getAttribute("value"), aArgResult,
"Should have the right value for 'aArg'.");
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
info("Found the watch expressions scope: " + scope);
let aExp = scope.get(aVar.querySelector(".name").getAttribute("value"));
info("Found the watch expression variable: " + aExp);
if (aDeletionFlag) {
ok(fetchedVariables, "The variables should have been fetched.");
ok(fetchedExpressions, "The variables should have been fetched.");
is(aExp, undefined, "The watch expression should not have been found.");
performCallback(scope);
return;
}
is(aExp.target.querySelector(".name").getAttribute("value"), aNewValue.trim(),
"Should have the right name for '" + aNewValue + "'.");
is(aExp.target.querySelector(".value").getAttribute("value"), aNewResult,
"Should have the right value for '" + aNewValue + "'.");
performCallback(scope);
}
makeChangesAndExitInputMode();
});
function performCallback(scope) {
executeSoon(function() {
aTest(scope);
aCallback(scope);
});
}
}
function testExprDeletion(aVar, aTest, aCallback, aArgResult,
aLocalScopeIndex = 1, aFinalFlag = null)
{
let testContinued = false;
let fetchedVariables = false;
let fetchedExpressions = false;
let countV = 0;
gDebugger.addEventListener("Debugger:FetchedVariables", function testV() {
// We expect 2 Debugger:FetchedVariables events, one from the global
// object scope and the regular one.
if (++countV < 2) {
info("Number of received Debugger:FetchedVariables events: " + countV);
return;
}
gDebugger.removeEventListener("Debugger:FetchedVariables", testV, false);
fetchedVariables = true;
executeSoon(continueTest);
}, false);
let countE = 0;
gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function testE() {
// We expect only one Debugger:FetchedWatchExpressions event, since all
// expressions are evaluated at the same time.
if (++countE < 1) {
info("Number of received Debugger:FetchedWatchExpressions events: " + countE);
return;
}
gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", testE, false);
fetchedExpressions = true;
executeSoon(continueTest);
}, false);
function continueTest() {
if ((testContinued || !fetchedVariables || !fetchedExpressions) && !aFinalFlag) {
return;
}
testContinued = true;
// Get the variable reference anew, since the old ones were discarded when
// we resumed.
var localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".scope")[aLocalScopeIndex],
localNodes = localScope.querySelector(".details").childNodes,
aArg = localNodes[1];
is(aArg.querySelector(".value").getAttribute("value"), aArgResult,
"Should have the right value for 'aArg'.");
let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
let scope = gVars._currHierarchy.get(label);
info("Found the watch expressions scope: " + scope);
if (aFinalFlag) {
ok(fetchedVariables, "The variables should have been fetched.");
ok(!fetchedExpressions, "The variables should never have been fetched.");
is(scope, undefined, "The watch expressions scope should not have been found.");
performCallback(scope);
return;
}
let aExp = scope.get(aVar.querySelector(".name").getAttribute("value"));
info("Found the watch expression variable: " + aExp);
is(aExp, undefined, "Should not have found the watch expression after deletion.");
performCallback(scope);
}
function performCallback(scope) {
executeSoon(function() {
aTest(scope);
aCallback(scope);
});
}
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".dbg-variables-delete"),
gDebugger);
}
function test1(scope) {
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
"There should be 5 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 5, "There should be 5 evaluations availalble");
is(gWatch._cache[0].target.inputNode.value, "document.title = 43",
"The first textbox input value is not the correct one");
is(gWatch._cache[0].attachment.expression, "document.title = 43",
"The first textbox input value is not the correct one");
is(gWatch._cache[1].target.inputNode.value, "document.title",
"The second textbox input value is not the correct one");
is(gWatch._cache[1].attachment.expression, "document.title",
"The second textbox input value is not the correct one");
is(gWatch._cache[2].target.inputNode.value, "aArg",
"The third textbox input value is not the correct one");
is(gWatch._cache[2].attachment.expression, "aArg",
"The third textbox input value is not the correct one");
is(gWatch._cache[3].target.inputNode.value, "ermahgerd",
"The fourth textbox input value is not the correct one");
is(gWatch._cache[3].attachment.expression, "ermahgerd",
"The fourth textbox input value is not the correct one");
is(gWatch._cache[4].target.inputNode.value, "this",
"The fifth textbox input value is not the correct one");
is(gWatch._cache[4].attachment.expression, "this",
"The fifth textbox input value is not the correct one");
}
function test2(scope) {
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 5,
"There should be 5 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 5, "There should be 5 evaluations availalble");
is(gWatch._cache[0].target.inputNode.value, "document.title = 43",
"The first textbox input value is not the correct one");
is(gWatch._cache[0].attachment.expression, "document.title = 43",
"The first textbox input value is not the correct one");
is(gWatch._cache[1].target.inputNode.value, "document.title",
"The second textbox input value is not the correct one");
is(gWatch._cache[1].attachment.expression, "document.title",
"The second textbox input value is not the correct one");
is(gWatch._cache[2].target.inputNode.value, "aArg = 44",
"The third textbox input value is not the correct one");
is(gWatch._cache[2].attachment.expression, "aArg = 44",
"The third textbox input value is not the correct one");
is(gWatch._cache[3].target.inputNode.value, "ermahgerd",
"The fourth textbox input value is not the correct one");
is(gWatch._cache[3].attachment.expression, "ermahgerd",
"The fourth textbox input value is not the correct one");
is(gWatch._cache[4].target.inputNode.value, "this",
"The fifth textbox input value is not the correct one");
is(gWatch._cache[4].attachment.expression, "this",
"The fifth textbox input value is not the correct one");
}
function test3(scope) {
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 4,
"There should be 4 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 4, "There should be 4 evaluations availalble");
is(gWatch._cache[0].target.inputNode.value, "document.title = 43",
"The first textbox input value is not the correct one");
is(gWatch._cache[0].attachment.expression, "document.title = 43",
"The first textbox input value is not the correct one");
is(gWatch._cache[1].target.inputNode.value, "document.title",
"The second textbox input value is not the correct one");
is(gWatch._cache[1].attachment.expression, "document.title",
"The second textbox input value is not the correct one");
is(gWatch._cache[2].target.inputNode.value, "ermahgerd",
"The third textbox input value is not the correct one");
is(gWatch._cache[2].attachment.expression, "ermahgerd",
"The third textbox input value is not the correct one");
is(gWatch._cache[3].target.inputNode.value, "this",
"The fourth textbox input value is not the correct one");
is(gWatch._cache[3].attachment.expression, "this",
"The fourth textbox input value is not the correct one");
}
function test4(scope) {
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 3,
"There should be 3 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 3, "There should be 3 evaluations availalble");
is(gWatch._cache[0].target.inputNode.value, "document.title",
"The first textbox input value is not the correct one");
is(gWatch._cache[0].attachment.expression, "document.title",
"The first textbox input value is not the correct one");
is(gWatch._cache[1].target.inputNode.value, "ermahgerd",
"The second textbox input value is not the correct one");
is(gWatch._cache[1].attachment.expression, "ermahgerd",
"The second textbox input value is not the correct one");
is(gWatch._cache[2].target.inputNode.value, "this",
"The third textbox input value is not the correct one");
is(gWatch._cache[2].attachment.expression, "this",
"The third textbox input value is not the correct one");
}
function test5(scope) {
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 2,
"There should be 2 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 2, "There should be 2 evaluations availalble");
is(gWatch._cache[0].target.inputNode.value, "ermahgerd",
"The second textbox input value is not the correct one");
is(gWatch._cache[0].attachment.expression, "ermahgerd",
"The second textbox input value is not the correct one");
is(gWatch._cache[1].target.inputNode.value, "this",
"The third textbox input value is not the correct one");
is(gWatch._cache[1].attachment.expression, "this",
"The third textbox input value is not the correct one");
}
function test6(scope) {
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 1,
"There should be 1 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
ok(scope, "There should be a wach expressions scope in the variables view");
is(scope._store.size, 1, "There should be 1 evaluation availalble");
is(gWatch._cache[0].target.inputNode.value, "ermahgerd",
"The third textbox input value is not the correct one");
is(gWatch._cache[0].attachment.expression, "ermahgerd",
"The third textbox input value is not the correct one");
}
function test7(scope) {
is(gWatch._container._parent.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
"There should be 0 hidden nodes in the watch expressions container");
is(gWatch._container._parent.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
"There should be 0 visible nodes in the watch expressions container");
is(scope, undefined, "There should be no watch expressions scope available.");
is(gWatch._cache.length, 0, "The watch expressions cache should be empty.");
}
function addWatchExpression(string) {
gWatch.addExpression(string);
gDebugger.editor.focus();
}
function resumeAndFinish() {
gDebugger.DebuggerController.activeThread.resume(function() {
closeDebuggerAndFinish();
});
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;
gTab = null;
gDebuggee = null;
gDebugger = null;
gWatch = null;
gVars = null;
});

View File

@ -4,6 +4,10 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
/**
* Make sure that the editing variables or properties values works properly.
*/
const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
var gPane = null;
@ -70,7 +74,7 @@ function testModification(aVar, aCallback, aNewValue, aNewResult) {
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
ok(aVar.querySelector(".element-value-input"),
"There should be an input element created.");
let count = 0;

View File

@ -6,7 +6,8 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<script type="text/javascript">
function ermahgerd() {
function ermahgerd(aArg) {
var t = document.title;
debugger;
(function() {
var a = undefined;

View File

@ -20,7 +20,9 @@ this.EXPORTED_SYMBOLS = ["VariablesView", "create"];
* Requires the devtools common.css and debugger.css skin stylesheets.
*
* To allow replacing variable or property values in this view, provide an
* "eval" function property.
* "eval" function property. To allow replacing variable or property values,
* provide a "switch" function. To handle deleting variables or properties,
* provide a "delete" function.
*
* @param nsIDOMNode aParentNode
* The parent node to hold this view.
@ -415,6 +417,8 @@ function Scope(aView, aName, aFlags = {}) {
this.ownerView = aView;
this.eval = aView.eval;
this.switch = aView.switch;
this.delete = aView.delete;
this._store = new Map();
this._init(aName.trim(), aFlags);
@ -635,6 +639,24 @@ Scope.prototype = {
*/
set twisty(aFlag) aFlag ? this.showArrow() : this.hideArrow(),
/**
* Specifies if editing variable or property names is allowed.
* This flag applies non-recursively to the current scope.
*/
allowNameInput: false,
/**
* Specifies if editing variable or property values is allowed.
* This flag applies non-recursively to the current scope.
*/
allowValueInput: true,
/**
* Specifies if removing variables or properties values is allowed.
* This flag applies non-recursively to the current scope.
*/
allowDeletion: false,
/**
* Gets the id associated with this item.
* @return string
@ -917,11 +939,14 @@ Scope.prototype = {
* The variable's descriptor.
*/
function Variable(aScope, aName, aDescriptor) {
this._onClose = this._onClose.bind(this);
this._displayTooltip = this._displayTooltip.bind(this);
this._activateInput = this._activateInput.bind(this);
this._deactivateInput = this._deactivateInput.bind(this);
this._saveInput = this._saveInput.bind(this);
this._onInputKeyPress = this._onInputKeyPress.bind(this);
this._activateNameInput = this._activateNameInput.bind(this);
this._activateValueInput = this._activateValueInput.bind(this);
this._deactivateNameInput = this._deactivateNameInput.bind(this);
this._deactivateValueInput = this._deactivateValueInput.bind(this);
this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
Scope.call(this, aScope, aName, aDescriptor);
this._setGrip(aDescriptor.value);
@ -1180,6 +1205,12 @@ create({ constructor: Variable, proto: Scope.prototype }, {
separatorLabel.hidden = true;
valueLabel.hidden = true;
}
if (this.ownerView.allowDeletion) {
let closeNode = this._closeNode = document.createElement("toolbarbutton");
closeNode.className = "dbg-variables-delete plain devtools-closebutton";
closeNode.addEventListener("click", this._onClose, false);
this._title.appendChild(closeNode);
}
},
/**
@ -1215,6 +1246,16 @@ create({ constructor: Variable, proto: Scope.prototype }, {
this._target.appendChild(tooltip);
this._target.setAttribute("tooltip", tooltip.id);
if (this.ownerView.allowNameInput) {
this._name.setAttribute("tooltiptext", L10N.getStr("variablesEditableNameTooltip"));
}
if (this.ownerView.allowValueInput) {
this._valueLabel.setAttribute("tooltiptext", L10N.getStr("variablesEditableValueTooltip"));
}
if (this.ownerView.allowDeletion) {
this._closeNode.setAttribute("tooltiptext", L10N.getStr("variablesCloseButtonTooltip"));
}
},
/**
@ -1255,44 +1296,54 @@ create({ constructor: Variable, proto: Scope.prototype }, {
_addEventListeners: function V__addEventListeners() {
this._arrow.addEventListener("mousedown", this.toggle, false);
this._name.addEventListener("mousedown", this.toggle, false);
this._valueLabel.addEventListener("click", this._activateInput, false);
this._name.addEventListener("dblclick", this._activateNameInput, false);
this._valueLabel.addEventListener("click", this._activateValueInput, false);
},
/**
* Makes this variable's value editable.
* The click listener for the close button.
*/
_activateInput: function V__activateInput(e) {
if (!this.eval) {
return;
}
let window = this.window;
let document = this.document;
_onClose: function V__onClose() {
this.hide();
let title = this._title;
let valueLabel = this._valueLabel;
let initialString = this._valueLabel.getAttribute("value");
if (this.delete) {
this.delete(this);
}
},
/**
* Creates a textbox node in place of a label.
*
* @param nsIDOMNode aLabel
* The label to be replaced with a textbox.
* @param string aClassName
* The class to be applied to the textbox.
* @param object aCallbacks
* An object containing the onKeypress and onBlur callbacks.
*/
_activateInput: function V__activateInput(aLabel, aClassName, aCallbacks) {
let initialString = aLabel.getAttribute("value");
// Create a texbox input element which will be shown in the current
// element's value location.
// element's specified label location.
let input = this.document.createElement("textbox");
input.setAttribute("value", initialString);
input.className = "plain element-input";
input.className = "plain " + aClassName;
input.width = this._target.clientWidth;
title.removeChild(valueLabel);
title.appendChild(input);
aLabel.parentNode.replaceChild(input, aLabel);
input.select();
// When the value is a string (displayed as "value"), then we probably want
// to change it to another string in the textbox, so to avoid typing the ""
// again, tackle with the selection bounds just a bit.
if (valueLabel.getAttribute("value").match(/^"[^"]*"$/)) {
if (aLabel.getAttribute("value").match(/^"[^"]*"$/)) {
input.selectionEnd--;
input.selectionStart++;
}
input.addEventListener("keypress", this._onInputKeyPress, false);
input.addEventListener("blur", this._deactivateInput, false);
input.addEventListener("keypress", aCallbacks.onKeypress, false);
input.addEventListener("blur", aCallbacks.onBlur, false);
this._prevExpandable = this.twisty;
this._prevExpanded = this.expanded;
@ -1302,18 +1353,17 @@ create({ constructor: Variable, proto: Scope.prototype }, {
},
/**
* Deactivates this variable's editable mode.
* Removes the textbox node in place of a label.
*
* @param nsIDOMNode aLabel
* The label which was replaced with a textbox.
* @param object aCallbacks
* An object containing the onKeypress and onBlur callbacks.
*/
_deactivateInput: function V__deactivateInput(e) {
let input = e.target;
let title = this._title;
let valueLabel = this._valueLabel;
title.removeChild(input);
title.appendChild(valueLabel);
input.removeEventListener("keypress", this._onInputKeyPress, false);
input.removeEventListener("blur", this._deactivateInput, false);
_deactivateInput: function V__deactivateInput(aLabel, aInput, aCallbacks) {
aInput.parentNode.replaceChild(aLabel, aInput);
aInput.removeEventListener("keypress", aCallbacks.onKeypress, false);
aInput.removeEventListener("blur", aCallbacks.onBlur, false);
this._locked = false;
this.twisty = this._prevExpandable;
@ -1321,37 +1371,123 @@ create({ constructor: Variable, proto: Scope.prototype }, {
},
/**
* Deactivates this variable's editable mode and evaluates a new value.
* Makes this variable's name editable.
*/
_saveInput: function V__saveInput(e) {
let input = e.target;
let valueLabel = this._valueLabel;
let initialString = this._valueLabel.getAttribute("value");
let currentString = input.value;
_activateNameInput: function V__activateNameInput() {
if (!this.ownerView.allowNameInput || !this.switch) {
return;
}
this._activateInput(this._name, "element-name-input", {
onKeypress: this._onNameInputKeyPress,
onBlur: this._deactivateNameInput
});
this._separatorLabel.hidden = true;
this._valueLabel.hidden = true;
},
this._deactivateInput(e);
/**
* Deactivates this variable's editable name mode.
*/
_deactivateNameInput: function V__deactivateNameInput(e) {
this._deactivateInput(this._name, e.target, {
onKeypress: this._onNameInputKeyPress,
onBlur: this._deactivateNameInput
});
this._separatorLabel.hidden = false;
this._valueLabel.hidden = false;
},
/**
* Makes this variable's value editable.
*/
_activateValueInput: function V__activateValueInput() {
if (!this.ownerView.allowValueInput || !this.eval) {
return;
}
this._activateInput(this._valueLabel, "element-value-input", {
onKeypress: this._onValueInputKeyPress,
onBlur: this._deactivateValueInput
});
},
/**
* Deactivates this variable's editable value mode.
*/
_deactivateValueInput: function V__deactivateValueInput(e) {
this._deactivateInput(this._valueLabel, e.target, {
onKeypress: this._onValueInputKeyPress,
onBlur: this._deactivateValueInput
});
},
/**
* Disables this variable prior to a new name switch or value evaluation.
*/
_disable: function V__disable() {
this.twisty = false;
this._separatorLabel.hidden = true;
this._valueLabel.hidden = true;
this._enum.hidden = true;
this._nonenum.hidden = true;
},
/**
* Deactivates this variable's editable mode and callbacks the new name.
*/
_saveNameInput: function V__saveNameInput(e) {
let input = e.target;
let initialString = this._name.getAttribute("value");
let currentString = input.value.trim();
this._deactivateNameInput(e);
if (initialString != currentString) {
this._arrow.setAttribute("invisible", "");
this._separatorLabel.hidden = true;
this._valueLabel.hidden = true;
this._enum.hidden = true;
this._nonenum.hidden = true;
this.eval("(" + this._symbolicName + "=" + currentString + ")");
this._disable();
this._name.value = currentString;
this.switch(this, currentString);
}
},
/**
* The key press listener for this variable's editable mode textbox.
* Deactivates this variable's editable mode and evaluates the new value.
*/
_onInputKeyPress: function V__onInputKeyPress(e) {
_saveValueInput: function V__saveValueInput(e) {
let input = e.target;
let initialString = this._valueLabel.getAttribute("value");
let currentString = input.value.trim();
this._deactivateValueInput(e);
if (initialString != currentString) {
this._disable();
this.eval(this._symbolicName + "=" + currentString);
}
},
/**
* The key press listener for this variable's editable name textbox.
*/
_onNameInputKeyPress: function V__onNameInputKeyPress(e) {
switch(e.keyCode) {
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
this._saveInput(e);
this._saveNameInput(e);
return;
case e.DOM_VK_ESCAPE:
this._deactivateInput(e);
this._deactivateNameInput(e);
return;
}
},
/**
* The key press listener for this variable's editable value textbox.
*/
_onValueInputKeyPress: function V__onValueInputKeyPress(e) {
switch(e.keyCode) {
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
this._saveValueInput(e);
return;
case e.DOM_VK_ESCAPE:
this._deactivateValueInput(e);
return;
}
},
@ -1361,6 +1497,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
_initialDescriptor: null,
_separatorLabel: null,
_valueLabel: null,
_closeNode: null,
_tooltip: null,
_valueGrip: null,
_valueString: "",
@ -1700,6 +1837,7 @@ XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() {
/**
* The separator label between the variables or properties name and value.
* This property applies non-recursively to the current scope.
*/
Scope.prototype.separator = L10N.getStr("variablesSeparatorLabel");

View File

@ -173,6 +173,18 @@ watchExpressionsScopeLabel=Watch expressions
# the global scope.
globalScopeLabel=Global
# LOCALIZATION NOTE (variablesEditableNameTooltip): The text that is displayed
# in the variables list on an item with an editable name.
variablesEditableNameTooltip=Double click to edit
# LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed
# in the variables list on an item with an editable name.
variablesEditableValueTooltip=Click to change value
# LOCALIZATION NOTE (variablesCloseButtonTooltip): The text that is displayed
# in the variables list on an item with which can be removed.
variablesCloseButtonTooltip=Click to remove
# LOCALIZATION NOTE (variablesSeparatorLabel): The text that is displayed
# in the variables list as a separator between the name and value.
variablesSeparatorLabel=:

View File

@ -161,6 +161,10 @@
font-weight: 600;
}
.dbg-stackframe-details {
-moz-padding-start: 4px;
}
/**
* Breakpoints view
*/
@ -209,10 +213,6 @@
-moz-padding-start: 8px;
}
.dbg-expression:last-child {
margin-bottom: 4px;
}
.dbg-expression-arrow {
width: 10px;
height: auto;
@ -236,6 +236,11 @@
min-height: 10px;
}
.dbg-variables-delete:not(:hover) {
-moz-image-region: rect(0, 32px, 16px, 16px);
opacity: 0.5;
}
/**
* Scope element
*/
@ -278,6 +283,7 @@
.variable > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
}
.variable:not([non-header]) > .details {
@ -304,6 +310,7 @@
.property > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
}
.property:not([non-header]) > .details {
@ -373,10 +380,16 @@
* Variables and properties editing
*/
#variables .element-input {
#variables .element-value-input {
-moz-margin-start: 5px !important;
}
#variables .element-name-input {
-moz-margin-start: -1px !important;
color: #048;
font-weight: 600;
}
/**
* Variables and properties searching
*/

View File

@ -163,6 +163,10 @@
font-weight: 600;
}
.dbg-stackframe-details {
-moz-padding-start: 4px;
}
/**
* Breakpoints view
*/
@ -211,10 +215,6 @@
-moz-padding-start: 8px;
}
.dbg-expression:last-child {
margin-bottom: 4px;
}
.dbg-expression-arrow {
width: 10px;
height: auto;
@ -238,6 +238,11 @@
min-height: 10px;
}
.dbg-variables-delete:not(:hover) {
-moz-image-region: rect(0, 32px, 16px, 16px);
opacity: 0.5;
}
/**
* Scope element
*/
@ -280,6 +285,7 @@
.variable > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
}
.variable:not([non-header]) > .details {
@ -306,6 +312,7 @@
.property > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
}
.property:not([non-header]) > .details {
@ -375,10 +382,16 @@
* Variables and properties editing
*/
#variables .element-input {
#variables .element-value-input {
-moz-margin-start: 5px !important;
}
#variables .element-name-input {
-moz-margin-start: -1px !important;
color: #048;
font-weight: 600;
}
/**
* Variables and properties searching
*/

View File

@ -169,6 +169,10 @@
font-weight: 600;
}
.dbg-stackframe-details {
-moz-padding-start: 4px;
}
/**
* Breakpoints view
*/
@ -217,10 +221,6 @@
-moz-padding-start: 8px;
}
.dbg-expression:last-child {
margin-bottom: 4px;
}
.dbg-expression-arrow {
width: 10px;
height: auto;
@ -244,6 +244,11 @@
min-height: 10px;
}
.dbg-variables-delete:not(:hover) {
-moz-image-region: rect(0, 32px, 16px, 16px);
opacity: 0.5;
}
/**
* Scope element
*/
@ -286,6 +291,7 @@
.variable > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
}
.variable:not([non-header]) > .details {
@ -312,6 +318,7 @@
.property > .title > .value {
-moz-padding-start: 6px;
-moz-padding-end: 4px;
}
.property:not([non-header]) > .details {
@ -381,10 +388,16 @@
* Variables and properties editing
*/
#variables .element-input {
#variables .element-value-input {
-moz-margin-start: 5px !important;
}
#variables .element-name-input {
-moz-margin-start: -1px !important;
color: #048;
font-weight: 600;
}
/**
* Variables and properties searching
*/