summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/pause/inlinePreview.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/actions/pause/inlinePreview.js')
-rw-r--r--devtools/client/debugger/src/actions/pause/inlinePreview.js244
1 files changed, 244 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/pause/inlinePreview.js b/devtools/client/debugger/src/actions/pause/inlinePreview.js
new file mode 100644
index 0000000000..e3a4e614c0
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/inlinePreview.js
@@ -0,0 +1,244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getOriginalFrameScope,
+ getGeneratedFrameScope,
+ getInlinePreviews,
+ getSelectedLocation,
+} from "../../selectors";
+import { features } from "../../utils/prefs";
+import { validateThreadContext } from "../../utils/context";
+
+// We need to display all variables in the current functional scope so
+// include all data for block scopes until the first functional scope
+function getLocalScopeLevels(originalAstScopes) {
+ let levels = 0;
+ while (
+ originalAstScopes[levels] &&
+ originalAstScopes[levels].type === "block"
+ ) {
+ levels++;
+ }
+ return levels;
+}
+
+export function generateInlinePreview(cx, frame) {
+ return async function ({ dispatch, getState, parserWorker, client }) {
+ if (!frame || !features.inlinePreview) {
+ return null;
+ }
+
+ const { thread } = cx;
+
+ // Avoid regenerating inline previews when we already have preview data
+ if (getInlinePreviews(getState(), thread, frame.id)) {
+ return null;
+ }
+
+ const originalFrameScopes = getOriginalFrameScope(
+ getState(),
+ thread,
+ frame.location.sourceId,
+ frame.id
+ );
+
+ const generatedFrameScopes = getGeneratedFrameScope(
+ getState(),
+ thread,
+ frame.id
+ );
+
+ let scopes = originalFrameScopes?.scope || generatedFrameScopes?.scope;
+
+ if (!scopes || !scopes.bindings) {
+ return null;
+ }
+
+ // It's important to use selectedLocation, because we don't know
+ // if we'll be viewing the original or generated frame location
+ const selectedLocation = getSelectedLocation(getState());
+ if (!selectedLocation) {
+ return null;
+ }
+
+ if (!parserWorker.isLocationSupported(selectedLocation)) {
+ return null;
+ }
+
+ const originalAstScopes = await parserWorker.getScopes(selectedLocation);
+ validateThreadContext(getState(), cx);
+ if (!originalAstScopes) {
+ return null;
+ }
+
+ const allPreviews = [];
+ const pausedOnLine = selectedLocation.line;
+ const levels = getLocalScopeLevels(originalAstScopes);
+
+ for (
+ let curLevel = 0;
+ curLevel <= levels && scopes && scopes.bindings;
+ curLevel++
+ ) {
+ const bindings = { ...scopes.bindings.variables };
+ scopes.bindings.arguments.forEach(argument => {
+ Object.keys(argument).forEach(key => {
+ bindings[key] = argument[key];
+ });
+ });
+
+ const previewBindings = Object.keys(bindings).map(async name => {
+ // We want to show values of properties of objects only and not
+ // function calls on other data types like someArr.forEach etc..
+ let properties = null;
+ const objectGrip = bindings[name].value;
+ if (objectGrip.actor && objectGrip.class === "Object") {
+ properties = await client.loadObjectProperties(
+ {
+ name,
+ path: name,
+ contents: { value: objectGrip },
+ },
+ cx.thread
+ );
+ }
+
+ const previewsFromBindings = getBindingValues(
+ originalAstScopes,
+ pausedOnLine,
+ name,
+ bindings[name].value,
+ curLevel,
+ properties
+ );
+
+ allPreviews.push(...previewsFromBindings);
+ });
+ await Promise.all(previewBindings);
+
+ scopes = scopes.parent;
+ }
+
+ // Sort previews by line and column so they're displayed in the right order in the editor
+ allPreviews.sort((previewA, previewB) => {
+ if (previewA.line < previewB.line) {
+ return -1;
+ }
+ if (previewA.line > previewB.line) {
+ return 1;
+ }
+ // If we have the same line number
+ return previewA.column < previewB.column ? -1 : 1;
+ });
+
+ const previews = {};
+ for (const preview of allPreviews) {
+ const { line } = preview;
+ if (!previews[line]) {
+ previews[line] = [];
+ }
+ previews[line].push(preview);
+ }
+
+ return dispatch({
+ type: "ADD_INLINE_PREVIEW",
+ thread,
+ frame,
+ previews,
+ });
+ };
+}
+
+function getBindingValues(
+ originalAstScopes,
+ pausedOnLine,
+ name,
+ value,
+ curLevel,
+ properties
+) {
+ const previews = [];
+
+ const binding = originalAstScopes[curLevel]?.bindings[name];
+ if (!binding) {
+ return previews;
+ }
+
+ // Show a variable only once ( an object and it's child property are
+ // counted as different )
+ const identifiers = new Set();
+
+ // We start from end as we want to show values besides variable
+ // located nearest to the breakpoint
+ for (let i = binding.refs.length - 1; i >= 0; i--) {
+ const ref = binding.refs[i];
+ // Subtracting 1 from line as codemirror lines are 0 indexed
+ const line = ref.start.line - 1;
+ const column = ref.start.column;
+ // We don't want to render inline preview below the paused line
+ if (line >= pausedOnLine - 1) {
+ continue;
+ }
+
+ const { displayName, displayValue } = getExpressionNameAndValue(
+ name,
+ value,
+ ref,
+ properties
+ );
+
+ // Variable with same name exists, display value of current or
+ // closest to the current scope's variable
+ if (identifiers.has(displayName)) {
+ continue;
+ }
+ identifiers.add(displayName);
+
+ previews.push({
+ line,
+ column,
+ name: displayName,
+ value: displayValue,
+ });
+ }
+ return previews;
+}
+
+function getExpressionNameAndValue(
+ name,
+ value,
+ // TODO: Add data type to ref
+ ref,
+ properties
+) {
+ let displayName = name;
+ let displayValue = value;
+
+ // Only variables of type Object will have properties
+ if (properties) {
+ let { meta } = ref;
+ // Presence of meta property means expression contains child property
+ // reference eg: objName.propName
+ while (meta) {
+ // Initially properties will be an array, after that it will be an object
+ if (displayValue === value) {
+ const property = properties.find(prop => prop.name === meta.property);
+ displayValue = property?.contents.value;
+ displayName += `.${meta.property}`;
+ } else if (displayValue?.preview?.ownProperties) {
+ const { ownProperties } = displayValue.preview;
+ Object.keys(ownProperties).forEach(prop => {
+ if (prop === meta.property) {
+ displayValue = ownProperties[prop].value;
+ displayName += `.${meta.property}`;
+ }
+ });
+ }
+ meta = meta.parent;
+ }
+ }
+
+ return { displayName, displayValue };
+}