autocomplete.js (12868B)
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 "use strict"; 6 7 const { 8 AUTOCOMPLETE_CLEAR, 9 AUTOCOMPLETE_DATA_RECEIVE, 10 AUTOCOMPLETE_PENDING_REQUEST, 11 AUTOCOMPLETE_RETRIEVE_FROM_CACHE, 12 } = require("resource://devtools/client/webconsole/constants.js"); 13 14 const { 15 analyzeInputString, 16 shouldInputBeAutocompleted, 17 } = require("resource://devtools/shared/webconsole/analyze-input-string.js"); 18 19 loader.lazyRequireGetter( 20 this, 21 "getSelectedTarget", 22 "resource://devtools/shared/commands/target/selectors/targets.js", 23 true 24 ); 25 26 /** 27 * Update the data used for the autocomplete popup in the console input (JsTerm). 28 * 29 * @param {boolean} force: True to force a call to the server (as opposed to retrieve 30 * from the cache). 31 * @param {Array<string>} getterPath: Array representing the getter access (i.e. 32 * `a.b.c.d.` is described as ['a', 'b', 'c', 'd'] ). 33 * @param {Array<string>} expressionVars: Array of the variables defined in the expression. 34 */ 35 function autocompleteUpdate(force, getterPath, expressionVars) { 36 return async ({ dispatch, getState, webConsoleUI, hud }) => { 37 if (hud.inputHasSelection()) { 38 return dispatch(autocompleteClear()); 39 } 40 41 const inputValue = hud.getInputValue(); 42 const mappedVars = hud.getMappedVariables() ?? {}; 43 const allVars = (expressionVars ?? []).concat(Object.keys(mappedVars)); 44 const frameActorId = await hud.getSelectedFrameActorID(); 45 46 const cursor = webConsoleUI.getInputCursor(); 47 48 const state = getState().autocomplete; 49 const { cache } = state; 50 if ( 51 !force && 52 (!inputValue || /^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor))) 53 ) { 54 return dispatch(autocompleteClear()); 55 } 56 57 const rawInput = inputValue.substring(0, cursor); 58 const retrieveFromCache = 59 !force && 60 cache && 61 cache.input && 62 rawInput.startsWith(cache.input) && 63 /[a-zA-Z0-9]$/.test(rawInput) && 64 frameActorId === cache.frameActorId; 65 66 if (retrieveFromCache) { 67 return dispatch(autoCompleteDataRetrieveFromCache(rawInput)); 68 } 69 70 const authorizedEvaluations = updateAuthorizedEvaluations( 71 state.authorizedEvaluations, 72 getterPath, 73 mappedVars 74 ); 75 76 const { input, originalExpression } = await getMappedInput( 77 rawInput, 78 mappedVars, 79 hud 80 ); 81 82 return dispatch( 83 autocompleteDataFetch({ 84 input, 85 frameActorId, 86 authorizedEvaluations, 87 force, 88 allVars, 89 mappedVars, 90 originalExpression, 91 }) 92 ); 93 }; 94 } 95 96 /** 97 * Combine or replace authorizedEvaluations with the newly authorized getter path, if any. 98 * 99 * @param {Array<Array<string>>} authorizedEvaluations Existing authorized evaluations (may 100 * be updated in place) 101 * @param {Array<string>} getterPath The new getter path 102 * @param {{[string]: string}} mappedVars Map of original to generated variable names. 103 * @returns {Array<Array<string>>} The updated authorized evaluations (the original array, 104 * if it was updated in place) 105 */ 106 function updateAuthorizedEvaluations( 107 authorizedEvaluations, 108 getterPath, 109 mappedVars 110 ) { 111 if (!Array.isArray(authorizedEvaluations) || !authorizedEvaluations.length) { 112 authorizedEvaluations = []; 113 } 114 115 if (Array.isArray(getterPath) && getterPath.length) { 116 // We need to check for any previous authorizations. For example, here if getterPath 117 // is ["a", "b", "c", "d"], we want to see if there was any other path that was 118 // authorized in a previous request. For that, we only add the previous 119 // authorizations if the last auth is contained in getterPath. (for the example, we 120 // would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]]) 121 const last = authorizedEvaluations[authorizedEvaluations.length - 1]; 122 123 const generatedPath = mappedVars[getterPath[0]]?.split("."); 124 if (generatedPath) { 125 getterPath = generatedPath.concat(getterPath.slice(1)); 126 } 127 128 const isMappedVariable = 129 generatedPath && getterPath.length === generatedPath.length; 130 const concat = !last || last.every((x, index) => x === getterPath[index]); 131 if (isMappedVariable) { 132 // If the path consists only of an original variable, add all the prefixes of its 133 // mapping. For example, for myVar => a.b.c, authorize a, a.b, and a.b.c. This 134 // ensures we'll only show a prompt for myVar once even if a.b and a.b.c are both 135 // unsafe getters. 136 authorizedEvaluations = generatedPath.map((_, i) => 137 generatedPath.slice(0, i + 1) 138 ); 139 } else if (concat) { 140 authorizedEvaluations.push(getterPath); 141 } else { 142 authorizedEvaluations = [getterPath]; 143 } 144 } 145 return authorizedEvaluations; 146 } 147 148 /** 149 * Apply source mapping to the autocomplete input. 150 * 151 * @param {string} rawInput The input to map. 152 * @param {{[string]: string}} mappedVars Map of original to generated variable names. 153 * @param {WebConsole} hud A reference to the webconsole hud. 154 * @returns {string} The source-mapped expression to autocomplete. 155 */ 156 async function getMappedInput(rawInput, mappedVars, hud) { 157 if (!mappedVars || !Object.keys(mappedVars).length) { 158 return { input: rawInput, originalExpression: undefined }; 159 } 160 161 const inputAnalysis = analyzeInputString(rawInput, 500); 162 if (!shouldInputBeAutocompleted(inputAnalysis)) { 163 return { input: rawInput, originalExpression: undefined }; 164 } 165 166 const { 167 mainExpression: originalExpression, 168 isPropertyAccess, 169 isElementAccess, 170 lastStatement, 171 } = inputAnalysis; 172 173 // If we're autocompleting a variable name, pass it through unchanged so that we 174 // show original variable names rather than generated ones. 175 // For example, if we have the mapping `myVariable` => `x`, show variables starting 176 // with myVariable rather than x. 177 if (!isPropertyAccess && !isElementAccess) { 178 return { input: lastStatement, originalExpression }; 179 } 180 181 let generated = 182 (await hud.getMappedExpression(originalExpression))?.expression ?? 183 originalExpression; 184 // Strip off the semicolon if the expression was converted to a statement 185 const trailingSemicolon = /;\s*$/; 186 if ( 187 trailingSemicolon.test(generated) && 188 !trailingSemicolon.test(originalExpression) 189 ) { 190 generated = generated.slice(0, generated.lastIndexOf(";")); 191 } 192 193 const suffix = lastStatement.slice(originalExpression.length); 194 return { input: generated + suffix, originalExpression }; 195 } 196 197 /** 198 * Called when the autocompletion data should be cleared. 199 */ 200 function autocompleteClear() { 201 return { 202 type: AUTOCOMPLETE_CLEAR, 203 }; 204 } 205 206 /** 207 * Called when the autocompletion data should be retrieved from the cache (i.e. 208 * client-side). 209 * 210 * @param {string} input: The input used to filter the cached data. 211 */ 212 function autoCompleteDataRetrieveFromCache(input) { 213 return { 214 type: AUTOCOMPLETE_RETRIEVE_FROM_CACHE, 215 input, 216 }; 217 } 218 219 let currentRequestId = 0; 220 function generateRequestId() { 221 return currentRequestId++; 222 } 223 224 /** 225 * Action that fetch autocompletion data from the server. 226 * 227 * @param {object} Object of the following shape: 228 * - {String} input: the expression that we want to complete. 229 * - {String} frameActorId: The id of the frame we want to autocomplete in. 230 * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space). 231 * - {Array} authorizedEvaluations: Array of the properties access which can be 232 * executed by the engine. 233 * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]] 234 * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`. 235 */ 236 function autocompleteDataFetch({ 237 input, 238 frameActorId, 239 force, 240 authorizedEvaluations, 241 allVars, 242 mappedVars, 243 originalExpression, 244 }) { 245 return async ({ dispatch, commands, webConsoleUI, hud }) => { 246 // Retrieve the right WebConsole front that relates either to (by order of priority): 247 // - the currently selected target in the context selector 248 // (contextSelectedTargetFront), 249 // - the currently selected Node in the inspector (selectedNodeActor), 250 // - the currently selected frame in the debugger (when paused) (frameActor), 251 // - the currently selected target in the iframe dropdown 252 // (selectedTargetFront from the TargetCommand) 253 const selectedNodeActorId = webConsoleUI.getSelectedNodeActorID(); 254 255 let targetFront = commands.targetCommand.selectedTargetFront; 256 // Note that getSelectedTargetFront will return null if we default to the top level target. 257 const contextSelectorTargetFront = getSelectedTarget( 258 hud.commands.targetCommand.store.getState() 259 ); 260 const selectedActorId = selectedNodeActorId || frameActorId; 261 if (contextSelectorTargetFront) { 262 targetFront = contextSelectorTargetFront; 263 } else if (selectedActorId) { 264 const selectedFront = commands.client.getFrontByID(selectedActorId); 265 if (selectedFront) { 266 targetFront = selectedFront.targetFront; 267 } 268 } 269 270 const webconsoleFront = await targetFront.getFront("console"); 271 272 const id = generateRequestId(); 273 dispatch({ type: AUTOCOMPLETE_PENDING_REQUEST, id }); 274 275 webconsoleFront 276 .autocomplete( 277 input, 278 undefined, 279 frameActorId, 280 selectedNodeActorId, 281 authorizedEvaluations, 282 allVars 283 ) 284 .then(data => { 285 if (data.isUnsafeGetter && originalExpression !== undefined) { 286 data.getterPath = unmapGetterPath( 287 data.getterPath, 288 originalExpression, 289 mappedVars 290 ); 291 } 292 return dispatch( 293 autocompleteDataReceive({ 294 id, 295 input, 296 force, 297 frameActorId, 298 data, 299 authorizedEvaluations, 300 }) 301 ); 302 }) 303 .catch(e => { 304 console.error("failed autocomplete", e); 305 dispatch(autocompleteClear()); 306 }); 307 }; 308 } 309 310 /** 311 * Replace generated variable names in an unsafe getter path with their original 312 * counterparts. 313 * 314 * @param {Array<string>} getterPath Array of properties leading up to and including the 315 * unsafe getter. 316 * @param {string} originalExpression The expression that was evaluated, before mapping. 317 * @param {{[string]: string}} mappedVars Map of original to generated variable names. 318 * @returns {Array<string>} An updated getter path containing original variables. 319 */ 320 function unmapGetterPath(getterPath, originalExpression, mappedVars) { 321 // We know that the original expression is a sequence of property accesses, that only 322 // the first part can be a mapped variable, and that the getter path must start with 323 // its generated path or be a prefix of it. 324 325 // Suppose we have the expression `foo.bar`, which maps to `a.b.c.bar`. 326 // Get the first part of the expression ("foo") 327 const originalVariable = /^[^.[?]*/s.exec(originalExpression)[0].trim(); 328 const generatedVariable = mappedVars[originalVariable]; 329 if (generatedVariable) { 330 // Get number of properties in "a.b.c" 331 const generatedVariableParts = generatedVariable.split("."); 332 // Replace ["a", "b", "c"] with "foo" in the getter path. 333 // Note that this will also work if the getter path ends inside of the mapped 334 // variable, like ["a", "b"]. 335 return [ 336 originalVariable, 337 ...getterPath.slice(generatedVariableParts.length), 338 ]; 339 } 340 return getterPath; 341 } 342 343 /** 344 * Called when we receive the autocompletion data from the server. 345 * 346 * @param {object} Object of the following shape: 347 * - {Integer} id: The autocompletion request id. This will be used in the reducer 348 * to check that we update the state with the last request results. 349 * - {String} input: the expression that we want to complete. 350 * - {String} frameActorId: The id of the frame we want to autocomplete in. 351 * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space). 352 * - {Object} data: The actual data returned from the server. 353 * - {Array} authorizedEvaluations: Array of the properties access which can be 354 * executed by the engine. 355 * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]] 356 * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`. 357 */ 358 function autocompleteDataReceive({ 359 id, 360 input, 361 frameActorId, 362 force, 363 data, 364 authorizedEvaluations, 365 }) { 366 return { 367 type: AUTOCOMPLETE_DATA_RECEIVE, 368 id, 369 input, 370 force, 371 frameActorId, 372 data, 373 authorizedEvaluations, 374 }; 375 } 376 377 module.exports = { 378 autocompleteClear, 379 autocompleteUpdate, 380 };