summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/sources/select.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/actions/sources/select.js')
-rw-r--r--devtools/client/debugger/src/actions/sources/select.js264
1 files changed, 264 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/sources/select.js b/devtools/client/debugger/src/actions/sources/select.js
new file mode 100644
index 0000000000..c4443432a0
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/select.js
@@ -0,0 +1,264 @@
+/* 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/>. */
+
+/**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+
+import { isOriginalId } from "devtools/client/shared/source-map-loader/index";
+
+import { setSymbols } from "./symbols";
+import { setInScopeLines } from "../ast";
+import { togglePrettyPrint } from "./prettyPrint";
+import { addTab, closeTab } from "../tabs";
+import { loadSourceText } from "./loadSourceText";
+import { mapDisplayNames } from "../pause";
+import { setBreakableLines } from ".";
+
+import { prefs } from "../../utils/prefs";
+import { isMinified } from "../../utils/source";
+import { createLocation } from "../../utils/location";
+import { getRelatedMapLocation } from "../../utils/source-maps";
+
+import {
+ getSource,
+ getFirstSourceActorForGeneratedSource,
+ getSourceByURL,
+ getPrettySource,
+ getSelectedLocation,
+ getShouldSelectOriginalLocation,
+ canPrettyPrintSource,
+ getIsCurrentThreadPaused,
+ getSourceTextContent,
+ tabExists,
+} from "../../selectors";
+
+// This is only used by jest tests (and within this module)
+export const setSelectedLocation = (
+ cx,
+ location,
+ shouldSelectOriginalLocation
+) => ({
+ type: "SET_SELECTED_LOCATION",
+ cx,
+ location,
+ shouldSelectOriginalLocation,
+});
+
+// This is only used by jest tests (and within this module)
+export const setPendingSelectedLocation = (cx, url, options) => ({
+ type: "SET_PENDING_SELECTED_LOCATION",
+ cx,
+ url,
+ line: options?.line,
+ column: options?.column,
+});
+
+// This is only used by jest tests (and within this module)
+export const clearSelectedLocation = cx => ({
+ type: "CLEAR_SELECTED_LOCATION",
+ cx,
+});
+
+/**
+ * Deterministically select a source that has a given URL. This will
+ * work regardless of the connection status or if the source exists
+ * yet.
+ *
+ * This exists mostly for external things to interact with the
+ * debugger.
+ */
+export function selectSourceURL(cx, url, options) {
+ return async ({ dispatch, getState }) => {
+ const source = getSourceByURL(getState(), url);
+ if (!source) {
+ return dispatch(setPendingSelectedLocation(cx, url, options));
+ }
+
+ const location = createLocation({ ...options, source });
+ return dispatch(selectLocation(cx, location));
+ };
+}
+
+/**
+ * Wrapper around selectLocation, which creates the location object for us.
+ * Note that it ignores the currently selected source and will select
+ * the precise generated/original source passed as argument.
+ *
+ * @param {Object} cx
+ * @param {String} source
+ * The precise source to select.
+ * @param {String} sourceActor
+ * The specific source actor of the source to
+ * select the source text. This is optional.
+ */
+export function selectSource(cx, source, sourceActor) {
+ return async ({ dispatch }) => {
+ // `createLocation` requires a source object, but we may use selectSource to close the last tab,
+ // where source will be null and the location will be an empty object.
+ const location = source ? createLocation({ source, sourceActor }) : {};
+
+ return dispatch(selectSpecificLocation(cx, location));
+ };
+}
+
+/**
+ * Select a new location.
+ * This will automatically select the source in the source tree (if visible)
+ * and open the source (a new tab and the source editor)
+ * as well as highlight a precise line in the editor.
+ *
+ * Note that by default, this may map your passed location to the original
+ * or generated location based on the selected source state. (see keepContext)
+ *
+ * @param {Object} cx
+ * @param {Object} location
+ * @param {Object} options
+ * @param {boolean} options.keepContext
+ * If false, this will ignore the currently selected source
+ * and select the generated or original location, even if we
+ * were currently selecting the other source type.
+ */
+export function selectLocation(cx, location, { keepContext = true } = {}) {
+ return async thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+
+ if (!client) {
+ // No connection, do nothing. This happens when the debugger is
+ // shut down too fast and it tries to display a default source.
+ return;
+ }
+
+ let source = location.source;
+
+ if (!source) {
+ // If there is no source we deselect the current selected source
+ dispatch(clearSelectedLocation(cx));
+ return;
+ }
+
+ // Preserve the current source map context (original / generated)
+ // when navigating to a new location.
+ // i.e. if keepContext isn't manually overriden to false,
+ // we will convert the source we want to select to either
+ // original/generated in order to match the currently selected one.
+ // If the currently selected source is original, we will
+ // automatically map `location` to refer to the original source,
+ // even if that used to refer only to the generated source.
+ let shouldSelectOriginalLocation = getShouldSelectOriginalLocation(
+ getState()
+ );
+ if (keepContext) {
+ if (shouldSelectOriginalLocation != isOriginalId(location.sourceId)) {
+ // getRelatedMapLocation will convert to the related generated/original location.
+ // i.e if the original location is passed, the related generated location will be returned and vice versa.
+ location = await getRelatedMapLocation(location, thunkArgs);
+ // Note that getRelatedMapLocation may return the exact same location.
+ // For example, if the source-map is half broken, it may return a generated location
+ // while we were selecting original locations. So we may be seeing bundles intermittently
+ // when stepping through broken source maps. And we will see original sources when stepping
+ // through functional original sources.
+
+ source = location.source;
+ }
+ } else {
+ shouldSelectOriginalLocation = isOriginalId(location.sourceId);
+ }
+
+ let sourceActor = location.sourceActor;
+ if (!sourceActor) {
+ sourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ location = createLocation({ ...location, sourceActor });
+ }
+
+ if (!tabExists(getState(), source.id)) {
+ dispatch(addTab(source, sourceActor));
+ }
+
+ dispatch(setSelectedLocation(cx, location, shouldSelectOriginalLocation));
+
+ await dispatch(loadSourceText(cx, source, sourceActor));
+
+ await dispatch(setBreakableLines(cx, location));
+
+ const loadedSource = getSource(getState(), source.id);
+
+ if (!loadedSource) {
+ // If there was a navigation while we were loading the loadedSource
+ return;
+ }
+
+ const sourceTextContent = getSourceTextContent(getState(), location);
+
+ if (
+ keepContext &&
+ prefs.autoPrettyPrint &&
+ !getPrettySource(getState(), loadedSource.id) &&
+ canPrettyPrintSource(getState(), location) &&
+ isMinified(source, sourceTextContent)
+ ) {
+ await dispatch(togglePrettyPrint(cx, loadedSource.id));
+ dispatch(closeTab(cx, loadedSource));
+ }
+
+ await dispatch(setSymbols({ cx, location }));
+ dispatch(setInScopeLines(cx));
+
+ if (getIsCurrentThreadPaused(getState())) {
+ await dispatch(mapDisplayNames(cx));
+ }
+ };
+}
+
+/**
+ * Select a location while ignoring the currently selected source.
+ * This will select the generated location even if the currently
+ * select source is an original source. And the other way around.
+ *
+ * @param {Object} cx
+ * @param {Object} location
+ * The location to select, object which includes enough
+ * information to specify a precise source, line and column.
+ */
+export function selectSpecificLocation(cx, location) {
+ return selectLocation(cx, location, { keepContext: false });
+}
+
+/**
+ * Select the "mapped location".
+ *
+ * If the passed location is on a generated source, select the
+ * related location in the original source.
+ * If the passed location is on an original source, select the
+ * related location in the generated source.
+ */
+export function jumpToMappedLocation(cx, location) {
+ return async function (thunkArgs) {
+ const { client, dispatch } = thunkArgs;
+ if (!client) {
+ return null;
+ }
+
+ // Map to either an original or a generated source location
+ const pairedLocation = await getRelatedMapLocation(location, thunkArgs);
+
+ return dispatch(selectSpecificLocation(cx, pairedLocation));
+ };
+}
+
+// This is only used by tests
+export function jumpToMappedSelectedLocation(cx) {
+ return async function ({ dispatch, getState }) {
+ const location = getSelectedLocation(getState());
+ if (!location) {
+ return;
+ }
+
+ await dispatch(jumpToMappedLocation(cx, location));
+ };
+}