diff options
Diffstat (limited to 'devtools/client/debugger/src/actions/expressions.js')
-rw-r--r-- | devtools/client/debugger/src/actions/expressions.js | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/expressions.js b/devtools/client/debugger/src/actions/expressions.js new file mode 100644 index 0000000000..8ccb6013ba --- /dev/null +++ b/devtools/client/debugger/src/actions/expressions.js @@ -0,0 +1,210 @@ +/* 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 { + 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, parserWorker }) => { + 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 ({ getState, dispatch, parserWorker }) => { + 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 + ); +} |