/* 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 . */ import { getExpression, getExpressions, getSelectedSource, getSelectedScopeMappings, getSelectedFrameBindings, getIsPaused, getSelectedFrame, getCurrentThread, isMapScopesEnabled, } from "../selectors/index"; import { PROMISE } from "./utils/middleware/promise"; import { wrapExpression } from "../utils/expressions"; import { features } from "../utils/prefs"; /** * Add expression for debugger to watch * * @param {string} input */ export function addExpression(input) { return async ({ dispatch, getState }) => { if (!input) { return null; } // If the expression already exists, only update its evaluation let expression = getExpression(getState(), input); if (!expression) { // This will only display the expression input, // evaluateExpression will update its value. dispatch({ type: "ADD_EXPRESSION", input }); expression = getExpression(getState(), input); // When there is an expression error, we won't store the expression if (!expression) { return null; } } return dispatch(evaluateExpression(expression)); }; } export function autocomplete(input, cursor) { return async ({ dispatch, getState, client }) => { if (!input) { return; } const thread = getCurrentThread(getState()); const selectedFrame = getSelectedFrame(getState(), thread); const result = await client.autocomplete(input, cursor, selectedFrame?.id); // Pass both selectedFrame and thread in case selectedFrame is null dispatch({ type: "AUTOCOMPLETE", selectedFrame, thread, input, result }); }; } export function clearAutocomplete() { return { type: "CLEAR_AUTOCOMPLETE" }; } export function updateExpression(input, expression) { return async ({ dispatch }) => { if (!input) { return; } dispatch({ type: "UPDATE_EXPRESSION", expression, input, }); await dispatch(evaluateExpressionsForCurrentContext()); }; } /** * * @param {object} expression * @param {string} expression.input */ export function deleteExpression(expression) { return { type: "DELETE_EXPRESSION", input: expression.input, }; } export function evaluateExpressionsForCurrentContext() { return async ({ getState, dispatch }) => { const thread = getCurrentThread(getState()); const selectedFrame = getSelectedFrame(getState(), thread); await dispatch(evaluateExpressions(selectedFrame)); }; } /** * Update all the expressions by querying the server for updated values. * * @param {object} selectedFrame * If defined, will evaluate the expression against this given frame, * otherwise it will use the global scope of the thread. */ export function evaluateExpressions(selectedFrame) { return async function ({ dispatch, getState, client }) { const expressions = getExpressions(getState()); const inputs = expressions.map(({ input }) => input); // Fallback to global scope of the current thread when selectedFrame is null const thread = selectedFrame?.thread || getCurrentThread(getState()); const results = await client.evaluateExpressions(inputs, { // We will only have a specific frame when passing a Selected frame context. frameId: selectedFrame?.id, threadId: thread, }); // Pass both selectedFrame and thread in case selectedFrame is null dispatch({ type: "EVALUATE_EXPRESSIONS", selectedFrame, // As `selectedFrame` can be null, pass `thread` to help // the reducer know what is the related thread of this action. thread, inputs, results, }); }; } function evaluateExpression(expression) { return async function (thunkArgs) { let { input } = expression; if (!input) { console.warn("Expressions should not be empty"); return null; } const { dispatch, getState, client } = thunkArgs; const thread = getCurrentThread(getState()); const selectedFrame = getSelectedFrame(getState(), thread); const selectedSource = getSelectedSource(getState()); // Only map when we are paused and if the currently selected source is original, // and the paused location is also original. if ( selectedFrame && selectedSource && selectedFrame.location.source.isOriginal && selectedSource.isOriginal ) { const mapResult = await getMappedExpression( input, selectedFrame.thread, thunkArgs ); if (mapResult) { input = mapResult.expression; } } // Pass both selectedFrame and thread in case selectedFrame is null return dispatch({ type: "EVALUATE_EXPRESSION", selectedFrame, // When we aren't passing a frame, we have to pass a thread to the pause reducer thread: selectedFrame ? null : thread, input: expression.input, [PROMISE]: client.evaluate(wrapExpression(input), { // When evaluating against the global scope (when not paused) // frameId will be null here. frameId: selectedFrame?.id, }), }); }; } /** * Gets information about original variable names from the source map * and replaces all possible generated names. */ export function getMappedExpression(expression, thread, thunkArgs) { const { getState, parserWorker } = thunkArgs; const mappings = getSelectedScopeMappings(getState(), thread); const bindings = getSelectedFrameBindings(getState(), thread); // We bail early if we do not need to map the expression. This is important // because mapping an expression can be slow if the parserWorker // worker is busy doing other work. // // 1. there are no mappings - we do not need to map original expressions // 2. does not contain `await` - we do not need to map top level awaits // 3. does not contain `=` - we do not need to map assignments const shouldMapScopes = isMapScopesEnabled(getState()) && mappings; if (!shouldMapScopes && !expression.match(/(await|=)/)) { return null; } return parserWorker.mapExpression( expression, mappings, bindings || [], features.mapExpressionBindings && getIsPaused(getState(), thread), features.mapAwaitExpression ); }