/* 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 . */
// @flow
import {
isOriginalId,
isGeneratedId,
originalToGeneratedId,
} from "devtools-source-map";
import { uniqBy, zip } from "lodash";
import {
getSource,
getSourceFromId,
getBreakpointPositionsForSource,
getSourceActorsForSource,
} from "../../selectors";
import type {
MappedLocation,
Range,
Source,
SourceLocation,
SourceId,
BreakpointPositions,
Context,
} from "../../types";
import { makeBreakpointId } from "../../utils/breakpoint";
import {
memoizeableAction,
type MemoizedAction,
} from "../../utils/memoizableAction";
import { fulfilled } from "../../utils/async-value";
import type { ThunkArgs } from "../../actions/types";
import { loadSourceActorBreakpointColumns } from "../source-actors";
type LocationsList = {
number: ?(number[]),
};
async function mapLocations(
generatedLocations: SourceLocation[],
{ sourceMaps }: ThunkArgs
): Promise {
if (generatedLocations.length == 0) {
return [];
}
const originalLocations = await sourceMaps.getOriginalLocations(
generatedLocations
);
return zip(
originalLocations,
generatedLocations
).map(([location, generatedLocation]) => ({ location, generatedLocation }));
}
// Filter out positions, that are not in the original source Id
function filterBySource(
positions: MappedLocation[],
sourceId: SourceId
): MappedLocation[] {
if (!isOriginalId(sourceId)) {
return positions;
}
return positions.filter(position => position.location.sourceId == sourceId);
}
function filterByUniqLocation(positions: MappedLocation[]): MappedLocation[] {
return uniqBy(positions, ({ location }) => makeBreakpointId(location));
}
function convertToList(
results: LocationsList,
source: Source
): SourceLocation[] {
const { id, url } = source;
const positions = [];
for (const line in results) {
for (const column of results[line]) {
positions.push({
line: Number(line),
column,
sourceId: id,
sourceUrl: url,
});
}
}
return positions;
}
function groupByLine(results: MappedLocation[], sourceId: SourceId, line) {
const isOriginal = isOriginalId(sourceId);
const positions = {};
// Ensure that we have an entry for the line fetched
if (typeof line === "number") {
positions[line] = [];
}
for (const result of results) {
const location = isOriginal ? result.location : result.generatedLocation;
if (!positions[location.line]) {
positions[location.line] = [];
}
positions[location.line].push(result);
}
return positions;
}
async function _setBreakpointPositions(
cx: Context,
sourceId: SourceId,
line,
thunkArgs: ThunkArgs
): Promise {
const { client, dispatch, getState, sourceMaps } = thunkArgs;
let generatedSource = getSource(getState(), sourceId);
if (!generatedSource) {
return;
}
const results = {};
if (isOriginalId(sourceId)) {
// Explicitly typing ranges is required to work around the following issue
// https://github.com/facebook/flow/issues/5294
const ranges: Range[] = await sourceMaps.getGeneratedRangesForOriginal(
sourceId,
true
);
const generatedSourceId = originalToGeneratedId(sourceId);
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,
};
}
const actorBps = await Promise.all(
getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
client.getSourceActorBreakpointPositions(actor, range)
)
);
for (const actorPositions of actorBps) {
for (const rangeLine of Object.keys(actorPositions)) {
let columns = actorPositions[parseInt(rangeLine, 10)];
const existing = results[rangeLine];
if (existing) {
columns = [...new Set([...existing, ...columns])];
}
results[rangeLine] = columns;
}
}
}
} else {
if (typeof line !== "number") {
throw new Error("Line is required for generated sources");
}
const actorColumns = await Promise.all(
getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
dispatch(loadSourceActorBreakpointColumns({ id: actor.id, line, cx }))
)
);
for (const columns of actorColumns) {
results[line] = (results[line] || []).concat(columns);
}
}
let positions = convertToList(results, generatedSource);
positions = await mapLocations(positions, thunkArgs);
positions = filterBySource(positions, sourceId);
positions = filterByUniqLocation(positions);
positions = groupByLine(positions, sourceId, line);
const source = getSource(getState(), sourceId);
// NOTE: it's possible that the source was removed during a navigate
if (!source) {
return;
}
dispatch({
type: "ADD_BREAKPOINT_POSITIONS",
cx,
source,
positions,
});
}
function generatedSourceActorKey(state, sourceId: SourceId): string {
const generatedSource = getSource(
state,
isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
);
const actors = generatedSource
? getSourceActorsForSource(state, generatedSource.id).map(
({ actor }) => actor
)
: [];
return [sourceId, ...actors].join(":");
}
export const setBreakpointPositions: MemoizedAction<
{| cx: Context, sourceId: SourceId, line?: number |},
?BreakpointPositions
> = memoizeableAction("setBreakpointPositions", {
getValue: ({ sourceId, line }, { getState }) => {
const positions = getBreakpointPositionsForSource(getState(), sourceId);
if (!positions) {
return null;
}
if (isGeneratedId(sourceId) && line && !positions[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({ sourceId, line }, { getState }) {
const key = generatedSourceActorKey(getState(), sourceId);
return isGeneratedId(sourceId) && line ? `${key}-${line}` : key;
},
action: async ({ cx, sourceId, line }, thunkArgs) =>
_setBreakpointPositions(cx, sourceId, line, thunkArgs),
});