author | Miriam <bmiriam1230@gmail.com> |
Sat, 14 Sep 2019 14:13:54 +0000 | |
changeset 493155 | f26512d3707796059727bf53542e473c1fa01a17 |
parent 493154 | 173761e4fd6620f93d5b8b1c54f657524f3c4152 |
child 493156 | 54f895ee30ef8150582654d153bf674eaea1f68d |
push id | 36574 |
push user | btara@mozilla.com |
push date | Sat, 14 Sep 2019 21:21:09 +0000 |
treeherder | mozilla-central@0765fd605a48 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | nchevobbe |
bugs | 1574192 |
milestone | 71.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/actions.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/actions.js @@ -2,17 +2,18 @@ * 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/>. */ // @flow 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; type ThunkArg = { getState: () => {}, dispatch: Dispatch, }; @@ -68,16 +69,60 @@ function nodePropertiesLoaded( properties: GripProperties ) { return { type: "NODE_PROPERTIES_LOADED", data: { node, actor, properties }, }; } +/* + * 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); }; } /* * This action is dispatched when the `roots` prop, provided by a consumer of @@ -94,18 +139,24 @@ function rootsChanged(props: Props) { type: "ROOTS_CHANGED", data: 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); + } } } function invokeGetter( node: Node, targetGrip: object, receiverId: string | null, getterName: string @@ -133,9 +184,11 @@ function invokeGetter( module.exports = { closeObjectInspector, invokeGetter, nodeExpand, nodeCollapse, nodeLoadProperties, nodePropertiesLoaded, rootsChanged, + addWatchpoint, + removeWatchpoint, };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspector.css +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspector.css @@ -50,8 +50,36 @@ .tree.object-inspector .tree-node.focused * { color: inherit; } .tree-node.focused button.open-inspector, .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; +}
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspector.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspector.js @@ -105,16 +105,22 @@ class ObjectInspector extends Component< removeOutdatedNodesFromCache(nextProps) { // When the roots changes, we can wipe out everything. 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. if (nextProps.evaluations > this.props.evaluations) { for (const key of nextProps.evaluations.keys()) { if (!this.props.evaluations.has(key)) { this.cachedNodes.delete(key); } } @@ -129,16 +135,17 @@ class ObjectInspector extends Component< // - OR there are new evaluations // - OR the expanded paths number changed, and all of them have properties // loaded // - OR the expanded paths number did not changed, but old and new sets // differ // - 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 && [...nextProps.expandedPaths].every(path => nextProps.loadedProperties.has(path) )) || (expandedPaths.size === nextProps.expandedPaths.size && [...nextProps.expandedPaths].some(key => !expandedPaths.has(key))) || @@ -266,17 +273,16 @@ class ObjectInspector extends Component< inline, nowrap: disableWrap, "object-inspector": true, }), autoExpandAll, autoExpandDepth, initiallyExpanded, - isExpanded: item => expandedPaths && expandedPaths.has(item.path), isExpandable: this.isNodeExpandable, focused: this.focusedItem, active: this.activeItem, getRoots: this.getRoots, getParent, getChildren: this.getItemChildren,
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspectorItem.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspectorItem.js @@ -75,16 +75,22 @@ type Props = { focused: boolean, expanded: boolean, setExpanded: (Node, boolean) => any, } ) => any, }; class ObjectInspectorItem extends Component<Props> { + static get defaultProps() { + return { + onContextMenu: () => {}, + }; + } + // eslint-disable-next-line complexity getLabelAndValue(): { value?: string | Element, label?: string, } { const { item, depth, expanded, mode } = this.props; const label = item.name; @@ -198,16 +204,17 @@ class ObjectInspectorItem extends Compon const { item, depth, focused, expanded, onCmdCtrlClick, onDoubleClick, dimTopLevelWindow, + onContextMenu, } = this.props; const parentElementProps: Object = { className: classnames("node object-node", { focused, lessen: !expanded && (nodeIsDefaultProperties(item) || @@ -240,16 +247,17 @@ class ObjectInspectorItem extends Compon // So we need to also check if the arrow was clicked. if ( Utils.selection.documentHasSelection() && !(e.target && e.target.matches && e.target.matches(".arrow")) ) { e.stopPropagation(); } }, + onContextMenu: e => onContextMenu(e, item), }; if (onDoubleClick) { parentElementProps.onDoubleClick = e => { e.stopPropagation(); onDoubleClick(item, { depth, focused, @@ -287,29 +295,45 @@ class ObjectInspectorItem extends Compon }); } : undefined, }, 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 } = this.props; const { label, value } = this.getLabelAndValue(); const labelElement = this.renderLabel(label); const delimiter = value && labelElement ? dom.span({ className: "object-delimiter" }, ": ") : null; return dom.div( this.getTreeItemProps(), arrow, labelElement, delimiter, - value + value, + this.renderWatchpointButton() ); } } module.exports = ObjectInspectorItem;
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/index.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/index.js @@ -1,9 +1,10 @@ /* 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/>. */ 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 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/reducer.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/reducer.js @@ -1,20 +1,22 @@ /* 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/>. */ 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, }; } function reducer( state: State = initialState(), action: ReduxAction = {} ): State { const { type, data } = action; @@ -28,16 +30,44 @@ function reducer( } if (type === "NODE_COLLAPSE") { const expandedPaths = new Set(state.expandedPaths); expandedPaths.delete(data.node.path); 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 ? new Set(state.actors || []).add(data.actor) : state.actors, loadedProperties: new Map(state.loadedProperties).set( data.node.path, action.data.properties @@ -61,52 +91,70 @@ function reducer( (data.result.value.throw || data.result.value.return), }), }); } // 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; } function getExpandedPaths(state) { return getObjectInspectorState(state).expandedPaths; } function getExpandedPathKeys(state) { return [...getExpandedPaths(state).keys()]; } function getActors(state) { return getObjectInspectorState(state).actors; } +function getWatchpoints(state) { + return getObjectInspectorState(state).watchpoints; +} + function getLoadedProperties(state) { return getObjectInspectorState(state).loadedProperties; } function getLoadedPropertyKeys(state) { return [...getLoadedProperties(state).keys()]; } function getEvaluations(state) { return getObjectInspectorState(state).evaluations; } const selectors = { getActors, + getWatchpoints, getEvaluations, getExpandedPathKeys, getExpandedPaths, getLoadedProperties, getLoadedPropertyKeys, }; Object.defineProperty(module.exports, "__esModule", {
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/__snapshots__/classnames.js.snap +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/__snapshots__/classnames.js.snap @@ -19,16 +19,17 @@ exports[`ObjectInspector - classnames ha id="root" onClick={[Function]} onKeyDownCapture={null} role="treeitem" > <div className="node object-node" onClick={[Function]} + onContextMenu={[Function]} > <span className="object-label" > root </span> <span className="object-delimiter" @@ -64,16 +65,17 @@ exports[`ObjectInspector - classnames ha id="root" onClick={[Function]} onKeyDownCapture={null} role="treeitem" > <div className="node object-node" onClick={[Function]} + onContextMenu={[Function]} > <span className="object-label" > root </span> <span className="object-delimiter" @@ -109,16 +111,17 @@ exports[`ObjectInspector - classnames ha id="root" onClick={[Function]} onKeyDownCapture={null} role="treeitem" > <div className="node object-node" onClick={[Function]} + onContextMenu={[Function]} > <span className="object-label" > root </span> <span className="object-delimiter"
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/__snapshots__/window.js.snap +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/__snapshots__/window.js.snap @@ -20,16 +20,17 @@ exports[`ObjectInspector - dimTopLevelWi id="window" onClick={[Function]} onKeyDownCapture={null} role="treeitem" > <div className="node object-node" onClick={[Function]} + onContextMenu={[Function]} > <button className="arrow" /> <span className="object-label" > window @@ -75,16 +76,17 @@ exports[`ObjectInspector - dimTopLevelWi id="root" onClick={[Function]} onKeyDownCapture={null} role="treeitem" > <div className="node object-node focused" onClick={[Function]} + onContextMenu={[Function]} > <button className="arrow expanded" /> <span className="object-label" > root @@ -104,16 +106,17 @@ exports[`ObjectInspector - dimTopLevelWi <span className="tree-indent tree-last-indent" > ā </span> <div className="node object-node" onClick={[Function]} + onContextMenu={[Function]} > <button className="arrow" /> <span className="object-label" > window @@ -158,16 +161,17 @@ exports[`ObjectInspector - dimTopLevelWi id="window" onClick={[Function]} onKeyDownCapture={null} role="treeitem" > <div className="node object-node lessen" onClick={[Function]} + onContextMenu={[Function]} > <button className="arrow" /> <span className="object-label" > window @@ -213,16 +217,17 @@ exports[`ObjectInspector - dimTopLevelWi id="window" onClick={[Function]} onKeyDownCapture={null} role="treeitem" > <div className="node object-node focused" onClick={[Function]} + onContextMenu={[Function]} > <button className="arrow expanded" /> <span className="object-label" > window @@ -256,16 +261,17 @@ exports[`ObjectInspector - dimTopLevelWi <span className="tree-indent tree-last-indent" > ā </span> <div className="node object-node lessen" onClick={[Function]} + onContextMenu={[Function]} > <span className="object-label" > <prototype> </span> <span className="object-delimiter"
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/release-actors.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/release-actors.js @@ -56,16 +56,17 @@ function mount(props, { initialState } = describe("release actors", () => { it("calls release actors when unmount", () => { const { wrapper, client } = mount( {}, { initialState: { actors: new Set(["actor 1", "actor 2"]), + watchpoints: new Map(), }, } ); wrapper.unmount(); expect(client.releaseActor.mock.calls).toHaveLength(2); expect(client.releaseActor.mock.calls[0][0]).toBe("actor 1");
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/make-numerical-buckets.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/make-numerical-buckets.js @@ -15,21 +15,17 @@ describe("makeNumericalBuckets", () => { }); const nodes = makeNumericalBuckets(node); const names = nodes.map(n => n.name); const paths = nodes.map(n => n.path.toString()); 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. it.skip("does not create a numerical bucket for a single node", () => { const node = createNode({ name: "root", contents: { value: gripArrayStubs.get("Array(101)"), @@ -55,20 +51,17 @@ describe("makeNumericalBuckets", () => { }); const nodes = makeNumericalBuckets(node); const names = nodes.map(n => n.name); const paths = nodes.map(n => n.path.toString()); 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", () => { const node = createNode({ name: "root", contents: { value: gripArrayStubs.get("Array(23456)"), }, @@ -129,16 +122,14 @@ describe("makeNumericalBuckets", () => { const lastBucketPaths = lastBucketNodes.map(n => n.path.toString()); expect(lastBucketNames).toEqual([ "[23000ā¦23099]", "[23100ā¦23199]", "[23200ā¦23299]", "[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]" ); }); });
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js +++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js @@ -811,16 +811,25 @@ function getChildren(options: { if (!hasLoadedProps) { return []; } 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; } function getNumericalPropertiesCount(item: Node): number { if (nodeIsBucket(item)) { return item.meta.endIndex - item.meta.startIndex + 1; } @@ -917,16 +926,17 @@ module.exports = { createNode, createGetterNode, createSetterNode, getActor, getChildren, getChildrenWithEvaluations, getClosestGripNode, getClosestNonBucketNode, + getPathExpression, getParent, getParentGripValue, getNonPrototypeParentGripValue, getNumericalPropertiesCount, getValue, makeNodesForEntries, makeNodesForPromiseProperties, makeNodesForProperties,
--- a/devtools/client/debugger/src/actions/index.js +++ b/devtools/client/debugger/src/actions/index.js @@ -17,27 +17,31 @@ import * as quickOpen from "./quick-open import * as sourceTree from "./source-tree"; import * as sources from "./sources"; import * as sourcesActors from "./source-actors"; import * as tabs from "./tabs"; 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, ...breakpoints, ...expressions, ...eventListeners, ...sources, ...sourcesActors, ...tabs, ...pause, ...ui, ...fileSearch, + ...objectInspector.actions, ...projectTextSearch, ...quickOpen, ...sourceTree, ...threads, ...toolbox, ...preview, };
--- a/devtools/client/debugger/src/client/firefox/commands.js +++ b/devtools/client/debugger/src/client/firefox/commands.js @@ -183,16 +183,35 @@ async function sourceContents({ function setXHRBreakpoint(path: string, method: string) { return currentThreadFront.setXHRBreakpoint(path, method); } 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) { const { sourceUrl, line, column } = location; const sourceId = location.sourceId || ""; // $FlowIgnore return `${sourceUrl}:${sourceId}:${line}:${column}`; } @@ -509,16 +528,18 @@ const clientCommands = { sourceContents, getSourceForActor, getSourceActorBreakpointPositions, getSourceActorBreakableLines, hasBreakpoint, setBreakpoint, setXHRBreakpoint, removeXHRBreakpoint, + addWatchpoint, + removeWatchpoint, removeBreakpoint, evaluate, evaluateInFrame, evaluateExpressions, navigate, reload, getProperties, getFrameScopes,
--- a/devtools/client/debugger/src/client/firefox/types.js +++ b/devtools/client/debugger/src/client/firefox/types.js @@ -255,17 +255,17 @@ export type DebuggerClient = { mainRoot: { traits: any, getFront: string => Promise<*>, listProcesses: () => Promise<{ processes: ProcessDescriptor }>, }, connect: () => Promise<*>, request: (packet: Object) => Promise<*>, attachConsole: (actor: String, listeners: Array<*>) => Promise<*>, - createObjectClient: (grip: Grip) => {}, + createObjectClient: (grip: Grip) => ObjectClient, release: (actor: String) => {}, }; type ProcessDescriptor = Object; /** * A grip is a JSON value that refers to a specific JavaScript value in the * debuggee. Grips appear anywhere an arbitrary value from the debuggee needs @@ -333,16 +333,22 @@ export type SourceClient = { /** * ObjectClient * @memberof firefox * @static */ export type ObjectClient = { getPrototypeAndProperties: () => any, + addWatchpoint: ( + property: string, + label: string, + watchpointType: string + ) => {}, + removeWatchpoint: (property: string) => {}, }; /** * ThreadFront * @memberof firefox * @static */ export type ThreadFront = {
--- a/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js +++ b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js @@ -1,17 +1,19 @@ /* 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/>. */ // @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, getSelectedFrame, getGeneratedFrameScope, getOriginalFrameScope, getPauseReason, isMapScopesEnabled, @@ -40,16 +42,18 @@ type Props = { why: Why, mapScopesEnabled: boolean, openLink: typeof actions.openLink, openElementInInspector: typeof actions.openElementInInspectorCommand, highlightDomElement: typeof actions.highlightDomElement, unHighlightDomElement: typeof actions.unHighlightDomElement, toggleMapScopes: typeof actions.toggleMapScopes, setExpandedScope: typeof actions.setExpandedScope, + addWatchpoint: typeof actions.addWatchpoint, + removeWatchpoint: typeof actions.removeWatchpoint, expandedScopes: string[], }; type State = { originalScopes: ?(NamedValue[]), generatedScopes: ?(NamedValue[]), showOriginal: boolean, }; @@ -106,16 +110,60 @@ class Scopes extends PureComponent<Props }); } } onToggleMapScopes = () => { 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, isLoading, openLink, openElementInInspector, highlightDomElement, unHighlightDomElement, @@ -142,16 +190,17 @@ class Scopes extends PureComponent<Props disableWrap={true} dimTopLevelWindow={true} openLink={openLink} createObjectClient={grip => createObjectClient(grip)} onDOMNodeClick={grip => openElementInInspector(grip)} onInspectIconClick={grip => openElementInInspector(grip)} onDOMNodeMouseOver={grip => highlightDomElement(grip)} onDOMNodeMouseOut={grip => unHighlightDomElement(grip)} + onContextMenu={this.onContextMenu} setExpanded={(path, expand) => setExpandedScope(cx, path, expand)} initiallyExpanded={initiallyExpanded} /> </div> ); } let stateText = L10N.getStr("scopes.notPaused"); @@ -218,10 +267,12 @@ export default connect( mapStateToProps, { openLink: actions.openLink, openElementInInspector: actions.openElementInInspectorCommand, highlightDomElement: actions.highlightDomElement, unHighlightDomElement: actions.unHighlightDomElement, toggleMapScopes: actions.toggleMapScopes, setExpandedScope: actions.setExpandedScope, + addWatchpoint: actions.addWatchpoint, + removeWatchpoint: actions.removeWatchpoint, } )(Scopes);
--- a/devtools/client/debugger/src/reducers/pause.js +++ b/devtools/client/debugger/src/reducers/pause.js @@ -639,16 +639,26 @@ export function getInlinePreviews( thread: ThreadId, frameId: string ): Previews { return getThreadPauseState(state.pause, thread).inlinePreview[ getGeneratedFrameId(frameId) ]; } +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, frameId: string, line: number, expression: string ) { const previews = getThreadPauseState(state.pause, thread).inlinePreview[
--- a/devtools/client/debugger/src/utils/pause/why.js +++ b/devtools/client/debugger/src/utils/pause/why.js @@ -12,16 +12,18 @@ import type { Why } from "../../types"; // "interrupted", "attached" const reasons = { debuggerStatement: "whyPaused.debuggerStatement", breakpoint: "whyPaused.breakpoint", exception: "whyPaused.exception", resumeLimit: "whyPaused.resumeLimit", breakpointConditionThrown: "whyPaused.breakpointConditionThrown", eventBreakpoint: "whyPaused.eventBreakpoint", + getWatchpoint: "whyPaused.getWatchpoint", + setWatchpoint: "whyPaused.setWatchpoint", mutationBreakpoint: "whyPaused.mutationBreakpoint", interrupted: "whyPaused.interrupted", replayForcedPause: "whyPaused.replayForcedPause", // V8 DOM: "whyPaused.breakpoint", EventListener: "whyPaused.pauseOnDOMEvents", XHR: "whyPaused.xhr",
--- a/devtools/client/debugger/src/utils/prefs.js +++ b/devtools/client/debugger/src/utils/prefs.js @@ -67,16 +67,17 @@ if (isDevelopment()) { pref("devtools.debugger.features.original-blackbox", true); pref("devtools.debugger.features.event-listeners-breakpoints", true); pref("devtools.debugger.features.dom-mutation-breakpoints", true); pref("devtools.debugger.features.log-points", true); pref("devtools.debugger.features.inline-preview", true); 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", { fission: ["Bool", "browsertoolbox.fission"], logging: ["Bool", "debugger.logging"], editorWrapping: ["Bool", "debugger.ui.editor-wrapping"], alphabetizeOutline: ["Bool", "debugger.alphabetize-outline"], autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"], @@ -135,16 +136,17 @@ export const features = new PrefsHelper( xhrBreakpoints: ["Bool", "xhr-breakpoints"], originalBlackbox: ["Bool", "original-blackbox"], eventListenersBreakpoints: ["Bool", "event-listeners-breakpoints"], domMutationBreakpoints: ["Bool", "dom-mutation-breakpoints"], logPoints: ["Bool", "log-points"], showOverlayStepButtons: ["Bool", "overlay-step-buttons"], inlinePreview: ["Bool", "inline-preview"], logEventBreakpoints: ["Bool", "log-event-breakpoints"], + watchpoints: ["Bool", "watchpoints"], }); export const asyncStore = asyncStoreHelper("debugger", { pendingBreakpoints: ["pending-breakpoints", {}], tabs: ["tabs", []], xhrBreakpoints: ["xhr-breakpoints", []], eventListenerBreakpoints: ["event-listener-breakpoints", undefined], });
--- a/devtools/client/debugger/test/mochitest/browser.ini +++ b/devtools/client/debugger/test/mochitest/browser.ini @@ -162,8 +162,10 @@ skip-if = (os == 'linux' && debug) || (o [browser_dbg-inline-script-offset.js] [browser_dbg-scopes-xrays.js] [browser_dbg-merge-scopes.js] [browser_dbg-message-run-to-completion.js] [browser_dbg-remember-expanded-scopes.js] [browser_dbg-bfcache.js] [browser_dbg-gc-breakpoint-positions.js] [browser_dbg-gc-sources.js] +[browser_dbg-watchpoints.js] +skip-if = debug
new file mode 100644 --- /dev/null +++ b/devtools/client/debugger/test/mochitest/browser_dbg-watchpoints.js @@ -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); +});
new file mode 100644 --- /dev/null +++ b/devtools/client/debugger/test/mochitest/examples/doc-watchpoints.html @@ -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>
--- a/devtools/client/debugger/test/mochitest/helpers.js +++ b/devtools/client/debugger/test/mochitest/helpers.js @@ -414,17 +414,17 @@ function isPaused(dbg) { // Make sure the debugger is paused at a certain source ID and line. function assertPausedAtSourceAndLine(dbg, expectedSourceId, expectedLine) { assertPaused(dbg); const frames = dbg.selectors.getCurrentThreadFrames(); 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. async function getThreads(dbg) { await dbg.actions.updateThreads(); return dbg.selectors.getThreads(); } @@ -1286,16 +1286,19 @@ const selectors = { projectSerchExpandedResults: ".project-text-search .result", threadsPaneItems: ".threads-pane .thread", threadsPaneItem: i => `.threads-pane .thread:nth-child(${i})`, threadsPaneItemPause: i => `${selectors.threadsPaneItem(i)} .pause-badge`, CodeMirrorLines: ".CodeMirror-lines", 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) { let selector = selectors[elementName]; if (!selector) { throw new Error(`The selector ${elementName} is not defined`); } @@ -1386,16 +1389,17 @@ function shiftClickElement(dbg, elementN function rightClickElement(dbg, elementName, ...args) { const selector = getSelector(elementName, ...args); const doc = dbg.win.document; return rightClickEl(dbg, doc.querySelector(selector)); } function rightClickEl(dbg, el) { const doc = dbg.win.document; + el.scrollIntoView(); EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win); } async function clickGutter(dbg, line) { const el = await codeMirrorGutterElement(dbg, line); clickDOMElement(dbg, el); } @@ -1438,16 +1442,20 @@ function toggleScopes(dbg) { function toggleExpressionNode(dbg, index) { return toggleObjectInspectorNode(findElement(dbg, "expressionNode", index)); } 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; } function getScopeValue(dbg, index) { return findElement(dbg, "scopeValue", index).innerText; } @@ -1457,16 +1465,28 @@ function toggleObjectInspectorNode(node) log(`Toggling node ${node.innerText}`); node.click(); return waitUntil( () => objectInspector.querySelectorAll(".node").length !== properties ); } +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; } function getCoordsFromPosition(cm, { line, ch }) { return cm.charCoords({ line: ~~line, ch: ~~ch }); }
--- a/devtools/client/locales/en-US/debugger.properties +++ b/devtools/client/locales/en-US/debugger.properties @@ -486,16 +486,20 @@ xhrBreakpoints.label=Add XHR breakpoint # LOCALIZATION NOTE (xhrBreakpoints.item.label): message displayed when reaching a breakpoint for XHR requests. %S is replaced by the path provided as condition for the breakpoint. xhrBreakpoints.item.label=URL contains ā%Sā # LOCALIZATION NOTE (pauseOnAnyXHR): The pause on any XHR checkbox description # 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 sourceTabs.closeTab.accesskey=c sourceTabs.closeTab.key=CmdOrCtrl+W # LOCALIZATION NOTE (sourceTabs.closeOtherTabs): Editor source tab context menu item # for closing the other tabs. @@ -771,16 +775,21 @@ whyPaused.breakpointConditionThrown=Erro # xml http request whyPaused.xhr=Paused on XMLHttpRequest # LOCALIZATION NOTE (whyPaused.promiseRejection): The text that is displayed # in a info block explaining how the debugger is currently paused on a # 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 whyPaused.assert=Paused on assertion # LOCALIZATION NOTE (whyPaused.debugCommand): The text that is displayed # in a info block explaining how the debugger is currently paused on a # debugger statement
--- a/devtools/client/preferences/debugger.js +++ b/devtools/client/preferences/debugger.js @@ -81,8 +81,9 @@ pref("devtools.debugger.features.xhr-bre pref("devtools.debugger.features.original-blackbox", true); pref("devtools.debugger.features.event-listeners-breakpoints", true); pref("devtools.debugger.features.dom-mutation-breakpoints", true); pref("devtools.debugger.features.log-points", true); 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);
--- a/devtools/client/shared/components/reps/reps.css +++ b/devtools/client/shared/components/reps/reps.css @@ -453,8 +453,36 @@ button.invoke-getter { .tree.object-inspector .tree-node.focused * { color: inherit; } .tree-node.focused button.open-inspector, .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; +}
--- a/devtools/client/shared/components/reps/reps.js +++ b/devtools/client/shared/components/reps/reps.js @@ -2316,16 +2316,26 @@ function getChildren(options) { return []; } if (!hasLoadedProps) { return []; } 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) { return item.parent; } function getNumericalPropertiesCount(item) { if (nodeIsBucket(item)) { @@ -2426,16 +2436,17 @@ module.exports = { createNode, createGetterNode, createSetterNode, getActor, getChildren, getChildrenWithEvaluations, getClosestGripNode, getClosestNonBucketNode, + getPathExpression, getParent, getParentGripValue, getNonPrototypeParentGripValue, getNumericalPropertiesCount, getValue, makeNodesForEntries, makeNodesForPromiseProperties, makeNodesForProperties, @@ -2479,22 +2490,24 @@ module.exports = { /***/ }), /***/ 115: /***/ (function(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 }; } function reducer(state = initialState(), action = {}) { const { type, data } = action; @@ -2512,16 +2525,44 @@ function reducer(state = initialState(), if (type === "NODE_COLLAPSE") { const expandedPaths = new Set(state.expandedPaths); expandedPaths.delete(data.node.path); 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 ? new Set(state.actors || []).add(data.actor) : state.actors, loadedProperties: new Map(state.loadedProperties).set(data.node.path, action.data.properties) }); } if (type === "ROOTS_CHANGED") { @@ -2535,52 +2576,69 @@ function reducer(state = initialState(), getterValue: data.result && data.result.value && (data.result.value.throw || data.result.value.return) }) }); } // 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; } function getExpandedPaths(state) { return getObjectInspectorState(state).expandedPaths; } function getExpandedPathKeys(state) { return [...getExpandedPaths(state).keys()]; } function getActors(state) { return getObjectInspectorState(state).actors; } +function getWatchpoints(state) { + return getObjectInspectorState(state).watchpoints; +} + function getLoadedProperties(state) { return getObjectInspectorState(state).loadedProperties; } function getLoadedPropertyKeys(state) { return [...getLoadedProperties(state).keys()]; } function getEvaluations(state) { return getObjectInspectorState(state).evaluations; } const selectors = { getActors, + getWatchpoints, getEvaluations, getExpandedPathKeys, getExpandedPaths, getLoadedProperties, getLoadedPropertyKeys }; Object.defineProperty(module.exports, "__esModule", { value: true @@ -7390,19 +7448,22 @@ module.exports = { * 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/>. */ const ObjectInspector = __webpack_require__(483); const utils = __webpack_require__(116); const reducer = __webpack_require__(115); +const actions = __webpack_require__(485); + module.exports = { ObjectInspector, utils, + actions, reducer }; /***/ }), /***/ 483: /***/ (function(module, exports, __webpack_require__) { @@ -7516,16 +7577,22 @@ class ObjectInspector extends Component } } removeOutdatedNodesFromCache(nextProps) { // When the roots changes, we can wipe out everything. 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. if (nextProps.evaluations > this.props.evaluations) { for (const key of nextProps.evaluations.keys()) { if (!this.props.evaluations.has(key)) { this.cachedNodes.delete(key); @@ -7544,17 +7611,17 @@ class ObjectInspector extends Component // - OR there are new evaluations // - OR the expanded paths number changed, and all of them have properties // loaded // - OR the expanded paths number did not changed, but old and new sets // differ // - 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() { this.props.closeObjectInspector(); } getItemChildren(item) { const { @@ -7742,18 +7809,24 @@ module.exports = __WEBPACK_EXTERNAL_MODU /* 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/>. */ const { loadItemProperties } = __webpack_require__(196); const { + getPathExpression, + getValue +} = __webpack_require__(114); + +const { getLoadedProperties, - getActors + getActors, + getWatchpoints } = __webpack_require__(115); /** * This action is responsible for expanding a given node, which also means that * it will call the action responsible to fetch properties. */ function nodeExpand(node, actor) { return async ({ @@ -7812,16 +7885,77 @@ function nodePropertiesLoaded(node, acto type: "NODE_PROPERTIES_LOADED", data: { 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 ({ getState, client }) => { releaseActors(getState(), client); }; @@ -7847,19 +7981,24 @@ function rootsChanged(props) { type: "ROOTS_CHANGED", data: 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); + } } } function invokeGetter(node, targetGrip, receiverId, getterName) { return async ({ dispatch, client, getState @@ -7882,17 +8021,19 @@ function invokeGetter(node, targetGrip, module.exports = { closeObjectInspector, invokeGetter, nodeExpand, nodeCollapse, nodeLoadProperties, nodePropertiesLoaded, - rootsChanged + rootsChanged, + addWatchpoint, + removeWatchpoint }; /***/ }), /***/ 486: /***/ (function(module, exports) { // removed by extract-text-webpack-plugin @@ -7952,17 +8093,23 @@ const { nodeIsLongString, nodeHasFullText, nodeHasGetter, getNonPrototypeParentGripValue, getParentGripValue } = Utils.node; class ObjectInspectorItem extends Component { - // eslint-disable-next-line complexity + static get defaultProps() { + return { + onContextMenu: () => {} + }; + } // eslint-disable-next-line complexity + + getLabelAndValue() { const { item, depth, expanded, mode } = this.props; const label = item.name; @@ -8067,17 +8214,18 @@ class ObjectInspectorItem extends Compon getTreeItemProps() { const { item, depth, focused, expanded, onCmdCtrlClick, onDoubleClick, - dimTopLevelWindow + dimTopLevelWindow, + onContextMenu } = this.props; const parentElementProps = { className: classnames("node object-node", { focused, lessen: !expanded && (nodeIsDefaultProperties(item) || nodeIsPrototype(item) || nodeIsGetter(item) || nodeIsSetter(item) || dimTopLevelWindow === true && nodeIsWindow(item) && depth === 0), block: nodeIsBlock(item) }), onClick: e => { @@ -8096,17 +8244,18 @@ class ObjectInspectorItem extends Compon // user clicked on the arrow itself. Indeed because the arrow is an // image, clicking on it does not remove any existing text selection. // So we need to also check if the arrow was clicked. if (Utils.selection.documentHasSelection() && !(e.target && e.target.matches && e.target.matches(".arrow"))) { e.stopPropagation(); } - } + }, + onContextMenu: e => onContextMenu(e, item) }; if (onDoubleClick) { parentElementProps.onDoubleClick = e => { e.stopPropagation(); onDoubleClick(item, { depth, focused, @@ -8144,29 +8293,47 @@ class ObjectInspectorItem extends Compon focused, expanded, setExpanded: this.props.setExpanded }); } : undefined }, 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 } = this.props; const { label, value } = this.getLabelAndValue(); const labelElement = this.renderLabel(label); 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()); } } module.exports = ObjectInspectorItem; /***/ }),
--- a/devtools/server/actors/object.js +++ b/devtools/server/actors/object.js @@ -118,44 +118,44 @@ const proto = { const desc = this.obj.getOwnPropertyDescriptor(property); if (desc.set || desc.get) { return; } 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, }); }; if (watchpointType === "get") { this.obj.defineProperty(property, { configurable: desc.configurable, enumerable: desc.enumerable, set: this.obj.makeDebuggeeValue(v => { desc.value = v; }), get: this.obj.makeDebuggeeValue(() => { - pauseAndRespond(); + pauseAndRespond("getWatchpoint"); return desc.value; }), }); } if (watchpointType === "set") { this.obj.defineProperty(property, { configurable: desc.configurable, enumerable: desc.enumerable, set: this.obj.makeDebuggeeValue(v => { - pauseAndRespond(); + pauseAndRespond("setWatchpoint"); desc.value = v; }), get: this.obj.makeDebuggeeValue(v => { return desc.value; }), }); } },
--- a/devtools/server/actors/root.js +++ b/devtools/server/actors/root.js @@ -175,16 +175,18 @@ RootActor.prototype = { // Version 1 - Firefox 65: Introduces a duration-based buffer. It can be controlled // by adding a `duration` property (in seconds) to the options passed to // `front.startProfiler`. This is an optional parameter but it will throw an error if // the profiled Firefox doesn't accept it. perfActorVersion: 1, // 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, }, /** * Return a 'hello' packet as specified by the Remote Debugging Protocol. */ sayHello: function() {
--- a/devtools/server/tests/unit/test_watchpoint-01.js +++ b/devtools/server/tests/unit/test_watchpoint-01.js @@ -46,17 +46,17 @@ async function testSetWatchpoint({ threa const args = packet.frame.arguments; const obj = args[0]; const objClient = threadFront.pauseGrip(obj); await objClient.addWatchpoint("a", "obj.a", "set"); //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); } async function testGetWatchpoint({ threadFront, debuggee }) { function evaluateTestCode(debuggee) { /* eslint-disable */ @@ -86,17 +86,17 @@ async function testGetWatchpoint({ threa const args = packet.frame.arguments; const obj = args[0]; const objClient = threadFront.pauseGrip(obj); await objClient.addWatchpoint("a", "obj.a", "get"); //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); } async function testRemoveWatchpoint({ threadFront, debuggee }) { function evaluateTestCode(debuggee) { /* eslint-disable */