summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/object-inspector
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/object-inspector')
-rw-r--r--devtools/client/shared/components/object-inspector/actions.js225
-rw-r--r--devtools/client/shared/components/object-inspector/components/ObjectInspector.css99
-rw-r--r--devtools/client/shared/components/object-inspector/components/ObjectInspector.js371
-rw-r--r--devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js285
-rw-r--r--devtools/client/shared/components/object-inspector/components/moz.build10
-rw-r--r--devtools/client/shared/components/object-inspector/index.js10
-rw-r--r--devtools/client/shared/components/object-inspector/moz.build16
-rw-r--r--devtools/client/shared/components/object-inspector/reducer.js147
-rw-r--r--devtools/client/shared/components/object-inspector/utils/client.js124
-rw-r--r--devtools/client/shared/components/object-inspector/utils/index.js52
-rw-r--r--devtools/client/shared/components/object-inspector/utils/load-properties.js260
-rw-r--r--devtools/client/shared/components/object-inspector/utils/moz.build13
-rw-r--r--devtools/client/shared/components/object-inspector/utils/node.js1039
-rw-r--r--devtools/client/shared/components/object-inspector/utils/selection.js16
14 files changed, 2667 insertions, 0 deletions
diff --git a/devtools/client/shared/components/object-inspector/actions.js b/devtools/client/shared/components/object-inspector/actions.js
new file mode 100644
index 0000000000..370f1b161a
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/actions.js
@@ -0,0 +1,225 @@
+/* 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 } = require("resource://devtools/client/shared/components/object-inspector/utils/load-properties.js");
+const {
+ getPathExpression,
+ getParentFront,
+ getParentGripValue,
+ getValue,
+ nodeIsBucket,
+ getFront,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const { getLoadedProperties, getWatchpoints } = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+
+/**
+ * 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 ({ dispatch }) => {
+ dispatch({ type: "NODE_EXPAND", data: { node } });
+ dispatch(nodeLoadProperties(node, actor));
+ };
+}
+
+function nodeCollapse(node) {
+ return {
+ type: "NODE_COLLAPSE",
+ data: { node },
+ };
+}
+
+/*
+ * This action checks if we need to fetch properties, entries, prototype and
+ * symbols for a given node. If we do, it will call the appropriate ObjectFront
+ * functions.
+ */
+function nodeLoadProperties(node, actor) {
+ return async ({ dispatch, client, getState }) => {
+ const state = getState();
+ const loadedProperties = getLoadedProperties(state);
+ if (loadedProperties.has(node.path)) {
+ return;
+ }
+
+ try {
+ const properties = await loadItemProperties(
+ node,
+ client,
+ loadedProperties
+ );
+
+ // If the client does not have a releaseActor function, it means the actors are
+ // handled directly by the consumer, so we don't need to track them.
+ if (!client || !client.releaseActor) {
+ actor = null;
+ }
+
+ dispatch(nodePropertiesLoaded(node, actor, properties));
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function nodePropertiesLoaded(node, actor, properties) {
+ return {
+ 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;
+ let object = getValue(parent);
+
+ if (nodeIsBucket(parent)) {
+ object = getValue(parent.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 { parent, name } = item;
+ let object = getValue(parent);
+
+ if (nodeIsBucket(parent)) {
+ object = getValue(parent.parent);
+ }
+
+ const property = name;
+ const path = parent.path;
+ const actor = object.actor;
+
+ await client.removeWatchpoint(object, property);
+
+ dispatch({
+ type: "REMOVE_WATCHPOINT",
+ data: { path, property, actor },
+ });
+ };
+}
+
+function getActorIDs(roots) {
+ if (!roots) {
+ return []
+ }
+
+ const actorIds = [];
+ for (const root of roots) {
+ const front = getFront(root);
+ if (front?.actorID) {
+ actorIds.push(front.actorID);
+ }
+ }
+
+ return actorIds;
+}
+
+function closeObjectInspector(roots) {
+ return ({ client }) => {
+ releaseActors(client, roots);
+ };
+}
+
+/*
+ * This action is dispatched when the `roots` prop, provided by a consumer of
+ * the ObjectInspector (inspector, console, …), is modified. It will clean the
+ * internal state properties (expandedPaths, loadedProperties, …) and release
+ * the actors consumed with the previous roots.
+ * It takes a props argument which reflects what is passed by the upper-level
+ * consumer.
+ */
+function rootsChanged(roots, oldRoots) {
+ return ({ dispatch, client }) => {
+ releaseActors(client, oldRoots, roots);
+ dispatch({
+ type: "ROOTS_CHANGED",
+ data: roots,
+ });
+ };
+}
+
+/**
+ * Release any actors we don't need anymore
+ *
+ * @param {Object} client: Object with a `releaseActor` method
+ * @param {Array} oldRoots: The roots in which we want to cleanup now-unused actors
+ * @param {Array} newRoots: The current roots (might have item that are also in oldRoots)
+ */
+async function releaseActors(client, oldRoots, newRoots = []) {
+ if (!client?.releaseActor ) {
+ return;
+ }
+
+ let actorIdsToRelease = getActorIDs(oldRoots);
+ if (newRoots.length) {
+ const newActorIds = getActorIDs(newRoots);
+ actorIdsToRelease = actorIdsToRelease.filter(id => !newActorIds.includes(id));
+ }
+
+ if (!actorIdsToRelease.length) {
+ return;
+ }
+ await Promise.all(actorIdsToRelease.map(client.releaseActor));
+}
+
+function invokeGetter(node, receiverId) {
+ return async ({ dispatch, client, getState }) => {
+ try {
+ const objectFront =
+ getParentFront(node) ||
+ client.createObjectFront(getParentGripValue(node));
+ const getterName = node.propertyName || node.name;
+
+ const result = await objectFront.getPropertyValue(getterName, receiverId);
+ dispatch({
+ type: "GETTER_INVOKED",
+ data: {
+ node,
+ result,
+ },
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+module.exports = {
+ closeObjectInspector,
+ invokeGetter,
+ nodeExpand,
+ nodeCollapse,
+ nodeLoadProperties,
+ nodePropertiesLoaded,
+ rootsChanged,
+ addWatchpoint,
+ removeWatchpoint,
+};
diff --git a/devtools/client/shared/components/object-inspector/components/ObjectInspector.css b/devtools/client/shared/components/object-inspector/components/ObjectInspector.css
new file mode 100644
index 0000000000..d7b9b3ffa7
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/ObjectInspector.css
@@ -0,0 +1,99 @@
+/* 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/. */
+
+.tree.object-inspector .node.object-node {
+ display: inline-block;
+}
+
+.tree.object-inspector .object-label,
+.tree.object-inspector .object-label * {
+ color: var(--theme-highlight-blue);
+}
+
+.tree.object-inspector .node .unavailable {
+ color: var(--theme-comment);
+}
+
+.tree.object-inspector .lessen,
+.tree.object-inspector .lessen *,
+.tree.object-inspector .lessen .object-label,
+.tree.object-inspector .lessen .object-label * {
+ color: var(--theme-comment);
+}
+
+.tree.object-inspector .block .object-label,
+.tree.object-inspector .block .object-label * {
+ color: var(--theme-body-color);
+}
+
+.tree.object-inspector .block .object-label::before {
+ content: "☲";
+ font-size: 1.1em;
+ display: inline;
+ padding-inline-end: 2px;
+ line-height: 14px;
+}
+
+.object-inspector .object-delimiter {
+ color: var(--theme-comment);
+ white-space: pre-wrap;
+}
+
+.object-inspector .tree-node .arrow {
+ display: inline-block;
+ vertical-align: middle;
+ margin-inline-start: -1px;
+}
+
+/* Focused styles */
+.tree.object-inspector .tree-node.focused * {
+ color: inherit;
+}
+
+.tree-node.focused button.open-inspector {
+ fill: currentColor;
+}
+
+.tree-node.focused button.invoke-getter {
+ background-color: currentColor;
+}
+
+button[class*="remove-watchpoint-"] {
+ background: url("chrome://devtools/content/debugger/images/webconsole-logpoint.svg")
+ no-repeat;
+ display: inline-block;
+ vertical-align: top;
+ height: 13px;
+ width: 15px;
+ margin: 1px 4px 0px 20px;
+ padding: 0;
+ border: none;
+ -moz-context-properties: fill, stroke;
+ cursor: pointer;
+}
+
+button.remove-watchpoint-set {
+ fill: var(--breakpoint-fill);
+ stroke: var(--breakpoint-fill);
+}
+
+button.remove-watchpoint-get {
+ fill: var(--purple-60);
+ stroke: var(--purple-60);
+}
+
+button.remove-watchpoint-getorset {
+ fill: var(--yellow-60);
+ stroke: var(--yellow-60);
+}
+
+.tree-node.focused button[class*="remove-watchpoint-"] {
+ stroke: white;
+}
+
+/* Don't display the light grey background we have on button hover */
+.theme-dark button[class*="remove-watchpoint-"]:hover,
+.theme-light button[class*="remove-watchpoint-"]:hover {
+ background-color: transparent;
+}
diff --git a/devtools/client/shared/components/object-inspector/components/ObjectInspector.js b/devtools/client/shared/components/object-inspector/components/ObjectInspector.js
new file mode 100644
index 0000000000..a8af00d32a
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/ObjectInspector.js
@@ -0,0 +1,371 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+ createElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+ Provider,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+loader.lazyRequireGetter(
+ this,
+ "createStore",
+ "resource://devtools/client/shared/redux/create-store.js"
+);
+
+const actions = require("resource://devtools/client/shared/components/object-inspector/actions.js");
+const {
+ getExpandedPaths,
+ getLoadedProperties,
+ getEvaluations,
+ default: reducer,
+} = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+
+const Tree = createFactory(require("resource://devtools/client/shared/components/Tree.js"));
+
+const ObjectInspectorItem = createFactory(
+ require("resource://devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js")
+);
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const { renderRep, shouldRenderRootsInReps } = Utils;
+const {
+ getChildrenWithEvaluations,
+ getActor,
+ getEvaluatedItem,
+ getParent,
+ getValue,
+ nodeIsPrimitive,
+ nodeHasGetter,
+ nodeHasSetter,
+} = Utils.node;
+
+// This implements a component that renders an interactive inspector
+// for looking at JavaScript objects. It expects descriptions of
+// objects from the protocol, and will dynamically fetch children
+// properties as objects are expanded.
+//
+// If you want to inspect a single object, pass the name and the
+// protocol descriptor of it:
+//
+// ObjectInspector({
+// name: "foo",
+// desc: { writable: true, ..., { value: { actor: "1", ... }}},
+// ...
+// })
+//
+// If you want multiple top-level objects (like scopes), you can pass
+// an array of manually constructed nodes as `roots`:
+//
+// ObjectInspector({
+// roots: [{ name: ... }, ...],
+// ...
+// });
+
+// There are 3 types of nodes: a simple node with a children array, an
+// object that has properties that should be children when they are
+// fetched, and a primitive value that should be displayed with no
+// children.
+
+class ObjectInspector extends Component {
+ static defaultProps = {
+ autoReleaseObjectActors: true
+ };
+ constructor(props) {
+ super();
+ this.cachedNodes = new Map();
+
+ const self = this;
+
+ self.getItemChildren = this.getItemChildren.bind(this);
+ self.isNodeExpandable = this.isNodeExpandable.bind(this);
+ self.setExpanded = this.setExpanded.bind(this);
+ self.focusItem = this.focusItem.bind(this);
+ self.activateItem = this.activateItem.bind(this);
+ self.getRoots = this.getRoots.bind(this);
+ self.getNodeKey = this.getNodeKey.bind(this);
+ self.shouldItemUpdate = this.shouldItemUpdate.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.roots = this.props.roots;
+ this.focusedItem = this.props.focusedItem;
+ this.activeItem = this.props.activeItem;
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate(nextProps) {
+ this.removeOutdatedNodesFromCache(nextProps);
+
+ if (this.roots !== nextProps.roots) {
+ // Since the roots changed, we assume the properties did as well,
+ // so we need to cleanup the component internal state.
+ const oldRoots = this.roots;
+ this.roots = nextProps.roots;
+ this.focusedItem = nextProps.focusedItem;
+ this.activeItem = nextProps.activeItem;
+ if (this.props.rootsChanged) {
+ this.props.rootsChanged(this.roots, oldRoots);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const { expandedPaths, loadedProperties, evaluations } = this.props;
+
+ // We should update if:
+ // - there are new loaded properties
+ // - 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))) ||
+ this.focusedItem !== nextProps.focusedItem ||
+ this.activeItem !== nextProps.activeItem ||
+ this.roots !== nextProps.roots
+ );
+ }
+
+ componentWillUnmount() {
+ if (this.props.autoReleaseObjectActors){
+ this.props.closeObjectInspector(this.props.roots);
+ }
+ }
+
+ getItemChildren(item) {
+ const { loadedProperties, evaluations } = this.props;
+ const { cachedNodes } = this;
+
+ return getChildrenWithEvaluations({
+ evaluations,
+ loadedProperties,
+ cachedNodes,
+ item,
+ });
+ }
+
+ getRoots() {
+ const { evaluations, roots } = this.props;
+ const length = roots.length;
+
+ for (let i = 0; i < length; i++) {
+ let rootItem = roots[i];
+
+ if (evaluations.has(rootItem.path)) {
+ roots[i] = getEvaluatedItem(rootItem, evaluations);
+ }
+ }
+
+ return roots;
+ }
+
+ getNodeKey(item) {
+ return item.path && typeof item.path.toString === "function"
+ ? item.path.toString()
+ : JSON.stringify(item);
+ }
+
+ isNodeExpandable(item) {
+ if (
+ nodeIsPrimitive(item) ||
+ item.contents?.value?.useCustomFormatter
+ ) {
+ return false;
+ }
+
+ if (nodeHasSetter(item) || nodeHasGetter(item)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ setExpanded(item, expand) {
+ if (!this.isNodeExpandable(item)) {
+ return;
+ }
+
+ const {
+ nodeExpand,
+ nodeCollapse,
+ recordTelemetryEvent,
+ setExpanded,
+ roots,
+ } = this.props;
+
+ if (expand === true) {
+ const actor = getActor(item, roots);
+ nodeExpand(item, actor);
+ if (recordTelemetryEvent) {
+ recordTelemetryEvent("object_expanded");
+ }
+ } else {
+ nodeCollapse(item);
+ }
+
+ if (setExpanded) {
+ setExpanded(item, expand);
+ }
+ }
+
+ focusItem(item) {
+ const { focusable = true, onFocus } = this.props;
+
+ if (focusable && this.focusedItem !== item) {
+ this.focusedItem = item;
+ this.forceUpdate();
+
+ if (onFocus) {
+ onFocus(item);
+ }
+ }
+ }
+
+ activateItem(item) {
+ const { focusable = true, onActivate } = this.props;
+
+ if (focusable && this.activeItem !== item) {
+ this.activeItem = item;
+ this.forceUpdate();
+
+ if (onActivate) {
+ onActivate(item);
+ }
+ }
+ }
+
+ shouldItemUpdate(prevItem, nextItem) {
+ const value = getValue(nextItem);
+ // Long string should always update because fullText loading will not
+ // trigger item re-render.
+ return value && value.type === "longString";
+ }
+
+ render() {
+ const {
+ autoExpandAll = true,
+ autoExpandDepth = 1,
+ initiallyExpanded,
+ focusable = true,
+ disableWrap = false,
+ expandedPaths,
+ inline,
+ } = this.props;
+
+ const classNames = ["object-inspector"];
+ if (inline) {
+ classNames.push("inline");
+ }
+ if (disableWrap) {
+ classNames.push("nowrap");
+ }
+
+ return Tree({
+ className: classNames.join(" "),
+
+ 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,
+ getKey: this.getNodeKey,
+
+ onExpand: item => this.setExpanded(item, true),
+ onCollapse: item => this.setExpanded(item, false),
+ onFocus: focusable ? this.focusItem : null,
+ onActivate: focusable ? this.activateItem : null,
+
+ shouldItemUpdate: this.shouldItemUpdate,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ ObjectInspectorItem({
+ ...this.props,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ setExpanded: this.setExpanded,
+ }),
+ });
+ }
+}
+
+function mapStateToProps(state, props) {
+ return {
+ expandedPaths: getExpandedPaths(state),
+ loadedProperties: getLoadedProperties(state),
+ evaluations: getEvaluations(state),
+ };
+}
+
+const OI = connect(mapStateToProps, actions)(ObjectInspector);
+
+module.exports = props => {
+ const { roots, standalone = false } = props;
+
+ if (roots.length == 0) {
+ return null;
+ }
+
+ if (shouldRenderRootsInReps(roots, props)) {
+ return renderRep(roots[0], props);
+ }
+
+ const oiElement = createElement(OI, props);
+
+ if (!standalone) {
+ return oiElement;
+ }
+
+ const store = createStore(reducer);
+ return createElement(Provider, { store }, oiElement);
+};
diff --git a/devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js b/devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js
new file mode 100644
index 0000000000..534ac0e13b
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/ObjectInspectorItem.js
@@ -0,0 +1,285 @@
+/* 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 { Component } = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+
+const Utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+
+const {
+ getValue,
+ nodeHasAccessors,
+ nodeHasProperties,
+ nodeIsBlock,
+ nodeIsDefaultProperties,
+ nodeIsFunction,
+ nodeIsGetter,
+ nodeIsMapEntry,
+ nodeIsMissingArguments,
+ nodeIsOptimizedOut,
+ nodeIsPrimitive,
+ nodeIsPrototype,
+ nodeIsSetter,
+ nodeIsUninitializedBinding,
+ nodeIsUnmappedBinding,
+ nodeIsUnscopedBinding,
+ nodeIsWindow,
+ nodeIsLongString,
+ nodeHasFullText,
+ nodeHasGetter,
+ getNonPrototypeParentGripValue,
+} = Utils.node;
+
+class ObjectInspectorItem extends Component {
+ static get defaultProps() {
+ return {
+ onContextMenu: () => {},
+ renderItemActions: () => null,
+ };
+ }
+
+ // eslint-disable-next-line complexity
+ getLabelAndValue() {
+ const { item, depth, expanded, mode } = this.props;
+
+ const label = item.name;
+ const isPrimitive = nodeIsPrimitive(item);
+
+ if (nodeIsOptimizedOut(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(optimized away)"),
+ };
+ }
+
+ if (nodeIsUninitializedBinding(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(uninitialized)"),
+ };
+ }
+
+ if (nodeIsUnmappedBinding(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(unmapped)"),
+ };
+ }
+
+ if (nodeIsUnscopedBinding(item)) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(unscoped)"),
+ };
+ }
+
+ const itemValue = getValue(item);
+ const unavailable =
+ isPrimitive &&
+ itemValue &&
+ itemValue.hasOwnProperty &&
+ itemValue.hasOwnProperty("unavailable");
+
+ if (nodeIsMissingArguments(item) || unavailable) {
+ return {
+ label,
+ value: dom.span({ className: "unavailable" }, "(unavailable)"),
+ };
+ }
+
+ if (
+ nodeIsFunction(item) &&
+ !nodeIsGetter(item) &&
+ !nodeIsSetter(item) &&
+ (mode === MODE.TINY || !mode)
+ ) {
+ return {
+ label: Utils.renderRep(item, {
+ ...this.props,
+ functionName: label,
+ }),
+ };
+ }
+
+ if (
+ nodeHasProperties(item) ||
+ nodeHasAccessors(item) ||
+ nodeIsMapEntry(item) ||
+ nodeIsLongString(item) ||
+ isPrimitive
+ ) {
+ const repProps = { ...this.props };
+ if (depth > 0) {
+ repProps.mode = mode === MODE.LONG ? MODE.SHORT : MODE.TINY;
+ }
+
+
+ if (nodeIsLongString(item)) {
+ repProps.member = {
+ open: nodeHasFullText(item) && expanded,
+ };
+ }
+
+ if (nodeHasGetter(item)) {
+ const receiverGrip = getNonPrototypeParentGripValue(item);
+ if (receiverGrip) {
+ Object.assign(repProps, {
+ onInvokeGetterButtonClick: () =>
+ this.props.invokeGetter(item, receiverGrip.actor),
+ });
+ }
+ }
+
+ return {
+ label,
+ value: Utils.renderRep(item, repProps),
+ };
+ }
+
+ return {
+ label,
+ };
+ }
+
+ getTreeItemProps() {
+ const {
+ item,
+ depth,
+ focused,
+ expanded,
+ onCmdCtrlClick,
+ onDoubleClick,
+ dimTopLevelWindow,
+ onContextMenu,
+ } = this.props;
+
+ const classNames = ["node", "object-node"];
+ if (focused) {
+ classNames.push("focused");
+ }
+
+ if (nodeIsBlock(item)) {
+ classNames.push("block");
+ }
+
+ if (
+ !expanded &&
+ (nodeIsDefaultProperties(item) ||
+ nodeIsPrototype(item) ||
+ nodeIsGetter(item) ||
+ nodeIsSetter(item) ||
+ (dimTopLevelWindow === true && nodeIsWindow(item) && depth === 0))
+ ) {
+ classNames.push("lessen");
+ }
+
+ const parentElementProps = {
+ className: classNames.join(" "),
+ onClick: e => {
+ if (
+ onCmdCtrlClick &&
+ ((isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey))
+ ) {
+ onCmdCtrlClick(item, {
+ depth,
+ event: e,
+ focused,
+ expanded,
+ });
+ e.stopPropagation();
+ return;
+ }
+
+ // If this click happened because the user selected some text, bail out.
+ // Note that if the user selected some text before and then clicks here,
+ // the previously selected text will be first unselected, unless the
+ // 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 (
+ e.target &&
+ Utils.selection.documentHasSelection(e.target.ownerDocument) &&
+ !(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,
+ expanded,
+ });
+ };
+ }
+
+ return parentElementProps;
+ }
+
+ renderLabel(label) {
+ if (label === null || typeof label === "undefined") {
+ return null;
+ }
+
+ const { item, depth, focused, expanded, onLabelClick } = this.props;
+ return dom.span(
+ {
+ className: "object-label",
+ onClick: onLabelClick
+ ? event => {
+ event.stopPropagation();
+
+ // If the user selected text, bail out.
+ if (
+ Utils.selection.documentHasSelection(event.target.ownerDocument)
+ ) {
+ return;
+ }
+
+ onLabelClick(item, {
+ depth,
+ focused,
+ expanded,
+ setExpanded: this.props.setExpanded,
+ });
+ }
+ : undefined,
+ },
+ label
+ );
+ }
+
+ render() {
+ const { arrow, renderItemActions, item } = 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,
+ renderItemActions(item)
+ );
+ }
+}
+
+module.exports = ObjectInspectorItem;
diff --git a/devtools/client/shared/components/object-inspector/components/moz.build b/devtools/client/shared/components/object-inspector/components/moz.build
new file mode 100644
index 0000000000..a1744891f2
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/components/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "ObjectInspector.js",
+ "ObjectInspectorItem.js",
+)
diff --git a/devtools/client/shared/components/object-inspector/index.js b/devtools/client/shared/components/object-inspector/index.js
new file mode 100644
index 0000000000..34e4d30086
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/index.js
@@ -0,0 +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("resource://devtools/client/shared/components/object-inspector/components/ObjectInspector.js");
+const utils = require("resource://devtools/client/shared/components/object-inspector/utils/index.js");
+const reducer = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+const actions = require("resource://devtools/client/shared/components/object-inspector/actions.js");
+
+module.exports = { ObjectInspector, utils, actions, reducer };
diff --git a/devtools/client/shared/components/object-inspector/moz.build b/devtools/client/shared/components/object-inspector/moz.build
new file mode 100644
index 0000000000..14f9c285ba
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "components",
+ "utils",
+]
+
+DevToolsModules(
+ "actions.js",
+ "index.js",
+ "reducer.js",
+)
diff --git a/devtools/client/shared/components/object-inspector/reducer.js b/devtools/client/shared/components/object-inspector/reducer.js
new file mode 100644
index 0000000000..aa8af2b529
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/reducer.js
@@ -0,0 +1,147 @@
+/* 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 initialOIState(overrides) {
+ return {
+ expandedPaths: new Set(),
+ loadedProperties: new Map(),
+ evaluations: new Map(),
+ watchpoints: new Map(),
+ ...overrides,
+ };
+}
+
+function reducer(state = initialOIState(), action = {}) {
+ const { type, data } = action;
+
+ const cloneState = overrides => ({ ...state, ...overrides });
+
+ if (type === "NODE_EXPAND") {
+ return cloneState({
+ expandedPaths: new Set(state.expandedPaths).add(data.node.path),
+ });
+ }
+
+ 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({
+ loadedProperties: new Map(state.loadedProperties).set(
+ data.node.path,
+ action.data.properties
+ ),
+ });
+ }
+
+ if (type === "ROOTS_CHANGED") {
+ return cloneState();
+ }
+
+ if (type === "GETTER_INVOKED") {
+ return cloneState({
+ evaluations: new Map(state.evaluations).set(data.node.path, {
+ 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 initialOIState({ 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 || state;
+}
+
+function getExpandedPaths(state) {
+ return getObjectInspectorState(state).expandedPaths;
+}
+
+function getExpandedPathKeys(state) {
+ return [...getExpandedPaths(state).keys()];
+}
+
+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 = {
+ getWatchpoints,
+ getEvaluations,
+ getExpandedPathKeys,
+ getExpandedPaths,
+ getLoadedProperties,
+ getLoadedPropertyKeys,
+};
+
+Object.defineProperty(module.exports, "__esModule", {
+ value: true,
+});
+module.exports = { ...selectors, initialOIState };
+module.exports.default = reducer;
diff --git a/devtools/client/shared/components/object-inspector/utils/client.js b/devtools/client/shared/components/object-inspector/utils/client.js
new file mode 100644
index 0000000000..eaa42be05a
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/client.js
@@ -0,0 +1,124 @@
+/* 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 {
+ getValue,
+ nodeHasFullText,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+async function enumIndexedProperties(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumProperties({
+ ignoreNonIndexedProperties: true,
+ });
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumIndexedProperties", e);
+ return {};
+ }
+}
+
+async function enumNonIndexedProperties(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumProperties({
+ ignoreIndexedProperties: true,
+ });
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumNonIndexedProperties", e);
+ return {};
+ }
+}
+
+async function enumEntries(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumEntries();
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumEntries", e);
+ return {};
+ }
+}
+
+async function enumSymbols(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumSymbols();
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumSymbols", e);
+ return {};
+ }
+}
+
+async function enumPrivateProperties(objectFront, start, end) {
+ try {
+ const iterator = await objectFront.enumPrivateProperties();
+ const response = await iteratorSlice(iterator, start, end);
+ return response;
+ } catch (e) {
+ console.error("Error in enumPrivateProperties", e);
+ return {};
+ }
+}
+
+async function getPrototype(objectFront) {
+ if (typeof objectFront.getPrototype !== "function") {
+ console.error("objectFront.getPrototype is not a function");
+ return Promise.resolve({});
+ }
+ return objectFront.getPrototype();
+}
+
+async function getFullText(longStringFront, item) {
+ const { initial, fullText, length } = getValue(item);
+ // Return fullText property if it exists so that it can be added to the
+ // loadedProperties map.
+ if (nodeHasFullText(item)) {
+ return { fullText };
+ }
+
+ try {
+ const substring = await longStringFront.substring(initial.length, length);
+ return {
+ fullText: initial + substring,
+ };
+ } catch (e) {
+ console.error("LongStringFront.substring", e);
+ throw e;
+ }
+}
+
+async function getPromiseState(objectFront) {
+ return objectFront.getPromiseState();
+}
+
+async function getProxySlots(objectFront) {
+ return objectFront.getProxySlots();
+}
+
+function iteratorSlice(iterator, start, end) {
+ start = start || 0;
+ const count = end ? end - start + 1 : iterator.count;
+
+ if (count === 0) {
+ return Promise.resolve({});
+ }
+ return iterator.slice(start, count);
+}
+
+module.exports = {
+ enumEntries,
+ enumIndexedProperties,
+ enumNonIndexedProperties,
+ enumPrivateProperties,
+ enumSymbols,
+ getPrototype,
+ getFullText,
+ getPromiseState,
+ getProxySlots,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/index.js b/devtools/client/shared/components/object-inspector/utils/index.js
new file mode 100644
index 0000000000..13b3fd0049
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/index.js
@@ -0,0 +1,52 @@
+/* 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 client = require("resource://devtools/client/shared/components/object-inspector/utils/client.js");
+const loadProperties = require("resource://devtools/client/shared/components/object-inspector/utils/load-properties.js");
+const node = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+const { nodeIsError, nodeIsPrimitive } = node;
+const selection = require("resource://devtools/client/shared/components/object-inspector/utils/selection.js");
+
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/reps/constants.js");
+const {
+ REPS: { Rep, Grip },
+} = require("resource://devtools/client/shared/components/reps/reps/rep.js");
+
+function shouldRenderRootsInReps(roots, props = {}) {
+ if (roots.length !== 1) {
+ return false;
+ }
+
+ const root = roots[0];
+ const name = root && root.name;
+
+ return (
+ (name === null || typeof name === "undefined") &&
+ (nodeIsPrimitive(root) ||
+ (root?.contents?.value?.useCustomFormatter === true &&
+ Array.isArray(root?.contents?.value?.header)) ||
+ (nodeIsError(root) && props?.customFormat === true))
+ );
+}
+
+function renderRep(item, props) {
+ return Rep({
+ ...props,
+ front: item.contents.front,
+ object: node.getValue(item),
+ mode: props.mode || MODE.TINY,
+ defaultRep: Grip,
+ });
+}
+
+module.exports = {
+ client,
+ loadProperties,
+ node,
+ renderRep,
+ selection,
+ shouldRenderRootsInReps,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/load-properties.js b/devtools/client/shared/components/object-inspector/utils/load-properties.js
new file mode 100644
index 0000000000..42525e54f1
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/load-properties.js
@@ -0,0 +1,260 @@
+/* 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 {
+ enumEntries,
+ enumIndexedProperties,
+ enumNonIndexedProperties,
+ enumPrivateProperties,
+ enumSymbols,
+ getPrototype,
+ getFullText,
+ getPromiseState,
+ getProxySlots,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/client.js");
+
+const {
+ getClosestGripNode,
+ getClosestNonBucketNode,
+ getFront,
+ getValue,
+ nodeHasAccessors,
+ nodeHasProperties,
+ nodeIsBucket,
+ nodeIsDefaultProperties,
+ nodeIsEntries,
+ nodeIsMapEntry,
+ nodeIsPrimitive,
+ nodeIsPromise,
+ nodeIsProxy,
+ nodeNeedsNumericalBuckets,
+ nodeIsLongString,
+} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
+
+function loadItemProperties(item, client, loadedProperties, threadActorID) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+ let front = getFront(gripItem);
+
+ if (!front && value && client && client.getFrontByID) {
+ front = client.getFrontByID(value.actor);
+ }
+
+ const getObjectFront = function() {
+ if (!front) {
+ front = client.createObjectFront(
+ value,
+ client.getFrontByID(threadActorID)
+ );
+ }
+
+ return front;
+ };
+
+ const [start, end] = item.meta
+ ? [item.meta.startIndex, item.meta.endIndex]
+ : [];
+
+ const promises = [];
+
+ if (shouldLoadItemIndexedProperties(item, loadedProperties)) {
+ promises.push(enumIndexedProperties(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemNonIndexedProperties(item, loadedProperties)) {
+ promises.push(enumNonIndexedProperties(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemEntries(item, loadedProperties)) {
+ promises.push(enumEntries(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemPrototype(item, loadedProperties)) {
+ promises.push(getPrototype(getObjectFront()));
+ }
+
+ if (shouldLoadItemPrivateProperties(item, loadedProperties)) {
+ promises.push(enumPrivateProperties(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemSymbols(item, loadedProperties)) {
+ promises.push(enumSymbols(getObjectFront(), start, end));
+ }
+
+ if (shouldLoadItemFullText(item, loadedProperties)) {
+ const longStringFront = front || client.createLongStringFront(value);
+ promises.push(getFullText(longStringFront, item));
+ }
+
+ if (shouldLoadItemPromiseState(item, loadedProperties)) {
+ promises.push(getPromiseState(getObjectFront()));
+ }
+
+ if (shouldLoadItemProxySlots(item, loadedProperties)) {
+ promises.push(getProxySlots(getObjectFront()));
+ }
+
+ return Promise.all(promises).then(mergeResponses);
+}
+
+function mergeResponses(responses) {
+ const data = {};
+
+ for (const response of responses) {
+ if (response.hasOwnProperty("ownProperties")) {
+ data.ownProperties = { ...data.ownProperties, ...response.ownProperties };
+ }
+
+ if (response.privateProperties && response.privateProperties.length > 0) {
+ data.privateProperties = response.privateProperties;
+ }
+
+ if (response.ownSymbols && response.ownSymbols.length > 0) {
+ data.ownSymbols = response.ownSymbols;
+ }
+
+ if (response.prototype) {
+ data.prototype = response.prototype;
+ }
+
+ if (response.fullText) {
+ data.fullText = response.fullText;
+ }
+
+ if (response.promiseState) {
+ data.promiseState = response.promiseState;
+ }
+
+ if (response.proxyTarget && response.proxyHandler) {
+ data.proxyTarget = response.proxyTarget;
+ data.proxyHandler = response.proxyHandler;
+ }
+ }
+
+ return data;
+}
+
+function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ nodeHasProperties(gripItem) &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsProxy(item) &&
+ !nodeNeedsNumericalBuckets(item) &&
+ !nodeIsEntries(getClosestNonBucketNode(item)) &&
+ // The data is loaded when expanding the window node.
+ !nodeIsDefaultProperties(item)
+ );
+}
+
+function shouldLoadItemNonIndexedProperties(
+ item,
+ loadedProperties = new Map()
+) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ nodeHasProperties(gripItem) &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsProxy(item) &&
+ !nodeIsEntries(getClosestNonBucketNode(item)) &&
+ !nodeIsBucket(item) &&
+ // The data is loaded when expanding the window node.
+ !nodeIsDefaultProperties(item)
+ );
+}
+
+function shouldLoadItemEntries(item, loadedProperties = new Map()) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ nodeIsEntries(getClosestNonBucketNode(item)) &&
+ !loadedProperties.has(item.path) &&
+ !nodeNeedsNumericalBuckets(item)
+ );
+}
+
+function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
+ const value = getValue(item);
+
+ return (
+ value &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsBucket(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsDefaultProperties(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsPrimitive(item) &&
+ !nodeIsLongString(item) &&
+ !nodeIsProxy(item)
+ );
+}
+
+function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
+ const value = getValue(item);
+
+ return (
+ value &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsBucket(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsDefaultProperties(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsPrimitive(item) &&
+ !nodeIsLongString(item) &&
+ !nodeIsProxy(item)
+ );
+}
+
+function shouldLoadItemPrivateProperties(item, loadedProperties = new Map()) {
+ const value = getValue(item);
+
+ return (
+ value &&
+ value?.preview?.privatePropertiesLength &&
+ !loadedProperties.has(item.path) &&
+ !nodeIsBucket(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsDefaultProperties(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsPrimitive(item) &&
+ !nodeIsLongString(item) &&
+ !nodeIsProxy(item)
+ );
+}
+
+function shouldLoadItemFullText(item, loadedProperties = new Map()) {
+ return !loadedProperties.has(item.path) && nodeIsLongString(item);
+}
+
+function shouldLoadItemPromiseState(item, loadedProperties = new Map()) {
+ return !loadedProperties.has(item.path) && nodeIsPromise(item);
+}
+
+function shouldLoadItemProxySlots(item, loadedProperties = new Map()) {
+ return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
+module.exports = {
+ loadItemProperties,
+ mergeResponses,
+ shouldLoadItemEntries,
+ shouldLoadItemIndexedProperties,
+ shouldLoadItemNonIndexedProperties,
+ shouldLoadItemPrototype,
+ shouldLoadItemSymbols,
+ shouldLoadItemFullText,
+ shouldLoadItemPromiseState,
+ shouldLoadItemProxySlots,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/moz.build b/devtools/client/shared/components/object-inspector/utils/moz.build
new file mode 100644
index 0000000000..1301b2aca6
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "client.js",
+ "index.js",
+ "load-properties.js",
+ "node.js",
+ "selection.js",
+)
diff --git a/devtools/client/shared/components/object-inspector/utils/node.js b/devtools/client/shared/components/object-inspector/utils/node.js
new file mode 100644
index 0000000000..7b4d1fb0ce
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/node.js
@@ -0,0 +1,1039 @@
+/* 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 {
+ maybeEscapePropertyName,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const ArrayRep = require("resource://devtools/client/shared/components/reps/reps/array.js");
+const GripArrayRep = require("resource://devtools/client/shared/components/reps/reps/grip-array.js");
+const GripMap = require("resource://devtools/client/shared/components/reps/reps/grip-map.js");
+const GripEntryRep = require("resource://devtools/client/shared/components/reps/reps/grip-entry.js");
+const ErrorRep = require("resource://devtools/client/shared/components/reps/reps/error.js");
+const BigIntRep = require("resource://devtools/client/shared/components/reps/reps/big-int.js");
+const {
+ isLongString,
+} = require("resource://devtools/client/shared/components/reps/reps/string.js");
+
+const MAX_NUMERICAL_PROPERTIES = 100;
+
+const NODE_TYPES = {
+ BUCKET: Symbol("[n…m]"),
+ DEFAULT_PROPERTIES: Symbol("<default properties>"),
+ ENTRIES: Symbol("<entries>"),
+ GET: Symbol("<get>"),
+ GRIP: Symbol("GRIP"),
+ MAP_ENTRY_KEY: Symbol("<key>"),
+ MAP_ENTRY_VALUE: Symbol("<value>"),
+ PROMISE_REASON: Symbol("<reason>"),
+ PROMISE_STATE: Symbol("<state>"),
+ PROMISE_VALUE: Symbol("<value>"),
+ PROXY_HANDLER: Symbol("<handler>"),
+ PROXY_TARGET: Symbol("<target>"),
+ SET: Symbol("<set>"),
+ PROTOTYPE: Symbol("<prototype>"),
+ BLOCK: Symbol("☲"),
+};
+
+let WINDOW_PROPERTIES = {};
+
+if (typeof window === "object") {
+ WINDOW_PROPERTIES = Object.getOwnPropertyNames(window);
+}
+
+function getType(item) {
+ return item.type;
+}
+
+function getValue(item) {
+ if (nodeHasValue(item)) {
+ return item.contents.value;
+ }
+
+ if (nodeHasGetterValue(item)) {
+ return item.contents.getterValue;
+ }
+
+ if (nodeHasAccessors(item)) {
+ return item.contents;
+ }
+
+ return undefined;
+}
+
+function getFront(item) {
+ return item && item.contents && item.contents.front;
+}
+
+function getActor(item, roots) {
+ const isRoot = isNodeRoot(item, roots);
+ const value = getValue(item);
+ return isRoot || !value ? null : value.actor;
+}
+
+function isNodeRoot(item, roots) {
+ const gripItem = getClosestGripNode(item);
+ const value = getValue(gripItem);
+
+ return (
+ value &&
+ roots.some(root => {
+ const rootValue = getValue(root);
+ return rootValue && rootValue.actor === value.actor;
+ })
+ );
+}
+
+function nodeIsBucket(item) {
+ return getType(item) === NODE_TYPES.BUCKET;
+}
+
+function nodeIsEntries(item) {
+ return getType(item) === NODE_TYPES.ENTRIES;
+}
+
+function nodeIsMapEntry(item) {
+ return GripEntryRep.supportsObject(getValue(item));
+}
+
+function nodeHasChildren(item) {
+ return Array.isArray(item.contents);
+}
+
+function nodeHasValue(item) {
+ return item && item.contents && item.contents.hasOwnProperty("value");
+}
+
+function nodeHasGetterValue(item) {
+ return item && item.contents && item.contents.hasOwnProperty("getterValue");
+}
+
+function nodeIsObject(item) {
+ const value = getValue(item);
+ return value && value.type === "object";
+}
+
+function nodeIsArrayLike(item) {
+ const value = getValue(item);
+ return GripArrayRep.supportsObject(value) || ArrayRep.supportsObject(value);
+}
+
+function nodeIsFunction(item) {
+ const value = getValue(item);
+ return value && value.class === "Function";
+}
+
+function nodeIsOptimizedOut(item) {
+ const value = getValue(item);
+ return !nodeHasChildren(item) && value && value.optimizedOut;
+}
+
+function nodeIsUninitializedBinding(item) {
+ const value = getValue(item);
+ return value && value.uninitialized;
+}
+
+// Used to check if an item represents a binding that exists in a sourcemap's
+// original file content, but does not match up with a binding found in the
+// generated code.
+function nodeIsUnmappedBinding(item) {
+ const value = getValue(item);
+ return value && value.unmapped;
+}
+
+// Used to check if an item represents a binding that exists in the debugger's
+// parser result, but does not match up with a binding returned by the
+// devtools server.
+function nodeIsUnscopedBinding(item) {
+ const value = getValue(item);
+ return value && value.unscoped;
+}
+
+function nodeIsMissingArguments(item) {
+ const value = getValue(item);
+ return !nodeHasChildren(item) && value && value.missingArguments;
+}
+
+function nodeHasProperties(item) {
+ return !nodeHasChildren(item) && nodeIsObject(item);
+}
+
+function nodeIsPrimitive(item) {
+ return (
+ nodeIsBigInt(item) ||
+ (!nodeHasChildren(item) &&
+ !nodeHasProperties(item) &&
+ !nodeIsEntries(item) &&
+ !nodeIsMapEntry(item) &&
+ !nodeHasAccessors(item) &&
+ !nodeIsBucket(item) &&
+ !nodeIsLongString(item))
+ );
+}
+
+function nodeIsDefaultProperties(item) {
+ return getType(item) === NODE_TYPES.DEFAULT_PROPERTIES;
+}
+
+function isDefaultWindowProperty(name) {
+ return WINDOW_PROPERTIES.includes(name);
+}
+
+function nodeIsPromise(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ return value.class == "Promise";
+}
+
+function nodeIsProxy(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ return value.class == "Proxy";
+}
+
+function nodeIsPrototype(item) {
+ return getType(item) === NODE_TYPES.PROTOTYPE;
+}
+
+function nodeIsWindow(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ return value.class == "Window";
+}
+
+function nodeIsGetter(item) {
+ return getType(item) === NODE_TYPES.GET;
+}
+
+function nodeIsSetter(item) {
+ return getType(item) === NODE_TYPES.SET;
+}
+
+function nodeIsBlock(item) {
+ return getType(item) === NODE_TYPES.BLOCK;
+}
+
+function nodeIsError(item) {
+ return ErrorRep.supportsObject(getValue(item));
+}
+
+function nodeIsLongString(item) {
+ return isLongString(getValue(item));
+}
+
+function nodeIsBigInt(item) {
+ return BigIntRep.supportsObject(getValue(item));
+}
+
+function nodeHasFullText(item) {
+ const value = getValue(item);
+ return nodeIsLongString(item) && value.hasOwnProperty("fullText");
+}
+
+function nodeHasGetter(item) {
+ const getter = getNodeGetter(item);
+ return getter && getter.type !== "undefined";
+}
+
+function nodeHasSetter(item) {
+ const setter = getNodeSetter(item);
+ return setter && setter.type !== "undefined";
+}
+
+function nodeHasAccessors(item) {
+ return nodeHasGetter(item) || nodeHasSetter(item);
+}
+
+function nodeSupportsNumericalBucketing(item) {
+ // We exclude elements with entries since it's the <entries> node
+ // itself that can have buckets.
+ return (
+ (nodeIsArrayLike(item) && !nodeHasEntries(item)) ||
+ nodeIsEntries(item) ||
+ nodeIsBucket(item)
+ );
+}
+
+function nodeHasEntries(item) {
+ const value = getValue(item);
+ if (!value) {
+ return false;
+ }
+
+ const className = value.class;
+ return (
+ className === "Map" ||
+ className === "Set" ||
+ className === "WeakMap" ||
+ className === "WeakSet" ||
+ className === "Storage" ||
+ className === "URLSearchParams" ||
+ className === "Headers" ||
+ className === "FormData" ||
+ className === "MIDIInputMap" ||
+ className === "MIDIOutputMap"
+ );
+}
+
+function nodeNeedsNumericalBuckets(item) {
+ return (
+ nodeSupportsNumericalBucketing(item) &&
+ getNumericalPropertiesCount(item) > MAX_NUMERICAL_PROPERTIES
+ );
+}
+
+function makeNodesForPromiseProperties(loadedProps, item) {
+ const { reason, value, state } = loadedProps.promiseState;
+ const properties = [];
+
+ if (state) {
+ properties.push(
+ createNode({
+ parent: item,
+ name: "<state>",
+ contents: { value: state },
+ type: NODE_TYPES.PROMISE_STATE,
+ })
+ );
+ }
+
+ if (reason) {
+ properties.push(
+ createNode({
+ parent: item,
+ name: "<reason>",
+ contents: {
+ value: reason.getGrip ? reason.getGrip() : reason,
+ front: reason.getGrip ? reason : null,
+ },
+ type: NODE_TYPES.PROMISE_REASON,
+ })
+ );
+ }
+
+ if (value) {
+ properties.push(
+ createNode({
+ parent: item,
+ name: "<value>",
+ contents: {
+ value: value.getGrip ? value.getGrip() : value,
+ front: value.getGrip ? value : null,
+ },
+ type: NODE_TYPES.PROMISE_VALUE,
+ })
+ );
+ }
+
+ return properties;
+}
+
+function makeNodesForProxyProperties(loadedProps, item) {
+ const { proxyHandler, proxyTarget } = loadedProps;
+
+ const isProxyHandlerFront = proxyHandler && proxyHandler.getGrip;
+ const proxyHandlerGrip = isProxyHandlerFront
+ ? proxyHandler.getGrip()
+ : proxyHandler;
+ const proxyHandlerFront = isProxyHandlerFront ? proxyHandler : null;
+
+ const isProxyTargetFront = proxyTarget && proxyTarget.getGrip;
+ const proxyTargetGrip = isProxyTargetFront
+ ? proxyTarget.getGrip()
+ : proxyTarget;
+ const proxyTargetFront = isProxyTargetFront ? proxyTarget : null;
+
+ return [
+ createNode({
+ parent: item,
+ name: "<target>",
+ contents: { value: proxyTargetGrip, front: proxyTargetFront },
+ type: NODE_TYPES.PROXY_TARGET,
+ }),
+ createNode({
+ parent: item,
+ name: "<handler>",
+ contents: { value: proxyHandlerGrip, front: proxyHandlerFront },
+ type: NODE_TYPES.PROXY_HANDLER,
+ }),
+ ];
+}
+
+function makeNodesForEntries(item) {
+ const nodeName = "<entries>";
+
+ return createNode({
+ parent: item,
+ name: nodeName,
+ contents: null,
+ type: NODE_TYPES.ENTRIES,
+ });
+}
+
+function makeNodesForMapEntry(item) {
+ const nodeValue = getValue(item);
+ if (!nodeValue || !nodeValue.preview) {
+ return [];
+ }
+
+ const { key, value } = nodeValue.preview;
+ const isKeyFront = key && key.getGrip;
+ const keyGrip = isKeyFront ? key.getGrip() : key;
+ const keyFront = isKeyFront ? key : null;
+
+ const isValueFront = value && value.getGrip;
+ const valueGrip = isValueFront ? value.getGrip() : value;
+ const valueFront = isValueFront ? value : null;
+
+ return [
+ createNode({
+ parent: item,
+ name: "<key>",
+ contents: { value: keyGrip, front: keyFront },
+ type: NODE_TYPES.MAP_ENTRY_KEY,
+ }),
+ createNode({
+ parent: item,
+ name: "<value>",
+ contents: { value: valueGrip, front: valueFront },
+ type: NODE_TYPES.MAP_ENTRY_VALUE,
+ }),
+ ];
+}
+
+function getNodeGetter(item) {
+ return item && item.contents ? item.contents.get : undefined;
+}
+
+function getNodeSetter(item) {
+ return item && item.contents ? item.contents.set : undefined;
+}
+
+function sortProperties(properties) {
+ return properties.sort((a, b) => {
+ // Sort numbers in ascending order and sort strings lexicographically
+ const aInt = parseInt(a, 10);
+ const bInt = parseInt(b, 10);
+
+ if (isNaN(aInt) || isNaN(bInt)) {
+ return a > b ? 1 : -1;
+ }
+
+ return aInt - bInt;
+ });
+}
+
+function makeNumericalBuckets(parent) {
+ const numProperties = getNumericalPropertiesCount(parent);
+
+ // We want to have at most a hundred slices.
+ const bucketSize =
+ 10 ** Math.max(2, Math.ceil(Math.log10(numProperties)) - 2);
+ const numBuckets = Math.ceil(numProperties / bucketSize);
+
+ const buckets = [];
+ for (let i = 1; i <= numBuckets; i++) {
+ const minKey = (i - 1) * bucketSize;
+ const maxKey = Math.min(i * bucketSize - 1, numProperties - 1);
+ const startIndex = nodeIsBucket(parent) ? parent.meta.startIndex : 0;
+ const minIndex = startIndex + minKey;
+ const maxIndex = startIndex + maxKey;
+ const bucketName = `[${minIndex}…${maxIndex}]`;
+
+ buckets.push(
+ createNode({
+ parent,
+ name: bucketName,
+ contents: null,
+ type: NODE_TYPES.BUCKET,
+ meta: {
+ startIndex: minIndex,
+ endIndex: maxIndex,
+ },
+ })
+ );
+ }
+ return buckets;
+}
+
+function makeDefaultPropsBucket(propertiesNames, parent, ownProperties) {
+ const userPropertiesNames = [];
+ const defaultProperties = [];
+
+ propertiesNames.forEach(name => {
+ if (isDefaultWindowProperty(name)) {
+ defaultProperties.push(name);
+ } else {
+ userPropertiesNames.push(name);
+ }
+ });
+
+ const nodes = makeNodesForOwnProps(
+ userPropertiesNames,
+ parent,
+ ownProperties
+ );
+
+ if (defaultProperties.length > 0) {
+ const defaultPropertiesNode = createNode({
+ parent,
+ name: "<default properties>",
+ contents: null,
+ type: NODE_TYPES.DEFAULT_PROPERTIES,
+ });
+
+ const defaultNodes = makeNodesForOwnProps(
+ defaultProperties,
+ defaultPropertiesNode,
+ ownProperties
+ );
+ nodes.push(setNodeChildren(defaultPropertiesNode, defaultNodes));
+ }
+ return nodes;
+}
+
+function makeNodesForOwnProps(propertiesNames, parent, ownProperties) {
+ return propertiesNames.map(name => {
+ const property = ownProperties[name];
+
+ let propertyValue = property;
+ if (property && property.hasOwnProperty("getterValue")) {
+ propertyValue = property.getterValue;
+ } else if (property && property.hasOwnProperty("value")) {
+ propertyValue = property.value;
+ }
+
+ // propertyValue can be a front (LongString or Object) or a primitive grip.
+ const isFront = propertyValue && propertyValue.getGrip;
+ const front = isFront ? propertyValue : null;
+ const grip = isFront ? front.getGrip() : propertyValue;
+
+ return createNode({
+ parent,
+ name: maybeEscapePropertyName(name),
+ propertyName: name,
+ contents: {
+ ...(property || {}),
+ value: grip,
+ front,
+ },
+ });
+ });
+}
+
+function makeNodesForProperties(objProps, parent) {
+ const {
+ ownProperties = {},
+ ownSymbols,
+ privateProperties,
+ prototype,
+ safeGetterValues,
+ } = objProps;
+
+ const parentValue = getValue(parent);
+ const allProperties = { ...ownProperties, ...safeGetterValues };
+
+ // Ignore properties that are neither non-concrete nor getters/setters.
+ const propertiesNames = sortProperties(Object.keys(allProperties)).filter(
+ name => {
+ if (!allProperties[name]) {
+ return false;
+ }
+
+ const properties = Object.getOwnPropertyNames(allProperties[name]);
+ return properties.some(property =>
+ ["value", "getterValue", "get", "set"].includes(property)
+ );
+ }
+ );
+
+ const isParentNodeWindow = parentValue && parentValue.class == "Window";
+ const nodes = isParentNodeWindow
+ ? makeDefaultPropsBucket(propertiesNames, parent, allProperties)
+ : makeNodesForOwnProps(propertiesNames, parent, allProperties);
+
+ if (Array.isArray(ownSymbols)) {
+ ownSymbols.forEach((ownSymbol, index) => {
+ const descriptorValue = ownSymbol?.descriptor?.value;
+ const hasGrip = descriptorValue?.getGrip;
+ const symbolGrip = hasGrip ? descriptorValue.getGrip() : descriptorValue;
+ const symbolFront = hasGrip ? ownSymbol.descriptor.value : null;
+
+ nodes.push(
+ createNode({
+ parent,
+ name: ownSymbol.name,
+ path: `symbol-${index}`,
+ contents: {
+ value: symbolGrip,
+ front: symbolFront,
+ },
+ })
+ );
+ }, this);
+ }
+
+ if (Array.isArray(privateProperties)) {
+ privateProperties.forEach((privateProperty, index) => {
+ const descriptorValue = privateProperty?.descriptor?.value;
+ const hasGrip = descriptorValue?.getGrip;
+ const privatePropertyGrip = hasGrip
+ ? descriptorValue.getGrip()
+ : descriptorValue;
+ const privatePropertyFront = hasGrip
+ ? privateProperty.descriptor.value
+ : null;
+
+ nodes.push(
+ createNode({
+ parent,
+ name: privateProperty.name,
+ path: `private-${index}`,
+ contents: {
+ value: privatePropertyGrip,
+ front: privatePropertyFront,
+ },
+ })
+ );
+ }, this);
+ }
+
+ if (nodeIsPromise(parent)) {
+ nodes.push(...makeNodesForPromiseProperties(objProps, parent));
+ }
+
+ if (nodeHasEntries(parent)) {
+ nodes.push(makeNodesForEntries(parent));
+ }
+
+ // Add accessor nodes if needed
+ const defaultPropertiesNode = isParentNodeWindow
+ ? nodes.find(node => nodeIsDefaultProperties(node))
+ : null;
+
+ for (const name of propertiesNames) {
+ const property = allProperties[name];
+ const isDefaultProperty =
+ isParentNodeWindow &&
+ defaultPropertiesNode &&
+ isDefaultWindowProperty(name);
+ const parentNode = isDefaultProperty ? defaultPropertiesNode : parent;
+ const parentContentsArray =
+ isDefaultProperty && defaultPropertiesNode
+ ? defaultPropertiesNode.contents
+ : nodes;
+
+ if (property.get && property.get.type !== "undefined") {
+ parentContentsArray.push(
+ createGetterNode({
+ parent: parentNode,
+ property,
+ name,
+ })
+ );
+ }
+
+ if (property.set && property.set.type !== "undefined") {
+ parentContentsArray.push(
+ createSetterNode({
+ parent: parentNode,
+ property,
+ name,
+ })
+ );
+ }
+ }
+
+ // Add the prototype if it exists and is not null
+ if (prototype && prototype.type !== "null") {
+ nodes.push(makeNodeForPrototype(objProps, parent));
+ }
+
+ return nodes;
+}
+
+function setNodeFullText(loadedProps, node) {
+ if (nodeHasFullText(node) || !nodeIsLongString(node)) {
+ return node;
+ }
+
+ const { fullText } = loadedProps;
+ if (nodeHasValue(node)) {
+ node.contents.value.fullText = fullText;
+ } else if (nodeHasGetterValue(node)) {
+ node.contents.getterValue.fullText = fullText;
+ }
+
+ return node;
+}
+
+function makeNodeForPrototype(objProps, parent) {
+ const { prototype } = objProps || {};
+
+ // Add the prototype if it exists and is not null
+ if (prototype && prototype.type !== "null") {
+ return createNode({
+ parent,
+ name: "<prototype>",
+ contents: {
+ value: prototype.getGrip ? prototype.getGrip() : prototype,
+ front: prototype.getGrip ? prototype : null,
+ },
+ type: NODE_TYPES.PROTOTYPE,
+ });
+ }
+
+ return null;
+}
+
+function createNode(options) {
+ const {
+ parent,
+ name,
+ propertyName,
+ path,
+ contents,
+ type = NODE_TYPES.GRIP,
+ meta,
+ } = options;
+
+ if (contents === undefined) {
+ return null;
+ }
+
+ // The path is important to uniquely identify the item in the entire
+ // tree. This helps debugging & optimizes React's rendering of large
+ // lists. The path will be separated by property name.
+
+ return {
+ parent,
+ name,
+ // `name` can be escaped; propertyName contains the original property name.
+ propertyName,
+ path: createPath(parent && parent.path, path || name),
+ contents,
+ type,
+ meta,
+ };
+}
+
+function createGetterNode({ parent, property, name }) {
+ const isFront = property.get && property.get.getGrip;
+ const grip = isFront ? property.get.getGrip() : property.get;
+ const front = isFront ? property.get : null;
+
+ return createNode({
+ parent,
+ name: `<get ${name}()>`,
+ contents: { value: grip, front },
+ type: NODE_TYPES.GET,
+ });
+}
+
+function createSetterNode({ parent, property, name }) {
+ const isFront = property.set && property.set.getGrip;
+ const grip = isFront ? property.set.getGrip() : property.set;
+ const front = isFront ? property.set : null;
+
+ return createNode({
+ parent,
+ name: `<set ${name}()>`,
+ contents: { value: grip, front },
+ type: NODE_TYPES.SET,
+ });
+}
+
+function setNodeChildren(node, children) {
+ node.contents = children;
+ return node;
+}
+
+function getEvaluatedItem(item, evaluations) {
+ if (!evaluations.has(item.path)) {
+ return item;
+ }
+
+ const evaluation = evaluations.get(item.path);
+ const isFront =
+ evaluation && evaluation.getterValue && evaluation.getterValue.getGrip;
+
+ const contents = isFront
+ ? {
+ getterValue: evaluation.getterValue.getGrip(),
+ front: evaluation.getterValue,
+ }
+ : evaluations.get(item.path);
+
+ return {
+ ...item,
+ contents,
+ };
+}
+
+function getChildrenWithEvaluations(options) {
+ const { item, loadedProperties, cachedNodes, evaluations } = options;
+
+ const children = getChildren({
+ loadedProperties,
+ cachedNodes,
+ item,
+ });
+
+ if (Array.isArray(children)) {
+ return children.map(i => getEvaluatedItem(i, evaluations));
+ }
+
+ if (children) {
+ return getEvaluatedItem(children, evaluations);
+ }
+
+ return [];
+}
+
+function getChildren(options) {
+ const { cachedNodes, item, loadedProperties = new Map() } = options;
+
+ const key = item.path;
+ if (cachedNodes && cachedNodes.has(key)) {
+ return cachedNodes.get(key);
+ }
+
+ const loadedProps = loadedProperties.get(key);
+ const hasLoadedProps = loadedProperties.has(key);
+
+ // Because we are dynamically creating the tree as the user
+ // expands it (not precalculated tree structure), we cache child
+ // arrays. This not only helps performance, but is necessary
+ // because the expanded state depends on instances of nodes
+ // being the same across renders. If we didn't do this, each
+ // node would be a new instance every render.
+ // If the node needs properties, we only add children to
+ // the cache if the properties are loaded.
+ const addToCache = children => {
+ if (cachedNodes) {
+ cachedNodes.set(item.path, children);
+ }
+ return children;
+ };
+
+ // Nodes can either have children already, or be an object with
+ // properties that we need to go and fetch.
+ if (nodeHasChildren(item)) {
+ return addToCache(item.contents);
+ }
+
+ if (nodeIsMapEntry(item)) {
+ return addToCache(makeNodesForMapEntry(item));
+ }
+
+ if (nodeIsProxy(item) && hasLoadedProps) {
+ return addToCache(makeNodesForProxyProperties(loadedProps, item));
+ }
+
+ if (nodeIsLongString(item) && hasLoadedProps) {
+ // Set longString object's fullText to fetched one.
+ return addToCache(setNodeFullText(loadedProps, item));
+ }
+
+ if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
+ // Even if we have numerical buckets, we should have loaded non indexed
+ // properties.
+ const bucketNodes = makeNumericalBuckets(item);
+ return addToCache(
+ bucketNodes.concat(makeNodesForProperties(loadedProps, item))
+ );
+ }
+
+ if (!nodeIsEntries(item) && !nodeIsBucket(item) && !nodeHasProperties(item)) {
+ 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) {
+ const parent = nodeIsBucket(item.parent) ? item.parent.parent : item.parent;
+ return `${getPathExpression(parent)}.${item.name}`;
+ }
+
+ return item.name;
+}
+
+function getParent(item) {
+ return item.parent;
+}
+
+function getNumericalPropertiesCount(item) {
+ if (nodeIsBucket(item)) {
+ return item.meta.endIndex - item.meta.startIndex + 1;
+ }
+
+ const value = getValue(getClosestGripNode(item));
+ if (!value) {
+ return 0;
+ }
+
+ if (GripArrayRep.supportsObject(value)) {
+ return GripArrayRep.getLength(value);
+ }
+
+ if (GripMap.supportsObject(value)) {
+ return GripMap.getLength(value);
+ }
+
+ // TODO: We can also have numerical properties on Objects, but at the
+ // moment we don't have a way to distinguish them from non-indexed properties,
+ // as they are all computed in a ownPropertiesLength property.
+
+ return 0;
+}
+
+function getClosestGripNode(item) {
+ const type = getType(item);
+ if (
+ type !== NODE_TYPES.BUCKET &&
+ type !== NODE_TYPES.DEFAULT_PROPERTIES &&
+ type !== NODE_TYPES.ENTRIES
+ ) {
+ return item;
+ }
+
+ const parent = getParent(item);
+ if (!parent) {
+ return null;
+ }
+
+ return getClosestGripNode(parent);
+}
+
+function getClosestNonBucketNode(item) {
+ const type = getType(item);
+
+ if (type !== NODE_TYPES.BUCKET) {
+ return item;
+ }
+
+ const parent = getParent(item);
+ if (!parent) {
+ return null;
+ }
+
+ return getClosestNonBucketNode(parent);
+}
+
+function getParentGripNode(item) {
+ const parentNode = getParent(item);
+ if (!parentNode) {
+ return null;
+ }
+
+ return getClosestGripNode(parentNode);
+}
+
+function getParentGripValue(item) {
+ const parentGripNode = getParentGripNode(item);
+ if (!parentGripNode) {
+ return null;
+ }
+
+ return getValue(parentGripNode);
+}
+
+function getParentFront(item) {
+ const parentGripNode = getParentGripNode(item);
+ if (!parentGripNode) {
+ return null;
+ }
+
+ return getFront(parentGripNode);
+}
+
+function getNonPrototypeParentGripValue(item) {
+ const parentGripNode = getParentGripNode(item);
+ if (!parentGripNode) {
+ return null;
+ }
+
+ if (getType(parentGripNode) === NODE_TYPES.PROTOTYPE) {
+ return getNonPrototypeParentGripValue(parentGripNode);
+ }
+
+ return getValue(parentGripNode);
+}
+
+function createPath(parentPath, path) {
+ return parentPath ? `${parentPath}◦${path}` : path;
+}
+
+module.exports = {
+ createNode,
+ createGetterNode,
+ createSetterNode,
+ getActor,
+ getChildren,
+ getChildrenWithEvaluations,
+ getClosestGripNode,
+ getClosestNonBucketNode,
+ getEvaluatedItem,
+ getFront,
+ getPathExpression,
+ getParent,
+ getParentFront,
+ getParentGripValue,
+ getNonPrototypeParentGripValue,
+ getNumericalPropertiesCount,
+ getValue,
+ makeNodesForEntries,
+ makeNodesForPromiseProperties,
+ makeNodesForProperties,
+ makeNumericalBuckets,
+ nodeHasAccessors,
+ nodeHasChildren,
+ nodeHasEntries,
+ nodeHasProperties,
+ nodeHasGetter,
+ nodeHasSetter,
+ nodeIsBlock,
+ nodeIsBucket,
+ nodeIsDefaultProperties,
+ nodeIsEntries,
+ nodeIsError,
+ nodeIsLongString,
+ nodeHasFullText,
+ nodeIsFunction,
+ nodeIsGetter,
+ nodeIsMapEntry,
+ nodeIsMissingArguments,
+ nodeIsObject,
+ nodeIsOptimizedOut,
+ nodeIsPrimitive,
+ nodeIsPromise,
+ nodeIsPrototype,
+ nodeIsProxy,
+ nodeIsSetter,
+ nodeIsUninitializedBinding,
+ nodeIsUnmappedBinding,
+ nodeIsUnscopedBinding,
+ nodeIsWindow,
+ nodeNeedsNumericalBuckets,
+ nodeSupportsNumericalBucketing,
+ setNodeChildren,
+ sortProperties,
+ NODE_TYPES,
+};
diff --git a/devtools/client/shared/components/object-inspector/utils/selection.js b/devtools/client/shared/components/object-inspector/utils/selection.js
new file mode 100644
index 0000000000..fdcca7ff6b
--- /dev/null
+++ b/devtools/client/shared/components/object-inspector/utils/selection.js
@@ -0,0 +1,16 @@
+/* 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 documentHasSelection(doc = document) {
+ const selection = doc.defaultView.getSelection();
+ if (!selection) {
+ return false;
+ }
+
+ return selection.type === "Range";
+}
+
+module.exports = {
+ documentHasSelection,
+};