summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/object-inspector/utils
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/object-inspector/utils')
-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
6 files changed, 1504 insertions, 0 deletions
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,
+};