summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/reducers/sources.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/reducers/sources.js')
-rw-r--r--devtools/client/debugger/src/reducers/sources.js1153
1 files changed, 1153 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..f75d9f6893
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/sources.js
@@ -0,0 +1,1153 @@
+/* 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/>. */
+
+// @flow
+
+/**
+ * Sources reducer
+ * @module reducers/sources
+ */
+
+import { createSelector } from "reselect";
+import {
+ getPrettySourceURL,
+ underRoot,
+ getRelativeUrl,
+ isGenerated,
+ isOriginal as isOriginalSource,
+ getPlainUrl,
+ isPretty,
+ isJavaScript,
+} from "../utils/source";
+import {
+ createInitial,
+ insertResources,
+ updateResources,
+ hasResource,
+ getResource,
+ getMappedResource,
+ getResourceIds,
+ memoizeResourceShallow,
+ makeShallowQuery,
+ makeReduceAllQuery,
+ makeMapWithArgs,
+ type Resource,
+ type ResourceState,
+ type ReduceAllQuery,
+ type ShallowQuery,
+} from "../utils/resource";
+import { stripQuery } from "../utils/url";
+
+import { findPosition } from "../utils/breakpoint/breakpointPositions";
+import {
+ pending,
+ fulfilled,
+ rejected,
+ asSettled,
+ isFulfilled,
+} from "../utils/async-value";
+
+import type { AsyncValue, SettledValue } from "../utils/async-value";
+import { originalToGeneratedId } from "devtools-source-map";
+import { prefs } from "../utils/prefs";
+
+import {
+ hasSourceActor,
+ getSourceActor,
+ getSourceActors,
+ getAllThreadsBySource,
+ getBreakableLinesForSourceActors,
+ type SourceActorId,
+ type SourceActorOuterState,
+} from "./source-actors";
+import { getAllThreads } from "./threads";
+import type {
+ DisplaySource,
+ Source,
+ SourceId,
+ SourceActor,
+ SourceLocation,
+ SourceContent,
+ SourceWithContent,
+ ThreadId,
+ Thread,
+ MappedLocation,
+ BreakpointPosition,
+ BreakpointPositions,
+ URL,
+} from "../types";
+
+import type {
+ PendingSelectedLocation,
+ Selector,
+ State as AppState,
+} from "./types";
+
+import type { Action, DonePromiseAction, FocusItem } from "../actions/types";
+import type { LoadSourceAction } from "../actions/types/SourceAction";
+import { uniq } from "lodash";
+
+export type SourcesMap = { [SourceId]: Source };
+export type SourcesMapByThread = {
+ [ThreadId]: { [SourceId]: DisplaySource },
+};
+
+export type BreakpointPositionsMap = { [SourceId]: BreakpointPositions };
+type SourceActorMap = { [SourceId]: Array<SourceActorId> };
+
+type UrlsMap = { [string]: SourceId[] };
+type PlainUrlsMap = { [string]: string[] };
+
+export type SourceBase = {|
+ +id: SourceId,
+ +url: URL,
+ +isBlackBoxed: boolean,
+ +isPrettyPrinted: boolean,
+ +relativeUrl: URL,
+ +extensionName: ?string,
+ +isExtension: boolean,
+ +isWasm: boolean,
+ +isOriginal: boolean,
+|};
+
+export type SourceResource = Resource<{
+ ...SourceBase,
+ content: AsyncValue<SourceContent> | null,
+}>;
+export type SourceResourceState = ResourceState<SourceResource>;
+
+type IdsList = Array<SourceId>;
+
+export type SourcesState = {
+ epoch: number,
+
+ // All known sources.
+ sources: SourceResourceState,
+
+ breakpointPositions: BreakpointPositionsMap,
+ breakableLines: { [SourceId]: Array<number> },
+
+ // A link between each source object and the source actor they wrap over.
+ actors: SourceActorMap,
+
+ // All sources associated with a given URL. When using source maps, multiple
+ // sources can have the same URL.
+ urls: UrlsMap,
+
+ // All full URLs belonging to a given plain (query string stripped) URL.
+ // Query strings are only shown in the Sources tab if they are required for
+ // disambiguation.
+ plainUrls: PlainUrlsMap,
+
+ sourcesWithUrls: IdsList,
+
+ pendingSelectedLocation?: PendingSelectedLocation,
+ selectedLocation: ?SourceLocation,
+ projectDirectoryRoot: string,
+ projectDirectoryRootName: string,
+ chromeAndExtensionsEnabled: boolean,
+ focusedItem: ?FocusItem,
+ tabsBlackBoxed: any,
+};
+
+export function initialSourcesState(
+ state: ?{ tabsBlackBoxed: string[] }
+): SourcesState {
+ return {
+ sources: createInitial(),
+ urls: {},
+ plainUrls: {},
+ sourcesWithUrls: [],
+ content: {},
+ actors: {},
+ breakpointPositions: {},
+ breakableLines: {},
+ epoch: 1,
+ selectedLocation: undefined,
+ pendingSelectedLocation: prefs.pendingSelectedLocation,
+ projectDirectoryRoot: prefs.projectDirectoryRoot,
+ projectDirectoryRootName: prefs.projectDirectoryRootName,
+ chromeAndExtensionsEnabled: prefs.chromeAndExtensionsEnabled,
+ focusedItem: null,
+ tabsBlackBoxed: state?.tabsBlackBoxed ?? [],
+ };
+}
+
+function update(
+ state: SourcesState = initialSourcesState(),
+ action: Action
+): SourcesState {
+ let location = null;
+
+ switch (action.type) {
+ case "ADD_SOURCE":
+ return addSources(state, [action.source]);
+
+ case "ADD_SOURCES":
+ return addSources(state, action.sources);
+
+ case "INSERT_SOURCE_ACTORS":
+ return insertSourceActors(state, action);
+
+ case "REMOVE_SOURCE_ACTORS":
+ return removeSourceActors(state, action);
+
+ case "SET_SELECTED_LOCATION":
+ location = {
+ ...action.location,
+ url: action.source.url,
+ };
+
+ if (action.source.url) {
+ prefs.pendingSelectedLocation = location;
+ }
+
+ return {
+ ...state,
+ selectedLocation: {
+ sourceId: action.source.id,
+ ...action.location,
+ },
+ pendingSelectedLocation: location,
+ };
+
+ case "CLEAR_SELECTED_LOCATION":
+ location = { url: "" };
+ prefs.pendingSelectedLocation = location;
+
+ return {
+ ...state,
+ selectedLocation: null,
+ pendingSelectedLocation: location,
+ };
+
+ case "SET_PENDING_SELECTED_LOCATION":
+ location = {
+ url: action.url,
+ line: action.line,
+ column: action.column,
+ };
+
+ prefs.pendingSelectedLocation = location;
+ return { ...state, pendingSelectedLocation: location };
+
+ case "LOAD_SOURCE_TEXT":
+ return updateLoadedState(state, action);
+
+ case "BLACKBOX_SOURCES":
+ if (action.status === "done") {
+ const { shouldBlackBox } = action;
+ const { sources } = action.value;
+
+ state = updateBlackBoxListSources(state, sources, shouldBlackBox);
+ return updateBlackboxFlagSources(state, sources, shouldBlackBox);
+ }
+ break;
+
+ case "BLACKBOX":
+ if (action.status === "done") {
+ const { id, url } = action.source;
+ const { isBlackBoxed } = ((action: any): DonePromiseAction).value;
+ state = updateBlackBoxList(state, url, isBlackBoxed);
+ return updateBlackboxFlag(state, id, isBlackBoxed);
+ }
+ break;
+
+ case "SET_PROJECT_DIRECTORY_ROOT":
+ const { url, name } = action;
+ return updateProjectDirectoryRoot(state, url, name);
+
+ case "SET_ORIGINAL_BREAKABLE_LINES": {
+ const { breakableLines, sourceId } = action;
+ return {
+ ...state,
+ breakableLines: {
+ ...state.breakableLines,
+ [sourceId]: breakableLines,
+ },
+ };
+ }
+
+ case "ADD_BREAKPOINT_POSITIONS": {
+ const { source, positions } = action;
+ const breakpointPositions = state.breakpointPositions[source.id];
+
+ return {
+ ...state,
+ breakpointPositions: {
+ ...state.breakpointPositions,
+ [source.id]: { ...breakpointPositions, ...positions },
+ },
+ };
+ }
+ case "NAVIGATE":
+ return {
+ ...initialSourcesState(state),
+ epoch: state.epoch + 1,
+ };
+
+ case "SET_FOCUSED_SOURCE_ITEM":
+ return { ...state, focusedItem: action.item };
+ }
+
+ return state;
+}
+
+export const resourceAsSourceBase = memoizeResourceShallow(
+ ({ content, ...source }: SourceResource): SourceBase => source
+);
+
+const resourceAsSourceWithContent = memoizeResourceShallow(
+ ({ content, ...source }: SourceResource): SourceWithContent => ({
+ ...source,
+ content: asSettled(content),
+ })
+);
+
+/*
+ * Add sources to the sources store
+ * - Add the source to the sources store
+ * - Add the source URL to the urls map
+ */
+function addSources(state: SourcesState, sources: SourceBase[]): SourcesState {
+ const originalState = state;
+
+ state = {
+ ...state,
+ urls: { ...state.urls },
+ plainUrls: { ...state.plainUrls },
+ };
+
+ state.sources = insertResources(
+ state.sources,
+ sources.map(source => ({
+ ...source,
+ content: null,
+ }))
+ );
+
+ for (const source of sources) {
+ // 1. Update the source url map
+ const existing = state.urls[source.url] || [];
+ if (!existing.includes(source.id)) {
+ state.urls[source.url] = [...existing, source.id];
+ }
+
+ // 2. Update the plain url map
+ if (source.url) {
+ const plainUrl = getPlainUrl(source.url);
+ const existingPlainUrls = state.plainUrls[plainUrl] || [];
+ if (!existingPlainUrls.includes(source.url)) {
+ state.plainUrls[plainUrl] = [...existingPlainUrls, source.url];
+ }
+
+ // NOTE: we only want to copy the list once
+ if (originalState.sourcesWithUrls === state.sourcesWithUrls) {
+ state.sourcesWithUrls = [...state.sourcesWithUrls];
+ }
+
+ state.sourcesWithUrls.push(source.id);
+ }
+ }
+
+ state = updateRootRelativeValues(state, sources);
+
+ return state;
+}
+
+function insertSourceActors(state: SourcesState, action): SourcesState {
+ const { items } = action;
+ state = {
+ ...state,
+ actors: { ...state.actors },
+ };
+
+ for (const sourceActor of items) {
+ state.actors[sourceActor.source] = [
+ ...(state.actors[sourceActor.source] || []),
+ sourceActor.id,
+ ];
+ }
+
+ const scriptActors = items.filter(
+ item => item.introductionType === "scriptElement"
+ );
+ if (scriptActors.length > 0) {
+ const { ...breakpointPositions } = state.breakpointPositions;
+
+ // 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) {
+ delete breakpointPositions[source];
+ }
+
+ state = { ...state, breakpointPositions };
+ }
+
+ return state;
+}
+
+/*
+ * Update sources when the worker list changes.
+ * - filter source actor lists so that missing threads no longer appear
+ * - NOTE: we do not remove sources for destroyed threads
+ */
+function removeSourceActors(state: SourcesState, action): SourcesState {
+ const { items } = action;
+
+ const actors = new Set(items.map(item => item.id));
+ const sources = new Set(items.map(item => item.source));
+
+ state = {
+ ...state,
+ actors: { ...state.actors },
+ };
+
+ for (const source of sources) {
+ state.actors[source] = state.actors[source].filter(id => !actors.has(id));
+ }
+
+ return state;
+}
+
+/*
+ * Update sources when the project directory root changes
+ */
+function updateProjectDirectoryRoot(
+ state: SourcesState,
+ root: string,
+ name: string
+) {
+ // Only update prefs when projectDirectoryRoot isn't a thread actor,
+ // because when debugger is reopened, thread actor will change. See bug 1596323.
+ if (actorType(root) !== "thread") {
+ prefs.projectDirectoryRoot = root;
+ prefs.projectDirectoryRootName = name;
+ }
+
+ return updateRootRelativeValues(state, undefined, root, name);
+}
+
+/* Checks if a path is a thread actor or not
+ * e.g returns 'thread' for "server0.conn1.child1/workerTarget42/thread1"
+ */
+function actorType(actor: string): ?string {
+ const match = actor.match(/\/([a-z]+)\d+/);
+ return match ? match[1] : null;
+}
+
+function updateRootRelativeValues(
+ state: SourcesState,
+ sources?: $ReadOnlyArray<Source>,
+ projectDirectoryRoot?: string = state.projectDirectoryRoot,
+ projectDirectoryRootName?: string = state.projectDirectoryRootName
+): SourcesState {
+ const wrappedIdsOrIds: $ReadOnlyArray<Source> | Array<string> = sources
+ ? sources
+ : getResourceIds(state.sources);
+
+ state = {
+ ...state,
+ projectDirectoryRoot,
+ projectDirectoryRootName,
+ };
+
+ const relativeURLUpdates = wrappedIdsOrIds.map(wrappedIdOrId => {
+ const id =
+ typeof wrappedIdOrId === "string" ? wrappedIdOrId : wrappedIdOrId.id;
+ const source = getResource(state.sources, id);
+
+ return {
+ id,
+ relativeUrl: getRelativeUrl(source, state.projectDirectoryRoot),
+ };
+ });
+
+ state.sources = updateResources(state.sources, relativeURLUpdates);
+
+ return state;
+}
+
+/*
+ * Update a source's loaded text content.
+ */
+function updateLoadedState(
+ state: SourcesState,
+ action: LoadSourceAction
+): SourcesState {
+ const { sourceId } = action;
+
+ // If there was a navigation between the time the action was started and
+ // completed, we don't want to update the store.
+ if (action.epoch !== state.epoch || !hasResource(state.sources, sourceId)) {
+ return state;
+ }
+
+ let content;
+ if (action.status === "start") {
+ content = pending();
+ } else if (action.status === "error") {
+ content = rejected(action.error);
+ } else if (typeof action.value.text === "string") {
+ content = fulfilled({
+ type: "text",
+ value: action.value.text,
+ contentType: action.value.contentType,
+ });
+ } else {
+ content = fulfilled({
+ type: "wasm",
+ value: action.value.text,
+ });
+ }
+
+ return {
+ ...state,
+ sources: updateResources(state.sources, [
+ {
+ id: sourceId,
+ content,
+ },
+ ]),
+ };
+}
+
+/*
+ * Update a source when its state changes
+ * e.g. the text was loaded, it was blackboxed
+ */
+function updateBlackboxFlag(
+ state: SourcesState,
+ sourceId: SourceId,
+ isBlackBoxed: boolean
+): SourcesState {
+ // If there is no existing version of the source, it means that we probably
+ // ended up here as a result of an async action, and the sources were cleared
+ // between the action starting and the source being updated.
+ if (!hasResource(state.sources, sourceId)) {
+ // TODO: We may want to consider throwing here once we have a better
+ // handle on async action flow control.
+ return state;
+ }
+
+ return {
+ ...state,
+ sources: updateResources(state.sources, [
+ {
+ id: sourceId,
+ isBlackBoxed,
+ },
+ ]),
+ };
+}
+
+function updateBlackboxFlagSources(
+ state: SourcesState,
+ sources: Source[],
+ shouldBlackBox: boolean
+): SourcesState {
+ const sourcesToUpdate = [];
+
+ for (const source of sources) {
+ if (!hasResource(state.sources, source.id)) {
+ // TODO: We may want to consider throwing here once we have a better
+ // handle on async action flow control.
+ continue;
+ }
+
+ sourcesToUpdate.push({
+ id: source.id,
+ isBlackBoxed: shouldBlackBox,
+ });
+ }
+ state.sources = updateResources(state.sources, sourcesToUpdate);
+
+ return state;
+}
+
+function updateBlackboxTabs(tabs, url: URL, isBlackBoxed: boolean): void {
+ const i = tabs.indexOf(url);
+ if (i >= 0) {
+ if (!isBlackBoxed) {
+ tabs.splice(i, 1);
+ }
+ } else if (isBlackBoxed) {
+ tabs.push(url);
+ }
+}
+
+function updateBlackBoxList(
+ state: SourcesState,
+ url: URL,
+ isBlackBoxed: boolean
+): SourcesState {
+ const tabs = [...state.tabsBlackBoxed];
+ updateBlackboxTabs(tabs, url, isBlackBoxed);
+ return { ...state, tabsBlackBoxed: tabs };
+}
+
+function updateBlackBoxListSources(
+ state: SourcesState,
+ sources,
+ shouldBlackBox
+): SourcesState {
+ const tabs = [...state.tabsBlackBoxed];
+
+ sources.forEach(source => {
+ updateBlackboxTabs(tabs, source.url, shouldBlackBox);
+ });
+ return { ...state, tabsBlackBoxed: tabs };
+}
+
+// Selectors
+
+// Unfortunately, it's really hard to make these functions accept just
+// the state that we care about and still type it with Flow. The
+// problem is that we want to re-export all selectors from a single
+// module for the UI, and all of those selectors should take the
+// top-level app state, so we'd have to "wrap" them to automatically
+// pick off the piece of state we're interested in. It's impossible
+// (right now) to type those wrapped functions.
+type OuterState = { sources: SourcesState };
+
+const getSourcesState = (state: OuterState) => state.sources;
+
+export function getSourceThreads(
+ state: OuterState & SourceActorOuterState,
+ source: Source
+): ThreadId[] {
+ return uniq(
+ getSourceActors(state, state.sources.actors[source.id]).map(
+ actor => actor.thread
+ )
+ );
+}
+
+export function getSourceInSources(
+ sources: SourceResourceState,
+ id: string
+): ?Source {
+ return hasResource(sources, id)
+ ? getMappedResource(sources, id, resourceAsSourceBase)
+ : null;
+}
+
+export function getSource(state: OuterState, id: SourceId): ?Source {
+ return getSourceInSources(getSources(state), id);
+}
+
+export function getSourceFromId(state: OuterState, id: string): Source {
+ const source = getSource(state, id);
+ if (!source) {
+ throw new Error(`source ${id} does not exist`);
+ }
+ return source;
+}
+
+export function getSourceByActorId(
+ state: OuterState & SourceActorOuterState,
+ actorId: SourceActorId
+): ?Source {
+ if (!hasSourceActor(state, actorId)) {
+ return null;
+ }
+
+ return getSource(state, getSourceActor(state, actorId).source);
+}
+
+export function getSourcesByURLInSources(
+ sources: SourceResourceState,
+ urls: UrlsMap,
+ url: URL
+): Source[] {
+ if (!url || !urls[url]) {
+ return [];
+ }
+ return urls[url].map(id =>
+ getMappedResource(sources, id, resourceAsSourceBase)
+ );
+}
+
+export function getSourcesByURL(state: OuterState, url: URL): Source[] {
+ return getSourcesByURLInSources(getSources(state), getUrls(state), url);
+}
+
+export function getSourceByURL(state: OuterState, url: URL): ?Source {
+ const foundSources = getSourcesByURL(state, url);
+ return foundSources ? foundSources[0] : null;
+}
+
+export function getSpecificSourceByURLInSources(
+ sources: SourceResourceState,
+ urls: UrlsMap,
+ url: URL,
+ isOriginal: boolean
+): ?Source {
+ const foundSources = getSourcesByURLInSources(sources, urls, url);
+ if (foundSources) {
+ return foundSources.find(source => isOriginalSource(source) == isOriginal);
+ }
+ return null;
+}
+
+export function getSpecificSourceByURL(
+ state: OuterState,
+ url: URL,
+ isOriginal: boolean
+): ?Source {
+ return getSpecificSourceByURLInSources(
+ getSources(state),
+ getUrls(state),
+ url,
+ isOriginal
+ );
+}
+
+export function getOriginalSourceByURL(state: OuterState, url: URL): ?Source {
+ return getSpecificSourceByURL(state, url, true);
+}
+
+export function getGeneratedSourceByURL(state: OuterState, url: URL): ?Source {
+ return getSpecificSourceByURL(state, url, false);
+}
+
+export function getGeneratedSource(
+ state: OuterState,
+ source: ?Source
+): ?Source {
+ if (!source) {
+ return null;
+ }
+
+ if (isGenerated(source)) {
+ return source;
+ }
+
+ return getSourceFromId(state, originalToGeneratedId(source.id));
+}
+
+export function getGeneratedSourceById(
+ state: OuterState,
+ sourceId: SourceId
+): Source {
+ const generatedSourceId = originalToGeneratedId(sourceId);
+ return getSourceFromId(state, generatedSourceId);
+}
+
+export function getPendingSelectedLocation(state: OuterState) {
+ return state.sources.pendingSelectedLocation;
+}
+
+export function getPrettySource(state: OuterState, id: ?string) {
+ if (!id) {
+ return;
+ }
+
+ const source = getSource(state, id);
+ if (!source) {
+ return;
+ }
+
+ return getOriginalSourceByURL(state, getPrettySourceURL(source.url));
+}
+
+export function hasPrettySource(state: OuterState, id: string) {
+ return !!getPrettySource(state, id);
+}
+
+export function getSourcesUrlsInSources(
+ state: OuterState,
+ url: ?URL
+): string[] {
+ if (!url) {
+ return [];
+ }
+
+ const plainUrl = getPlainUrl(url);
+ return getPlainUrls(state)[plainUrl] || [];
+}
+
+export function getHasSiblingOfSameName(state: OuterState, source: ?Source) {
+ if (!source) {
+ return false;
+ }
+
+ return getSourcesUrlsInSources(state, source.url).length > 1;
+}
+
+const querySourceList: ReduceAllQuery<
+ SourceResource,
+ Array<Source>
+> = makeReduceAllQuery(resourceAsSourceBase, sources => sources.slice());
+
+export function getSources(state: OuterState): SourceResourceState {
+ return state.sources.sources;
+}
+
+export function getSourcesEpoch(state: OuterState) {
+ return state.sources.epoch;
+}
+
+export function getUrls(state: OuterState) {
+ return state.sources.urls;
+}
+
+export function getPlainUrls(state: OuterState) {
+ return state.sources.plainUrls;
+}
+
+export function getSourceList(state: OuterState): Source[] {
+ return querySourceList(getSources(state));
+}
+
+export function getDisplayedSourcesList(
+ state: OuterState & SourceActorOuterState & AppState
+): Source[] {
+ return ((Object.values(getDisplayedSources(state)): any).flatMap(
+ Object.values
+ ): any);
+}
+
+export function getExtensionNameBySourceUrl(
+ state: OuterState,
+ url: URL
+): ?string {
+ const match = getSourceList(state).find(
+ source => source.url && source.url.startsWith(url)
+ );
+ if (match && match.extensionName) {
+ return match.extensionName;
+ }
+}
+
+export function getSourceCount(state: OuterState): number {
+ return getSourceList(state).length;
+}
+
+export const getSelectedLocation: Selector<?SourceLocation> = createSelector(
+ getSourcesState,
+ sources => sources.selectedLocation
+);
+
+export const getSelectedSource: Selector<?Source> = createSelector(
+ getSelectedLocation,
+ getSources,
+ (
+ selectedLocation: ?SourceLocation,
+ sources: SourceResourceState
+ ): ?Source => {
+ if (!selectedLocation) {
+ return;
+ }
+
+ return getSourceInSources(sources, selectedLocation.sourceId);
+ }
+);
+
+type GSSWC = Selector<?SourceWithContent>;
+export const getSelectedSourceWithContent: GSSWC = createSelector(
+ getSelectedLocation,
+ getSources,
+ (
+ selectedLocation: ?SourceLocation,
+ sources: SourceResourceState
+ ): SourceWithContent | null => {
+ const source =
+ selectedLocation &&
+ getSourceInSources(sources, selectedLocation.sourceId);
+ return source
+ ? getMappedResource(sources, source.id, resourceAsSourceWithContent)
+ : null;
+ }
+);
+export function getSourceWithContent(
+ state: OuterState,
+ id: SourceId
+): SourceWithContent {
+ return getMappedResource(
+ state.sources.sources,
+ id,
+ resourceAsSourceWithContent
+ );
+}
+export function getSourceContent(
+ state: OuterState,
+ id: SourceId
+): SettledValue<SourceContent> | null {
+ const { content } = getResource(state.sources.sources, id);
+ return asSettled(content);
+}
+
+export function getSelectedSourceId(state: OuterState) {
+ const source = getSelectedSource((state: any));
+ return source?.id;
+}
+
+export function getProjectDirectoryRoot(state: OuterState): string {
+ return state.sources.projectDirectoryRoot;
+}
+
+export function getProjectDirectoryRootName(state: OuterState): string {
+ return state.sources.projectDirectoryRootName;
+}
+
+const queryAllDisplayedSources: ShallowQuery<
+ SourceResource,
+ {|
+ sourcesWithUrls: IdsList,
+ projectDirectoryRoot: string,
+ chromeAndExtensionsEnabled: boolean,
+ debuggeeIsWebExtension: boolean,
+ threads: Array<Thread>,
+ |},
+ Array<SourceId>
+> = makeShallowQuery({
+ filter: (_, { sourcesWithUrls }) => sourcesWithUrls,
+ map: makeMapWithArgs(
+ (
+ resource,
+ ident,
+ {
+ projectDirectoryRoot,
+ chromeAndExtensionsEnabled,
+ debuggeeIsWebExtension,
+ threads,
+ }
+ ) => ({
+ id: resource.id,
+ displayed:
+ underRoot(resource, projectDirectoryRoot, threads) &&
+ (!resource.isExtension ||
+ chromeAndExtensionsEnabled ||
+ debuggeeIsWebExtension),
+ })
+ ),
+ reduce: items =>
+ items.reduce((acc, { id, displayed }) => {
+ if (displayed) {
+ acc.push(id);
+ }
+ return acc;
+ }, []),
+});
+
+function getAllDisplayedSources(state: OuterState & AppState): Array<SourceId> {
+ return queryAllDisplayedSources(state.sources.sources, {
+ sourcesWithUrls: state.sources.sourcesWithUrls,
+ projectDirectoryRoot: state.sources.projectDirectoryRoot,
+ chromeAndExtensionsEnabled: state.sources.chromeAndExtensionsEnabled,
+ debuggeeIsWebExtension: state.threads.isWebExtension,
+ threads: getAllThreads(state),
+ });
+}
+
+type GetDisplayedSourceIDsSelector = (
+ OuterState & SourceActorOuterState
+) => { [ThreadId]: Set<SourceId> };
+const getDisplayedSourceIDs: GetDisplayedSourceIDsSelector = createSelector(
+ getAllThreadsBySource,
+ getAllDisplayedSources,
+ (threadsBySource, displayedSources) => {
+ const sourceIDsByThread = {};
+
+ for (const sourceId of displayedSources) {
+ const threads =
+ threadsBySource[sourceId] ||
+ threadsBySource[originalToGeneratedId(sourceId)] ||
+ [];
+
+ for (const thread of threads) {
+ if (!sourceIDsByThread[thread]) {
+ sourceIDsByThread[thread] = new Set();
+ }
+ sourceIDsByThread[thread].add(sourceId);
+ }
+ }
+ return sourceIDsByThread;
+ }
+);
+
+type GetDisplayedSourcesSelector = (
+ OuterState & SourceActorOuterState
+) => SourcesMapByThread;
+export const getDisplayedSources: GetDisplayedSourcesSelector = createSelector(
+ state => state.sources.sources,
+ getDisplayedSourceIDs,
+ (sources, idsByThread) => {
+ const result = {};
+
+ for (const thread of Object.keys(idsByThread)) {
+ const entriesByNoQueryURL = Object.create(null);
+
+ for (const id of idsByThread[thread]) {
+ if (!result[thread]) {
+ result[thread] = {};
+ }
+ const source = getResource(sources, id);
+
+ const entry = {
+ ...source,
+ displayURL: source.url,
+ };
+ result[thread][id] = entry;
+
+ const noQueryURL = stripQuery(entry.displayURL);
+ if (!entriesByNoQueryURL[noQueryURL]) {
+ entriesByNoQueryURL[noQueryURL] = [];
+ }
+ entriesByNoQueryURL[noQueryURL].push(entry);
+ }
+
+ // If the URL does not compete with another without the query string,
+ // we exclude the query string when rendering the source URL to keep the
+ // UI more easily readable.
+ for (const noQueryURL in entriesByNoQueryURL) {
+ const entries = entriesByNoQueryURL[noQueryURL];
+ if (entries.length === 1) {
+ entries[0].displayURL = noQueryURL;
+ }
+ }
+ }
+
+ return result;
+ }
+);
+
+export function getSourceActorsForSource(
+ state: OuterState & SourceActorOuterState,
+ id: SourceId
+): Array<SourceActor> {
+ const actors = state.sources.actors[id];
+ if (!actors) {
+ return [];
+ }
+
+ return getSourceActors(state, actors);
+}
+
+export function isSourceWithMap(
+ state: OuterState & SourceActorOuterState,
+ id: SourceId
+): boolean {
+ return getSourceActorsForSource(state, id).some(
+ sourceActor => sourceActor.sourceMapURL
+ );
+}
+
+export function canPrettyPrintSource(
+ state: OuterState & SourceActorOuterState,
+ id: SourceId
+): boolean {
+ const source: SourceWithContent = getSourceWithContent(state, id);
+ if (
+ !source ||
+ isPretty(source) ||
+ isOriginalSource(source) ||
+ (prefs.clientSourceMapsEnabled && isSourceWithMap(state, id))
+ ) {
+ return false;
+ }
+
+ const sourceContent =
+ source.content && isFulfilled(source.content) ? source.content.value : null;
+
+ if (!sourceContent || !isJavaScript(source, sourceContent)) {
+ return false;
+ }
+
+ return true;
+}
+
+export function getBreakpointPositions(
+ state: OuterState
+): BreakpointPositionsMap {
+ return state.sources.breakpointPositions;
+}
+
+export function getBreakpointPositionsForSource(
+ state: OuterState,
+ sourceId: SourceId
+): ?BreakpointPositions {
+ const positions = getBreakpointPositions(state);
+ return positions?.[sourceId];
+}
+
+export function hasBreakpointPositions(
+ state: OuterState,
+ sourceId: SourceId
+): boolean {
+ return !!getBreakpointPositionsForSource(state, sourceId);
+}
+
+export function getBreakpointPositionsForLine(
+ state: OuterState,
+ sourceId: SourceId,
+ line: number
+): ?Array<BreakpointPosition> {
+ const positions = getBreakpointPositionsForSource(state, sourceId);
+ return positions?.[line];
+}
+
+export function hasBreakpointPositionsForLine(
+ state: OuterState,
+ sourceId: SourceId,
+ line: number
+): boolean {
+ return !!getBreakpointPositionsForLine(state, sourceId, line);
+}
+
+export function getBreakpointPositionsForLocation(
+ state: OuterState,
+ location: SourceLocation
+): ?MappedLocation {
+ const { sourceId } = location;
+ const positions = getBreakpointPositionsForSource(state, sourceId);
+ return findPosition(positions, location);
+}
+
+export function getBreakableLines(
+ state: OuterState & SourceActorOuterState,
+ sourceId: SourceId
+): ?Array<number> {
+ if (!sourceId) {
+ return null;
+ }
+ const source = getSource(state, sourceId);
+ if (!source) {
+ return null;
+ }
+
+ if (isOriginalSource(source)) {
+ return state.sources.breakableLines[sourceId];
+ }
+
+ // We pull generated file breakable lines directly from the source actors
+ // so that breakable lines can be added as new source actors on HTML loads.
+ return getBreakableLinesForSourceActors(
+ state.sourceActors,
+ state.sources.actors[sourceId]
+ );
+}
+
+export const getSelectedBreakableLines: Selector<Set<number>> = createSelector(
+ state => {
+ const sourceId = getSelectedSourceId(state);
+ return sourceId && getBreakableLines(state, sourceId);
+ },
+ breakableLines => new Set(breakableLines || [])
+);
+
+export function isSourceLoadingOrLoaded(
+ state: OuterState,
+ sourceId: SourceId
+): boolean {
+ const { content } = getResource(state.sources.sources, sourceId);
+ return content !== null;
+}
+
+export function getBlackBoxList(state: OuterState): string[] {
+ return state.sources.tabsBlackBoxed;
+}
+
+export default update;