diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/debugger/src/actions/sources/select.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/actions/sources/select.js')
-rw-r--r-- | devtools/client/debugger/src/actions/sources/select.js | 368 |
1 files changed, 368 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..63200a398a --- /dev/null +++ b/devtools/client/debugger/src/actions/sources/select.js @@ -0,0 +1,368 @@ +/* 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 { setSymbols } from "./symbols"; +import { setInScopeLines } from "../ast/index"; +import { prettyPrintAndSelectSource } from "./prettyPrint"; +import { addTab, closeTab } from "../tabs"; +import { loadSourceText } from "./loadSourceText"; +import { setBreakableLines } from "./breakableLines"; + +import { prefs } from "../../utils/prefs"; +import { isMinified } from "../../utils/source"; +import { createLocation } from "../../utils/location"; +import { + getRelatedMapLocation, + getOriginalLocation, +} from "../../utils/source-maps"; + +import { + getSource, + getFirstSourceActorForGeneratedSource, + getSourceByURL, + getPrettySource, + getSelectedLocation, + getShouldSelectOriginalLocation, + canPrettyPrintSource, + getSourceTextContent, + tabExists, + hasSource, + hasSourceActor, + hasPrettyTab, + isSourceActorWithSourceMap, +} from "../../selectors/index"; + +// This is only used by jest tests (and within this module) +export const setSelectedLocation = ( + location, + shouldSelectOriginalLocation, + shouldHighlightSelectedLocation +) => ({ + type: "SET_SELECTED_LOCATION", + location, + shouldSelectOriginalLocation, + shouldHighlightSelectedLocation, +}); + +// This is only used by jest tests (and within this module) +export const setPendingSelectedLocation = (url, options) => ({ + type: "SET_PENDING_SELECTED_LOCATION", + url, + line: options?.line, + column: options?.column, +}); + +// This is only used by jest tests (and within this module) +export const clearSelectedLocation = () => ({ + type: "CLEAR_SELECTED_LOCATION", +}); + +/** + * 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(url, options) { + return async ({ dispatch, getState }) => { + const source = getSourceByURL(getState(), url); + if (!source) { + return dispatch(setPendingSelectedLocation(url, options)); + } + + const location = createLocation({ ...options, source }); + return dispatch(selectLocation(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 {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(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(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} 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. + * @param {boolean} options.highlight + * True by default. To be set to false in order to preveng highlighting the selected location in the editor. + * We will only show the location, but do not put a special background on the line. + */ +export function selectLocation( + location, + { keepContext = true, highlight = 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()); + 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) { + // Pretty print source may not be registered yet and getRelatedMapLocation may not return it. + // Wait for the pretty print source to be fully processed. + if ( + !location.source.isOriginal && + shouldSelectOriginalLocation && + hasPrettyTab(getState(), location.source) + ) { + // Note that prettyPrintAndSelectSource has already been called a bit before when this generated source has been added + // but it is a slow operation and is most likely not resolved yet. + // prettyPrintAndSelectSource uses memoization to avoid doing the operation more than once, while waiting from both callsites. + await dispatch(prettyPrintAndSelectSource(location.source)); + } + if (shouldSelectOriginalLocation != location.source.isOriginal) { + // 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 = location.source.isOriginal; + } + + 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(location, shouldSelectOriginalLocation, highlight) + ); + + await dispatch(loadSourceText(source, sourceActor)); + + // Stop the async work if we started selecting another location + if (getSelectedLocation(getState()) != location) { + return; + } + + await dispatch(setBreakableLines(location)); + + // Stop the async work if we started selecting another location + if (getSelectedLocation(getState()) != location) { + return; + } + + 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(prettyPrintAndSelectSource(loadedSource)); + dispatch(closeTab(loadedSource)); + } + + await dispatch(setSymbols(location)); + + // Stop the async work if we started selecting another location + if (getSelectedLocation(getState()) != location) { + return; + } + + // /!\ we don't historicaly wait for this async action + dispatch(setInScopeLines()); + + // When we select a generated source which has a sourcemap, + // asynchronously fetch the related original location in order to display + // the mapped location in the editor's footer. + if ( + !location.source.isOriginal && + isSourceActorWithSourceMap(getState(), sourceActor.id) + ) { + let originalLocation = await getOriginalLocation(location, thunkArgs, { + looseSearch: true, + }); + // We pass a null original location when the location doesn't map + // in order to know when we are done processing the source map. + // * `getOriginalLocation` would return the exact same location if it doesn't map + // * `getOriginalLocation` may also return a distinct location object, + // but refering to the same `source` object (which is the bundle) when it doesn't + // map to any known original location. + if (originalLocation.source === location.source) { + originalLocation = null; + } + dispatch({ + type: "SET_ORIGINAL_SELECTED_LOCATION", + location, + originalLocation, + }); + } + }; +} + +/** + * 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} location + * The location to select, object which includes enough + * information to specify a precise source, line and column. + */ +export function selectSpecificLocation(location) { + return selectLocation(location, { keepContext: false }); +} + +/** + * Similar to `selectSpecificLocation`, but if the precise Source object + * is missing, this will fallback to select any source having the same URL. + * In this fallback scenario, sources without a URL will be ignored. + * + * This is typically used when trying to select a source (e.g. in project search result) + * after reload, because the source objects are new on each new page load, but source + * with the same URL may still exist. + * + * @param {Object} location + * The location to select. + * @return {function} + * The action will return true if a matching source was found. + */ +export function selectSpecificLocationOrSameUrl(location) { + return async ({ dispatch, getState }) => { + // If this particular source no longer exists, open any matching URL. + // This will typically happen on reload. + if (!hasSource(getState(), location.source.id)) { + // Some sources, like evaled script won't have a URL attribute + // and can't be re-selected if we don't find the exact same source object. + if (!location.source.url) { + return false; + } + const source = getSourceByURL(getState(), location.source.url); + if (!source) { + return false; + } + // Also reset the sourceActor, as it won't match the same source. + const sourceActor = getFirstSourceActorForGeneratedSource( + getState(), + location.source.id + ); + location = createLocation({ ...location, source, sourceActor }); + } else if (!hasSourceActor(getState(), location.sourceActor.id)) { + // If the specific source actor no longer exists, match any still available. + const sourceActor = getFirstSourceActorForGeneratedSource( + getState(), + location.source.id + ); + location = createLocation({ ...location, sourceActor }); + } + await dispatch(selectSpecificLocation(location)); + return true; + }; +} + +/** + * 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(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(pairedLocation)); + }; +} + +// This is only used by tests +export function jumpToMappedSelectedLocation() { + return async function ({ dispatch, getState }) { + const location = getSelectedLocation(getState()); + if (!location) { + return; + } + + await dispatch(jumpToMappedLocation(location)); + }; +} |