expressions.js (6414B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import { 6 getExpression, 7 getExpressions, 8 getSelectedSource, 9 getSelectedScopeMappings, 10 getSelectedFrameBindings, 11 getIsPaused, 12 getSelectedFrame, 13 getCurrentThread, 14 isMapScopesEnabled, 15 } from "../selectors/index"; 16 const { 17 PROMISE, 18 } = require("resource://devtools/client/shared/redux/middleware/promise.js"); 19 import { wrapExpression } from "../utils/expressions"; 20 import { features } from "../utils/prefs"; 21 22 /** 23 * Add expression for debugger to watch 24 * 25 * @param {string} input 26 */ 27 export function addExpression(input) { 28 return async ({ dispatch, getState }) => { 29 if (!input) { 30 return null; 31 } 32 33 // If the expression already exists, only update its evaluation 34 let expression = getExpression(getState(), input); 35 if (!expression) { 36 // This will only display the expression input, 37 // evaluateExpression will update its value. 38 dispatch({ type: "ADD_EXPRESSION", input }); 39 40 expression = getExpression(getState(), input); 41 // When there is an expression error, we won't store the expression 42 if (!expression) { 43 return null; 44 } 45 } 46 47 return dispatch(evaluateExpression(expression)); 48 }; 49 } 50 51 export function autocomplete(input, cursor) { 52 return async ({ dispatch, getState, client }) => { 53 if (!input) { 54 return; 55 } 56 const thread = getCurrentThread(getState()); 57 const selectedFrame = getSelectedFrame(getState()); 58 const result = await client.autocomplete(input, cursor, selectedFrame?.id); 59 // Pass both selectedFrame and thread in case selectedFrame is null 60 dispatch({ type: "AUTOCOMPLETE", selectedFrame, thread, input, result }); 61 }; 62 } 63 64 export function clearAutocomplete() { 65 return { type: "CLEAR_AUTOCOMPLETE" }; 66 } 67 68 export function updateExpression(input, expression) { 69 return async ({ dispatch }) => { 70 if (!input) { 71 return; 72 } 73 74 dispatch({ 75 type: "UPDATE_EXPRESSION", 76 expression, 77 input, 78 }); 79 80 await dispatch(evaluateExpressionsForCurrentContext()); 81 }; 82 } 83 84 /** 85 * 86 * @param {object} expression 87 * @param {string} expression.input 88 */ 89 export function deleteExpression(expression) { 90 return { 91 type: "DELETE_EXPRESSION", 92 input: expression.input, 93 }; 94 } 95 96 export function evaluateExpressionsForCurrentContext() { 97 return async ({ getState, dispatch }) => { 98 const selectedFrame = getSelectedFrame(getState()); 99 await dispatch(evaluateExpressions(selectedFrame)); 100 }; 101 } 102 103 /** 104 * Update all the expressions by querying the server for updated values. 105 * 106 * @param {object} selectedFrame 107 * If defined, will evaluate the expression against this given frame, 108 * otherwise it will use the global scope of the thread. 109 */ 110 export function evaluateExpressions(selectedFrame) { 111 return async function ({ dispatch, getState, client }) { 112 const expressions = getExpressions(getState()); 113 const inputs = expressions.map(({ input }) => input); 114 // Fallback to global scope of the current thread when selectedFrame is null 115 const thread = selectedFrame?.thread || getCurrentThread(getState()); 116 const results = await client.evaluateExpressions(inputs, { 117 // We will only have a specific frame when passing a Selected frame context. 118 frameId: selectedFrame?.id, 119 threadId: thread, 120 }); 121 // Pass both selectedFrame and thread in case selectedFrame is null 122 dispatch({ 123 type: "EVALUATE_EXPRESSIONS", 124 125 selectedFrame, 126 // As `selectedFrame` can be null, pass `thread` to help 127 // the reducer know what is the related thread of this action. 128 thread, 129 130 inputs, 131 results, 132 }); 133 }; 134 } 135 136 function evaluateExpression(expression) { 137 return async function (thunkArgs) { 138 let { input } = expression; 139 if (!input) { 140 console.warn("Expressions should not be empty"); 141 return null; 142 } 143 144 const { dispatch, getState, client } = thunkArgs; 145 const thread = getCurrentThread(getState()); 146 const selectedFrame = getSelectedFrame(getState()); 147 148 const selectedSource = getSelectedSource(getState()); 149 // Only map when we are paused and if the currently selected source is original, 150 // and the paused location is also original. 151 if ( 152 selectedFrame && 153 selectedSource && 154 selectedFrame.location.source.isOriginal && 155 selectedSource.isOriginal 156 ) { 157 const mapResult = await getMappedExpression( 158 input, 159 selectedFrame.thread, 160 thunkArgs 161 ); 162 if (mapResult) { 163 input = mapResult.expression; 164 } 165 } 166 167 // Pass both selectedFrame and thread in case selectedFrame is null 168 return dispatch({ 169 type: "EVALUATE_EXPRESSION", 170 selectedFrame, 171 // When we aren't passing a frame, we have to pass a thread to the pause reducer 172 thread: selectedFrame ? null : thread, 173 input: expression.input, 174 [PROMISE]: client.evaluate(wrapExpression(input), { 175 // When evaluating against the global scope (when not paused) 176 // frameId will be null here. 177 frameId: selectedFrame?.id, 178 }), 179 }); 180 }; 181 } 182 183 /** 184 * Gets information about original variable names from the source map 185 * and replaces all possible generated names. 186 */ 187 export function getMappedExpression(expression, thread, thunkArgs) { 188 const { getState, parserWorker } = thunkArgs; 189 const mappings = getSelectedScopeMappings(getState(), thread); 190 const bindings = getSelectedFrameBindings(getState(), thread); 191 192 // We bail early if we do not need to map the expression. This is important 193 // because mapping an expression can be slow if the parserWorker 194 // worker is busy doing other work. 195 // 196 // 1. there are no mappings - we do not need to map original expressions 197 // 2. does not contain `await` - we do not need to map top level awaits 198 // 3. does not contain `=` - we do not need to map assignments 199 const shouldMapScopes = isMapScopesEnabled(getState()) && mappings; 200 if (!shouldMapScopes && !expression.match(/(await|=)/)) { 201 return null; 202 } 203 204 return parserWorker.mapExpression( 205 expression, 206 mappings, 207 bindings || [], 208 features.mapExpressionBindings && getIsPaused(getState(), thread), 209 features.mapAwaitExpression 210 ); 211 }