summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/expressions.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/actions/expressions.js')
-rw-r--r--devtools/client/debugger/src/actions/expressions.js210
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
+ );
+}