summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/actions/autocomplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/actions/autocomplete.js')
-rw-r--r--devtools/client/webconsole/actions/autocomplete.js376
1 files changed, 376 insertions, 0 deletions
diff --git a/devtools/client/webconsole/actions/autocomplete.js b/devtools/client/webconsole/actions/autocomplete.js
new file mode 100644
index 0000000000..e2d08351ec
--- /dev/null
+++ b/devtools/client/webconsole/actions/autocomplete.js
@@ -0,0 +1,376 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ AUTOCOMPLETE_CLEAR,
+ AUTOCOMPLETE_DATA_RECEIVE,
+ AUTOCOMPLETE_PENDING_REQUEST,
+ AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ analyzeInputString,
+ shouldInputBeAutocompleted,
+} = require("resource://devtools/shared/webconsole/analyze-input-string.js");
+
+loader.lazyRequireGetter(
+ this,
+ "getSelectedTarget",
+ "resource://devtools/shared/commands/target/selectors/targets.js",
+ true
+);
+
+/**
+ * Update the data used for the autocomplete popup in the console input (JsTerm).
+ *
+ * @param {Boolean} force: True to force a call to the server (as opposed to retrieve
+ * from the cache).
+ * @param {Array<String>} getterPath: Array representing the getter access (i.e.
+ * `a.b.c.d.` is described as ['a', 'b', 'c', 'd'] ).
+ * @param {Array<String>} expressionVars: Array of the variables defined in the expression.
+ */
+function autocompleteUpdate(force, getterPath, expressionVars) {
+ return async ({ dispatch, getState, webConsoleUI, hud }) => {
+ if (hud.inputHasSelection()) {
+ return dispatch(autocompleteClear());
+ }
+
+ const inputValue = hud.getInputValue();
+ const mappedVars = hud.getMappedVariables() ?? {};
+ const allVars = (expressionVars ?? []).concat(Object.keys(mappedVars));
+ const frameActorId = await hud.getSelectedFrameActorID();
+
+ const cursor = webConsoleUI.getInputCursor();
+
+ const state = getState().autocomplete;
+ const { cache } = state;
+ if (
+ !force &&
+ (!inputValue || /^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor)))
+ ) {
+ return dispatch(autocompleteClear());
+ }
+
+ const rawInput = inputValue.substring(0, cursor);
+ const retrieveFromCache =
+ !force &&
+ cache &&
+ cache.input &&
+ rawInput.startsWith(cache.input) &&
+ /[a-zA-Z0-9]$/.test(rawInput) &&
+ frameActorId === cache.frameActorId;
+
+ if (retrieveFromCache) {
+ return dispatch(autoCompleteDataRetrieveFromCache(rawInput));
+ }
+
+ const authorizedEvaluations = updateAuthorizedEvaluations(
+ state.authorizedEvaluations,
+ getterPath,
+ mappedVars
+ );
+
+ const { input, originalExpression } = await getMappedInput(
+ rawInput,
+ mappedVars,
+ hud
+ );
+
+ return dispatch(
+ autocompleteDataFetch({
+ input,
+ frameActorId,
+ authorizedEvaluations,
+ force,
+ allVars,
+ mappedVars,
+ originalExpression,
+ })
+ );
+ };
+}
+
+/**
+ * Combine or replace authorizedEvaluations with the newly authorized getter path, if any.
+ * @param {Array<Array<String>>} authorizedEvaluations Existing authorized evaluations (may
+ * be updated in place)
+ * @param {Array<String>} getterPath The new getter path
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @returns {Array<Array<String>>} The updated authorized evaluations (the original array,
+ * if it was updated in place) */
+function updateAuthorizedEvaluations(
+ authorizedEvaluations,
+ getterPath,
+ mappedVars
+) {
+ if (!Array.isArray(authorizedEvaluations) || !authorizedEvaluations.length) {
+ authorizedEvaluations = [];
+ }
+
+ if (Array.isArray(getterPath) && getterPath.length) {
+ // We need to check for any previous authorizations. For example, here if getterPath
+ // is ["a", "b", "c", "d"], we want to see if there was any other path that was
+ // authorized in a previous request. For that, we only add the previous
+ // authorizations if the last auth is contained in getterPath. (for the example, we
+ // would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]])
+ const last = authorizedEvaluations[authorizedEvaluations.length - 1];
+
+ const generatedPath = mappedVars[getterPath[0]]?.split(".");
+ if (generatedPath) {
+ getterPath = generatedPath.concat(getterPath.slice(1));
+ }
+
+ const isMappedVariable =
+ generatedPath && getterPath.length === generatedPath.length;
+ const concat = !last || last.every((x, index) => x === getterPath[index]);
+ if (isMappedVariable) {
+ // If the path consists only of an original variable, add all the prefixes of its
+ // mapping. For example, for myVar => a.b.c, authorize a, a.b, and a.b.c. This
+ // ensures we'll only show a prompt for myVar once even if a.b and a.b.c are both
+ // unsafe getters.
+ authorizedEvaluations = generatedPath.map((_, i) =>
+ generatedPath.slice(0, i + 1)
+ );
+ } else if (concat) {
+ authorizedEvaluations.push(getterPath);
+ } else {
+ authorizedEvaluations = [getterPath];
+ }
+ }
+ return authorizedEvaluations;
+}
+
+/**
+ * Apply source mapping to the autocomplete input.
+ * @param {String} rawInput The input to map.
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @param {WebConsole} hud A reference to the webconsole hud.
+ * @returns {String} The source-mapped expression to autocomplete.
+ */
+async function getMappedInput(rawInput, mappedVars, hud) {
+ if (!mappedVars || !Object.keys(mappedVars).length) {
+ return { input: rawInput, originalExpression: undefined };
+ }
+
+ const inputAnalysis = analyzeInputString(rawInput, 500);
+ if (!shouldInputBeAutocompleted(inputAnalysis)) {
+ return { input: rawInput, originalExpression: undefined };
+ }
+
+ const {
+ mainExpression: originalExpression,
+ isPropertyAccess,
+ isElementAccess,
+ lastStatement,
+ } = inputAnalysis;
+
+ // If we're autocompleting a variable name, pass it through unchanged so that we
+ // show original variable names rather than generated ones.
+ // For example, if we have the mapping `myVariable` => `x`, show variables starting
+ // with myVariable rather than x.
+ if (!isPropertyAccess && !isElementAccess) {
+ return { input: lastStatement, originalExpression };
+ }
+
+ let generated =
+ (await hud.getMappedExpression(originalExpression))?.expression ??
+ originalExpression;
+ // Strip off the semicolon if the expression was converted to a statement
+ const trailingSemicolon = /;\s*$/;
+ if (
+ trailingSemicolon.test(generated) &&
+ !trailingSemicolon.test(originalExpression)
+ ) {
+ generated = generated.slice(0, generated.lastIndexOf(";"));
+ }
+
+ const suffix = lastStatement.slice(originalExpression.length);
+ return { input: generated + suffix, originalExpression };
+}
+
+/**
+ * Called when the autocompletion data should be cleared.
+ */
+function autocompleteClear() {
+ return {
+ type: AUTOCOMPLETE_CLEAR,
+ };
+}
+
+/**
+ * Called when the autocompletion data should be retrieved from the cache (i.e.
+ * client-side).
+ *
+ * @param {String} input: The input used to filter the cached data.
+ */
+function autoCompleteDataRetrieveFromCache(input) {
+ return {
+ type: AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+ input,
+ };
+}
+
+let currentRequestId = 0;
+function generateRequestId() {
+ return currentRequestId++;
+}
+
+/**
+ * Action that fetch autocompletion data from the server.
+ *
+ * @param {Object} Object of the following shape:
+ * - {String} input: the expression that we want to complete.
+ * - {String} frameActorId: The id of the frame we want to autocomplete in.
+ * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
+ * - {Array} authorizedEvaluations: Array of the properties access which can be
+ * executed by the engine.
+ * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
+ */
+function autocompleteDataFetch({
+ input,
+ frameActorId,
+ force,
+ authorizedEvaluations,
+ allVars,
+ mappedVars,
+ originalExpression,
+}) {
+ return async ({ dispatch, commands, webConsoleUI, hud }) => {
+ // Retrieve the right WebConsole front that relates either to (by order of priority):
+ // - the currently selected target in the context selector
+ // (contextSelectedTargetFront),
+ // - the currently selected Node in the inspector (selectedNodeActor),
+ // - the currently selected frame in the debugger (when paused) (frameActor),
+ // - the currently selected target in the iframe dropdown
+ // (selectedTargetFront from the TargetCommand)
+ const selectedNodeActorId = webConsoleUI.getSelectedNodeActorID();
+
+ let targetFront = commands.targetCommand.selectedTargetFront;
+ // Note that getSelectedTargetFront will return null if we default to the top level target.
+ const contextSelectorTargetFront = getSelectedTarget(
+ hud.commands.targetCommand.store.getState()
+ );
+ const selectedActorId = selectedNodeActorId || frameActorId;
+ if (contextSelectorTargetFront) {
+ targetFront = contextSelectorTargetFront;
+ } else if (selectedActorId) {
+ const selectedFront = commands.client.getFrontByID(selectedActorId);
+ if (selectedFront) {
+ targetFront = selectedFront.targetFront;
+ }
+ }
+
+ const webconsoleFront = await targetFront.getFront("console");
+
+ const id = generateRequestId();
+ dispatch({ type: AUTOCOMPLETE_PENDING_REQUEST, id });
+
+ webconsoleFront
+ .autocomplete(
+ input,
+ undefined,
+ frameActorId,
+ selectedNodeActorId,
+ authorizedEvaluations,
+ allVars
+ )
+ .then(data => {
+ if (data.isUnsafeGetter && originalExpression !== undefined) {
+ data.getterPath = unmapGetterPath(
+ data.getterPath,
+ originalExpression,
+ mappedVars
+ );
+ }
+ return dispatch(
+ autocompleteDataReceive({
+ id,
+ input,
+ force,
+ frameActorId,
+ data,
+ authorizedEvaluations,
+ })
+ );
+ })
+ .catch(e => {
+ console.error("failed autocomplete", e);
+ dispatch(autocompleteClear());
+ });
+ };
+}
+
+/**
+ * Replace generated variable names in an unsafe getter path with their original
+ * counterparts.
+ * @param {Array<String>} getterPath Array of properties leading up to and including the
+ * unsafe getter.
+ * @param {String} originalExpression The expression that was evaluated, before mapping.
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @returns {Array<String>} An updated getter path containing original variables.
+ */
+function unmapGetterPath(getterPath, originalExpression, mappedVars) {
+ // We know that the original expression is a sequence of property accesses, that only
+ // the first part can be a mapped variable, and that the getter path must start with
+ // its generated path or be a prefix of it.
+
+ // Suppose we have the expression `foo.bar`, which maps to `a.b.c.bar`.
+ // Get the first part of the expression ("foo")
+ const originalVariable = /^[^.[?]*/s.exec(originalExpression)[0].trim();
+ const generatedVariable = mappedVars[originalVariable];
+ if (generatedVariable) {
+ // Get number of properties in "a.b.c"
+ const generatedVariableParts = generatedVariable.split(".");
+ // Replace ["a", "b", "c"] with "foo" in the getter path.
+ // Note that this will also work if the getter path ends inside of the mapped
+ // variable, like ["a", "b"].
+ return [
+ originalVariable,
+ ...getterPath.slice(generatedVariableParts.length),
+ ];
+ }
+ return getterPath;
+}
+
+/**
+ * Called when we receive the autocompletion data from the server.
+ *
+ * @param {Object} Object of the following shape:
+ * - {Integer} id: The autocompletion request id. This will be used in the reducer
+ * to check that we update the state with the last request results.
+ * - {String} input: the expression that we want to complete.
+ * - {String} frameActorId: The id of the frame we want to autocomplete in.
+ * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
+ * - {Object} data: The actual data returned from the server.
+ * - {Array} authorizedEvaluations: Array of the properties access which can be
+ * executed by the engine.
+ * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
+ */
+function autocompleteDataReceive({
+ id,
+ input,
+ frameActorId,
+ force,
+ data,
+ authorizedEvaluations,
+}) {
+ return {
+ type: AUTOCOMPLETE_DATA_RECEIVE,
+ id,
+ input,
+ force,
+ frameActorId,
+ data,
+ authorizedEvaluations,
+ };
+}
+
+module.exports = {
+ autocompleteClear,
+ autocompleteUpdate,
+};