summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js')
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js331
1 files changed, 331 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
new file mode 100644
index 0000000000..3729fd2741
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -0,0 +1,331 @@
+/* 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/>. */
+
+import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index";
+
+import {
+ getSource,
+ getSourceFromId,
+ getBreakpointPositionsForSource,
+ getSourceActorsForSource,
+} from "../../selectors/index";
+
+import { makeBreakpointId } from "../../utils/breakpoint/index";
+import { memoizeableAction } from "../../utils/memoizableAction";
+import { fulfilled } from "../../utils/async-value";
+import {
+ sourceMapToDebuggerLocation,
+ createLocation,
+} from "../../utils/location";
+import { validateSource } from "../../utils/context";
+
+/**
+ * Helper function which consumes breakpoints positions sent by the server
+ * and map them to location objects.
+ * During this process, the SourceMapLoader will be queried to map the positions from generated to original locations.
+ *
+ * @param {Object} breakpointPositions
+ * The positions to map related to the generated source:
+ * {
+ * 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
+ * 2: [ 2 ], // Line 2 is only breakable on column 2
+ * }
+ * @param {Object} generatedSource
+ * @param {Object} location
+ * The current location we are computing breakable positions.
+ * @param {Object} thunk arguments
+ * @return {Object}
+ * The mapped breakable locations in the original source:
+ * {
+ * 1: [ { source, line: 1, column: 2} , { source, line: 1, column 6 } ], // Line 1 is not mapped as location are same as breakpointPositions.
+ * 10: [ { source, line: 10, column: 28 } ], // Line 2 is mapped and locations and line key refers to the original source positions.
+ * }
+ */
+async function mapToLocations(
+ breakpointPositions,
+ generatedSource,
+ mappedLocation,
+ { getState, sourceMapLoader }
+) {
+ // Map breakable positions from generated to original locations.
+ let mappedBreakpointPositions = await sourceMapLoader.getOriginalLocations(
+ breakpointPositions,
+ generatedSource.id
+ );
+ // The Source Map Loader will return null when there is no source map for that generated source.
+ // Consider the map as unrelated to source map and process the source actor positions as-is.
+ if (!mappedBreakpointPositions) {
+ mappedBreakpointPositions = breakpointPositions;
+ }
+
+ const positions = {};
+
+ // Ensure that we have an entry for the line fetched
+ if (typeof mappedLocation.line === "number") {
+ positions[mappedLocation.line] = [];
+ }
+
+ const handledBreakpointIds = new Set();
+ const isOriginal = mappedLocation.source.isOriginal;
+ const originalSourceId = mappedLocation.source.id;
+
+ for (let line in mappedBreakpointPositions) {
+ // createLocation expects a number and not a string.
+ line = parseInt(line, 10);
+ for (const columnOrSourceMapLocation of mappedBreakpointPositions[line]) {
+ let location, generatedLocation;
+
+ // When processing a source unrelated to source map, `mappedBreakpointPositions` will be equal to `breakpointPositions`.
+ // and columnOrSourceMapLocation will always be a number.
+ // But it will also be a number if we process a source mapped file and SourceMapLoader didn't find any valid mapping
+ // for the current position (line and column).
+ // When this happen to be a number it means it isn't mapped and columnOrSourceMapLocation refers to the column index.
+ if (typeof columnOrSourceMapLocation == "number") {
+ // If columnOrSourceMapLocation is a number, it means that this location doesn't mapped to an original source.
+ // So if we are currently computation positions for an original source, we can skip this breakable positions.
+ if (isOriginal) {
+ continue;
+ }
+ location = generatedLocation = createLocation({
+ line,
+ column: columnOrSourceMapLocation,
+ source: generatedSource,
+ });
+ } else {
+ // Otherwise, for sources which are mapped. `columnOrSourceMapLocation` will be a SourceMapLoader location object.
+ // This location object will refer to the location where the current column (columnOrSourceMapLocation.generatedColumn)
+ // mapped in the original file.
+
+ // When computing positions for an original source, ignore the location if that mapped to another original source.
+ if (
+ isOriginal &&
+ columnOrSourceMapLocation.sourceId != originalSourceId
+ ) {
+ continue;
+ }
+
+ location = sourceMapToDebuggerLocation(
+ getState(),
+ columnOrSourceMapLocation
+ );
+
+ // Merge positions that refer to duplicated positions.
+ // Some sourcemaped positions might refer to the exact same source/line/column triple.
+ const breakpointId = makeBreakpointId(location);
+ if (handledBreakpointIds.has(breakpointId)) {
+ continue;
+ }
+ handledBreakpointIds.add(breakpointId);
+
+ generatedLocation = createLocation({
+ line,
+ column: columnOrSourceMapLocation.generatedColumn,
+ source: generatedSource,
+ });
+ }
+
+ // The positions stored in redux will be keyed by original source's line (if we are
+ // computing the original source positions), or the generated source line.
+ // Note that when we compute the bundle positions, location may refer to the original source,
+ // but we still want to use the generated location as key.
+ const keyLocation = isOriginal ? location : generatedLocation;
+ const keyLine = keyLocation.line;
+ if (!positions[keyLine]) {
+ positions[keyLine] = [];
+ }
+ positions[keyLine].push({ location, generatedLocation });
+ }
+ }
+
+ return positions;
+}
+
+async function _setBreakpointPositions(location, thunkArgs) {
+ const { client, dispatch, getState, sourceMapLoader } = thunkArgs;
+ const results = {};
+ let generatedSource = location.source;
+ if (location.source.isOriginal) {
+ const ranges = await sourceMapLoader.getGeneratedRangesForOriginal(
+ location.source.id,
+ true
+ );
+ const generatedSourceId = originalToGeneratedId(location.source.id);
+ generatedSource = getSourceFromId(getState(), generatedSourceId);
+
+ // Note: While looping here may not look ideal, in the vast majority of
+ // cases, the number of ranges here should be very small, and is quite
+ // likely to only be a single range.
+ for (const range of ranges) {
+ // Wrap infinite end positions to the next line to keep things simple
+ // and because we know we don't care about the end-line whitespace
+ // in this case.
+ if (range.end.column === Infinity) {
+ range.end = {
+ line: range.end.line + 1,
+ column: 0,
+ };
+ }
+
+ // Retrieve the positions for all the source actors for the related generated source.
+ // There might be many if it is loaded many times.
+ // We limit the retrieval of positions within the given range, so that we don't
+ // retrieve the whole bundle positions.
+ const allActorsPositions = await Promise.all(
+ getSourceActorsForSource(getState(), generatedSourceId).map(actor =>
+ client.getSourceActorBreakpointPositions(actor, range)
+ )
+ );
+
+ // `allActorsPositions` looks like this:
+ // [
+ // { // Positions for the first source actor
+ // 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
+ // 2: [ 2 ], // Line 2 is only breakable on column 2
+ // },
+ // {...} // Positions for another source actor
+ // ]
+ for (const actorPositions of allActorsPositions) {
+ for (const rangeLine in actorPositions) {
+ const columns = actorPositions[rangeLine];
+
+ // Merge all actors's breakable columns and avoid duplication of columns reported as breakable
+ const existing = results[rangeLine];
+ if (existing) {
+ for (const column of columns) {
+ if (!existing.includes(column)) {
+ existing.push(column);
+ }
+ }
+ } else {
+ results[rangeLine] = columns;
+ }
+ }
+ }
+ }
+ } else {
+ const { line } = location;
+ if (typeof line !== "number") {
+ throw new Error("Line is required for generated sources");
+ }
+
+ // We only retrieve the positions for the given requested line, that, for each source actor.
+ // There might be many source actor, if it is loaded many times.
+ // Or if this is an html page, with many inline scripts.
+ const allActorsBreakableColumns = await Promise.all(
+ getSourceActorsForSource(getState(), location.source.id).map(
+ async actor => {
+ const positions = await client.getSourceActorBreakpointPositions(
+ actor,
+ {
+ // Only retrieve positions for the given line
+ start: { line, column: 0 },
+ end: { line: line + 1, column: 0 },
+ }
+ );
+ return positions[line] || [];
+ }
+ )
+ );
+
+ for (const columns of allActorsBreakableColumns) {
+ // Merge all actors's breakable columns and avoid duplication of columns reported as breakable
+ const existing = results[line];
+ if (existing) {
+ for (const column of columns) {
+ if (!existing.includes(column)) {
+ existing.push(column);
+ }
+ }
+ } else {
+ results[line] = columns;
+ }
+ }
+ }
+
+ const positions = await mapToLocations(
+ results,
+ generatedSource,
+ location,
+ thunkArgs
+ );
+ // `mapToLocations` may compute for a little while asynchronously,
+ // ensure that the location is still valid before continuing.
+ validateSource(getState(), location.source);
+
+ dispatch({
+ type: "ADD_BREAKPOINT_POSITIONS",
+ source: location.source,
+ positions,
+ });
+}
+
+function generatedSourceActorKey(state, source) {
+ const generatedSource = getSource(
+ state,
+ source.isOriginal ? originalToGeneratedId(source.id) : source.id
+ );
+ const actors = generatedSource
+ ? getSourceActorsForSource(state, generatedSource.id).map(
+ ({ actor }) => actor
+ )
+ : [];
+ return [source.id, ...actors].join(":");
+}
+
+/**
+ * This method will force retrieving the breakable positions for a given source, on a given line.
+ * If this data has already been computed, it will returned the cached data.
+ *
+ * For original sources, this will query the SourceMap worker.
+ * For generated sources, this will query the DevTools server and the related source actors.
+ *
+ * @param Object options
+ * Dictionary object with many arguments:
+ * @param String options.sourceId
+ * The source we want to fetch breakable positions
+ * @param Number options.line
+ * The line we want to know which columns are breakable.
+ * (note that this seems to be optional for original sources)
+ * @return Array<Object>
+ * The list of all breakable positions, each object of this array will be like this:
+ * {
+ * line: Number
+ * column: Number
+ * source: Source object
+ * }
+ */
+export const setBreakpointPositions = memoizeableAction(
+ "setBreakpointPositions",
+ {
+ getValue: (location, { getState }) => {
+ const positions = getBreakpointPositionsForSource(
+ getState(),
+ location.source.id
+ );
+ if (!positions) {
+ return null;
+ }
+
+ if (
+ !location.source.isOriginal &&
+ location.line &&
+ !positions[location.line]
+ ) {
+ // We always return the full position dataset, but if a given line is
+ // not available, we treat the whole set as loading.
+ return null;
+ }
+
+ return fulfilled(positions);
+ },
+ createKey(location, { getState }) {
+ const key = generatedSourceActorKey(getState(), location.source);
+ return !location.source.isOriginal && location.line
+ ? `${key}-${location.line}`
+ : key;
+ },
+ action: async (location, thunkArgs) =>
+ _setBreakpointPositions(location, thunkArgs),
+ }
+);