summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/selectors
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/debugger/src/selectors
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/selectors')
-rw-r--r--devtools/client/debugger/src/selectors/ast.js32
-rw-r--r--devtools/client/debugger/src/selectors/breakpointAtLocation.js121
-rw-r--r--devtools/client/debugger/src/selectors/breakpointSources.js52
-rw-r--r--devtools/client/debugger/src/selectors/breakpoints.js86
-rw-r--r--devtools/client/debugger/src/selectors/event-listeners.js19
-rw-r--r--devtools/client/debugger/src/selectors/exceptions.js58
-rw-r--r--devtools/client/debugger/src/selectors/expressions.js34
-rw-r--r--devtools/client/debugger/src/selectors/getCallStackFrames.js53
-rw-r--r--devtools/client/debugger/src/selectors/index.js51
-rw-r--r--devtools/client/debugger/src/selectors/isLineInScope.js22
-rw-r--r--devtools/client/debugger/src/selectors/isSelectedFrameVisible.js40
-rw-r--r--devtools/client/debugger/src/selectors/moz.build35
-rw-r--r--devtools/client/debugger/src/selectors/pause.js267
-rw-r--r--devtools/client/debugger/src/selectors/pending-breakpoints.js20
-rw-r--r--devtools/client/debugger/src/selectors/preview.js11
-rw-r--r--devtools/client/debugger/src/selectors/project-text-search.js19
-rw-r--r--devtools/client/debugger/src/selectors/quick-open.js15
-rw-r--r--devtools/client/debugger/src/selectors/source-actors.js111
-rw-r--r--devtools/client/debugger/src/selectors/source-blackbox.js26
-rw-r--r--devtools/client/debugger/src/selectors/sources-content.js48
-rw-r--r--devtools/client/debugger/src/selectors/sources-tree.js151
-rw-r--r--devtools/client/debugger/src/selectors/sources.js358
-rw-r--r--devtools/client/debugger/src/selectors/tabs.js90
-rw-r--r--devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap165
-rw-r--r--devtools/client/debugger/src/selectors/test/getCallStackFrames.spec.js166
-rw-r--r--devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js145
-rw-r--r--devtools/client/debugger/src/selectors/threads.js56
-rw-r--r--devtools/client/debugger/src/selectors/ui.js85
-rw-r--r--devtools/client/debugger/src/selectors/visibleBreakpoints.js55
-rw-r--r--devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js185
30 files changed, 2576 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/selectors/ast.js b/devtools/client/debugger/src/selectors/ast.js
new file mode 100644
index 0000000000..f3384fdc58
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/ast.js
@@ -0,0 +1,32 @@
+/* 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 { makeBreakpointId } from "../utils/breakpoint";
+
+export function getSymbols(state, location) {
+ if (!location) {
+ return null;
+ }
+ if (location.source.isOriginal) {
+ return (
+ state.ast.mutableOriginalSourcesSymbols[location.source.id]?.value || null
+ );
+ }
+ if (!location.sourceActor) {
+ throw new Error(
+ "Expects a location with a source actor when passing non-original sources to getSymbols"
+ );
+ }
+ return (
+ state.ast.mutableSourceActorSymbols[location.sourceActor.id]?.value || null
+ );
+}
+
+export function getInScopeLines(state, location) {
+ return state.ast.mutableInScopeLines[makeBreakpointId(location)]?.lines;
+}
+
+export function hasInScopeLines(state, location) {
+ return !!getInScopeLines(state, location);
+}
diff --git a/devtools/client/debugger/src/selectors/breakpointAtLocation.js b/devtools/client/debugger/src/selectors/breakpointAtLocation.js
new file mode 100644
index 0000000000..c661894dbb
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/breakpointAtLocation.js
@@ -0,0 +1,121 @@
+/* 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 { getSelectedSource, getBreakpointPositionsForLine } from "./sources";
+import { getBreakpointsList } from "./breakpoints";
+import { isGenerated } from "../utils/source";
+
+function getColumn(column, selectedSource) {
+ if (column) {
+ return column;
+ }
+
+ return isGenerated(selectedSource) ? undefined : 0;
+}
+
+function getLocation(bp, selectedSource) {
+ return isGenerated(selectedSource)
+ ? bp.generatedLocation || bp.location
+ : bp.location;
+}
+
+function getBreakpointsForSource(state, selectedSource) {
+ const breakpoints = getBreakpointsList(state);
+
+ return breakpoints.filter(bp => {
+ const location = getLocation(bp, selectedSource);
+ return location.sourceId === selectedSource.id;
+ });
+}
+
+function findBreakpointAtLocation(
+ breakpoints,
+ selectedSource,
+ { line, column }
+) {
+ return breakpoints.find(breakpoint => {
+ const location = getLocation(breakpoint, selectedSource);
+ const sameLine = location.line === line;
+ if (!sameLine) {
+ return false;
+ }
+
+ if (column === undefined) {
+ return true;
+ }
+
+ return location.column === getColumn(column, selectedSource);
+ });
+}
+
+// returns the closest active column breakpoint
+function findClosestBreakpoint(breakpoints, column) {
+ if (!breakpoints || !breakpoints.length) {
+ return null;
+ }
+
+ const firstBreakpoint = breakpoints[0];
+ return breakpoints.reduce((closestBp, currentBp) => {
+ const currentColumn = currentBp.generatedLocation.column;
+ const closestColumn = closestBp.generatedLocation.column;
+ // check that breakpoint has a column.
+ if (column && currentColumn && closestColumn) {
+ const currentDistance = Math.abs(currentColumn - column);
+ const closestDistance = Math.abs(closestColumn - column);
+
+ return currentDistance < closestDistance ? currentBp : closestBp;
+ }
+ return closestBp;
+ }, firstBreakpoint);
+}
+
+/*
+ * Finds a breakpoint at a location (line, column) of the
+ * selected source.
+ *
+ * This is useful for finding a breakpoint when the
+ * user clicks in the gutter or on a token.
+ */
+export function getBreakpointAtLocation(state, location) {
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ throw new Error("no selectedSource");
+ }
+ const breakpoints = getBreakpointsForSource(state, selectedSource);
+
+ return findBreakpointAtLocation(breakpoints, selectedSource, location);
+}
+
+export function getBreakpointsAtLine(state, line) {
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ throw new Error("no selectedSource");
+ }
+ const breakpoints = getBreakpointsForSource(state, selectedSource);
+
+ return breakpoints.filter(
+ breakpoint => getLocation(breakpoint, selectedSource).line === line
+ );
+}
+
+export function getClosestBreakpoint(state, position) {
+ const columnBreakpoints = getBreakpointsAtLine(state, position.line);
+ const breakpoint = findClosestBreakpoint(columnBreakpoints, position.column);
+ return breakpoint;
+}
+
+export function getClosestBreakpointPosition(state, position) {
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ throw new Error("no selectedSource");
+ }
+
+ const columnBreakpoints = getBreakpointPositionsForLine(
+ state,
+ selectedSource.id,
+ position.line
+ );
+
+ return findClosestBreakpoint(columnBreakpoints, position.column);
+}
diff --git a/devtools/client/debugger/src/selectors/breakpointSources.js b/devtools/client/debugger/src/selectors/breakpointSources.js
new file mode 100644
index 0000000000..6de2772521
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/breakpointSources.js
@@ -0,0 +1,52 @@
+/* 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 { createSelector } from "reselect";
+import { getSelectedSource } from "./sources";
+import { getBreakpointsList } from "./breakpoints";
+import { getFilename } from "../utils/source";
+import { getSelectedLocation } from "../utils/selected-location";
+
+// Returns a list of sources with their related breakpoints:
+// [{ source, breakpoints: [breakpoint1, ...] }, ...]
+//
+// This only returns sources for which we have a visible breakpoint.
+// This will return either generated or original source based on the currently
+// selected source.
+export const getBreakpointSources = createSelector(
+ getBreakpointsList,
+ getSelectedSource,
+ (breakpoints, selectedSource) => {
+ const visibleBreakpoints = breakpoints.filter(
+ bp =>
+ !bp.options.hidden &&
+ (bp.text || bp.originalText || bp.options.condition || bp.disabled)
+ );
+
+ const sources = new Map();
+ for (const breakpoint of visibleBreakpoints) {
+ // Depending on the selected source, this will match the original or generated
+ // location of the given selected source.
+ const location = getSelectedLocation(breakpoint, selectedSource);
+ const { source } = location;
+
+ // We may have more than one breakpoint per source,
+ // so use the map to have a unique entry per source.
+ if (!sources.has(source)) {
+ sources.set(source, {
+ source,
+ breakpoints: [breakpoint],
+ filename: getFilename(source),
+ });
+ } else {
+ sources.get(source).breakpoints.push(breakpoint);
+ }
+ }
+
+ // Returns an array of breakpoints info per source, sorted by source's filename
+ return [...sources.values()].sort((a, b) =>
+ a.filename.localeCompare(b.filename)
+ );
+ }
+);
diff --git a/devtools/client/debugger/src/selectors/breakpoints.js b/devtools/client/debugger/src/selectors/breakpoints.js
new file mode 100644
index 0000000000..38b39f71f3
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/breakpoints.js
@@ -0,0 +1,86 @@
+/* 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 { createSelector } from "reselect";
+
+import { isGeneratedId } from "devtools/client/shared/source-map-loader/index";
+import { makeBreakpointId } from "../utils/breakpoint";
+
+// This method is only used from the main test helper
+export function getBreakpointsMap(state) {
+ return state.breakpoints.breakpoints;
+}
+
+export const getBreakpointsList = createSelector(
+ state => state.breakpoints.breakpoints,
+ breakpoints => Object.values(breakpoints)
+);
+
+export function getBreakpointCount(state) {
+ return getBreakpointsList(state).length;
+}
+
+export function getBreakpoint(state, location) {
+ if (!location) {
+ return undefined;
+ }
+
+ const breakpoints = getBreakpointsMap(state);
+ return breakpoints[makeBreakpointId(location)];
+}
+
+/**
+ * Gets the breakpoints on a line or within a range of lines
+ * @param {Object} state
+ * @param {Number} sourceId
+ * @param {Number|Object} lines - line or an object with a start and end range of lines
+ * @returns {Array} breakpoints
+ */
+export function getBreakpointsForSource(state, sourceId, lines) {
+ if (!sourceId) {
+ return [];
+ }
+
+ const isGeneratedSource = isGeneratedId(sourceId);
+ const breakpoints = getBreakpointsList(state);
+ return breakpoints.filter(bp => {
+ const location = isGeneratedSource ? bp.generatedLocation : bp.location;
+
+ if (lines) {
+ const isOnLineOrWithinRange =
+ typeof lines == "number"
+ ? location.line == lines
+ : location.line >= lines.start.line &&
+ location.line <= lines.end.line;
+ return location.sourceId === sourceId && isOnLineOrWithinRange;
+ }
+ return location.sourceId === sourceId;
+ });
+}
+
+export function getHiddenBreakpoint(state) {
+ const breakpoints = getBreakpointsList(state);
+ return breakpoints.find(bp => bp.options.hidden);
+}
+
+export function hasLogpoint(state, location) {
+ const breakpoint = getBreakpoint(state, location);
+ return breakpoint?.options.logValue;
+}
+
+export function getXHRBreakpoints(state) {
+ return state.breakpoints.xhrBreakpoints;
+}
+
+export const shouldPauseOnAnyXHR = createSelector(
+ getXHRBreakpoints,
+ xhrBreakpoints => {
+ const emptyBp = xhrBreakpoints.find(({ path }) => path.length === 0);
+ if (!emptyBp) {
+ return false;
+ }
+
+ return !emptyBp.disabled;
+ }
+);
diff --git a/devtools/client/debugger/src/selectors/event-listeners.js b/devtools/client/debugger/src/selectors/event-listeners.js
new file mode 100644
index 0000000000..dcbcd8109f
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/event-listeners.js
@@ -0,0 +1,19 @@
+/* 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/>. */
+
+export function getActiveEventListeners(state) {
+ return state.eventListenerBreakpoints.active;
+}
+
+export function getEventListenerBreakpointTypes(state) {
+ return state.eventListenerBreakpoints.categories;
+}
+
+export function getEventListenerExpanded(state) {
+ return state.eventListenerBreakpoints.expanded;
+}
+
+export function shouldLogEventBreakpoints(state) {
+ return state.eventListenerBreakpoints.logEventBreakpoints;
+}
diff --git a/devtools/client/debugger/src/selectors/exceptions.js b/devtools/client/debugger/src/selectors/exceptions.js
new file mode 100644
index 0000000000..30230706cd
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/exceptions.js
@@ -0,0 +1,58 @@
+/* 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 { createSelector } from "reselect";
+import { shallowEqual, arrayShallowEqual } from "../utils/shallow-equal";
+
+import { getSelectedSource, getSourceActorsForSource } from "./";
+
+export const getSelectedSourceExceptions = createSelector(
+ getSelectedSourceActors,
+ // Do not retrieve mutableExceptionsMap as it will never change and createSelector would
+ // prevent re-running the selector in case of modification. state.exception is the `state`
+ // in the reducer, which we take care of cloning in case of new exception.
+ state => state.exceptions,
+ (sourceActors, exceptionsState) => {
+ const { mutableExceptionsMap } = exceptionsState;
+ const sourceExceptions = [];
+
+ for (const sourceActor of sourceActors) {
+ const exceptions = mutableExceptionsMap.get(sourceActor.id);
+ if (exceptions) {
+ sourceExceptions.push(...exceptions);
+ }
+ }
+
+ return sourceExceptions;
+ },
+ // Shallow compare both input and output because of arrays being possibly always
+ // different instance but with same content.
+ {
+ memoizeOptions: {
+ equalityCheck: shallowEqual,
+ resultEqualityCheck: arrayShallowEqual,
+ },
+ }
+);
+
+function getSelectedSourceActors(state) {
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ return [];
+ }
+ return getSourceActorsForSource(state, selectedSource.id);
+}
+
+export function getSelectedException(state, line, column) {
+ const sourceExceptions = getSelectedSourceExceptions(state);
+
+ if (!sourceExceptions) {
+ return undefined;
+ }
+
+ return sourceExceptions.find(
+ sourceExc =>
+ sourceExc.lineNumber === line && sourceExc.columnNumber === column
+ );
+}
diff --git a/devtools/client/debugger/src/selectors/expressions.js b/devtools/client/debugger/src/selectors/expressions.js
new file mode 100644
index 0000000000..6cbe829943
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/expressions.js
@@ -0,0 +1,34 @@
+/* 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 { createSelector } from "reselect";
+
+const getExpressionsWrapper = state => state.expressions;
+
+export const getExpressions = createSelector(
+ getExpressionsWrapper,
+ expressions => expressions.expressions
+);
+
+const getAutocompleteMatches = createSelector(
+ getExpressionsWrapper,
+ expressions => expressions.autocompleteMatches
+);
+
+export function getExpression(state, input) {
+ return getExpressions(state).find(exp => exp.input == input);
+}
+
+export function getAutocompleteMatchset(state) {
+ const input = state.expressions.currentAutocompleteInput;
+ if (!input) {
+ return null;
+ }
+ return getAutocompleteMatches(state)[input];
+}
+
+export const getExpressionError = createSelector(
+ getExpressionsWrapper,
+ expressions => expressions.expressionError
+);
diff --git a/devtools/client/debugger/src/selectors/getCallStackFrames.js b/devtools/client/debugger/src/selectors/getCallStackFrames.js
new file mode 100644
index 0000000000..558e07aa17
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/getCallStackFrames.js
@@ -0,0 +1,53 @@
+/* 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 { getSelectedSource } from "./sources";
+import { getBlackBoxRanges } from "./source-blackbox";
+import { getCurrentThreadFrames } from "./pause";
+import { annotateFrames } from "../utils/pause/frames";
+import { isFrameBlackBoxed } from "../utils/source";
+import { createSelector } from "reselect";
+
+function getLocation(frame, isGeneratedSource) {
+ return isGeneratedSource
+ ? frame.generatedLocation || frame.location
+ : frame.location;
+}
+
+function getSourceForFrame(frame, isGeneratedSource) {
+ return getLocation(frame, isGeneratedSource).source;
+}
+
+function appendSource(frame, selectedSource) {
+ const isGeneratedSource = selectedSource && !selectedSource.isOriginal;
+ return {
+ ...frame,
+ location: getLocation(frame, isGeneratedSource),
+ source: getSourceForFrame(frame, isGeneratedSource),
+ };
+}
+
+export function formatCallStackFrames(
+ frames,
+ selectedSource,
+ blackboxedRanges
+) {
+ if (!frames) {
+ return null;
+ }
+
+ const formattedFrames = frames
+ .filter(frame => getSourceForFrame(frame))
+ .map(frame => appendSource(frame, selectedSource))
+ .filter(frame => !isFrameBlackBoxed(frame, blackboxedRanges));
+
+ return annotateFrames(formattedFrames);
+}
+
+export const getCallStackFrames = createSelector(
+ getCurrentThreadFrames,
+ getSelectedSource,
+ getBlackBoxRanges,
+ formatCallStackFrames
+);
diff --git a/devtools/client/debugger/src/selectors/index.js b/devtools/client/debugger/src/selectors/index.js
new file mode 100644
index 0000000000..66220ec101
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/index.js
@@ -0,0 +1,51 @@
+/* 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/>. */
+
+export * from "./ast";
+export * from "./breakpoints";
+export {
+ getClosestBreakpoint,
+ getBreakpointAtLocation,
+ getBreakpointsAtLine,
+ getClosestBreakpointPosition,
+} from "./breakpointAtLocation";
+export { getBreakpointSources } from "./breakpointSources";
+export * from "./event-listeners";
+export * from "./exceptions";
+export * from "./expressions";
+export { getCallStackFrames } from "./getCallStackFrames";
+export { isLineInScope } from "./isLineInScope";
+export { isSelectedFrameVisible } from "./isSelectedFrameVisible";
+export * from "./pause";
+export * from "./pending-breakpoints";
+export * from "./preview";
+export * from "./project-text-search";
+export * from "./quick-open";
+export * from "./source-actors";
+export * from "./source-blackbox";
+export * from "./sources-content";
+export * from "./sources-tree";
+export * from "./sources";
+export * from "./tabs";
+export * from "./threads";
+export * from "./ui";
+export {
+ getVisibleBreakpoints,
+ getFirstVisibleBreakpoints,
+} from "./visibleBreakpoints";
+export * from "./visibleColumnBreakpoints";
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+const { reducer } = objectInspector;
+
+Object.keys(reducer).forEach(function (key) {
+ if (key === "default" || key === "__esModule") {
+ return;
+ }
+ Object.defineProperty(exports, key, {
+ enumerable: true,
+ get: reducer[key],
+ });
+});
diff --git a/devtools/client/debugger/src/selectors/isLineInScope.js b/devtools/client/debugger/src/selectors/isLineInScope.js
new file mode 100644
index 0000000000..f8ca089b81
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/isLineInScope.js
@@ -0,0 +1,22 @@
+/* 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 { getInScopeLines } from "./ast";
+import { getVisibleSelectedFrame } from "./pause";
+
+// Checks if a line is considered in scope
+// We consider all lines in scope, if we do not have lines in scope.
+export function isLineInScope(state, line) {
+ const frame = getVisibleSelectedFrame(state);
+ if (!frame) {
+ return false;
+ }
+
+ const lines = getInScopeLines(state, frame.location);
+ if (!lines) {
+ return true;
+ }
+
+ return lines.includes(line);
+}
diff --git a/devtools/client/debugger/src/selectors/isSelectedFrameVisible.js b/devtools/client/debugger/src/selectors/isSelectedFrameVisible.js
new file mode 100644
index 0000000000..bd0dc7a456
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/isSelectedFrameVisible.js
@@ -0,0 +1,40 @@
+/* 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,
+ isOriginalId,
+} from "devtools/client/shared/source-map-loader/index";
+import { getSelectedFrame, getSelectedLocation, getCurrentThread } from ".";
+
+function getGeneratedId(sourceId) {
+ if (isOriginalId(sourceId)) {
+ return originalToGeneratedId(sourceId);
+ }
+
+ return sourceId;
+}
+
+/*
+ * Checks to if the selected frame's source is currently
+ * selected.
+ */
+export function isSelectedFrameVisible(state) {
+ const thread = getCurrentThread(state);
+ const selectedLocation = getSelectedLocation(state);
+ const selectedFrame = getSelectedFrame(state, thread);
+
+ if (!selectedFrame || !selectedLocation) {
+ return false;
+ }
+
+ if (isOriginalId(selectedLocation.sourceId)) {
+ return selectedLocation.sourceId === selectedFrame.location.sourceId;
+ }
+
+ return (
+ selectedLocation.sourceId ===
+ getGeneratedId(selectedFrame.location.sourceId)
+ );
+}
diff --git a/devtools/client/debugger/src/selectors/moz.build b/devtools/client/debugger/src/selectors/moz.build
new file mode 100644
index 0000000000..e58b638f0f
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/moz.build
@@ -0,0 +1,35 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += []
+
+CompiledModules(
+ "ast.js",
+ "breakpointAtLocation.js",
+ "breakpoints.js",
+ "breakpointSources.js",
+ "event-listeners.js",
+ "exceptions.js",
+ "expressions.js",
+ "getCallStackFrames.js",
+ "index.js",
+ "isLineInScope.js",
+ "isSelectedFrameVisible.js",
+ "pause.js",
+ "pending-breakpoints.js",
+ "preview.js",
+ "project-text-search.js",
+ "quick-open.js",
+ "source-actors.js",
+ "source-blackbox.js",
+ "sources-tree.js",
+ "sources-content.js",
+ "sources.js",
+ "tabs.js",
+ "threads.js",
+ "visibleBreakpoints.js",
+ "visibleColumnBreakpoints.js",
+ "ui.js",
+)
diff --git a/devtools/client/debugger/src/selectors/pause.js b/devtools/client/debugger/src/selectors/pause.js
new file mode 100644
index 0000000000..61900e9f8c
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/pause.js
@@ -0,0 +1,267 @@
+/* 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 { getThreadPauseState } from "../reducers/pause";
+import { getSelectedSourceId, getSelectedLocation } from "./sources";
+
+import { isGeneratedId } from "devtools/client/shared/source-map-loader/index";
+
+// eslint-disable-next-line
+import { getSelectedLocation as _getSelectedLocation } from "../utils/selected-location";
+import { createSelector } from "reselect";
+
+export const getSelectedFrame = createSelector(
+ (state, thread) => state.pause.threads[thread],
+ threadPauseState => {
+ if (!threadPauseState) return null;
+ const { selectedFrameId, frames } = threadPauseState;
+ if (frames) {
+ return frames.find(frame => frame.id == selectedFrameId);
+ }
+ return null;
+ }
+);
+
+export const getVisibleSelectedFrame = createSelector(
+ getSelectedLocation,
+ state => getSelectedFrame(state, getCurrentThread(state)),
+ (selectedLocation, selectedFrame) => {
+ if (!selectedFrame) {
+ return null;
+ }
+
+ const { id, displayName } = selectedFrame;
+
+ return {
+ id,
+ displayName,
+ location: _getSelectedLocation(selectedFrame, selectedLocation),
+ };
+ }
+);
+
+export function getContext(state) {
+ return state.pause.cx;
+}
+
+export function getThreadContext(state) {
+ return state.pause.threadcx;
+}
+
+export function getPauseReason(state, thread) {
+ return getThreadPauseState(state.pause, thread).why;
+}
+
+export function getShouldBreakpointsPaneOpenOnPause(state, thread) {
+ return getThreadPauseState(state.pause, thread)
+ .shouldBreakpointsPaneOpenOnPause;
+}
+
+export function getPauseCommand(state, thread) {
+ return getThreadPauseState(state.pause, thread).command;
+}
+
+export function isStepping(state, thread) {
+ return ["stepIn", "stepOver", "stepOut"].includes(
+ getPauseCommand(state, thread)
+ );
+}
+
+export function getCurrentThread(state) {
+ return getThreadContext(state).thread;
+}
+
+export function getIsPaused(state, thread) {
+ return getThreadPauseState(state.pause, thread).isPaused;
+}
+
+export function getIsCurrentThreadPaused(state) {
+ return getIsPaused(state, getCurrentThread(state));
+}
+
+export function isEvaluatingExpression(state, thread) {
+ return getThreadPauseState(state.pause, thread).command === "expression";
+}
+
+export function getIsWaitingOnBreak(state, thread) {
+ return getThreadPauseState(state.pause, thread).isWaitingOnBreak;
+}
+
+export function getShouldPauseOnExceptions(state) {
+ return state.pause.shouldPauseOnExceptions;
+}
+
+export function getShouldPauseOnCaughtExceptions(state) {
+ return state.pause.shouldPauseOnCaughtExceptions;
+}
+
+export function getFrames(state, thread) {
+ const { frames, framesLoading } = getThreadPauseState(state.pause, thread);
+ return framesLoading ? null : frames;
+}
+
+export function getCurrentThreadFrames(state) {
+ const { frames, framesLoading } = getThreadPauseState(
+ state.pause,
+ getCurrentThread(state)
+ );
+ return framesLoading ? null : frames;
+}
+
+function getGeneratedFrameId(frameId) {
+ if (frameId.includes("-originalFrame")) {
+ // The mapFrames can add original stack frames -- get generated frameId.
+ return frameId.substr(0, frameId.lastIndexOf("-originalFrame"));
+ }
+ return frameId;
+}
+
+export function getGeneratedFrameScope(state, thread, frameId) {
+ if (!frameId) {
+ return null;
+ }
+
+ return getFrameScopes(state, thread).generated[getGeneratedFrameId(frameId)];
+}
+
+export function getOriginalFrameScope(state, thread, sourceId, frameId) {
+ if (!frameId || !sourceId) {
+ return null;
+ }
+
+ const isGenerated = isGeneratedId(sourceId);
+ const original = getFrameScopes(state, thread).original[
+ getGeneratedFrameId(frameId)
+ ];
+
+ if (!isGenerated && original && (original.pending || original.scope)) {
+ return original;
+ }
+
+ return null;
+}
+
+// This is only used by tests
+export function getFrameScopes(state, thread) {
+ return getThreadPauseState(state.pause, thread).frameScopes;
+}
+
+export function getSelectedFrameBindings(state, thread) {
+ const scopes = getFrameScopes(state, thread);
+ const selectedFrameId = getSelectedFrameId(state, thread);
+ if (!scopes || !selectedFrameId) {
+ return null;
+ }
+
+ const frameScope = scopes.generated[selectedFrameId];
+ if (!frameScope || frameScope.pending) {
+ return null;
+ }
+
+ let currentScope = frameScope.scope;
+ let frameBindings = [];
+ while (currentScope && currentScope.type != "object") {
+ if (currentScope.bindings) {
+ const bindings = Object.keys(currentScope.bindings.variables);
+ const args = [].concat(
+ ...currentScope.bindings.arguments.map(argument =>
+ Object.keys(argument)
+ )
+ );
+
+ frameBindings = [...frameBindings, ...bindings, ...args];
+ }
+ currentScope = currentScope.parent;
+ }
+
+ return frameBindings;
+}
+
+function getFrameScope(state, thread, sourceId, frameId) {
+ return (
+ getOriginalFrameScope(state, thread, sourceId, frameId) ||
+ getGeneratedFrameScope(state, thread, frameId)
+ );
+}
+
+// This is only used by tests
+export function getSelectedScope(state, thread) {
+ const sourceId = getSelectedSourceId(state);
+ const frameId = getSelectedFrameId(state, thread);
+
+ const frameScope = getFrameScope(state, thread, sourceId, frameId);
+ if (!frameScope) {
+ return null;
+ }
+
+ return frameScope.scope || null;
+}
+
+export function getSelectedOriginalScope(state, thread) {
+ const sourceId = getSelectedSourceId(state);
+ const frameId = getSelectedFrameId(state, thread);
+ return getOriginalFrameScope(state, thread, sourceId, frameId);
+}
+
+export function getSelectedGeneratedScope(state, thread) {
+ const frameId = getSelectedFrameId(state, thread);
+ return getGeneratedFrameScope(state, thread, frameId);
+}
+
+export function getSelectedScopeMappings(state, thread) {
+ const frameId = getSelectedFrameId(state, thread);
+ if (!frameId) {
+ return null;
+ }
+
+ return getFrameScopes(state, thread).mappings[frameId];
+}
+
+export function getSelectedFrameId(state, thread) {
+ return getThreadPauseState(state.pause, thread).selectedFrameId;
+}
+
+export function isTopFrameSelected(state, thread) {
+ const selectedFrameId = getSelectedFrameId(state, thread);
+ const topFrame = getTopFrame(state, thread);
+ return selectedFrameId == topFrame?.id;
+}
+
+export function getTopFrame(state, thread) {
+ const frames = getFrames(state, thread);
+ return frames?.[0];
+}
+
+export function getSkipPausing(state) {
+ return state.pause.skipPausing;
+}
+
+export function getHighlightedCalls(state, thread) {
+ return getThreadPauseState(state.pause, thread).highlightedCalls;
+}
+
+export function isMapScopesEnabled(state) {
+ return state.pause.mapScopes;
+}
+
+export function getInlinePreviews(state, thread, frameId) {
+ return getThreadPauseState(state.pause, thread).inlinePreview[
+ getGeneratedFrameId(frameId)
+ ];
+}
+
+// This is only used by tests
+export function getSelectedInlinePreviews(state) {
+ const thread = getCurrentThread(state);
+ const frameId = getSelectedFrameId(state, thread);
+ if (!frameId) {
+ return null;
+ }
+
+ return getInlinePreviews(state, thread, frameId);
+}
+
+export function getLastExpandedScopes(state, thread) {
+ return getThreadPauseState(state.pause, thread).lastExpandedScopes;
+}
diff --git a/devtools/client/debugger/src/selectors/pending-breakpoints.js b/devtools/client/debugger/src/selectors/pending-breakpoints.js
new file mode 100644
index 0000000000..a05c43477d
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/pending-breakpoints.js
@@ -0,0 +1,20 @@
+/* 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/>. */
+
+export function getPendingBreakpoints(state) {
+ return state.pendingBreakpoints;
+}
+
+export function getPendingBreakpointList(state) {
+ return Object.values(getPendingBreakpoints(state));
+}
+
+export function getPendingBreakpointsForSource(state, source) {
+ return getPendingBreakpointList(state).filter(pendingBreakpoint => {
+ return (
+ pendingBreakpoint.location.sourceUrl === source.url ||
+ pendingBreakpoint.generatedLocation.sourceUrl == source.url
+ );
+ });
+}
diff --git a/devtools/client/debugger/src/selectors/preview.js b/devtools/client/debugger/src/selectors/preview.js
new file mode 100644
index 0000000000..adfee09002
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/preview.js
@@ -0,0 +1,11 @@
+/* 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/>. */
+
+export function getPreview(state) {
+ return state.preview.preview;
+}
+
+export function getPreviewCount(state) {
+ return state.preview.previewCount;
+}
diff --git a/devtools/client/debugger/src/selectors/project-text-search.js b/devtools/client/debugger/src/selectors/project-text-search.js
new file mode 100644
index 0000000000..3679fd931f
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/project-text-search.js
@@ -0,0 +1,19 @@
+/* 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/>. */
+
+export function getProjectSearchOperation(state) {
+ return state.projectTextSearch.ongoingSearch;
+}
+
+export function getProjectSearchResults(state) {
+ return state.projectTextSearch.results;
+}
+
+export function getProjectSearchStatus(state) {
+ return state.projectTextSearch.status;
+}
+
+export function getProjectSearchQuery(state) {
+ return state.projectTextSearch.query;
+}
diff --git a/devtools/client/debugger/src/selectors/quick-open.js b/devtools/client/debugger/src/selectors/quick-open.js
new file mode 100644
index 0000000000..8364c7bbf3
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/quick-open.js
@@ -0,0 +1,15 @@
+/* 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/>. */
+
+export function getQuickOpenEnabled(state) {
+ return state.quickOpen.enabled;
+}
+
+export function getQuickOpenQuery(state) {
+ return state.quickOpen.query;
+}
+
+export function getQuickOpenType(state) {
+ return state.quickOpen.searchType;
+}
diff --git a/devtools/client/debugger/src/selectors/source-actors.js b/devtools/client/debugger/src/selectors/source-actors.js
new file mode 100644
index 0000000000..4d7f915da2
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/source-actors.js
@@ -0,0 +1,111 @@
+/* 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/>. */
+
+/**
+ * Tells if a given Source Actor is registered in the redux store
+ *
+ * @param {Object} state
+ * @param {String} sourceActorId
+ * Source Actor ID
+ * @return {Boolean}
+ */
+export function hasSourceActor(state, sourceActorId) {
+ return state.sourceActors.mutableSourceActors.has(sourceActorId);
+}
+
+/**
+ * Get the Source Actor object. See create.js:createSourceActor()
+ *
+ * @param {Object} state
+ * @param {String} sourceActorId
+ * Source Actor ID
+ * @return {Object}
+ * The Source Actor object (if registered)
+ */
+export function getSourceActor(state, sourceActorId) {
+ return state.sourceActors.mutableSourceActors.get(sourceActorId);
+}
+
+/**
+ * Reports if the Source Actor relates to a valid source map / original source.
+ *
+ * @param {Object} state
+ * @param {String} sourceActorId
+ * Source Actor ID
+ * @return {Boolean}
+ * True if it has a valid source map/original object.
+ */
+export function isSourceActorWithSourceMap(state, sourceActorId) {
+ return state.sourceActors.mutableSourceActorsWithSourceMap.has(sourceActorId);
+}
+
+// Used by threads selectors
+/**
+ * Get all Source Actor objects for a given thread. See create.js:createSourceActor()
+ *
+ * @param {Object} state
+ * @param {Array<String>} threadActorIDs
+ * List of Thread IDs
+ * @return {Array<Object>}
+ */
+export function getSourceActorsForThread(state, threadActorIDs) {
+ if (!Array.isArray(threadActorIDs)) {
+ threadActorIDs = [threadActorIDs];
+ }
+ const actors = [];
+ for (const sourceActor of state.sourceActors.mutableSourceActors.values()) {
+ if (threadActorIDs.includes(sourceActor.thread)) {
+ actors.push(sourceActor);
+ }
+ }
+ return actors;
+}
+
+/**
+ * Get the list of all breakable lines for a given source actor.
+ *
+ * @param {Object} state
+ * @param {String} sourceActorId
+ * Source Actor ID
+ * @return {AsyncValue<Array<Number>>}
+ * List of all the breakable lines.
+ */
+export function getSourceActorBreakableLines(state, sourceActorId) {
+ return state.sourceActors.mutableBreakableLines.get(sourceActorId);
+}
+
+// Used by sources selectors
+/**
+ * Get the list of all breakable lines for a set of source actors.
+ *
+ * This is typically used to fetch the breakable lines of HTML sources
+ * which are made of multiple source actors (one per inline script).
+ *
+ * @param {Object} state
+ * @param {Array<String>} sourceActors
+ * List of Source Actors
+ * @param {Boolean} isHTML
+ * True, if we are fetching the breakable lines for an HTML source.
+ * For them, we have to aggregate the lines of each source actors.
+ * Otherwise, we might still have many source actors, but one per thread.
+ * In this case, we simply return the first source actor to have the lines ready.
+ * @return {Array<Number>}
+ * List of all the breakable lines.
+ */
+export function getBreakableLinesForSourceActors(state, sourceActors, isHTML) {
+ const allBreakableLines = [];
+ for (const sourceActor of sourceActors) {
+ const breakableLines = state.sourceActors.mutableBreakableLines.get(
+ sourceActor.id
+ );
+ if (breakableLines) {
+ if (isHTML) {
+ allBreakableLines.push(...breakableLines);
+ } else {
+ return breakableLines;
+ }
+ }
+ }
+ return allBreakableLines;
+}
diff --git a/devtools/client/debugger/src/selectors/source-blackbox.js b/devtools/client/debugger/src/selectors/source-blackbox.js
new file mode 100644
index 0000000000..afd5695f18
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/source-blackbox.js
@@ -0,0 +1,26 @@
+/* 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/>. */
+
+export function getBlackBoxRanges(state) {
+ return state.sourceBlackBox.blackboxedRanges;
+}
+
+export function isSourceBlackBoxed(state, source) {
+ // Only sources with a URL can be blackboxed.
+ if (!source.url) {
+ return false;
+ }
+ return state.sourceBlackBox.blackboxedSet.has(source.url);
+}
+
+export function isSourceOnSourceMapIgnoreList(state, source) {
+ if (!source) {
+ return false;
+ }
+ return getIgnoreListSourceUrls(state).includes(source.url);
+}
+
+export function getIgnoreListSourceUrls(state) {
+ return state.sourceBlackBox.sourceMapIgnoreListUrls;
+}
diff --git a/devtools/client/debugger/src/selectors/sources-content.js b/devtools/client/debugger/src/selectors/sources-content.js
new file mode 100644
index 0000000000..b7442fb555
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/sources-content.js
@@ -0,0 +1,48 @@
+/* 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 { asSettled } from "../utils/async-value";
+
+import {
+ getSelectedLocation,
+ getFirstSourceActorForGeneratedSource,
+} from "../selectors/sources";
+
+export function getSourceTextContent(state, location) {
+ if (location.source.isOriginal) {
+ return state.sourcesContent.mutableOriginalSourceTextContentMapBySourceId.get(
+ location.source.id
+ );
+ }
+
+ let { sourceActor } = location;
+ if (!sourceActor) {
+ sourceActor = getFirstSourceActorForGeneratedSource(
+ state,
+ location.source.id
+ );
+ }
+ return state.sourcesContent.mutableGeneratedSourceTextContentMapBySourceActorId.get(
+ sourceActor.id
+ );
+}
+
+export function getSettledSourceTextContent(state, location) {
+ const content = getSourceTextContent(state, location);
+ return asSettled(content);
+}
+
+export function getSelectedSourceTextContent(state) {
+ const location = getSelectedLocation(state);
+
+ if (!location) {
+ return null;
+ }
+
+ return getSourceTextContent(state, location);
+}
+
+export function getSourcesEpoch(state) {
+ return state.sourcesContent.epoch;
+}
diff --git a/devtools/client/debugger/src/selectors/sources-tree.js b/devtools/client/debugger/src/selectors/sources-tree.js
new file mode 100644
index 0000000000..8ef67c93ba
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/sources-tree.js
@@ -0,0 +1,151 @@
+/* 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 { createSelector } from "reselect";
+
+/**
+ * Main selector to build the SourceTree,
+ * but this is also the source of data for the QuickOpen dialog.
+ *
+ * If no project directory root is set, this will return the thread items.
+ * Otherwise this will return the items where we set the directory root.
+ */
+export const getSourcesTreeSources = createSelector(
+ getProjectDirectoryRoot,
+ state => state.sourcesTree.threadItems,
+ (projectDirectoryRoot, threadItems) => {
+ // Only accept thread which have their thread attribute set.
+ // This may come late, if we receive ADD_SOURCES before INSERT_THREAD.
+ // Also filter out threads which have no sources, in case we had
+ // INSERT_THREAD with no ADD_SOURCES.
+ threadItems = threadItems.filter(
+ item => !!item.thread && !!item.children.length
+ );
+
+ if (projectDirectoryRoot) {
+ const directory = getDirectoryForUniquePath(
+ projectDirectoryRoot,
+ threadItems
+ );
+ if (directory) {
+ return directory.children;
+ }
+ return [];
+ }
+
+ return threadItems;
+ }
+);
+
+// This is used by QuickOpen UI
+/**
+ * Main selector for the QuickOpen dialog.
+ *
+ * The returns the list of all the reducer's source objects
+ * that are possibly displayed in the Source Tree.
+ * This doesn't return Source Tree Items, but the source objects.
+ */
+export const getDisplayedSourcesList = createSelector(
+ getSourcesTreeSources,
+ roots => {
+ const sources = [];
+ function walk(item) {
+ if (item.type == "source") {
+ sources.push(item.source);
+ } else {
+ for (const child of item.children) {
+ walk(child);
+ }
+ }
+ }
+ for (const root of roots) {
+ walk(root);
+ }
+ return sources;
+ }
+);
+
+export function getExpandedState(state) {
+ return state.sourcesTree.expanded;
+}
+
+export function getFocusedSourceItem(state) {
+ return state.sourcesTree.focusedItem;
+}
+
+export function getProjectDirectoryRoot(state) {
+ return state.sourcesTree.projectDirectoryRoot;
+}
+
+export function getProjectDirectoryRootName(state) {
+ return state.sourcesTree.projectDirectoryRootName;
+}
+
+/**
+ * Lookup for project root item, matching the given "unique path".
+ */
+function getDirectoryForUniquePath(projectRoot, threadItems) {
+ const sections = projectRoot.split("|");
+ const thread = sections.shift();
+
+ const threadItem = threadItems.find(item => {
+ return (
+ item.uniquePath == thread ||
+ (thread == "top-level" && item.thread.isTopLevel)
+ );
+ });
+ if (!threadItem) {
+ dump(
+ `No thread item for: ${projectRoot} -- ${thread} -- ${Object.keys(
+ threadItems
+ )}\n`
+ );
+ return null;
+ }
+
+ // If we selected a thread, the project root is for a Thread Item
+ // and it only contains `${thread}`
+ if (!sections.length) {
+ return threadItem;
+ }
+
+ const group = sections.shift();
+ for (const child of threadItem.children) {
+ if (child.groupName != group) {
+ continue;
+ }
+ // In case we picked a group, return it...
+ // project root looked like this `${thread}|${group}`
+ if (!sections.length) {
+ return child;
+ }
+ // ..otherwise, we picked a directory, so look for it by traversing the tree
+ // project root looked like this `${thread}|${group}|${directoryPath}`
+ const path = sections.shift();
+ return findPathInDirectory(child, path);
+ }
+ dump(` Unable to find group: ${group}\n`);
+ return null;
+
+ function findPathInDirectory(directory, path) {
+ for (const child of directory.children) {
+ if (child.type == "directory") {
+ // `path` should be the absolute path from the group/domain
+ if (child.path == path) {
+ return child;
+ }
+ // Ignore folders which doesn't match the beginning of the lookup path
+ if (!path.startsWith(child.path)) {
+ continue;
+ }
+ const match = findPathInDirectory(child, path);
+ if (match) {
+ return match;
+ }
+ }
+ }
+ dump(`Unable to find directory: ${path}\n`);
+ return null;
+ }
+}
diff --git a/devtools/client/debugger/src/selectors/sources.js b/devtools/client/debugger/src/selectors/sources.js
new file mode 100644
index 0000000000..4d36a75865
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/sources.js
@@ -0,0 +1,358 @@
+/* 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 { createSelector } from "reselect";
+
+import {
+ getPrettySourceURL,
+ isGenerated,
+ isPretty,
+ isJavaScript,
+} from "../utils/source";
+
+import { findPosition } from "../utils/breakpoint/breakpointPositions";
+import { isFulfilled } from "../utils/async-value";
+
+import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index";
+import { prefs } from "../utils/prefs";
+
+import {
+ hasSourceActor,
+ getSourceActor,
+ getBreakableLinesForSourceActors,
+ isSourceActorWithSourceMap,
+} from "./source-actors";
+import { getSourceTextContent } from "./sources-content";
+
+export function hasSource(state, id) {
+ return state.sources.mutableSources.has(id);
+}
+
+export function getSource(state, id) {
+ return state.sources.mutableSources.get(id);
+}
+
+export function getSourceFromId(state, id) {
+ const source = getSource(state, id);
+ if (!source) {
+ console.warn(`source ${id} does not exist`);
+ }
+ return source;
+}
+
+export function getSourceByActorId(state, actorId) {
+ if (!hasSourceActor(state, actorId)) {
+ return null;
+ }
+
+ return getSource(state, getSourceActor(state, actorId).source);
+}
+
+function getSourcesByURL(state, url) {
+ return state.sources.mutableSourcesPerUrl.get(url) || [];
+}
+
+export function getSourceByURL(state, url) {
+ const foundSources = getSourcesByURL(state, url);
+ return foundSources[0];
+}
+
+// This is used by tabs selectors
+export function getSpecificSourceByURL(state, url, isOriginal) {
+ const foundSources = getSourcesByURL(state, url);
+ return foundSources.find(source => source.isOriginal == isOriginal);
+}
+
+function getOriginalSourceByURL(state, url) {
+ return getSpecificSourceByURL(state, url, true);
+}
+
+export function getGeneratedSourceByURL(state, url) {
+ return getSpecificSourceByURL(state, url, false);
+}
+
+export function getGeneratedSource(state, source) {
+ if (!source) {
+ return null;
+ }
+
+ if (isGenerated(source)) {
+ return source;
+ }
+
+ return getSourceFromId(state, originalToGeneratedId(source.id));
+}
+
+export function getPendingSelectedLocation(state) {
+ return state.sources.pendingSelectedLocation;
+}
+
+export function getPrettySource(state, id) {
+ if (!id) {
+ return null;
+ }
+
+ const source = getSource(state, id);
+ if (!source) {
+ return null;
+ }
+
+ return getOriginalSourceByURL(state, getPrettySourceURL(source.url));
+}
+
+// This is only used by Project Search and tests.
+export function getSourceList(state) {
+ return [...state.sources.mutableSources.values()];
+}
+
+// This is only used by tests and create.js
+export function getSourceCount(state) {
+ return state.sources.mutableSources.size;
+}
+
+export function getSelectedLocation(state) {
+ return state.sources.selectedLocation;
+}
+
+export const getSelectedSource = createSelector(
+ getSelectedLocation,
+ selectedLocation => {
+ if (!selectedLocation) {
+ return undefined;
+ }
+
+ return selectedLocation.source;
+ }
+);
+
+// This is used by tests and pause reducers
+export function getSelectedSourceId(state) {
+ const source = getSelectedSource(state);
+ return source?.id;
+}
+
+export function getShouldSelectOriginalLocation(state) {
+ return state.sources.shouldSelectOriginalLocation;
+}
+
+/**
+ * Gets the first source actor for the source and/or thread
+ * provided.
+ *
+ * @param {Object} state
+ * @param {String} sourceId
+ * The source used
+ * @param {String} [threadId]
+ * The thread to check, this is optional.
+ * @param {Object} sourceActor
+ *
+ */
+export function getFirstSourceActorForGeneratedSource(
+ state,
+ sourceId,
+ threadId
+) {
+ let source = getSource(state, sourceId);
+ // The source may have been removed if we are being called by async code
+ if (!source) {
+ return null;
+ }
+ if (source.isOriginal) {
+ source = getSource(state, originalToGeneratedId(source.id));
+ }
+ const actors = getSourceActorsForSource(state, source.id);
+ if (threadId) {
+ return actors.find(actorInfo => actorInfo.thread == threadId) || null;
+ }
+ return actors[0] || null;
+}
+
+/**
+ * Get the source actor of the source
+ *
+ * @param {Object} state
+ * @param {String} id
+ * The source id
+ * @return {Array<Object>}
+ * List of source actors
+ */
+export function getSourceActorsForSource(state, id) {
+ return state.sources.mutableSourceActors.get(id) || [];
+}
+
+export function isSourceWithMap(state, id) {
+ const actors = getSourceActorsForSource(state, id);
+ return actors.some(actor => isSourceActorWithSourceMap(state, actor.id));
+}
+
+export function canPrettyPrintSource(state, location) {
+ const { sourceId } = location;
+ const source = getSource(state, sourceId);
+ if (
+ !source ||
+ isPretty(source) ||
+ source.isOriginal ||
+ (prefs.clientSourceMapsEnabled && isSourceWithMap(state, sourceId))
+ ) {
+ return false;
+ }
+
+ const content = getSourceTextContent(state, location);
+ const sourceContent = content && isFulfilled(content) ? content.value : null;
+
+ if (
+ !sourceContent ||
+ (!isJavaScript(source, sourceContent) && !source.isHTML)
+ ) {
+ return false;
+ }
+
+ return true;
+}
+
+export function getPrettyPrintMessage(state, location) {
+ const source = location.source;
+ if (!source) {
+ return L10N.getStr("sourceTabs.prettyPrint");
+ }
+
+ if (isPretty(source)) {
+ return L10N.getStr("sourceFooter.prettyPrint.isPrettyPrintedMessage");
+ }
+
+ if (source.isOriginal) {
+ return L10N.getStr("sourceFooter.prettyPrint.isOriginalMessage");
+ }
+
+ if (prefs.clientSourceMapsEnabled && isSourceWithMap(state, source.id)) {
+ return L10N.getStr("sourceFooter.prettyPrint.hasSourceMapMessage");
+ }
+
+ const content = getSourceTextContent(state, location);
+
+ const sourceContent = content && isFulfilled(content) ? content.value : null;
+ if (!sourceContent) {
+ return L10N.getStr("sourceFooter.prettyPrint.noContentMessage");
+ }
+
+ if (!isJavaScript(source, sourceContent) && !source.isHTML) {
+ return L10N.getStr("sourceFooter.prettyPrint.isNotJavascriptMessage");
+ }
+
+ return L10N.getStr("sourceTabs.prettyPrint");
+}
+
+export function getBreakpointPositionsForSource(state, sourceId) {
+ return state.sources.mutableBreakpointPositions.get(sourceId);
+}
+
+// This is only used by one test
+export function hasBreakpointPositions(state, sourceId) {
+ return !!getBreakpointPositionsForSource(state, sourceId);
+}
+
+export function getBreakpointPositionsForLine(state, sourceId, line) {
+ const positions = getBreakpointPositionsForSource(state, sourceId);
+ return positions?.[line];
+}
+
+export function getBreakpointPositionsForLocation(state, location) {
+ const { sourceId } = location;
+ const positions = getBreakpointPositionsForSource(state, sourceId);
+ return findPosition(positions, location);
+}
+
+export function getBreakableLines(state, sourceId) {
+ if (!sourceId) {
+ return null;
+ }
+ const source = getSource(state, sourceId);
+ if (!source) {
+ return null;
+ }
+
+ if (source.isOriginal) {
+ return state.sources.mutableOriginalBreakableLines.get(sourceId);
+ }
+
+ const sourceActors = getSourceActorsForSource(state, sourceId);
+ if (!sourceActors.length) {
+ return null;
+ }
+
+ // 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, source.isHTML);
+}
+
+export const getSelectedBreakableLines = createSelector(
+ state => {
+ const sourceId = getSelectedSourceId(state);
+ return sourceId && getBreakableLines(state, sourceId);
+ },
+ breakableLines => new Set(breakableLines || [])
+);
+
+export function isSourceOverridden(state, source) {
+ if (!source || !source.url) {
+ return false;
+ }
+ return state.sources.mutableOverrideSources.has(source.url);
+}
+
+/**
+ * Compute the list of source actors and source objects to be removed
+ * when removing a given target/thread.
+ *
+ * @param {String} threadActorID
+ * The thread to be removed.
+ * @return {Object}
+ * An object with two arrays:
+ * - actors: list of source actor objects to remove
+ * - sources: list of source objects to remove
+ */
+export function getSourcesToRemoveForThread(state, threadActorID) {
+ const sourcesToRemove = [];
+ const actorsToRemove = [];
+
+ for (const [
+ sourceId,
+ actorsForSource,
+ ] of state.sources.mutableSourceActors.entries()) {
+ let removedActorsCount = 0;
+ // Find all actors for the current source which belongs to the given thread actor
+ for (const actor of actorsForSource) {
+ if (actor.thread == threadActorID) {
+ actorsToRemove.push(actor);
+ removedActorsCount++;
+ }
+ }
+
+ // If we are about to remove all source actors for the current source,
+ // or if for some unexpected reason we have a source with no actors,
+ // notify the caller to also remove this source.
+ if (
+ removedActorsCount == actorsForSource.length ||
+ !actorsForSource.length
+ ) {
+ sourcesToRemove.push(state.sources.mutableSources.get(sourceId));
+
+ // Also remove any original sources related to this generated source
+ const originalSourceIds =
+ state.sources.mutableOriginalSources.get(sourceId);
+ if (originalSourceIds?.length > 0) {
+ for (const originalSourceId of originalSourceIds) {
+ sourcesToRemove.push(
+ state.sources.mutableSources.get(originalSourceId)
+ );
+ }
+ }
+ }
+ }
+
+ return {
+ actors: actorsToRemove,
+ sources: sourcesToRemove,
+ };
+}
diff --git a/devtools/client/debugger/src/selectors/tabs.js b/devtools/client/debugger/src/selectors/tabs.js
new file mode 100644
index 0000000000..de2655756e
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/tabs.js
@@ -0,0 +1,90 @@
+/* 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 { createSelector } from "reselect";
+import { getPrettySourceURL } from "../utils/source";
+
+import { getSpecificSourceByURL } from "./sources";
+import { isOriginalId } from "devtools/client/shared/source-map-loader/index";
+import { isSimilarTab } from "../utils/tabs";
+
+export const getTabs = state => state.tabs.tabs;
+
+// Return the list of tabs which relates to an active source
+export const getSourceTabs = createSelector(getTabs, tabs =>
+ tabs.filter(tab => tab.source)
+);
+
+export const getSourcesForTabs = createSelector(getSourceTabs, sourceTabs => {
+ return sourceTabs.map(tab => tab.source);
+});
+
+export function tabExists(state, sourceId) {
+ return !!getSourceTabs(state).find(tab => tab.source.id == sourceId);
+}
+
+export function hasPrettyTab(state, sourceUrl) {
+ const prettyUrl = getPrettySourceURL(sourceUrl);
+ return !!getSourceTabs(state).find(tab => tab.url === prettyUrl);
+}
+
+/**
+ * Gets the next tab to select when a tab closes. Heuristics:
+ * 1. if the selected tab is available, it remains selected
+ * 2. if it is gone, the next available tab to the left should be active
+ * 3. if the first tab is active and closed, select the second tab
+ */
+export function getNewSelectedSource(state, tabList) {
+ const { selectedLocation } = state.sources;
+ const availableTabs = getTabs(state);
+ if (!selectedLocation) {
+ return null;
+ }
+
+ const selectedSource = selectedLocation.source;
+ if (!selectedSource) {
+ return null;
+ }
+
+ const matchingTab = availableTabs.find(tab =>
+ isSimilarTab(tab, selectedSource.url, isOriginalId(selectedSource.id))
+ );
+
+ if (matchingTab) {
+ const specificSelectedSource = getSpecificSourceByURL(
+ state,
+ selectedSource.url,
+ selectedSource.isOriginal
+ );
+
+ if (specificSelectedSource) {
+ return specificSelectedSource;
+ }
+
+ return null;
+ }
+
+ const tabUrls = tabList.map(tab => tab.url);
+ const leftNeighborIndex = Math.max(
+ tabUrls.indexOf(selectedSource.url) - 1,
+ 0
+ );
+ const lastAvailbleTabIndex = availableTabs.length - 1;
+ const newSelectedTabIndex = Math.min(leftNeighborIndex, lastAvailbleTabIndex);
+ const availableTab = availableTabs[newSelectedTabIndex];
+
+ if (availableTab) {
+ const tabSource = getSpecificSourceByURL(
+ state,
+ availableTab.url,
+ availableTab.isOriginal
+ );
+
+ if (tabSource) {
+ return tabSource;
+ }
+ }
+
+ return null;
+}
diff --git a/devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap b/devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap
new file mode 100644
index 0000000000..845d228d41
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap
@@ -0,0 +1,165 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`visible column breakpoints doesnt show breakpoints to the right 1`] = `
+Array [
+ Object {
+ "breakpoint": Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "options": Object {},
+ "originalText": "text",
+ "text": "text",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+]
+`;
+
+exports[`visible column breakpoints ignores single breakpoints 1`] = `
+Array [
+ Object {
+ "breakpoint": Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "options": Object {},
+ "originalText": "text",
+ "text": "text",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+ Object {
+ "breakpoint": null,
+ "location": Object {
+ "column": 3,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+]
+`;
+
+exports[`visible column breakpoints only shows visible breakpoints 1`] = `
+Array [
+ Object {
+ "breakpoint": Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "options": Object {},
+ "originalText": "text",
+ "text": "text",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+ Object {
+ "breakpoint": null,
+ "location": Object {
+ "column": 3,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+]
+`;
+
+exports[`visible column breakpoints simple 1`] = `
+Array [
+ Object {
+ "breakpoint": Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "id": "foo",
+ },
+ "sourceId": "foo",
+ },
+ "options": Object {},
+ "originalText": "text",
+ "text": "text",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+ Object {
+ "breakpoint": null,
+ "location": Object {
+ "column": 5,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/selectors/test/getCallStackFrames.spec.js b/devtools/client/debugger/src/selectors/test/getCallStackFrames.spec.js
new file mode 100644
index 0000000000..7d31446e48
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/test/getCallStackFrames.spec.js
@@ -0,0 +1,166 @@
+/* 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 { getCallStackFrames } from "../getCallStackFrames";
+
+describe("getCallStackFrames selector", () => {
+ describe("library annotation", () => {
+ it("annotates React frames", () => {
+ const source1 = { id: "source1", url: "webpack:///src/App.js" };
+ const source2 = {
+ id: "source2",
+ url: "webpack:///foo/node_modules/react-dom/lib/ReactCompositeComponent.js",
+ };
+ const state = {
+ frames: [
+ { location: { sourceId: "source1", source: source1 } },
+ { location: { sourceId: "source2", source: source2 } },
+ { location: { sourceId: "source2", source: source2 } },
+ ],
+ selectedSource: {
+ id: "sourceId-originalSource",
+ isOriginal: true,
+ },
+ };
+
+ const frames = getCallStackFrames.resultFunc(
+ state.frames,
+ state.selectedSource,
+ {}
+ );
+
+ expect(frames[0]).not.toHaveProperty("library");
+ expect(frames[1]).toHaveProperty("library", "React");
+ expect(frames[2]).toHaveProperty("library", "React");
+ });
+
+ // Multiple Babel async frame groups occur when you have an async function
+ // calling another async function (a common case).
+ //
+ // There are two possible frame groups that can occur depending on whether
+ // one sets a breakpoint before or after an await
+ it("annotates frames related to Babel async transforms", () => {
+ const appSource = { id: "app", url: "webpack///app.js" };
+ const bundleSource = { id: "bundle", url: "https://foo.com/bundle.js" };
+ const regeneratorSource = {
+ id: "regenerator",
+ url: "webpack:///foo/node_modules/regenerator-runtime/runtime.js",
+ };
+ const microtaskSource = {
+ id: "microtask",
+ url: "webpack:///foo/node_modules/core-js/modules/_microtask.js",
+ };
+ const promiseSource = {
+ id: "promise",
+ url: "webpack///foo/node_modules/core-js/modules/es6.promise.js",
+ };
+ const preAwaitGroup = [
+ {
+ displayName: "asyncAppFunction",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "tryCatch",
+ location: { source: regeneratorSource },
+ },
+ {
+ displayName: "invoke",
+ location: { source: regeneratorSource },
+ },
+ {
+ displayName: "defineIteratorMethods/</prototype[method]",
+ location: { source: regeneratorSource },
+ },
+ {
+ displayName: "step",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "_asyncToGenerator/</<",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "Promise",
+ location: { source: promiseSource },
+ },
+ {
+ displayName: "_asyncToGenerator/<",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "asyncAppFunction",
+ location: { source: appSource },
+ },
+ ];
+
+ const postAwaitGroup = [
+ {
+ displayName: "asyncAppFunction",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "tryCatch",
+ location: { source: regeneratorSource },
+ },
+ {
+ displayName: "invoke",
+ location: { source: regeneratorSource },
+ },
+ {
+ displayName: "defineIteratorMethods/</prototype[method]",
+ location: { source: regeneratorSource },
+ },
+ {
+ displayName: "step",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "step/<",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "run",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "notify/<",
+ location: { source: bundleSource },
+ },
+ {
+ displayName: "flush",
+ location: { source: microtaskSource },
+ },
+ ];
+
+ const state = {
+ frames: [...preAwaitGroup, ...postAwaitGroup],
+ selectedSource: {
+ id: "sourceId-originalSource",
+ isOriginal: true,
+ },
+ };
+
+ const frames = getCallStackFrames.resultFunc(
+ state.frames,
+ state.selectedSource,
+ {}
+ );
+
+ // frames from 1-8 and 10-17 are babel frames.
+ const babelFrames = [...frames.slice(1, 7), ...frames.slice(10, 7)];
+ const otherFrames = frames.filter(frame => !babelFrames.includes(frame));
+
+ expect(babelFrames).toEqual(
+ Array(babelFrames.length).fill(
+ expect.objectContaining({ library: "Babel" })
+ )
+ );
+ expect(otherFrames).not.toEqual(
+ Array(babelFrames.length).fill(
+ expect.objectContaining({ library: "Babel" })
+ )
+ );
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js b/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js
new file mode 100644
index 0000000000..276851b1ef
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js
@@ -0,0 +1,145 @@
+/* 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 { actions, createStore, makeSource } from "../../utils/test-head";
+import { createLocation } from "../../utils/location";
+
+import {
+ getColumnBreakpoints,
+ getFirstBreakpointPosition,
+} from "../visibleColumnBreakpoints";
+import {
+ makeMockSource,
+ makeMockSourceWithContent,
+ makeMockBreakpoint,
+} from "../../utils/test-mockup";
+
+function pp(line, column) {
+ return {
+ location: { sourceId: "foo", line, column },
+ generatedLocation: { sourceId: "foo", line, column },
+ };
+}
+
+function defaultSource() {
+ return makeMockSource(undefined, "foo");
+}
+
+function bp(line, column) {
+ return makeMockBreakpoint(defaultSource(), line, column);
+}
+
+const source = makeMockSourceWithContent(
+ undefined,
+ "foo.js",
+ undefined,
+ `function foo() {
+ console.log("hello");
+}
+console.log('bye');
+`
+);
+
+describe("visible column breakpoints", () => {
+ it("simple", () => {
+ const viewport = {
+ start: { line: 1, column: 0 },
+ end: { line: 10, column: 10 },
+ };
+ const pausePoints = [pp(1, 1), pp(1, 5), pp(3, 1)];
+ const breakpoints = [bp(1, 1), bp(4, 0), bp(4, 3)];
+
+ const columnBps = getColumnBreakpoints(
+ pausePoints,
+ breakpoints,
+ viewport,
+ source,
+ source.content
+ );
+ expect(columnBps).toMatchSnapshot();
+ });
+
+ it("ignores single breakpoints", () => {
+ const viewport = {
+ start: { line: 1, column: 0 },
+ end: { line: 10, column: 10 },
+ };
+ const pausePoints = [pp(1, 1), pp(1, 3), pp(2, 1)];
+ const breakpoints = [bp(1, 1)];
+ const columnBps = getColumnBreakpoints(
+ pausePoints,
+ breakpoints,
+ viewport,
+ source,
+ source.content
+ );
+ expect(columnBps).toMatchSnapshot();
+ });
+
+ it("only shows visible breakpoints", () => {
+ const viewport = {
+ start: { line: 1, column: 0 },
+ end: { line: 10, column: 10 },
+ };
+ const pausePoints = [pp(1, 1), pp(1, 3), pp(20, 1)];
+ const breakpoints = [bp(1, 1)];
+
+ const columnBps = getColumnBreakpoints(
+ pausePoints,
+ breakpoints,
+ viewport,
+ source,
+ source.content
+ );
+ expect(columnBps).toMatchSnapshot();
+ });
+
+ it("doesnt show breakpoints to the right", () => {
+ const viewport = {
+ start: { line: 1, column: 0 },
+ end: { line: 10, column: 10 },
+ };
+ const pausePoints = [pp(1, 1), pp(1, 15), pp(20, 1)];
+ const breakpoints = [bp(1, 1), bp(1, 15)];
+
+ const columnBps = getColumnBreakpoints(
+ pausePoints,
+ breakpoints,
+ viewport,
+ source,
+ source.content
+ );
+ expect(columnBps).toMatchSnapshot();
+ });
+});
+
+describe("getFirstBreakpointPosition", () => {
+ it("sorts the positions by column", async () => {
+ const store = createStore();
+ const { dispatch, getState } = store;
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+
+ dispatch({
+ type: "ADD_BREAKPOINT_POSITIONS",
+ positions: [pp(1, 5), pp(1, 3)],
+ source: fooSource,
+ });
+
+ const position = getFirstBreakpointPosition(
+ getState(),
+ createLocation({
+ line: 1,
+ source: fooSource,
+ })
+ );
+
+ if (!position) {
+ throw new Error("There should be a position");
+ }
+ expect(position.location.column).toEqual(3);
+ });
+});
diff --git a/devtools/client/debugger/src/selectors/threads.js b/devtools/client/debugger/src/selectors/threads.js
new file mode 100644
index 0000000000..8e3054ce7a
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/threads.js
@@ -0,0 +1,56 @@
+/* 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 { createSelector } from "reselect";
+import { parse } from "../utils/url";
+
+export const getThreads = createSelector(
+ state => state.threads.threads,
+ threads => threads.filter(thread => !isMainThread(thread))
+);
+
+export const getAllThreads = createSelector(
+ getMainThread,
+ getThreads,
+ (mainThread, threads) => {
+ const orderedThreads = Array.from(threads).sort((threadA, threadB) => {
+ if (threadA.name === threadB.name) {
+ return 0;
+ }
+ return threadA.name < threadB.name ? -1 : 1;
+ });
+ return [mainThread, ...orderedThreads].filter(Boolean);
+ }
+);
+
+function isMainThread(thread) {
+ return thread.isTopLevel;
+}
+
+export function getMainThread(state) {
+ return state.threads.threads.find(isMainThread);
+}
+
+/*
+ * Gets domain from the main thread url (without www prefix)
+ */
+export function getMainThreadHost(state) {
+ const url = getMainThread(state)?.url;
+ if (!url) {
+ return null;
+ }
+ const { host } = parse(url);
+ if (!host) {
+ return null;
+ }
+ return host.startsWith("www.") ? host.substring("www.".length) : host;
+}
+
+export function getThread(state, threadActor) {
+ return getAllThreads(state).find(thread => thread.actor === threadActor);
+}
+
+export function getIsThreadCurrentlyTracing(state, thread) {
+ return state.threads.mutableTracingThreads.has(thread);
+}
diff --git a/devtools/client/debugger/src/selectors/ui.js b/devtools/client/debugger/src/selectors/ui.js
new file mode 100644
index 0000000000..635a41d985
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/ui.js
@@ -0,0 +1,85 @@
+/* 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 { getSelectedSource } from "./sources";
+
+export function getSelectedPrimaryPaneTab(state) {
+ return state.ui.selectedPrimaryPaneTab;
+}
+
+export function getActiveSearch(state) {
+ return state.ui.activeSearch;
+}
+
+export function getFrameworkGroupingState(state) {
+ return state.ui.frameworkGroupingOn;
+}
+
+export function getPaneCollapse(state, position) {
+ if (position == "start") {
+ return state.ui.startPanelCollapsed;
+ }
+
+ return state.ui.endPanelCollapsed;
+}
+
+export function getHighlightedLineRangeForSelectedSource(state) {
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ return null;
+ }
+ // Only return the highlighted line range if it matches the selected source
+ const highlightedLineRange = state.ui.highlightedLineRange;
+ if (
+ highlightedLineRange &&
+ selectedSource.id == highlightedLineRange.sourceId
+ ) {
+ return highlightedLineRange;
+ }
+ return null;
+}
+
+export function getConditionalPanelLocation(state) {
+ return state.ui.conditionalPanelLocation;
+}
+
+export function getLogPointStatus(state) {
+ return state.ui.isLogPoint;
+}
+
+export function getOrientation(state) {
+ return state.ui.orientation;
+}
+
+export function getViewport(state) {
+ return state.ui.viewport;
+}
+
+export function getCursorPosition(state) {
+ return state.ui.cursorPosition;
+}
+
+export function getInlinePreview(state) {
+ return state.ui.inlinePreviewEnabled;
+}
+
+export function getEditorWrapping(state) {
+ return state.ui.editorWrappingEnabled;
+}
+
+export function getJavascriptTracingLogMethod(state) {
+ return state.ui.javascriptTracingLogMethod;
+}
+
+export function getSearchOptions(state, searchKey) {
+ return state.ui.mutableSearchOptions[searchKey];
+}
+
+export function getHideIgnoredSources(state) {
+ return state.ui.hideIgnoredSources;
+}
+
+export function isSourceMapIgnoreListEnabled(state) {
+ return state.ui.sourceMapIgnoreListEnabled;
+}
diff --git a/devtools/client/debugger/src/selectors/visibleBreakpoints.js b/devtools/client/debugger/src/selectors/visibleBreakpoints.js
new file mode 100644
index 0000000000..2e4a8f7212
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/visibleBreakpoints.js
@@ -0,0 +1,55 @@
+/* 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 { createSelector } from "reselect";
+
+import { getBreakpointsList } from "./breakpoints";
+import { getSelectedSource } from "./sources";
+
+import { sortSelectedBreakpoints } from "../utils/breakpoint";
+import { getSelectedLocation } from "../utils/selected-location";
+
+/*
+ * Finds the breakpoints, which appear in the selected source.
+ */
+export const getVisibleBreakpoints = createSelector(
+ getSelectedSource,
+ getBreakpointsList,
+ (selectedSource, breakpoints) => {
+ if (!selectedSource) {
+ return null;
+ }
+
+ return breakpoints.filter(
+ bp =>
+ selectedSource &&
+ getSelectedLocation(bp, selectedSource).sourceId === selectedSource.id
+ );
+ }
+);
+
+/*
+ * Finds the first breakpoint per line, which appear in the selected source.
+ */
+export const getFirstVisibleBreakpoints = createSelector(
+ getVisibleBreakpoints,
+ getSelectedSource,
+ (breakpoints, selectedSource) => {
+ if (!breakpoints || !selectedSource) {
+ return [];
+ }
+
+ // Filter the array so it only return the first breakpoint when there's multiple
+ // breakpoints on the same line.
+ const handledLines = new Set();
+ return sortSelectedBreakpoints(breakpoints, selectedSource).filter(bp => {
+ const line = getSelectedLocation(bp, selectedSource).line;
+ if (handledLines.has(line)) {
+ return false;
+ }
+ handledLines.add(line);
+ return true;
+ });
+ }
+);
diff --git a/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
new file mode 100644
index 0000000000..5ed391c7e4
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
@@ -0,0 +1,185 @@
+/* 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 { createSelector } from "reselect";
+
+import {
+ getViewport,
+ getSelectedSource,
+ getSelectedSourceTextContent,
+ getBreakpointPositionsForSource,
+} from "./index";
+import { getVisibleBreakpoints } from "./visibleBreakpoints";
+import { getSelectedLocation } from "../utils/selected-location";
+import { sortSelectedLocations } from "../utils/location";
+import { getLineText } from "../utils/source";
+
+function contains(location, range) {
+ return (
+ location.line >= range.start.line &&
+ location.line <= range.end.line &&
+ (!location.column ||
+ (location.column >= range.start.column &&
+ location.column <= range.end.column))
+ );
+}
+
+function groupBreakpoints(breakpoints, selectedSource) {
+ const breakpointsMap = {};
+ if (!breakpoints) {
+ return breakpointsMap;
+ }
+
+ for (const breakpoint of breakpoints) {
+ if (breakpoint.options.hidden) {
+ continue;
+ }
+ const location = getSelectedLocation(breakpoint, selectedSource);
+ const { line, column } = location;
+
+ if (!breakpointsMap[line]) {
+ breakpointsMap[line] = {};
+ }
+
+ if (!breakpointsMap[line][column]) {
+ breakpointsMap[line][column] = [];
+ }
+
+ breakpointsMap[line][column].push(breakpoint);
+ }
+
+ return breakpointsMap;
+}
+
+function findBreakpoint(location, breakpointMap) {
+ const { line, column } = location;
+ const breakpoints = breakpointMap[line]?.[column];
+
+ if (!breakpoints) {
+ return null;
+ }
+ return breakpoints[0];
+}
+
+function filterByLineCount(positions, selectedSource) {
+ const lineCount = {};
+
+ for (const breakpoint of positions) {
+ const { line } = getSelectedLocation(breakpoint, selectedSource);
+ if (!lineCount[line]) {
+ lineCount[line] = 0;
+ }
+ lineCount[line] = lineCount[line] + 1;
+ }
+
+ return positions.filter(
+ breakpoint =>
+ lineCount[getSelectedLocation(breakpoint, selectedSource).line] > 1
+ );
+}
+
+function filterVisible(positions, selectedSource, viewport) {
+ return positions.filter(columnBreakpoint => {
+ const location = getSelectedLocation(columnBreakpoint, selectedSource);
+ return viewport && contains(location, viewport);
+ });
+}
+
+function filterByBreakpoints(positions, selectedSource, breakpointMap) {
+ return positions.filter(position => {
+ const location = getSelectedLocation(position, selectedSource);
+ return breakpointMap[location.line];
+ });
+}
+
+// Filters out breakpoints to the right of the line. (bug 1552039)
+function filterInLine(positions, selectedSource, selectedContent) {
+ return positions.filter(position => {
+ const location = getSelectedLocation(position, selectedSource);
+ const lineText = getLineText(
+ selectedSource.id,
+ selectedContent,
+ location.line
+ );
+
+ return lineText.length >= (location.column || 0);
+ });
+}
+
+function formatPositions(positions, selectedSource, breakpointMap) {
+ return positions.map(position => {
+ const location = getSelectedLocation(position, selectedSource);
+ return {
+ location,
+ breakpoint: findBreakpoint(location, breakpointMap),
+ };
+ });
+}
+
+function convertToList(breakpointPositions) {
+ return [].concat(...Object.values(breakpointPositions));
+}
+
+export function getColumnBreakpoints(
+ positions,
+ breakpoints,
+ viewport,
+ selectedSource,
+ selectedSourceTextContent
+) {
+ if (!positions || !selectedSource) {
+ return [];
+ }
+
+ // We only want to show a column breakpoint if several conditions are matched
+ // - it is the first breakpoint to appear at an the original location
+ // - the position is in the current viewport
+ // - there is atleast one other breakpoint on that line
+ // - there is a breakpoint on that line
+ const breakpointMap = groupBreakpoints(breakpoints, selectedSource);
+ positions = filterByLineCount(positions, selectedSource);
+ positions = filterVisible(positions, selectedSource, viewport);
+ positions = filterInLine(
+ positions,
+ selectedSource,
+ selectedSourceTextContent
+ );
+ positions = filterByBreakpoints(positions, selectedSource, breakpointMap);
+
+ return formatPositions(positions, selectedSource, breakpointMap);
+}
+
+const getVisibleBreakpointPositions = createSelector(
+ state => {
+ const source = getSelectedSource(state);
+ if (!source) {
+ return null;
+ }
+ return getBreakpointPositionsForSource(state, source.id);
+ },
+ sourcePositions => {
+ return convertToList(sourcePositions || []);
+ }
+);
+
+export const visibleColumnBreakpoints = createSelector(
+ getVisibleBreakpointPositions,
+ getVisibleBreakpoints,
+ getViewport,
+ getSelectedSource,
+ getSelectedSourceTextContent,
+ getColumnBreakpoints
+);
+
+export function getFirstBreakpointPosition(state, location) {
+ const positions = getBreakpointPositionsForSource(state, location.sourceId);
+ if (!positions) {
+ return null;
+ }
+
+ return sortSelectedLocations(convertToList(positions), location.source).find(
+ position =>
+ getSelectedLocation(position, location.source).line == location.line
+ );
+}