diff options
Diffstat (limited to 'devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js')
-rw-r--r-- | devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js | 273 |
1 files changed, 273 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..263b476364 --- /dev/null +++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js @@ -0,0 +1,273 @@ +/* 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 { + isOriginalId, + isGeneratedId, + originalToGeneratedId, +} from "devtools/client/shared/source-map-loader/index"; + +import { + getSource, + getSourceFromId, + getBreakpointPositionsForSource, + getSourceActorsForSource, +} from "../../selectors"; + +import { makeBreakpointId } from "../../utils/breakpoint"; +import { memoizeableAction } from "../../utils/memoizableAction"; +import { fulfilled } from "../../utils/async-value"; +import { + debuggerToSourceMapLocation, + sourceMapToDebuggerLocation, + createLocation, +} from "../../utils/location"; + +async function mapLocations(generatedLocations, { getState, sourceMapLoader }) { + if (!generatedLocations.length) { + return []; + } + + const originalLocations = await sourceMapLoader.getOriginalLocations( + generatedLocations.map(debuggerToSourceMapLocation) + ); + return originalLocations.map((location, index) => ({ + // If location is null, this particular location doesn't map to any original source. + location: location + ? sourceMapToDebuggerLocation(getState(), location) + : generatedLocations[index], + generatedLocation: generatedLocations[index], + })); +} + +// Filter out positions, that are not in the original source Id +function filterBySource(positions, sourceId) { + if (!isOriginalId(sourceId)) { + return positions; + } + return positions.filter(position => position.location.sourceId == sourceId); +} + +/** + * Merge positions that refer to duplicated positions. + * Some sourcemaped positions might refer to the exact same source/line/column triple. + * + * @param {Array<{location, generatedLocation}>} positions: List of possible breakable positions + * @returns {Array<{location, generatedLocation}>} A new, filtered array. + */ +function filterByUniqLocation(positions) { + const handledBreakpointIds = new Set(); + return positions.filter(({ location }) => { + const breakpointId = makeBreakpointId(location); + if (handledBreakpointIds.has(breakpointId)) { + return false; + } + + handledBreakpointIds.add(breakpointId); + return true; + }); +} + +function convertToList(results, source) { + const positions = []; + + for (const line in results) { + for (const column of results[line]) { + positions.push( + createLocation({ + line: Number(line), + column, + source, + sourceUrl: source.url, + }) + ); + } + } + + return positions; +} + +function groupByLine(results, 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, location, thunkArgs) { + const { client, dispatch, getState, sourceMapLoader } = thunkArgs; + const results = {}; + let generatedSource = location.source; + if (isOriginalId(location.sourceId)) { + const ranges = await sourceMapLoader.getGeneratedRangesForOriginal( + location.sourceId, + true + ); + const generatedSourceId = originalToGeneratedId(location.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(), generatedSourceId).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 { + const { line } = location; + if (typeof line !== "number") { + throw new Error("Line is required for generated sources"); + } + + const actorColumns = await Promise.all( + getSourceActorsForSource(getState(), location.sourceId).map( + async actor => { + const positions = await client.getSourceActorBreakpointPositions( + actor, + { + start: { line: line, column: 0 }, + end: { line: line + 1, column: 0 }, + } + ); + return positions[line] || []; + } + ) + ); + + for (const columns of actorColumns) { + results[line] = (results[line] || []).concat(columns); + } + } + + let positions = convertToList(results, generatedSource); + positions = await mapLocations(positions, thunkArgs); + + positions = filterBySource(positions, location.sourceId); + positions = filterByUniqLocation(positions); + positions = groupByLine(positions, location.sourceId, location.line); + + const source = getSource(getState(), location.sourceId); + // NOTE: it's possible that the source was removed during a navigation + if (!source) { + return; + } + + dispatch({ + type: "ADD_BREAKPOINT_POSITIONS", + cx, + source, + positions, + }); +} + +function generatedSourceActorKey(state, sourceId) { + const generatedSource = getSource( + state, + isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId + ); + const actors = generatedSource + ? getSourceActorsForSource(state, generatedSource.id).map( + ({ actor }) => actor + ) + : []; + return [sourceId, ...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 + * sourceId: String + * sourceUrl: String + * } + */ +export const setBreakpointPositions = memoizeableAction( + "setBreakpointPositions", + { + getValue: ({ location }, { getState }) => { + const positions = getBreakpointPositionsForSource( + getState(), + location.sourceId + ); + if (!positions) { + return null; + } + + if ( + isGeneratedId(location.sourceId) && + 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.sourceId); + return isGeneratedId(location.sourceId) && location.line + ? `${key}-${location.line}` + : key; + }, + action: async ({ cx, location }, thunkArgs) => + _setBreakpointPositions(cx, location, thunkArgs), + } +); |