Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "../sources/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,
getSourceByActorId,
getSelectedFrame,
getCurrentThread,
} from "../../selectors/index";
// This is only used by jest tests (and within this module)
export const setSelectedLocation = (
location,
shouldSelectOriginalLocation,
shouldHighlightSelectedLocation,
shouldScrollToSelectedLocation
) => ({
type: "SET_SELECTED_LOCATION",
location,
shouldSelectOriginalLocation,
shouldHighlightSelectedLocation,
shouldScrollToSelectedLocation,
});
// 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",
});
export const setDefaultSelectedLocation = shouldSelectOriginalLocation => ({
type: "SET_DEFAULT_SELECTED_LOCATION",
shouldSelectOriginalLocation,
});
/**
* 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));
};
}
export function selectSourceBySourceActorID(sourceActorId, options) {
return async thunkArgs => {
const { dispatch, getState } = thunkArgs;
const source = getSourceByActorId(getState(), sourceActorId);
if (!source) {
throw new Error(`Unable to find source actor with id ${sourceActorId}`);
}
const generatedLocation = createLocation({ ...options, source });
const originalLocation = await getOriginalLocation(
generatedLocation,
thunkArgs
);
return dispatch(selectLocation(originalLocation));
};
}
/**
* 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));
};
}
/**
* Helper for `selectLocation`.
* Based on `keepContext` argument passed to `selectLocation`,
* this will automatically select the related mapped source (original or generated).
*
* @param {Object} location
* The location to select.
* @param {Boolean} keepContext
* If true, will try to select a mapped source.
* @param {Object} thunkArgs
* @return {Object}
* Object with two attributes:
* - `shouldSelectOriginalLocation`, to know if we should keep trying to select the original location
* - `newLocation`, for the final location to select
*/
async function mayBeSelectMappedSource(location, keepContext, thunkArgs) {
const { getState, dispatch } = thunkArgs;
// 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) {
// Only try to map if the source is mapped. i.e. is original source or a bundle with a valid source map comment
if (
location.source.isOriginal ||
isSourceActorWithSourceMap(getState(), location.sourceActor.id)
) {
// 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.
}
} else if (
location.source.isOriginal ||
isSourceActorWithSourceMap(getState(), location.sourceActor.id)
) {
// Only update this setting if the source is mapped. i.e. don't update if we select a regular source.
// The source is mapped when it is either:
// - an original source,
// - a bundle with a source map comment referencing a source map URL.
shouldSelectOriginalLocation = location.source.isOriginal;
}
return { shouldSelectOriginalLocation, newLocation: 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.
* @param {boolean} options.scroll
* True by default. Is set to false to stop the editor from scrolling to the location that has been selected.
* e.g is when clicking in the editor to just show the selected line / column in the footer
*/
export function selectLocation(
location,
{ keepContext = true, highlight = true, scroll = 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;
}
let sourceActor = location.sourceActor;
if (!sourceActor) {
sourceActor = getFirstSourceActorForGeneratedSource(
getState(),
source.id
);
location = createLocation({ ...location, sourceActor });
}
const lastSelectedLocation = getSelectedLocation(getState());
const { shouldSelectOriginalLocation, newLocation } =
await mayBeSelectMappedSource(location, keepContext, thunkArgs);
// Ignore the request if another location was selected while we were waiting for mayBeSelectMappedSource async completion
if (getSelectedLocation(getState()) != lastSelectedLocation) {
return;
}
// Update all local variables after mapping
location = newLocation;
source = location.source;
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,
scroll
)
);
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));
}
const selectedFrame = getSelectedFrame(
getState(),
getCurrentThread(getState())
);
if (
selectedFrame &&
(selectedFrame.location.source.id == location.source.id ||
selectedFrame.generatedLocation.source.id == location.source.id)
) {
// This is done from selectLocation and not from paused and selectFrame actions
// because we may select either original or generated location while being paused
// and we would like to also fetch the symbols.
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);
// If we are on a non-mapped source, this will return the same location
// so ignore the request.
if (pairedLocation == location) {
return null;
}
return dispatch(selectSpecificLocation(pairedLocation));
};
}
export function jumpToMappedSelectedLocation() {
return async function ({ dispatch, getState }) {
const location = getSelectedLocation(getState());
if (!location) {
return;
}
await dispatch(jumpToMappedLocation(location));
};
}