Source code
Revision control
Copy as Markdown
Other Tools
/* 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
import {
getOriginalFrameScope,
getGeneratedFrameScope,
getSelectedFrameInlinePreviews,
getSelectedLocation,
getSelectedFrame,
} from "../../selectors/index";
import { features } from "../../utils/prefs";
import { validateSelectedFrame } 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;
}
/**
* Update the inline previews for the currently selected frame.
*/
export function generateInlinePreview() {
return async function ({ dispatch, getState, parserWorker, client }) {
if (!features.inlinePreview) {
return null;
}
// Avoid regenerating inline previews when we already have preview data
if (getSelectedFrameInlinePreviews(getState())) {
return null;
}
const selectedFrame = getSelectedFrame(getState());
const originalFrameScopes = getOriginalFrameScope(
getState(),
selectedFrame
);
const generatedFrameScopes = getGeneratedFrameScope(
getState(),
selectedFrame
);
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);
// Bailout if we resumed or moved to another frame while computing the scope
validateSelectedFrame(getState(), selectedFrame);
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 },
},
selectedFrame.thread
);
}
const previewsFromBindings = getBindingValues(
originalAstScopes,
pausedOnLine,
name,
bindings[name].value,
curLevel,
properties
);
allPreviews.push(...previewsFromBindings);
});
await Promise.all(previewBindings);
// Bailout if we resumed or moved to another frame while fetching the values from the backend
validateSelectedFrame(getState(), selectedFrame);
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",
selectedFrame,
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,
// This attribute helps distinguish pause from trace previews
type: "paused",
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 };
}