diff options
Diffstat (limited to 'devtools/client/debugger/src/reducers/sources.js')
-rw-r--r-- | devtools/client/debugger/src/reducers/sources.js | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/reducers/sources.js b/devtools/client/debugger/src/reducers/sources.js new file mode 100644 index 0000000000..0d1c55be13 --- /dev/null +++ b/devtools/client/debugger/src/reducers/sources.js @@ -0,0 +1,361 @@ +/* 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/>. */ + +/** + * Sources reducer + * @module reducers/sources + */ + +import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index"; +import { prefs } from "../utils/prefs"; +import { createPendingSelectedLocation } from "../utils/location"; + +export function initialSourcesState(state) { + /* eslint sort-keys: "error" */ + return { + /** + * List of all breakpoint positions for all sources (generated and original). + * Map of source id (string) to dictionary object whose keys are line numbers + * and values of array of positions. + * A position is an object made with two attributes: + * location and generatedLocation. Both refering to breakpoint positions + * in original and generated sources. + * In case of generated source, the two location will be the same. + * + * Map(source id => Dictionary(int => array<Position>)) + */ + mutableBreakpointPositions: new Map(), + + /** + * List of all breakable lines for original sources only. + * + * Map(source id => array<int : breakable line numbers>) + */ + mutableOriginalBreakableLines: new Map(), + + /** + * Map of the source id's to one or more related original source id's + * Only generated sources which have related original sources will be maintained here. + * + * Map(source id => array<Original Source ID>) + */ + mutableOriginalSources: new Map(), + + /** + * List of override objects whose sources texts have been locally overridden. + * + * Object { sourceUrl, path } + */ + mutableOverrideSources: state?.mutableOverrideSources || new Map(), + + /** + * Mapping of source id's to one or more source-actor's. + * Dictionary whose keys are source id's and values are arrays + * made of all the related source-actor's. + * Note: The source mapped here are only generated sources. + * + * "source" are the objects stored in this reducer, in the `sources` attribute. + * "source-actor" are the objects stored in the "source-actors.js" reducer, in its `sourceActors` attribute. + * + * Map(source id => array<Source Actor object>) + */ + mutableSourceActors: new Map(), + + /** + * All currently available sources. + * + * See create.js: `createSourceObject` method for the description of stored objects. + */ + mutableSources: new Map(), + + /** + * All sources associated with a given URL. When using source maps, multiple + * sources can have the same URL. + * + * Map(url => array<source>) + */ + mutableSourcesPerUrl: new Map(), + + /** + * When we want to select a source that isn't available yet, use this. + * The location object should have a url attribute instead of a sourceId. + * + * See `createPendingSelectedLocation` for the definition of this object. + */ + pendingSelectedLocation: prefs.pendingSelectedLocation, + + /** + * The actual currently selected location. + * Only set if the related source is already registered in the sources reducer. + * Otherwise, pendingSelectedLocation should be used. Typically for sources + * which are about to be created. + * + * It also includes line and column information. + * + * See `createLocation` for the definition of this object. + */ + selectedLocation: undefined, + + /** + * By default, if we have a source-mapped source, we would automatically try + * to select and show the content of the original source. But, if we explicitly + * select a generated source, we remember this choice. That, until we explicitly + * select an original source. + * Note that selections related to non-source-mapped sources should never + * change this setting. + */ + shouldSelectOriginalLocation: true, + }; + /* eslint-disable sort-keys */ +} + +function update(state = initialSourcesState(), action) { + switch (action.type) { + case "ADD_SOURCES": + return addSources(state, action.sources); + + case "ADD_ORIGINAL_SOURCES": + return addSources(state, action.originalSources); + + case "INSERT_SOURCE_ACTORS": + return insertSourceActors(state, action); + + case "SET_SELECTED_LOCATION": { + let pendingSelectedLocation = null; + + if (action.location.source.url) { + pendingSelectedLocation = createPendingSelectedLocation( + action.location + ); + prefs.pendingSelectedLocation = pendingSelectedLocation; + } + + return { + ...state, + selectedLocation: action.location, + pendingSelectedLocation, + shouldSelectOriginalLocation: action.shouldSelectOriginalLocation, + }; + } + + case "CLEAR_SELECTED_LOCATION": { + const pendingSelectedLocation = { url: "" }; + prefs.pendingSelectedLocation = pendingSelectedLocation; + + return { + ...state, + selectedLocation: null, + pendingSelectedLocation, + }; + } + + case "SET_PENDING_SELECTED_LOCATION": { + const pendingSelectedLocation = { + url: action.url, + line: action.line, + column: action.column, + }; + + prefs.pendingSelectedLocation = pendingSelectedLocation; + return { ...state, pendingSelectedLocation }; + } + + case "SET_ORIGINAL_BREAKABLE_LINES": { + state.mutableOriginalBreakableLines.set( + action.sourceId, + action.breakableLines + ); + + return { + ...state, + }; + } + + case "ADD_BREAKPOINT_POSITIONS": { + // Merge existing and new reported position if some where already stored + let positions = state.mutableBreakpointPositions.get(action.source.id); + if (positions) { + positions = { ...positions, ...action.positions }; + } else { + positions = action.positions; + } + + state.mutableBreakpointPositions.set(action.source.id, positions); + + return { + ...state, + }; + } + + case "REMOVE_THREAD": { + return removeSourcesAndActors(state, action); + } + + case "SET_OVERRIDE": { + state.mutableOverrideSources.set(action.url, action.path); + return state; + } + + case "REMOVE_OVERRIDE": { + if (state.mutableOverrideSources.has(action.url)) { + state.mutableOverrideSources.delete(action.url); + } + return state; + } + } + + return state; +} + +/* + * Add sources to the sources store + * - Add the source to the sources store + * - Add the source URL to the source url map + */ +function addSources(state, sources) { + for (const source of sources) { + state.mutableSources.set(source.id, source); + + // Update the source url map + const existing = state.mutableSourcesPerUrl.get(source.url); + if (existing) { + // We never return this array from selectors as-is, + // we either return the first entry or lookup for a precise entry + // so we can mutate it. + existing.push(source); + } else { + state.mutableSourcesPerUrl.set(source.url, [source]); + } + + // In case of original source, maintain the mapping of generated source to original sources map. + if (source.isOriginal) { + const generatedSourceId = originalToGeneratedId(source.id); + let originalSourceIds = + state.mutableOriginalSources.get(generatedSourceId); + if (!originalSourceIds) { + originalSourceIds = []; + state.mutableOriginalSources.set(generatedSourceId, originalSourceIds); + } + // We never return this array out of selectors, so mutate the list + originalSourceIds.push(source.id); + } + } + + return { ...state }; +} + +function removeSourcesAndActors(state, action) { + const { + mutableSourcesPerUrl, + mutableSources, + mutableOriginalSources, + mutableSourceActors, + mutableOriginalBreakableLines, + mutableBreakpointPositions, + } = state; + + const newState = { ...state }; + + for (const removedSource of action.sources) { + const sourceId = removedSource.id; + + // Clear the urls Map + const sourceUrl = removedSource.url; + if (sourceUrl) { + const sourcesForSameUrl = ( + mutableSourcesPerUrl.get(sourceUrl) || [] + ).filter(s => s != removedSource); + if (!sourcesForSameUrl.length) { + // All sources with this URL have been removed + mutableSourcesPerUrl.delete(sourceUrl); + } else { + // There are other sources still alive with the same URL + mutableSourcesPerUrl.set(sourceUrl, sourcesForSameUrl); + } + } + + mutableSources.delete(sourceId); + + // Note that the caller of this method queried the reducer state + // to aggregate the related original sources. + // So if we were having related original sources, they will be + // in `action.sources`. + mutableOriginalSources.delete(sourceId); + + // If a source is removed, immediately remove all its related source actors. + // It can speed-up the following for loop cleaning actors. + mutableSourceActors.delete(sourceId); + + if (removedSource.isOriginal) { + mutableOriginalBreakableLines.delete(sourceId); + } + + mutableBreakpointPositions.delete(sourceId); + + if (newState.selectedLocation?.source == removedSource) { + newState.selectedLocation = null; + } + } + + for (const removedActor of action.actors) { + const sourceId = removedActor.source; + const actorsForSource = mutableSourceActors.get(sourceId); + // actors may have already been cleared by the previous for..loop + if (!actorsForSource) { + continue; + } + const idx = actorsForSource.indexOf(removedActor); + if (idx != -1) { + actorsForSource.splice(idx, 1); + // While the Map is mutable, we expect new array instance on each new change + mutableSourceActors.set(sourceId, [...actorsForSource]); + } + + // Remove the entry in the Map if there is no more actors for that source + if (!actorsForSource.length) { + mutableSourceActors.delete(sourceId); + } + + if (newState.selectedLocation?.sourceActor == removedActor) { + newState.selectedLocation = null; + } + } + + return newState; +} + +function insertSourceActors(state, action) { + const { sourceActors } = action; + + const { mutableSourceActors } = state; + // The `sourceActor` objects are defined from `newGeneratedSources` action: + // https://searchfox.org/mozilla-central/rev/4646b826a25d3825cf209db890862b45fa09ffc3/devtools/client/debugger/src/actions/sources/newSources.js#300-314 + for (const sourceActor of sourceActors) { + const sourceId = sourceActor.source; + // We always clone the array of source actors as we return it from selectors. + // So the map is mutable, but its values are considered immutable and will change + // anytime there is a new actor added per source ID. + const existing = mutableSourceActors.get(sourceId); + if (existing) { + mutableSourceActors.set(sourceId, [...existing, sourceActor]); + } else { + mutableSourceActors.set(sourceId, [sourceActor]); + } + } + + const scriptActors = sourceActors.filter( + item => item.introductionType === "scriptElement" + ); + if (scriptActors.length) { + // If new HTML sources are being added, we need to clear the breakpoint + // positions since the new source is a <script> with new breakpoints. + for (const { source } of scriptActors) { + state.mutableBreakpointPositions.delete(source); + } + } + + return { ...state }; +} + +export default update; |