Bug 1574192 - Initial watchpoints front end commit. r=nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D43487

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Miriam 2019-09-14 14:13:54 +00:00
Родитель 2b19541fd2
Коммит 57718b6d7a
29 изменённых файлов: 593 добавлений и 38 удалений

Просмотреть файл

@ -7,7 +7,8 @@
import type { GripProperties, Node, Props, ReduxAction } from "./types";
const { loadItemProperties } = require("./utils/load-properties");
const { getLoadedProperties, getActors } = require("./reducer");
const { getPathExpression, getValue } = require("./utils/node");
const { getLoadedProperties, getActors, getWatchpoints } = require("./reducer");
type Dispatch = ReduxAction => void;
@ -73,6 +74,50 @@ function nodePropertiesLoaded(
};
}
/*
* This action adds a property watchpoint to an object
*/
function addWatchpoint(item, watchpoint: string) {
return async function({ dispatch, client }: ThunkArgs) {
const { parent, name } = item;
const object = getValue(parent);
if (!object) {
return;
}
const path = parent.path;
const property = name;
const label = getPathExpression(item);
const actor = object.actor;
await client.addWatchpoint(object, property, label, watchpoint);
dispatch({
type: "SET_WATCHPOINT",
data: { path, watchpoint, property, actor },
});
};
}
/*
* This action removes a property watchpoint from an object
*/
function removeWatchpoint(item) {
return async function({ dispatch, client }: ThunkArgs) {
const object = getValue(item.parent);
const property = item.name;
const path = item.parent.path;
const actor = object.actor;
await client.removeWatchpoint(object, property);
dispatch({
type: "REMOVE_WATCHPOINT",
data: { path, property, actor },
});
};
}
function closeObjectInspector() {
return async ({ getState, client }: ThunkArg) => {
releaseActors(getState(), client);
@ -99,8 +144,14 @@ function rootsChanged(props: Props) {
function releaseActors(state, client) {
const actors = getActors(state);
const watchpoints = getWatchpoints(state);
for (const actor of actors) {
client.releaseActor(actor);
// Watchpoints are stored in object actors.
// If we release the actor we lose the watchpoint.
if (!watchpoints.has(actor)) {
client.releaseActor(actor);
}
}
}
@ -138,4 +189,6 @@ module.exports = {
nodeLoadProperties,
nodePropertiesLoaded,
rootsChanged,
addWatchpoint,
removeWatchpoint,
};

Просмотреть файл

@ -55,3 +55,31 @@
.tree-node.focused button.invoke-getter {
background-color: currentColor;
}
button.remove-set-watchpoint {
mask: url("resource://devtools/client/debugger/images/webconsole-logpoint.svg")
no-repeat;
display: inline-block;
vertical-align: top;
height: 15px;
width: 15px;
margin: 0 4px 0px 20px;
padding: 0;
border: none;
background-color: var(--breakpoint-fill);
cursor: pointer;
}
button.remove-get-watchpoint {
mask: url("resource://devtools/client/debugger/images/webconsole-logpoint.svg")
no-repeat;
display: inline-block;
vertical-align: top;
height: 15px;
width: 15px;
margin: 0 4px 0px 20px;
padding: 0;
border: none;
background-color: var(--purple-60);
cursor: pointer;
}

Просмотреть файл

@ -110,6 +110,12 @@ class ObjectInspector extends Component<Props> {
return;
}
for (const [path, properties] of nextProps.loadedProperties) {
if (properties !== this.props.loadedProperties.get(path)) {
this.cachedNodes.delete(path);
}
}
// If there are new evaluations, we want to remove the existing cached
// nodes from the cache.
if (nextProps.evaluations > this.props.evaluations) {
@ -134,6 +140,7 @@ class ObjectInspector extends Component<Props> {
// - OR the focused node changed.
// - OR the active node changed.
return (
loadedProperties !== nextProps.loadedProperties ||
loadedProperties.size !== nextProps.loadedProperties.size ||
evaluations.size !== nextProps.evaluations.size ||
(expandedPaths.size !== nextProps.expandedPaths.size &&
@ -271,7 +278,6 @@ class ObjectInspector extends Component<Props> {
autoExpandAll,
autoExpandDepth,
initiallyExpanded,
isExpanded: item => expandedPaths && expandedPaths.has(item.path),
isExpandable: this.isNodeExpandable,
focused: this.focusedItem,

Просмотреть файл

@ -80,6 +80,12 @@ type Props = {
};
class ObjectInspectorItem extends Component<Props> {
static get defaultProps() {
return {
onContextMenu: () => {},
};
}
// eslint-disable-next-line complexity
getLabelAndValue(): {
value?: string | Element,
@ -203,6 +209,7 @@ class ObjectInspectorItem extends Component<Props> {
onCmdCtrlClick,
onDoubleClick,
dimTopLevelWindow,
onContextMenu,
} = this.props;
const parentElementProps: Object = {
@ -245,6 +252,7 @@ class ObjectInspectorItem extends Component<Props> {
e.stopPropagation();
}
},
onContextMenu: e => onContextMenu(e, item),
};
if (onDoubleClick) {
@ -292,6 +300,21 @@ class ObjectInspectorItem extends Component<Props> {
);
}
renderWatchpointButton() {
const { item, removeWatchpoint } = this.props;
if (!item || !item.contents || !item.contents.watchpoint) {
return;
}
const watchpoint = item.contents.watchpoint;
return dom.button({
className: `remove-${watchpoint}-watchpoint`,
title: L10N.getStr("watchpoints.removeWatchpoint"),
onClick: () => removeWatchpoint(item),
});
}
render() {
const { arrow } = this.props;
@ -307,7 +330,8 @@ class ObjectInspectorItem extends Component<Props> {
arrow,
labelElement,
delimiter,
value
value,
this.renderWatchpointButton()
);
}
}

Просмотреть файл

@ -5,5 +5,6 @@
const ObjectInspector = require("./components/ObjectInspector");
const utils = require("./utils");
const reducer = require("./reducer");
const actions = require("./actions");
module.exports = { ObjectInspector, utils, reducer };
module.exports = { ObjectInspector, utils, actions, reducer };

Просмотреть файл

@ -4,12 +4,14 @@
import type { ReduxAction, State } from "./types";
function initialState() {
function initialState(overrides) {
return {
expandedPaths: new Set(),
loadedProperties: new Map(),
evaluations: new Map(),
actors: new Set(),
watchpoints: new Map(),
...overrides,
};
}
@ -33,6 +35,34 @@ function reducer(
return cloneState({ expandedPaths });
}
if (type == "SET_WATCHPOINT") {
const { watchpoint, property, path } = data;
const obj = state.loadedProperties.get(path);
return cloneState({
loadedProperties: new Map(state.loadedProperties).set(
path,
updateObject(obj, property, watchpoint)
),
watchpoints: new Map(state.watchpoints).set(data.actor, data.watchpoint),
});
}
if (type === "REMOVE_WATCHPOINT") {
const { path, property, actor } = data;
const obj = state.loadedProperties.get(path);
const watchpoints = new Map(state.watchpoints);
watchpoints.delete(actor);
return cloneState({
loadedProperties: new Map(state.loadedProperties).set(
path,
updateObject(obj, property, null)
),
watchpoints: watchpoints,
});
}
if (type === "NODE_PROPERTIES_LOADED") {
return cloneState({
actors: data.actor
@ -66,12 +96,25 @@ function reducer(
// NOTE: we clear the state on resume because otherwise the scopes pane
// would be out of date. Bug 1514760
if (type === "RESUME" || type == "NAVIGATE") {
return initialState();
return initialState({ watchpoints: state.watchpoints });
}
return state;
}
function updateObject(obj, property, watchpoint) {
return {
...obj,
ownProperties: {
...obj.ownProperties,
[property]: {
...obj.ownProperties[property],
watchpoint,
},
},
};
}
function getObjectInspectorState(state) {
return state.objectInspector;
}
@ -88,6 +131,10 @@ function getActors(state) {
return getObjectInspectorState(state).actors;
}
function getWatchpoints(state) {
return getObjectInspectorState(state).watchpoints;
}
function getLoadedProperties(state) {
return getObjectInspectorState(state).loadedProperties;
}
@ -102,6 +149,7 @@ function getEvaluations(state) {
const selectors = {
getActors,
getWatchpoints,
getEvaluations,
getExpandedPathKeys,
getExpandedPaths,

Просмотреть файл

@ -24,6 +24,7 @@ exports[`ObjectInspector - classnames has the expected class 1`] = `
<div
className="node object-node"
onClick={[Function]}
onContextMenu={[Function]}
>
<span
className="object-label"
@ -69,6 +70,7 @@ exports[`ObjectInspector - classnames has the inline class when inline prop is t
<div
className="node object-node"
onClick={[Function]}
onContextMenu={[Function]}
>
<span
className="object-label"
@ -114,6 +116,7 @@ exports[`ObjectInspector - classnames has the nowrap class when disableWrap prop
<div
className="node object-node"
onClick={[Function]}
onContextMenu={[Function]}
>
<span
className="object-label"

Просмотреть файл

@ -25,6 +25,7 @@ exports[`ObjectInspector - dimTopLevelWindow renders collapsed top-level window
<div
className="node object-node"
onClick={[Function]}
onContextMenu={[Function]}
>
<button
className="arrow"
@ -80,6 +81,7 @@ exports[`ObjectInspector - dimTopLevelWindow renders sub-level window 1`] = `
<div
className="node object-node focused"
onClick={[Function]}
onContextMenu={[Function]}
>
<button
className="arrow expanded"
@ -109,6 +111,7 @@ exports[`ObjectInspector - dimTopLevelWindow renders sub-level window 1`] = `
<div
className="node object-node"
onClick={[Function]}
onContextMenu={[Function]}
>
<button
className="arrow"
@ -163,6 +166,7 @@ exports[`ObjectInspector - dimTopLevelWindow renders window as expected when dim
<div
className="node object-node lessen"
onClick={[Function]}
onContextMenu={[Function]}
>
<button
className="arrow"
@ -218,6 +222,7 @@ exports[`ObjectInspector - dimTopLevelWindow renders window as expected when dim
<div
className="node object-node focused"
onClick={[Function]}
onContextMenu={[Function]}
>
<button
className="arrow expanded"
@ -261,6 +266,7 @@ exports[`ObjectInspector - dimTopLevelWindow renders window as expected when dim
<div
className="node object-node lessen"
onClick={[Function]}
onContextMenu={[Function]}
>
<span
className="object-label"

Просмотреть файл

@ -61,6 +61,7 @@ describe("release actors", () => {
{
initialState: {
actors: new Set(["actor 1", "actor 2"]),
watchpoints: new Map(),
},
}
);

Просмотреть файл

@ -20,11 +20,7 @@ describe("makeNumericalBuckets", () => {
expect(names).toEqual(["[0…99]", "[100…199]", "[200…233]"]);
expect(paths).toEqual([
"root◦[0…99]",
"root◦[100…199]",
"root◦[200…233]",
]);
expect(paths).toEqual(["root◦[0…99]", "root◦[100…199]", "root◦[200…233]"]);
});
// TODO: Re-enable when we have support for lonely node.
@ -60,10 +56,7 @@ describe("makeNumericalBuckets", () => {
expect(names).toEqual(["[0…99]", "[100…101]"]);
expect(paths).toEqual([
"root◦bucket_0-99",
"root◦bucket_100-101",
]);
expect(paths).toEqual(["root◦bucket_0-99", "root◦bucket_100-101"]);
});
it("creates sub-buckets when needed", () => {
@ -134,9 +127,7 @@ describe("makeNumericalBuckets", () => {
"[23300…23399]",
"[23400…23455]",
]);
expect(lastBucketPaths[0]).toEqual(
"root◦[23000…23455]◦[23000…23099]"
);
expect(lastBucketPaths[0]).toEqual("root◦[23000…23455]◦[23000…23099]");
expect(lastBucketPaths[lastBucketPaths.length - 1]).toEqual(
"root◦[23000…23455]◦[23400…23455]"
);

Просмотреть файл

@ -816,6 +816,15 @@ function getChildren(options: {
return addToCache(makeNodesForProperties(loadedProps, item));
}
// Builds an expression that resolves to the value of the item in question
// e.g. `b` in { a: { b: 2 } } resolves to `a.b`
function getPathExpression(item) {
if (item && item.parent) {
return `${getPathExpression(item.parent)}.${item.name}`;
}
return item.name;
}
function getParent(item: Node): Node | null {
return item.parent;
}
@ -922,6 +931,7 @@ module.exports = {
getChildrenWithEvaluations,
getClosestGripNode,
getClosestNonBucketNode,
getPathExpression,
getParent,
getParentGripValue,
getNonPrototypeParentGripValue,

Просмотреть файл

@ -22,6 +22,9 @@ import * as threads from "./threads";
import * as toolbox from "./toolbox";
import * as preview from "./preview";
// eslint-disable-next-line import/named
import { objectInspector } from "devtools-reps";
export default {
...ast,
...navigation,
@ -34,6 +37,7 @@ export default {
...pause,
...ui,
...fileSearch,
...objectInspector.actions,
...projectTextSearch,
...quickOpen,
...sourceTree,

Просмотреть файл

@ -188,6 +188,25 @@ function removeXHRBreakpoint(path: string, method: string) {
return currentThreadFront.removeXHRBreakpoint(path, method);
}
function addWatchpoint(
object: Grip,
property: string,
label: string,
watchpointType: string
) {
if (currentTarget.traits.watchpoints) {
const objectClient = createObjectClient(object);
return objectClient.addWatchpoint(property, label, watchpointType);
}
}
function removeWatchpoint(object: Grip, property: string) {
if (currentTarget.traits.watchpoints) {
const objectClient = createObjectClient(object);
return objectClient.removeWatchpoint(property);
}
}
// Get the string key to use for a breakpoint location.
// See also duplicate code in breakpoint-actor-map.js :(
function locationKey(location: BreakpointLocation) {
@ -514,6 +533,8 @@ const clientCommands = {
setBreakpoint,
setXHRBreakpoint,
removeXHRBreakpoint,
addWatchpoint,
removeWatchpoint,
removeBreakpoint,
evaluate,
evaluateInFrame,

Просмотреть файл

@ -260,7 +260,7 @@ export type DebuggerClient = {
connect: () => Promise<*>,
request: (packet: Object) => Promise<*>,
attachConsole: (actor: String, listeners: Array<*>) => Promise<*>,
createObjectClient: (grip: Grip) => {},
createObjectClient: (grip: Grip) => ObjectClient,
release: (actor: String) => {},
};
@ -338,6 +338,12 @@ export type SourceClient = {
*/
export type ObjectClient = {
getPrototypeAndProperties: () => any,
addWatchpoint: (
property: string,
label: string,
watchpointType: string
) => {},
removeWatchpoint: (property: string) => {},
};
/**

Просмотреть файл

@ -4,9 +4,11 @@
// @flow
import React, { PureComponent } from "react";
import { showMenu } from "devtools-contextmenu";
import { connect } from "../../utils/connect";
import actions from "../../actions";
import { createObjectClient } from "../../client/firefox";
import { features } from "../../utils/prefs";
import {
getSelectedSource,
@ -45,6 +47,8 @@ type Props = {
unHighlightDomElement: typeof actions.unHighlightDomElement,
toggleMapScopes: typeof actions.toggleMapScopes,
setExpandedScope: typeof actions.setExpandedScope,
addWatchpoint: typeof actions.addWatchpoint,
removeWatchpoint: typeof actions.removeWatchpoint,
expandedScopes: string[],
};
@ -111,6 +115,50 @@ class Scopes extends PureComponent<Props, State> {
this.props.toggleMapScopes();
};
onContextMenu = (event, item) => {
const { addWatchpoint, removeWatchpoint } = this.props;
if (!features.watchpoints || !item.parent || !item.parent.contents) {
return;
}
if (!item.contents || item.contents.watchpoint) {
const removeWatchpointItem = {
id: "node-menu-remove-watchpoint",
// NOTE: we're going to update the UI to add a "break on..."
// sub menu. At that point we'll translate the strings. bug 1580591
label: "Remove watchpoint",
disabled: false,
click: () => removeWatchpoint(item),
};
const menuItems = [removeWatchpointItem];
return showMenu(event, menuItems);
}
// NOTE: we're going to update the UI to add a "break on..."
// sub menu. At that point we'll translate the strings. bug 1580591
const addSetWatchpointLabel = "Pause on set";
const addGetWatchpointLabel = "Pause on get";
const addSetWatchpoint = {
id: "node-menu-add-set-watchpoint",
label: addSetWatchpointLabel,
disabled: false,
click: () => addWatchpoint(item, "set"),
};
const addGetWatchpoint = {
id: "node-menu-add-get-watchpoint",
label: addGetWatchpointLabel,
disabled: false,
click: () => addWatchpoint(item, "get"),
};
const menuItems = [addGetWatchpoint, addSetWatchpoint];
showMenu(event, menuItems);
};
renderScopesList() {
const {
cx,
@ -147,6 +195,7 @@ class Scopes extends PureComponent<Props, State> {
onInspectIconClick={grip => openElementInInspector(grip)}
onDOMNodeMouseOver={grip => highlightDomElement(grip)}
onDOMNodeMouseOut={grip => unHighlightDomElement(grip)}
onContextMenu={this.onContextMenu}
setExpanded={(path, expand) => setExpandedScope(cx, path, expand)}
initiallyExpanded={initiallyExpanded}
/>
@ -223,5 +272,7 @@ export default connect(
unHighlightDomElement: actions.unHighlightDomElement,
toggleMapScopes: actions.toggleMapScopes,
setExpandedScope: actions.setExpandedScope,
addWatchpoint: actions.addWatchpoint,
removeWatchpoint: actions.removeWatchpoint,
}
)(Scopes);

Просмотреть файл

@ -644,6 +644,16 @@ export function getInlinePreviews(
];
}
export function getSelectedInlinePreviews(state: State) {
const thread = getCurrentThread(state);
const frameId = getSelectedFrameId(state, thread);
if (!frameId) {
return null;
}
return getInlinePreviews(state, thread, frameId);
}
export function getInlinePreviewExpression(
state: State,
thread: ThreadId,

Просмотреть файл

@ -17,6 +17,8 @@ const reasons = {
resumeLimit: "whyPaused.resumeLimit",
breakpointConditionThrown: "whyPaused.breakpointConditionThrown",
eventBreakpoint: "whyPaused.eventBreakpoint",
getWatchpoint: "whyPaused.getWatchpoint",
setWatchpoint: "whyPaused.setWatchpoint",
mutationBreakpoint: "whyPaused.mutationBreakpoint",
interrupted: "whyPaused.interrupted",
replayForcedPause: "whyPaused.replayForcedPause",

Просмотреть файл

@ -72,6 +72,7 @@ if (isDevelopment()) {
pref("devtools.debugger.log-actions", true);
pref("devtools.debugger.features.overlay-step-buttons", true);
pref("devtools.debugger.features.log-event-breakpoints", false);
pref("devtools.debugger.features.watchpoints", false);
}
export const prefs = new PrefsHelper("devtools", {
@ -140,6 +141,7 @@ export const features = new PrefsHelper("devtools.debugger.features", {
showOverlayStepButtons: ["Bool", "overlay-step-buttons"],
inlinePreview: ["Bool", "inline-preview"],
logEventBreakpoints: ["Bool", "log-event-breakpoints"],
watchpoints: ["Bool", "watchpoints"],
});
export const asyncStore = asyncStoreHelper("debugger", {

Просмотреть файл

@ -167,3 +167,5 @@ skip-if = (os == 'linux' && debug) || (os == 'linux' && asan) || ccov #Bug 1456
[browser_dbg-bfcache.js]
[browser_dbg-gc-breakpoint-positions.js]
[browser_dbg-gc-sources.js]
[browser_dbg-watchpoints.js]
skip-if = debug

Просмотреть файл

@ -0,0 +1,38 @@
/* 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/>. */
// Tests adding a watchpoint
add_task(async function() {
pushPref("devtools.debugger.features.watchpoints", true);
const dbg = await initDebugger("doc-sources.html");
await navigate(dbg, "doc-watchpoints.html", "doc-watchpoints.html");
await selectSource(dbg, "doc-watchpoints.html");
await waitForPaused(dbg);
info(`Add a get watchpoint at b`);
await toggleScopeNode(dbg, 3);
const addedWatchpoint = waitForDispatch(dbg, "SET_WATCHPOINT");
await rightClickScopeNode(dbg, 5);
selectContextMenuItem(dbg, selectors.addGetWatchpoint);
await addedWatchpoint;
info(`Resume and wait to pause at the access to b on line 12`);
resume(dbg);
await waitForPaused(dbg);
await waitForState(dbg, () => dbg.selectors.getSelectedInlinePreviews());
assertPausedAtSourceAndLine(
dbg,
findSource(dbg, "doc-watchpoints.html").id,
12
);
const removedWatchpoint = waitForDispatch(dbg, "REMOVE_WATCHPOINT");
await rightClickScopeNode(dbg, 5);
selectContextMenuItem(dbg, selectors.removeWatchpoint);
await removedWatchpoint;
resume(dbg);
await waitForRequestsToSettle(dbg);
});

Просмотреть файл

@ -0,0 +1,21 @@
<!DOCTYPE HTML>
<html>
<script>
const obj = { a: { b: 2, c: 3 }, b: 2 };
debugger
obj.b = 3;
obj.a.b = 3;
obj.a.b = 4;
console.log(obj.b);
obj.b = 4;
debugger;
</script>
<body>
Hello World!
</body>
</html>

Просмотреть файл

@ -419,7 +419,7 @@ function assertPausedAtSourceAndLine(dbg, expectedSourceId, expectedLine) {
ok(frames.length >= 1, "Got at least one frame");
const { sourceId, line } = frames[0].location;
ok(sourceId == expectedSourceId, "Frame has correct source");
ok(line == expectedLine, "Frame has correct line");
ok(line == expectedLine, `Frame paused at ${line}, but expected ${expectedLine}`);
}
// Get any workers associated with the debugger.
@ -1291,6 +1291,9 @@ const selectors = {
inlinePreviewLables: ".CodeMirror-linewidget .inline-preview-label",
inlinePreviewValues: ".CodeMirror-linewidget .inline-preview-value",
inlinePreviewOpenInspector: ".inline-preview-value button.open-inspector",
addGetWatchpoint: "#node-menu-add-get-watchpoint",
addSetWatchpoint: "#node-menu-add-set-watchpoint",
removeWatchpoint: "#node-menu-remove-watchpoint"
};
function getSelector(elementName, ...args) {
@ -1391,6 +1394,7 @@ function rightClickElement(dbg, elementName, ...args) {
function rightClickEl(dbg, el) {
const doc = dbg.win.document;
el.scrollIntoView();
EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win);
}
@ -1443,6 +1447,10 @@ function toggleScopeNode(dbg, index) {
return toggleObjectInspectorNode(findElement(dbg, "scopeNode", index));
}
function rightClickScopeNode(dbg, index) {
rightClickObjectInspectorNode(dbg, findElement(dbg, "scopeNode", index));
}
function getScopeLabel(dbg, index) {
return findElement(dbg, "scopeNode", index).innerText;
}
@ -1462,6 +1470,18 @@ function toggleObjectInspectorNode(node) {
);
}
function rightClickObjectInspectorNode(dbg, node) {
const objectInspector = node.closest(".object-inspector");
const properties = objectInspector.querySelectorAll(".node").length;
log(`Right clicking node ${node.innerText}`);
rightClickEl(dbg, node);
return waitUntil(
() => objectInspector.querySelectorAll(".node").length !== properties
);
}
function getCM(dbg) {
const el = dbg.win.document.querySelector(".CodeMirror");
return el.CodeMirror;

Просмотреть файл

@ -491,6 +491,10 @@ xhrBreakpoints.item.label=URL contains “%S”
# when the debugger will pause on any XHR requests.
pauseOnAnyXHR=Pause on any URL
# LOCALIZATION NOTE (watchpoints.removeWatchpoint): This is the text that appears in the
# context menu to delete a watchpoint on an object property.
watchpoints.removeWatchpoint=Remove watchpoint
# LOCALIZATION NOTE (sourceTabs.closeTab): Editor source tab context menu item
# for closing the selected tab below the mouse.
sourceTabs.closeTab=Close tab
@ -776,6 +780,11 @@ whyPaused.xhr=Paused on XMLHttpRequest
# promise rejection
whyPaused.promiseRejection=Paused on promise rejection
# LOCALIZATION NOTE (whyPaused.getWatchpoint): The text that is displayed
# in a info block explaining how the debugger is currently paused at a
# watchpoint on an object property
whyPaused.getWatchpoint=Paused on property access
# LOCALIZATION NOTE (whyPaused.assert): The text that is displayed
# in a info block explaining how the debugger is currently paused on an
# assert

Просмотреть файл

@ -86,3 +86,4 @@ pref("devtools.debugger.features.overlay-step-buttons", false);
pref("devtools.debugger.features.overlay-step-buttons", true);
pref("devtools.debugger.features.inline-preview", true);
pref("devtools.debugger.features.log-event-breakpoints", false);
pref("devtools.debugger.features.watchpoints", false);

Просмотреть файл

@ -458,3 +458,31 @@ button.invoke-getter {
.tree-node.focused button.invoke-getter {
background-color: currentColor;
}
button.remove-set-watchpoint {
mask: url("resource://devtools/client/debugger/images/webconsole-logpoint.svg")
no-repeat;
display: inline-block;
vertical-align: top;
height: 15px;
width: 15px;
margin: 0 4px 0px 20px;
padding: 0;
border: none;
background-color: var(--breakpoint-fill);
cursor: pointer;
}
button.remove-get-watchpoint {
mask: url("resource://devtools/client/debugger/images/webconsole-logpoint.svg")
no-repeat;
display: inline-block;
vertical-align: top;
height: 15px;
width: 15px;
margin: 0 4px 0px 20px;
padding: 0;
border: none;
background-color: var(--purple-60);
cursor: pointer;
}

Просмотреть файл

@ -2321,6 +2321,16 @@ function getChildren(options) {
}
return addToCache(makeNodesForProperties(loadedProps, item));
} // Builds an expression that resolves to the value of the item in question
// e.g. `b` in { a: { b: 2 } } resolves to `a.b`
function getPathExpression(item) {
if (item && item.parent) {
return `${getPathExpression(item.parent)}.${item.name}`;
}
return item.name;
}
function getParent(item) {
@ -2431,6 +2441,7 @@ module.exports = {
getChildrenWithEvaluations,
getClosestGripNode,
getClosestNonBucketNode,
getPathExpression,
getParent,
getParentGripValue,
getNonPrototypeParentGripValue,
@ -2484,12 +2495,14 @@ module.exports = {
/* 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/>. */
function initialState() {
function initialState(overrides) {
return {
expandedPaths: new Set(),
loadedProperties: new Map(),
evaluations: new Map(),
actors: new Set()
actors: new Set(),
watchpoints: new Map(),
...overrides
};
}
@ -2517,6 +2530,34 @@ function reducer(state = initialState(), action = {}) {
});
}
if (type == "SET_WATCHPOINT") {
const {
watchpoint,
property,
path
} = data;
const obj = state.loadedProperties.get(path);
return cloneState({
loadedProperties: new Map(state.loadedProperties).set(path, updateObject(obj, property, watchpoint)),
watchpoints: new Map(state.watchpoints).set(data.actor, data.watchpoint)
});
}
if (type === "REMOVE_WATCHPOINT") {
const {
path,
property,
actor
} = data;
const obj = state.loadedProperties.get(path);
const watchpoints = new Map(state.watchpoints);
watchpoints.delete(actor);
return cloneState({
loadedProperties: new Map(state.loadedProperties).set(path, updateObject(obj, property, null)),
watchpoints: watchpoints
});
}
if (type === "NODE_PROPERTIES_LOADED") {
return cloneState({
actors: data.actor ? new Set(state.actors || []).add(data.actor) : state.actors,
@ -2540,12 +2581,24 @@ function reducer(state = initialState(), action = {}) {
if (type === "RESUME" || type == "NAVIGATE") {
return initialState();
return initialState({
watchpoints: state.watchpoints
});
}
return state;
}
function updateObject(obj, property, watchpoint) {
return { ...obj,
ownProperties: { ...obj.ownProperties,
[property]: { ...obj.ownProperties[property],
watchpoint
}
}
};
}
function getObjectInspectorState(state) {
return state.objectInspector;
}
@ -2562,6 +2615,10 @@ function getActors(state) {
return getObjectInspectorState(state).actors;
}
function getWatchpoints(state) {
return getObjectInspectorState(state).watchpoints;
}
function getLoadedProperties(state) {
return getObjectInspectorState(state).loadedProperties;
}
@ -2576,6 +2633,7 @@ function getEvaluations(state) {
const selectors = {
getActors,
getWatchpoints,
getEvaluations,
getExpandedPathKeys,
getExpandedPaths,
@ -7395,9 +7453,12 @@ const utils = __webpack_require__(116);
const reducer = __webpack_require__(115);
const actions = __webpack_require__(485);
module.exports = {
ObjectInspector,
utils,
actions,
reducer
};
@ -7521,6 +7582,12 @@ class ObjectInspector extends Component {
if (this.roots !== nextProps.roots) {
this.cachedNodes.clear();
return;
}
for (const [path, properties] of nextProps.loadedProperties) {
if (properties !== this.props.loadedProperties.get(path)) {
this.cachedNodes.delete(path);
}
} // If there are new evaluations, we want to remove the existing cached
// nodes from the cache.
@ -7549,7 +7616,7 @@ class ObjectInspector extends Component {
// - OR the focused node changed.
// - OR the active node changed.
return loadedProperties.size !== nextProps.loadedProperties.size || evaluations.size !== nextProps.evaluations.size || expandedPaths.size !== nextProps.expandedPaths.size && [...nextProps.expandedPaths].every(path => nextProps.loadedProperties.has(path)) || expandedPaths.size === nextProps.expandedPaths.size && [...nextProps.expandedPaths].some(key => !expandedPaths.has(key)) || this.focusedItem !== nextProps.focusedItem || this.activeItem !== nextProps.activeItem || this.roots !== nextProps.roots;
return loadedProperties !== nextProps.loadedProperties || loadedProperties.size !== nextProps.loadedProperties.size || evaluations.size !== nextProps.evaluations.size || expandedPaths.size !== nextProps.expandedPaths.size && [...nextProps.expandedPaths].every(path => nextProps.loadedProperties.has(path)) || expandedPaths.size === nextProps.expandedPaths.size && [...nextProps.expandedPaths].some(key => !expandedPaths.has(key)) || this.focusedItem !== nextProps.focusedItem || this.activeItem !== nextProps.activeItem || this.roots !== nextProps.roots;
}
componentWillUnmount() {
@ -7746,9 +7813,15 @@ const {
loadItemProperties
} = __webpack_require__(196);
const {
getPathExpression,
getValue
} = __webpack_require__(114);
const {
getLoadedProperties,
getActors
getActors,
getWatchpoints
} = __webpack_require__(115);
/**
@ -7817,6 +7890,67 @@ function nodePropertiesLoaded(node, actor, properties) {
}
};
}
/*
* This action adds a property watchpoint to an object
*/
function addWatchpoint(item, watchpoint) {
return async function ({
dispatch,
client
}) {
const {
parent,
name
} = item;
const object = getValue(parent);
if (!object) {
return;
}
const path = parent.path;
const property = name;
const label = getPathExpression(item);
const actor = object.actor;
await client.addWatchpoint(object, property, label, watchpoint);
dispatch({
type: "SET_WATCHPOINT",
data: {
path,
watchpoint,
property,
actor
}
});
};
}
/*
* This action removes a property watchpoint from an object
*/
function removeWatchpoint(item) {
return async function ({
dispatch,
client
}) {
const object = getValue(item.parent);
const property = item.name;
const path = item.parent.path;
const actor = object.actor;
await client.removeWatchpoint(object, property);
dispatch({
type: "REMOVE_WATCHPOINT",
data: {
path,
property,
actor
}
});
};
}
function closeObjectInspector() {
return async ({
@ -7852,9 +7986,14 @@ function rootsChanged(props) {
function releaseActors(state, client) {
const actors = getActors(state);
const watchpoints = getWatchpoints(state);
for (const actor of actors) {
client.releaseActor(actor);
// Watchpoints are stored in object actors.
// If we release the actor we lose the watchpoint.
if (!watchpoints.has(actor)) {
client.releaseActor(actor);
}
}
}
@ -7887,7 +8026,9 @@ module.exports = {
nodeCollapse,
nodeLoadProperties,
nodePropertiesLoaded,
rootsChanged
rootsChanged,
addWatchpoint,
removeWatchpoint
};
/***/ }),
@ -7957,7 +8098,13 @@ const {
} = Utils.node;
class ObjectInspectorItem extends Component {
// eslint-disable-next-line complexity
static get defaultProps() {
return {
onContextMenu: () => {}
};
} // eslint-disable-next-line complexity
getLabelAndValue() {
const {
item,
@ -8072,7 +8219,8 @@ class ObjectInspectorItem extends Component {
expanded,
onCmdCtrlClick,
onDoubleClick,
dimTopLevelWindow
dimTopLevelWindow,
onContextMenu
} = this.props;
const parentElementProps = {
className: classnames("node object-node", {
@ -8101,7 +8249,8 @@ class ObjectInspectorItem extends Component {
if (Utils.selection.documentHasSelection() && !(e.target && e.target.matches && e.target.matches(".arrow"))) {
e.stopPropagation();
}
}
},
onContextMenu: e => onContextMenu(e, item)
};
if (onDoubleClick) {
@ -8149,6 +8298,24 @@ class ObjectInspectorItem extends Component {
}, label);
}
renderWatchpointButton() {
const {
item,
removeWatchpoint
} = this.props;
if (!item || !item.contents || !item.contents.watchpoint) {
return;
}
const watchpoint = item.contents.watchpoint;
return dom.button({
className: `remove-${watchpoint}-watchpoint`,
title: L10N.getStr("watchpoints.removeWatchpoint"),
onClick: () => removeWatchpoint(item)
});
}
render() {
const {
arrow
@ -8161,7 +8328,7 @@ class ObjectInspectorItem extends Component {
const delimiter = value && labelElement ? dom.span({
className: "object-delimiter"
}, ": ") : null;
return dom.div(this.getTreeItemProps(), arrow, labelElement, delimiter, value);
return dom.div(this.getTreeItemProps(), arrow, labelElement, delimiter, value, this.renderWatchpointButton());
}
}

Просмотреть файл

@ -123,10 +123,10 @@ const proto = {
this._originalDescriptors.set(property, { desc, watchpointType });
const pauseAndRespond = () => {
const pauseAndRespond = type => {
const frame = this.thread.dbg.getNewestFrame();
this.thread._pauseAndRespond(frame, {
type: "watchpoint",
type: type,
message: label,
});
};
@ -139,7 +139,7 @@ const proto = {
desc.value = v;
}),
get: this.obj.makeDebuggeeValue(() => {
pauseAndRespond();
pauseAndRespond("getWatchpoint");
return desc.value;
}),
});
@ -150,7 +150,7 @@ const proto = {
configurable: desc.configurable,
enumerable: desc.enumerable,
set: this.obj.makeDebuggeeValue(v => {
pauseAndRespond();
pauseAndRespond("setWatchpoint");
desc.value = v;
}),
get: this.obj.makeDebuggeeValue(v => {

Просмотреть файл

@ -180,6 +180,8 @@ RootActor.prototype = {
// Supports native log points and modifying the condition/log of an existing
// breakpoints. Fx66+
nativeLogpoints: true,
// Supports watchpoints in the server for Fx71+
watchpoints: true,
// support older browsers for Fx69+
hasThreadFront: true,
},

Просмотреть файл

@ -51,7 +51,7 @@ async function testSetWatchpoint({ threadFront, debuggee }) {
//Test that watchpoint triggers pause on set.
const packet2 = await resumeAndWaitForPause(threadFront);
Assert.equal(packet2.frame.where.line, 4);
Assert.equal(packet2.why.type, "watchpoint");
Assert.equal(packet2.why.type, "setWatchpoint");
Assert.equal(obj.preview.ownProperties.a.value, 1);
await resume(threadFront);
@ -91,7 +91,7 @@ async function testGetWatchpoint({ threadFront, debuggee }) {
//Test that watchpoint triggers pause on get.
const packet2 = await resumeAndWaitForPause(threadFront);
Assert.equal(packet2.frame.where.line, 4);
Assert.equal(packet2.why.type, "watchpoint");
Assert.equal(packet2.why.type, "getWatchpoint");
Assert.equal(obj.preview.ownProperties.a.value, 1);
await resume(threadFront);