summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/reducers
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/reducers')
-rw-r--r--devtools/client/debugger/src/reducers/ast.js136
-rw-r--r--devtools/client/debugger/src/reducers/async-requests.js33
-rw-r--r--devtools/client/debugger/src/reducers/breakpoints.js246
-rw-r--r--devtools/client/debugger/src/reducers/event-listeners.js81
-rw-r--r--devtools/client/debugger/src/reducers/exceptions.js124
-rw-r--r--devtools/client/debugger/src/reducers/expressions.js187
-rw-r--r--devtools/client/debugger/src/reducers/file-search.js117
-rw-r--r--devtools/client/debugger/src/reducers/index.js86
-rw-r--r--devtools/client/debugger/src/reducers/moz.build28
-rw-r--r--devtools/client/debugger/src/reducers/pause.js711
-rw-r--r--devtools/client/debugger/src/reducers/pending-breakpoints.js102
-rw-r--r--devtools/client/debugger/src/reducers/preview.js69
-rw-r--r--devtools/client/debugger/src/reducers/project-text-search.js112
-rw-r--r--devtools/client/debugger/src/reducers/quick-open.js71
-rw-r--r--devtools/client/debugger/src/reducers/source-actors.js296
-rw-r--r--devtools/client/debugger/src/reducers/source-tree.js58
-rw-r--r--devtools/client/debugger/src/reducers/sources.js1153
-rw-r--r--devtools/client/debugger/src/reducers/tabs.js308
-rw-r--r--devtools/client/debugger/src/reducers/tests/breakpoints.spec.js81
-rw-r--r--devtools/client/debugger/src/reducers/tests/quick-open.spec.js64
-rw-r--r--devtools/client/debugger/src/reducers/tests/sources.spec.js127
-rw-r--r--devtools/client/debugger/src/reducers/tests/ui.spec.js35
-rw-r--r--devtools/client/debugger/src/reducers/threads.js115
-rw-r--r--devtools/client/debugger/src/reducers/types.js71
-rw-r--r--devtools/client/debugger/src/reducers/ui.js234
25 files changed, 4645 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/reducers/ast.js b/devtools/client/debugger/src/reducers/ast.js
new file mode 100644
index 0000000000..2ab6871e5f
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/ast.js
@@ -0,0 +1,136 @@
+/* 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
+
+/**
+ * Ast reducer
+ * @module reducers/ast
+ */
+
+import { makeBreakpointId } from "../utils/breakpoint";
+
+import type { SymbolDeclarations } from "../workers/parser";
+
+import type { Source, SourceLocation } from "../types";
+import type { Action, DonePromiseAction } from "../actions/types";
+
+type EmptyLinesType = number[];
+
+export type LoadedSymbols = SymbolDeclarations;
+export type Symbols = LoadedSymbols | {| loading: true |};
+
+export type EmptyLinesMap = { [k: string]: EmptyLinesType };
+export type SymbolsMap = { [k: string]: Symbols };
+
+export type SourceMetaDataType = {
+ framework: ?string,
+};
+
+export type SourceMetaDataMap = { [k: string]: SourceMetaDataType };
+
+export type ASTState = {
+ +symbols: SymbolsMap,
+ +inScopeLines: { [string]: Array<number> },
+};
+
+export function initialASTState(): ASTState {
+ return {
+ symbols: {},
+ inScopeLines: {},
+ };
+}
+
+function update(state: ASTState = initialASTState(), action: Action): ASTState {
+ switch (action.type) {
+ case "SET_SYMBOLS": {
+ const { sourceId } = action;
+ if (action.status === "start") {
+ return {
+ ...state,
+ symbols: { ...state.symbols, [sourceId]: { loading: true } },
+ };
+ }
+
+ const value = ((action: any): DonePromiseAction).value;
+ return {
+ ...state,
+ symbols: { ...state.symbols, [sourceId]: value },
+ };
+ }
+
+ case "IN_SCOPE_LINES": {
+ return {
+ ...state,
+ inScopeLines: {
+ ...state.inScopeLines,
+ [makeBreakpointId(action.location)]: action.lines,
+ },
+ };
+ }
+
+ case "RESUME": {
+ return { ...state, inScopeLines: {} };
+ }
+
+ case "NAVIGATE": {
+ return initialASTState();
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+// NOTE: we'd like to have the app state fully typed
+// https://github.com/firefox-devtools/debugger/blob/master/src/reducers/sources.js#L179-L185
+type OuterState = { ast: ASTState };
+
+export function getSymbols(state: OuterState, source: ?Source): ?Symbols {
+ if (!source) {
+ return null;
+ }
+
+ return state.ast.symbols[source.id] || null;
+}
+
+export function hasSymbols(state: OuterState, source: Source): boolean {
+ const symbols = getSymbols(state, source);
+
+ if (!symbols) {
+ return false;
+ }
+
+ return !symbols.loading;
+}
+
+export function getFramework(state: OuterState, source: Source): ?string {
+ const symbols = getSymbols(state, source);
+ if (symbols && !symbols.loading) {
+ return symbols.framework;
+ }
+}
+
+export function isSymbolsLoading(state: OuterState, source: ?Source): boolean {
+ const symbols = getSymbols(state, source);
+ if (!symbols) {
+ return false;
+ }
+
+ return symbols.loading;
+}
+
+export function getInScopeLines(state: OuterState, location: SourceLocation) {
+ return state.ast.inScopeLines[makeBreakpointId(location)];
+}
+
+export function hasInScopeLines(
+ state: OuterState,
+ location: SourceLocation
+): boolean {
+ return !!getInScopeLines(state, location);
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/async-requests.js b/devtools/client/debugger/src/reducers/async-requests.js
new file mode 100644
index 0000000000..e3675f2cdb
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/async-requests.js
@@ -0,0 +1,33 @@
+/* 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
+
+/**
+ * Async request reducer
+ * @module reducers/async-request
+ */
+
+const initialAsyncRequestState = [];
+
+function update(state: string[] = initialAsyncRequestState, action: any) {
+ const { seqId } = action;
+
+ if (action.type === "NAVIGATE") {
+ return initialAsyncRequestState;
+ } else if (seqId) {
+ let newState;
+ if (action.status === "start") {
+ newState = [...state, seqId];
+ } else if (action.status === "error" || action.status === "done") {
+ newState = (state.filter(id => id !== seqId): string[]);
+ }
+
+ return newState;
+ }
+
+ return state;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/breakpoints.js b/devtools/client/debugger/src/reducers/breakpoints.js
new file mode 100644
index 0000000000..a1d74e83e5
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/breakpoints.js
@@ -0,0 +1,246 @@
+/* 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
+
+/**
+ * Breakpoints reducer
+ * @module reducers/breakpoints
+ */
+
+import { isGeneratedId } from "devtools-source-map";
+import { isEqual } from "lodash";
+
+import { makeBreakpointId } from "../utils/breakpoint";
+
+// eslint-disable-next-line max-len
+import { getBreakpointsList as getBreakpointsListSelector } from "../selectors/breakpoints";
+
+import type {
+ XHRBreakpoint,
+ Breakpoint,
+ BreakpointId,
+ SourceId,
+ SourceLocation,
+} from "../types";
+import type { Action } from "../actions/types";
+
+export type BreakpointsMap = { [BreakpointId]: Breakpoint };
+export type XHRBreakpointsList = $ReadOnlyArray<XHRBreakpoint>;
+
+export type BreakpointsState = {
+ breakpoints: BreakpointsMap,
+ xhrBreakpoints: XHRBreakpointsList,
+ breakpointsDisabled: boolean,
+};
+
+export function initialBreakpointsState(
+ xhrBreakpoints?: XHRBreakpointsList = []
+): BreakpointsState {
+ return {
+ breakpoints: {},
+ xhrBreakpoints,
+ breakpointsDisabled: false,
+ };
+}
+
+function update(
+ state: BreakpointsState = initialBreakpointsState(),
+ action: Action
+): BreakpointsState {
+ switch (action.type) {
+ case "SET_BREAKPOINT": {
+ if (action.status === "start") {
+ return setBreakpoint(state, action);
+ }
+ return state;
+ }
+
+ case "REMOVE_BREAKPOINT": {
+ if (action.status === "start") {
+ return removeBreakpoint(state, action);
+ }
+ return state;
+ }
+
+ case "REMOVE_BREAKPOINTS": {
+ return { ...state, breakpoints: {} };
+ }
+
+ case "NAVIGATE": {
+ return initialBreakpointsState(state.xhrBreakpoints);
+ }
+
+ case "SET_XHR_BREAKPOINT": {
+ return addXHRBreakpoint(state, action);
+ }
+
+ case "REMOVE_XHR_BREAKPOINT": {
+ return removeXHRBreakpoint(state, action);
+ }
+
+ case "UPDATE_XHR_BREAKPOINT": {
+ return updateXHRBreakpoint(state, action);
+ }
+
+ case "ENABLE_XHR_BREAKPOINT": {
+ return updateXHRBreakpoint(state, action);
+ }
+
+ case "DISABLE_XHR_BREAKPOINT": {
+ return updateXHRBreakpoint(state, action);
+ }
+ }
+
+ return state;
+}
+
+function addXHRBreakpoint(state, action) {
+ const { xhrBreakpoints } = state;
+ const { breakpoint } = action;
+ const { path, method } = breakpoint;
+
+ const existingBreakpointIndex = state.xhrBreakpoints.findIndex(
+ bp => bp.path === path && bp.method === method
+ );
+
+ if (existingBreakpointIndex === -1) {
+ return {
+ ...state,
+ xhrBreakpoints: [...xhrBreakpoints, breakpoint],
+ };
+ } else if (xhrBreakpoints[existingBreakpointIndex] !== breakpoint) {
+ const newXhrBreakpoints = [...xhrBreakpoints];
+ newXhrBreakpoints[existingBreakpointIndex] = breakpoint;
+ return {
+ ...state,
+ xhrBreakpoints: newXhrBreakpoints,
+ };
+ }
+
+ return state;
+}
+
+function removeXHRBreakpoint(state, action) {
+ const { breakpoint } = action;
+ const { xhrBreakpoints } = state;
+
+ if (action.status === "start") {
+ return state;
+ }
+
+ return {
+ ...state,
+ xhrBreakpoints: xhrBreakpoints.filter(bp => !isEqual(bp, breakpoint)),
+ };
+}
+
+function updateXHRBreakpoint(state, action) {
+ const { breakpoint, index } = action;
+ const { xhrBreakpoints } = state;
+ const newXhrBreakpoints = [...xhrBreakpoints];
+ newXhrBreakpoints[index] = breakpoint;
+ return {
+ ...state,
+ xhrBreakpoints: newXhrBreakpoints,
+ };
+}
+
+function setBreakpoint(state, { breakpoint }): BreakpointsState {
+ const id = makeBreakpointId(breakpoint.location);
+ const breakpoints = { ...state.breakpoints, [id]: breakpoint };
+ return { ...state, breakpoints };
+}
+
+function removeBreakpoint(state, { location }): BreakpointsState {
+ const id = makeBreakpointId(location);
+ const breakpoints = { ...state.breakpoints };
+ delete breakpoints[id];
+ return { ...state, breakpoints };
+}
+
+function isMatchingLocation(location1, location2) {
+ return isEqual(location1, location2);
+}
+
+// Selectors
+// TODO: these functions should be moved out of the reducer
+
+type OuterState = { breakpoints: BreakpointsState };
+
+export function getBreakpointsMap(state: OuterState): BreakpointsMap {
+ return state.breakpoints.breakpoints;
+}
+
+export function getBreakpointsList(state: OuterState): Breakpoint[] {
+ return getBreakpointsListSelector((state: any));
+}
+
+export function getBreakpointCount(state: OuterState): number {
+ return getBreakpointsList(state).length;
+}
+
+export function getBreakpoint(
+ state: OuterState,
+ location: ?SourceLocation
+): ?Breakpoint {
+ if (!location) {
+ return undefined;
+ }
+
+ const breakpoints = getBreakpointsMap(state);
+ return breakpoints[makeBreakpointId(location)];
+}
+
+export function getBreakpointsDisabled(state: OuterState): boolean {
+ const breakpoints = getBreakpointsList(state);
+ return breakpoints.every(breakpoint => breakpoint.disabled);
+}
+
+export function getBreakpointsForSource(
+ state: OuterState,
+ sourceId: SourceId,
+ line: ?number
+): Breakpoint[] {
+ if (!sourceId) {
+ return [];
+ }
+
+ const isGeneratedSource = isGeneratedId(sourceId);
+ const breakpoints = getBreakpointsList(state);
+ return breakpoints.filter(bp => {
+ const location = isGeneratedSource ? bp.generatedLocation : bp.location;
+ return location.sourceId === sourceId && (!line || line == location.line);
+ });
+}
+
+export function getBreakpointForLocation(
+ state: OuterState,
+ location: ?SourceLocation
+): ?Breakpoint {
+ if (!location) {
+ return undefined;
+ }
+
+ const isGeneratedSource = isGeneratedId(location.sourceId);
+ return getBreakpointsList(state).find(bp => {
+ const loc = isGeneratedSource ? bp.generatedLocation : bp.location;
+ return isMatchingLocation(loc, location);
+ });
+}
+
+export function getHiddenBreakpoint(state: OuterState): ?Breakpoint {
+ const breakpoints = getBreakpointsList(state);
+ return breakpoints.find(bp => bp.options.hidden);
+}
+
+export function hasLogpoint(
+ state: OuterState,
+ location: ?SourceLocation
+): ?string {
+ const breakpoint = getBreakpoint(state, location);
+ return breakpoint?.options.logValue;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/event-listeners.js b/devtools/client/debugger/src/reducers/event-listeners.js
new file mode 100644
index 0000000000..1b8140e497
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/event-listeners.js
@@ -0,0 +1,81 @@
+/* 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
+
+import { prefs } from "../utils/prefs";
+
+import type { State } from "./types";
+import type {
+ EventListenerAction,
+ EventListenerActiveList,
+ EventListenerCategoryList,
+ EventListenerExpandedList,
+} from "../actions/types";
+
+export type EventListenersState = {|
+ // XXX: The `active` property is expected by the thread-utils module at
+ // devtools/client/shared/thread-utils.js . If the name is updated here,
+ // thread-utils.js should be updated at the same time.
+ +active: EventListenerActiveList,
+ +categories: EventListenerCategoryList,
+ +expanded: EventListenerExpandedList,
+ +logEventBreakpoints: boolean,
+|};
+
+export function initialEventListenerState(): EventListenersState {
+ return {
+ active: [],
+ categories: [],
+ expanded: [],
+ logEventBreakpoints: prefs.logEventBreakpoints,
+ };
+}
+
+function update(
+ state: EventListenersState = initialEventListenerState(),
+ action: EventListenerAction
+) {
+ switch (action.type) {
+ case "UPDATE_EVENT_LISTENERS":
+ return { ...state, active: action.active };
+
+ case "RECEIVE_EVENT_LISTENER_TYPES":
+ return { ...state, categories: action.categories };
+
+ case "UPDATE_EVENT_LISTENER_EXPANDED":
+ return { ...state, expanded: action.expanded };
+
+ case "TOGGLE_EVENT_LISTENERS": {
+ const { logEventBreakpoints } = action;
+ prefs.logEventBreakpoints = logEventBreakpoints;
+ return { ...state, logEventBreakpoints };
+ }
+
+ default:
+ return state;
+ }
+}
+
+export function getActiveEventListeners(state: State): EventListenerActiveList {
+ return state.eventListenerBreakpoints.active;
+}
+
+export function getEventListenerBreakpointTypes(
+ state: State
+): EventListenerCategoryList {
+ return state.eventListenerBreakpoints.categories;
+}
+
+export function getEventListenerExpanded(
+ state: State
+): EventListenerExpandedList {
+ return state.eventListenerBreakpoints.expanded;
+}
+
+export function shouldLogEventBreakpoints(state: State) {
+ return state.eventListenerBreakpoints.logEventBreakpoints;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/exceptions.js b/devtools/client/debugger/src/reducers/exceptions.js
new file mode 100644
index 0000000000..d60e34343b
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/exceptions.js
@@ -0,0 +1,124 @@
+/* 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
+
+/**
+ * Exceptions reducer
+ * @module reducers/exceptionss
+ */
+
+import { createSelector } from "reselect";
+import { getSelectedSource, getSourceActorsForSource } from "../selectors";
+
+import type { Exception, SourceActorId, SourceActor } from "../types";
+
+import type { Action } from "../actions/types";
+import type { Selector, State } from "./types";
+
+export type ExceptionMap = { [SourceActorId]: Exception[] };
+
+export type ExceptionState = {
+ exceptions: ExceptionMap,
+};
+
+export function initialExceptionsState() {
+ return {
+ exceptions: {},
+ };
+}
+
+function update(
+ state: ExceptionState = initialExceptionsState(),
+ action: Action
+): ExceptionState {
+ switch (action.type) {
+ case "ADD_EXCEPTION":
+ return updateExceptions(state, action);
+ }
+ return state;
+}
+
+function updateExceptions(state, action) {
+ const { exception } = action;
+ const sourceActorId = exception.sourceActorId;
+
+ if (state.exceptions[sourceActorId]) {
+ const sourceExceptions = state.exceptions[sourceActorId];
+ return {
+ ...state,
+ exceptions: {
+ ...state.exceptions,
+ [sourceActorId]: [...sourceExceptions, exception],
+ },
+ };
+ }
+ return {
+ ...state,
+ exceptions: {
+ ...state.exceptions,
+ [sourceActorId]: [exception],
+ },
+ };
+}
+
+// Selectors
+export function getExceptionsMap(state: State): ExceptionMap {
+ return state.exceptions.exceptions;
+}
+
+export const getSelectedSourceExceptions: Selector<
+ Exception[]
+> = createSelector(
+ getSelectedSourceActors,
+ getExceptionsMap,
+ (sourceActors: Array<SourceActor>, exceptions) => {
+ const sourceExceptions = [];
+
+ sourceActors.forEach(sourceActor => {
+ const actorId = sourceActor.id;
+
+ if (exceptions[actorId]) {
+ sourceExceptions.push(...exceptions[actorId]);
+ }
+ });
+
+ return sourceExceptions;
+ }
+);
+
+function getSelectedSourceActors(state) {
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ return [];
+ }
+ return getSourceActorsForSource(state, selectedSource.id);
+}
+
+export function hasException(
+ state: State,
+ line: number,
+ column: number
+): boolean {
+ return !!getSelectedException(state, line, column);
+}
+
+export function getSelectedException(
+ state: State,
+ line: number,
+ column: number
+): ?Exception {
+ const sourceExceptions = getSelectedSourceExceptions(state);
+
+ if (!sourceExceptions) {
+ return;
+ }
+
+ return sourceExceptions.find(
+ sourceExc =>
+ sourceExc.lineNumber === line && sourceExc.columnNumber === column
+ );
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/expressions.js b/devtools/client/debugger/src/reducers/expressions.js
new file mode 100644
index 0000000000..9e657a3d42
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/expressions.js
@@ -0,0 +1,187 @@
+/* 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
+
+/**
+ * Expressions reducer
+ * @module reducers/expressions
+ */
+
+import { omit, zip } from "lodash";
+
+import { createSelector } from "reselect";
+import { prefs } from "../utils/prefs";
+
+import type { Expression } from "../types";
+import type { Selector, State } from "../reducers/types";
+import type { Action } from "../actions/types";
+
+type AutocompleteMatches = { [string]: string[] };
+export type ExpressionState = {
+ expressions: Expression[],
+ expressionError: boolean,
+ autocompleteMatches: AutocompleteMatches,
+ currentAutocompleteInput: string | null,
+};
+
+export const initialExpressionState = () => ({
+ expressions: restoreExpressions(),
+ expressionError: false,
+ autocompleteMatches: {},
+ currentAutocompleteInput: null,
+});
+
+function update(
+ state: ExpressionState = initialExpressionState(),
+ action: Action
+): ExpressionState {
+ switch (action.type) {
+ case "ADD_EXPRESSION":
+ if (action.expressionError) {
+ return { ...state, expressionError: !!action.expressionError };
+ }
+ return appendExpressionToList(state, {
+ input: action.input,
+ value: null,
+ updating: true,
+ });
+
+ case "UPDATE_EXPRESSION":
+ const key = action.expression.input;
+ const newState = updateExpressionInList(state, key, {
+ input: action.input,
+ value: null,
+ updating: true,
+ });
+
+ return { ...newState, expressionError: !!action.expressionError };
+
+ case "EVALUATE_EXPRESSION":
+ return updateExpressionInList(state, action.input, {
+ input: action.input,
+ value: action.value,
+ updating: false,
+ });
+
+ case "EVALUATE_EXPRESSIONS":
+ const { inputs, results } = action;
+
+ return zip(inputs, results).reduce(
+ (_state, [input, result]) =>
+ updateExpressionInList(_state, input, {
+ input,
+ value: result,
+ updating: false,
+ }),
+ state
+ );
+
+ case "DELETE_EXPRESSION":
+ return deleteExpression(state, action.input);
+
+ case "CLEAR_EXPRESSION_ERROR":
+ return { ...state, expressionError: false };
+
+ case "AUTOCOMPLETE":
+ const { matchProp, matches } = action.result;
+
+ return {
+ ...state,
+ currentAutocompleteInput: matchProp,
+ autocompleteMatches: {
+ ...state.autocompleteMatches,
+ [matchProp]: matches,
+ },
+ };
+
+ case "CLEAR_AUTOCOMPLETE":
+ return {
+ ...state,
+ autocompleteMatches: {},
+ currentAutocompleteInput: "",
+ };
+ }
+
+ return state;
+}
+
+function restoreExpressions(): Expression[] {
+ const exprs = prefs.expressions;
+ if (exprs.length == 0) {
+ return [];
+ }
+
+ return exprs;
+}
+
+function storeExpressions({ expressions }): void {
+ prefs.expressions = expressions.map(expression => omit(expression, "value"));
+}
+
+function appendExpressionToList(
+ state: ExpressionState,
+ value: any
+): ExpressionState {
+ const newState = { ...state, expressions: [...state.expressions, value] };
+
+ storeExpressions(newState);
+ return newState;
+}
+
+function updateExpressionInList(
+ state: ExpressionState,
+ key: string,
+ value: any
+): ExpressionState {
+ const list = [...state.expressions];
+ const index = list.findIndex(e => e.input == key);
+ list[index] = value;
+
+ const newState = { ...state, expressions: list };
+ storeExpressions(newState);
+ return newState;
+}
+
+function deleteExpression(
+ state: ExpressionState,
+ input: string
+): ExpressionState {
+ const list = [...state.expressions];
+ const index = list.findIndex(e => e.input == input);
+ list.splice(index, 1);
+ const newState = { ...state, expressions: list };
+ storeExpressions(newState);
+ return newState;
+}
+
+const getExpressionsWrapper = state => state.expressions;
+
+export const getExpressions: Selector<Array<Expression>> = createSelector(
+ getExpressionsWrapper,
+ expressions => expressions.expressions
+);
+
+export const getAutocompleteMatches: Selector<AutocompleteMatches> = createSelector(
+ getExpressionsWrapper,
+ expressions => expressions.autocompleteMatches
+);
+
+export function getExpression(state: State, input: string): ?Expression {
+ return getExpressions(state).find(exp => exp.input == input);
+}
+
+export function getAutocompleteMatchset(state: State) {
+ const input = state.expressions.currentAutocompleteInput;
+ if (input) {
+ return getAutocompleteMatches(state)[input];
+ }
+}
+
+export const getExpressionError: Selector<boolean> = createSelector(
+ getExpressionsWrapper,
+ expressions => expressions.expressionError
+);
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/file-search.js b/devtools/client/debugger/src/reducers/file-search.js
new file mode 100644
index 0000000000..fbddf6d12b
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/file-search.js
@@ -0,0 +1,117 @@
+/* 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
+
+/**
+ * File Search reducer
+ * @module reducers/fileSearch
+ */
+
+import { prefs } from "../utils/prefs";
+
+import type { Action } from "../actions/types";
+
+export type Modifiers = {
+ caseSensitive: boolean,
+ wholeWord: boolean,
+ regexMatch: boolean,
+};
+
+export type MatchedLocations = {
+ line: number,
+ ch: number,
+};
+
+export type SearchResults = {
+ matches: Array<MatchedLocations>,
+ matchIndex: number,
+ index: number,
+ count: number,
+};
+
+export type FileSearchState = {
+ searchResults: SearchResults,
+ query: string,
+ modifiers: Modifiers,
+};
+
+const emptySearchResults = Object.freeze({
+ matches: Object.freeze([]),
+ matchIndex: -1,
+ index: -1,
+ count: 0,
+});
+
+export const initialFileSearchState = () => ({
+ query: "",
+ searchResults: emptySearchResults,
+ modifiers: {
+ caseSensitive: prefs.fileSearchCaseSensitive,
+ wholeWord: prefs.fileSearchWholeWord,
+ regexMatch: prefs.fileSearchRegexMatch,
+ },
+});
+
+function update(
+ state: FileSearchState = initialFileSearchState(),
+ action: Action
+): FileSearchState {
+ switch (action.type) {
+ case "UPDATE_FILE_SEARCH_QUERY": {
+ return { ...state, query: action.query };
+ }
+
+ case "UPDATE_SEARCH_RESULTS": {
+ return { ...state, searchResults: action.results };
+ }
+
+ case "TOGGLE_FILE_SEARCH_MODIFIER": {
+ const actionVal = !state.modifiers[action.modifier];
+
+ if (action.modifier == "caseSensitive") {
+ prefs.fileSearchCaseSensitive = actionVal;
+ }
+
+ if (action.modifier == "wholeWord") {
+ prefs.fileSearchWholeWord = actionVal;
+ }
+
+ if (action.modifier == "regexMatch") {
+ prefs.fileSearchRegexMatch = actionVal;
+ }
+
+ return {
+ ...state,
+ modifiers: { ...state.modifiers, [action.modifier]: actionVal },
+ };
+ }
+
+ case "NAVIGATE": {
+ return { ...state, query: "", searchResults: emptySearchResults };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+// NOTE: we'd like to have the app state fully typed
+// https://github.com/firefox-devtools/debugger/blob/master/src/reducers/sources.js#L179-L185
+type OuterState = { fileSearch: FileSearchState };
+
+export function getFileSearchQuery(state: OuterState): string {
+ return state.fileSearch.query;
+}
+
+export function getFileSearchModifiers(state: OuterState): Modifiers {
+ return state.fileSearch.modifiers;
+}
+
+export function getFileSearchResults(state: OuterState): SearchResults {
+ return state.fileSearch.searchResults;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/index.js b/devtools/client/debugger/src/reducers/index.js
new file mode 100644
index 0000000000..6000f81053
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/index.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/>. */
+
+// @flow
+
+/**
+ * Reducer index
+ * @module reducers/index
+ */
+
+import expressions, { initialExpressionState } from "./expressions";
+import sourceActors from "./source-actors";
+import sources, { initialSourcesState } from "./sources";
+import tabs, { initialTabState } from "./tabs";
+import breakpoints, { initialBreakpointsState } from "./breakpoints";
+import pendingBreakpoints from "./pending-breakpoints";
+import asyncRequests from "./async-requests";
+import pause, { initialPauseState } from "./pause";
+import ui, { initialUIState } from "./ui";
+import fileSearch, { initialFileSearchState } from "./file-search";
+import ast, { initialASTState } from "./ast";
+import preview, { initialPreviewState } from "./preview";
+import projectTextSearch, {
+ initialProjectTextSearchState,
+} from "./project-text-search";
+import quickOpen, { initialQuickOpenState } from "./quick-open";
+import sourceTree, { initialSourcesTreeState } from "./source-tree";
+import threads, { initialThreadsState } from "./threads";
+import eventListenerBreakpoints, {
+ initialEventListenerState,
+} from "./event-listeners";
+import exceptions, { initialExceptionsState } from "./exceptions";
+
+import type { SourceActorsState } from "./source-actors";
+
+// $FlowIgnore
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+import { createInitial } from "../utils/resource";
+
+export function initialState() {
+ return {
+ sources: initialSourcesState(),
+ expressions: initialExpressionState(),
+ sourceActors: (createInitial(): SourceActorsState),
+ tabs: initialTabState(),
+ breakpoints: initialBreakpointsState(),
+ pendingBreakpoints: {},
+ asyncRequests: [],
+ pause: initialPauseState(),
+ ui: initialUIState(),
+ fileSearch: initialFileSearchState(),
+ ast: initialASTState(),
+ projectTextSearch: initialProjectTextSearchState(),
+ quickOpen: initialQuickOpenState(),
+ sourceTree: initialSourcesTreeState(),
+ threads: initialThreadsState(),
+ objectInspector: objectInspector.reducer.initialOIState(),
+ eventListenerBreakpoints: initialEventListenerState(),
+ preview: initialPreviewState(),
+ exceptions: initialExceptionsState(),
+ };
+}
+
+export default {
+ expressions,
+ sourceActors,
+ sources,
+ tabs,
+ breakpoints,
+ pendingBreakpoints,
+ asyncRequests,
+ pause,
+ ui,
+ fileSearch,
+ ast,
+ projectTextSearch,
+ quickOpen,
+ sourceTree,
+ threads,
+ objectInspector: objectInspector.reducer.default,
+ eventListenerBreakpoints,
+ preview,
+ exceptions,
+};
diff --git a/devtools/client/debugger/src/reducers/moz.build b/devtools/client/debugger/src/reducers/moz.build
new file mode 100644
index 0000000000..bada00cbfc
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/moz.build
@@ -0,0 +1,28 @@
+# 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",
+ "async-requests.js",
+ "breakpoints.js",
+ "event-listeners.js",
+ "exceptions.js",
+ "expressions.js",
+ "file-search.js",
+ "index.js",
+ "pause.js",
+ "pending-breakpoints.js",
+ "preview.js",
+ "project-text-search.js",
+ "quick-open.js",
+ "source-actors.js",
+ "source-tree.js",
+ "sources.js",
+ "tabs.js",
+ "threads.js",
+ "ui.js",
+)
diff --git a/devtools/client/debugger/src/reducers/pause.js b/devtools/client/debugger/src/reducers/pause.js
new file mode 100644
index 0000000000..fd2954025f
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/pause.js
@@ -0,0 +1,711 @@
+/* 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
+/* eslint complexity: ["error", 35]*/
+
+/**
+ * Pause reducer
+ * @module reducers/pause
+ */
+
+import { isGeneratedId } from "devtools-source-map";
+import { prefs } from "../utils/prefs";
+import { getSelectedSourceId } from "./sources";
+import { getSelectedFrame } from "../selectors/pause";
+
+import type { OriginalScope } from "../utils/pause/mapScopes";
+import type { Action } from "../actions/types";
+import type { State } from "./types";
+import type {
+ Why,
+ Scope,
+ SourceId,
+ ChromeFrame,
+ FrameId,
+ MappedLocation,
+ ThreadId,
+ Context,
+ ThreadContext,
+ Previews,
+ SourceLocation,
+ HighlightedCalls,
+} from "../types";
+
+export type Command = null | "stepOver" | "stepIn" | "stepOut" | "resume";
+
+// Pause state associated with an individual thread.
+type ThreadPauseState = {
+ why: ?Why,
+ isWaitingOnBreak: boolean,
+ frames: ?(any[]),
+ framesLoading: boolean,
+ frameScopes: {
+ generated: {
+ [FrameId]: {
+ pending: boolean,
+ scope: Scope,
+ },
+ },
+ original: {
+ [FrameId]: {
+ pending: boolean,
+ scope: OriginalScope,
+ },
+ },
+ mappings: {
+ [FrameId]: {
+ [string]: string | null,
+ },
+ },
+ },
+ selectedFrameId: ?string,
+
+ // Scope items that have been expanded in the current pause.
+ expandedScopes: Set<string>,
+
+ // Scope items that were expanded in the last pause. This is separate from
+ // expandedScopes so that (a) the scope pane's ObjectInspector does not depend
+ // on the current expanded scopes and we don't have to re-render the entire
+ // ObjectInspector when an element is expanded or collapsed, and (b) so that
+ // the expanded scopes are regenerated when we pause at a new location and we
+ // don't have to worry about pruning obsolete scope entries.
+ lastExpandedScopes: string[],
+
+ command: Command,
+ lastCommand: Command,
+ wasStepping: boolean,
+ previousLocation: ?MappedLocation,
+ inlinePreview: {
+ [FrameId]: Object,
+ },
+ highlightedCalls: ?HighlightedCalls,
+};
+
+// Pause state describing all threads.
+export type PauseState = {
+ cx: Context,
+ threadcx: ThreadContext,
+ threads: { [ThreadId]: ThreadPauseState },
+ skipPausing: boolean,
+ mapScopes: boolean,
+ shouldPauseOnExceptions: boolean,
+ shouldPauseOnCaughtExceptions: boolean,
+ previewLocation: ?SourceLocation,
+};
+
+export function initialPauseState(thread: ThreadId = "UnknownThread") {
+ return {
+ cx: {
+ navigateCounter: 0,
+ },
+ threadcx: {
+ navigateCounter: 0,
+ thread,
+ isPaused: false,
+ pauseCounter: 0,
+ },
+ previewLocation: null,
+ highlightedCalls: null,
+ threads: {},
+ skipPausing: prefs.skipPausing,
+ mapScopes: prefs.mapScopes,
+ shouldPauseOnExceptions: prefs.pauseOnExceptions,
+ shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions,
+ };
+}
+
+const resumedPauseState = {
+ frames: null,
+ framesLoading: false,
+ frameScopes: {
+ generated: {},
+ original: {},
+ mappings: {},
+ },
+ selectedFrameId: null,
+ why: null,
+ inlinePreview: {},
+ highlightedCalls: null,
+};
+
+const createInitialPauseState = () => ({
+ ...resumedPauseState,
+ isWaitingOnBreak: false,
+ command: null,
+ lastCommand: null,
+ previousLocation: null,
+ expandedScopes: new Set(),
+ lastExpandedScopes: [],
+});
+
+function getThreadPauseState(state: PauseState, thread: ThreadId) {
+ // Thread state is lazily initialized so that we don't have to keep track of
+ // the current set of worker threads.
+ return state.threads[thread] || createInitialPauseState();
+}
+
+function update(
+ state: PauseState = initialPauseState(),
+ action: Action
+): PauseState {
+ // Actions need to specify any thread they are operating on. These helpers
+ // manage updating the pause state for that thread.
+ const threadState = () => {
+ if (!action.thread) {
+ throw new Error(`Missing thread in action ${action.type}`);
+ }
+ return getThreadPauseState(state, action.thread);
+ };
+
+ const updateThreadState = newThreadState => {
+ if (!action.thread) {
+ throw new Error(`Missing thread in action ${action.type}`);
+ }
+ return {
+ ...state,
+ threads: {
+ ...state.threads,
+ [action.thread]: { ...threadState(), ...newThreadState },
+ },
+ };
+ };
+
+ switch (action.type) {
+ case "SELECT_THREAD": {
+ return {
+ ...state,
+ threadcx: {
+ ...state.threadcx,
+ thread: action.thread,
+ isPaused: !!threadState().frames,
+ pauseCounter: state.threadcx.pauseCounter + 1,
+ },
+ };
+ }
+
+ case "PAUSED": {
+ const { thread, frame, why } = action;
+
+ state = {
+ ...state,
+ previewLocation: null,
+ threadcx: {
+ ...state.threadcx,
+ pauseCounter: state.threadcx.pauseCounter + 1,
+ thread,
+ isPaused: true,
+ },
+ };
+ return updateThreadState({
+ isWaitingOnBreak: false,
+ selectedFrameId: frame ? frame.id : undefined,
+ frames: frame ? [frame] : undefined,
+ framesLoading: true,
+ frameScopes: { ...resumedPauseState.frameScopes },
+ why,
+ });
+ }
+
+ case "FETCHED_FRAMES": {
+ const { frames } = action;
+ return updateThreadState({ frames, framesLoading: false });
+ }
+
+ case "PREVIEW_PAUSED_LOCATION": {
+ return { ...state, previewLocation: action.location };
+ }
+
+ case "CLEAR_PREVIEW_PAUSED_LOCATION": {
+ return { ...state, previewLocation: null };
+ }
+
+ case "MAP_FRAMES": {
+ const { selectedFrameId, frames } = action;
+ return updateThreadState({ frames, selectedFrameId });
+ }
+
+ case "MAP_FRAME_DISPLAY_NAMES": {
+ const { frames } = action;
+ return updateThreadState({ frames });
+ }
+
+ case "ADD_SCOPES": {
+ const { frame, status, value } = action;
+ const selectedFrameId = frame.id;
+
+ const generated = {
+ ...threadState().frameScopes.generated,
+ [selectedFrameId]: {
+ pending: status !== "done",
+ scope: value,
+ },
+ };
+
+ return updateThreadState({
+ frameScopes: {
+ ...threadState().frameScopes,
+ generated,
+ },
+ });
+ }
+
+ case "MAP_SCOPES": {
+ const { frame, status, value } = action;
+ const selectedFrameId = frame.id;
+
+ const original = {
+ ...threadState().frameScopes.original,
+ [selectedFrameId]: {
+ pending: status !== "done",
+ scope: value?.scope,
+ },
+ };
+
+ const mappings = {
+ ...threadState().frameScopes.mappings,
+ [selectedFrameId]: value?.mappings,
+ };
+
+ return updateThreadState({
+ frameScopes: {
+ ...threadState().frameScopes,
+ original,
+ mappings,
+ },
+ });
+ }
+
+ case "BREAK_ON_NEXT":
+ return updateThreadState({ isWaitingOnBreak: true });
+
+ case "SELECT_FRAME":
+ return updateThreadState({ selectedFrameId: action.frame.id });
+
+ case "CONNECT":
+ return {
+ ...initialPauseState(action.mainThreadActorID),
+ };
+
+ case "PAUSE_ON_EXCEPTIONS": {
+ const { shouldPauseOnExceptions, shouldPauseOnCaughtExceptions } = action;
+
+ prefs.pauseOnExceptions = shouldPauseOnExceptions;
+ prefs.pauseOnCaughtExceptions = shouldPauseOnCaughtExceptions;
+
+ // Preserving for the old debugger
+ prefs.ignoreCaughtExceptions = !shouldPauseOnCaughtExceptions;
+
+ return {
+ ...state,
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ };
+ }
+
+ case "COMMAND":
+ if (action.status === "start") {
+ return updateThreadState({
+ ...resumedPauseState,
+ command: action.command,
+ lastCommand: action.command,
+ previousLocation: getPauseLocation(threadState(), action),
+ });
+ }
+ return updateThreadState({ command: null });
+
+ case "RESUME": {
+ if (action.thread == state.threadcx.thread) {
+ state = {
+ ...state,
+ threadcx: {
+ ...state.threadcx,
+ pauseCounter: state.threadcx.pauseCounter + 1,
+ isPaused: false,
+ },
+ };
+ }
+ return updateThreadState({
+ ...resumedPauseState,
+ wasStepping: !!action.wasStepping,
+ expandedScopes: new Set(),
+ lastExpandedScopes: [...threadState().expandedScopes],
+ });
+ }
+
+ case "EVALUATE_EXPRESSION":
+ return updateThreadState({
+ command: action.status === "start" ? "expression" : null,
+ });
+
+ case "NAVIGATE": {
+ const navigateCounter = state.cx.navigateCounter + 1;
+ return {
+ ...state,
+ cx: {
+ navigateCounter,
+ },
+ threadcx: {
+ navigateCounter,
+ thread: action.mainThread.actor,
+ pauseCounter: 0,
+ isPaused: false,
+ },
+ threads: {
+ [action.mainThread.actor]: {
+ ...getThreadPauseState(state, action.mainThread.actor),
+ ...resumedPauseState,
+ },
+ },
+ };
+ }
+
+ case "TOGGLE_SKIP_PAUSING": {
+ const { skipPausing } = action;
+ prefs.skipPausing = skipPausing;
+
+ return { ...state, skipPausing };
+ }
+
+ case "TOGGLE_MAP_SCOPES": {
+ const { mapScopes } = action;
+ prefs.mapScopes = mapScopes;
+ return { ...state, mapScopes };
+ }
+
+ case "SET_EXPANDED_SCOPE": {
+ const { path, expanded } = action;
+ const expandedScopes = new Set(threadState().expandedScopes);
+ if (expanded) {
+ expandedScopes.add(path);
+ } else {
+ expandedScopes.delete(path);
+ }
+ return updateThreadState({ expandedScopes });
+ }
+
+ case "ADD_INLINE_PREVIEW": {
+ const { frame, previews } = action;
+ const selectedFrameId = frame.id;
+
+ return updateThreadState({
+ inlinePreview: {
+ ...threadState().inlinePreview,
+ [selectedFrameId]: previews,
+ },
+ });
+ }
+
+ case "HIGHLIGHT_CALLS": {
+ const { highlightedCalls } = action;
+ return updateThreadState({ ...threadState(), highlightedCalls });
+ }
+
+ case "UNHIGHLIGHT_CALLS": {
+ return updateThreadState({
+ ...threadState(),
+ highlightedCalls: null,
+ });
+ }
+ }
+
+ return state;
+}
+
+function getPauseLocation(state, action) {
+ const { frames, previousLocation } = state;
+
+ // NOTE: We store the previous location so that we ensure that we
+ // do not stop at the same location twice when we step over.
+ if (action.command !== "stepOver") {
+ return null;
+ }
+
+ const frame = frames?.[0];
+ if (!frame) {
+ return previousLocation;
+ }
+
+ return {
+ location: frame.location,
+ generatedLocation: frame.generatedLocation,
+ };
+}
+
+// Selectors
+
+export function getContext(state: State) {
+ return state.pause.cx;
+}
+
+export function getThreadContext(state: State) {
+ return state.pause.threadcx;
+}
+
+export function getPauseReason(state: State, thread: ThreadId): ?Why {
+ return getThreadPauseState(state.pause, thread).why;
+}
+
+export function getPauseCommand(state: State, thread: ThreadId): Command {
+ return getThreadPauseState(state.pause, thread).command;
+}
+
+export function wasStepping(state: State, thread: ThreadId): boolean {
+ return getThreadPauseState(state.pause, thread).wasStepping;
+}
+
+export function isStepping(state: State, thread: ThreadId) {
+ return ["stepIn", "stepOver", "stepOut"].includes(
+ getPauseCommand(state, thread)
+ );
+}
+
+export function getCurrentThread(state: State) {
+ return getThreadContext(state).thread;
+}
+
+export function getIsPaused(state: State, thread: ThreadId) {
+ return !!getThreadPauseState(state.pause, thread).frames;
+}
+
+export function getPreviousPauseFrameLocation(state: State, thread: ThreadId) {
+ return getThreadPauseState(state.pause, thread).previousLocation;
+}
+
+export function isEvaluatingExpression(state: State, thread: ThreadId) {
+ return getThreadPauseState(state.pause, thread).command === "expression";
+}
+
+export function getIsWaitingOnBreak(state: State, thread: ThreadId) {
+ return getThreadPauseState(state.pause, thread).isWaitingOnBreak;
+}
+
+export function getShouldPauseOnExceptions(state: State) {
+ return state.pause.shouldPauseOnExceptions;
+}
+
+export function getShouldPauseOnCaughtExceptions(state: State) {
+ return state.pause.shouldPauseOnCaughtExceptions;
+}
+
+export function getFrames(state: State, thread: ThreadId) {
+ const { frames, framesLoading } = getThreadPauseState(state.pause, thread);
+ return framesLoading ? null : frames;
+}
+
+export function getCurrentThreadFrames(state: State) {
+ const { frames, framesLoading } = getThreadPauseState(
+ state.pause,
+ getCurrentThread(state)
+ );
+ return framesLoading ? null : frames;
+}
+
+function getGeneratedFrameId(frameId: string): string {
+ 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: State,
+ thread: ThreadId,
+ frameId: ?string
+) {
+ if (!frameId) {
+ return null;
+ }
+
+ return getFrameScopes(state, thread).generated[getGeneratedFrameId(frameId)];
+}
+
+export function getOriginalFrameScope(
+ state: State,
+ thread: ThreadId,
+ sourceId: ?SourceId,
+ frameId: ?string
+): ?{
+ pending: boolean,
+ +scope: OriginalScope | Scope,
+} {
+ 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;
+}
+
+export function getFrameScopes(state: State, thread: ThreadId) {
+ return getThreadPauseState(state.pause, thread).frameScopes;
+}
+
+export function getSelectedFrameBindings(state: State, thread: ThreadId) {
+ 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;
+ }
+
+ 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;
+}
+
+export function getFrameScope(
+ state: State,
+ thread: ThreadId,
+ sourceId: ?SourceId,
+ frameId: ?string
+): ?{
+ pending: boolean,
+ +scope: OriginalScope | Scope,
+} {
+ return (
+ getOriginalFrameScope(state, thread, sourceId, frameId) ||
+ getGeneratedFrameScope(state, thread, frameId)
+ );
+}
+
+export function getSelectedScope(state: State, thread: ThreadId) {
+ 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: State, thread: ThreadId) {
+ const sourceId = getSelectedSourceId(state);
+ const frameId = getSelectedFrameId(state, thread);
+ return getOriginalFrameScope(state, thread, sourceId, frameId);
+}
+
+export function getSelectedGeneratedScope(state: State, thread: ThreadId) {
+ const frameId = getSelectedFrameId(state, thread);
+ return getGeneratedFrameScope(state, thread, frameId);
+}
+
+export function getSelectedScopeMappings(
+ state: State,
+ thread: ThreadId
+): {
+ [string]: string | null,
+} | null {
+ const frameId = getSelectedFrameId(state, thread);
+ if (!frameId) {
+ return null;
+ }
+
+ return getFrameScopes(state, thread).mappings[frameId];
+}
+
+export function getSelectedFrameId(state: State, thread: ThreadId) {
+ return getThreadPauseState(state.pause, thread).selectedFrameId;
+}
+
+export function isTopFrameSelected(state: State, thread: ThreadId) {
+ const selectedFrameId = getSelectedFrameId(state, thread);
+ const topFrame = getTopFrame(state, thread);
+ return selectedFrameId == topFrame?.id;
+}
+
+export function getTopFrame(state: State, thread: ThreadId) {
+ const frames = getFrames(state, thread);
+ return frames?.[0];
+}
+
+export function getSkipPausing(state: State) {
+ return state.pause.skipPausing;
+}
+
+export function getHighlightedCalls(state: State, thread: ThreadId) {
+ return getThreadPauseState(state.pause, thread).highlightedCalls;
+}
+
+export function isMapScopesEnabled(state: State) {
+ return state.pause.mapScopes;
+}
+
+export function getInlinePreviews(
+ state: State,
+ thread: ThreadId,
+ frameId: string
+): Previews {
+ return getThreadPauseState(state.pause, thread).inlinePreview[
+ getGeneratedFrameId(frameId)
+ ];
+}
+
+export function getSelectedInlinePreviews(state: State) {
+ const thread = getCurrentThread(state);
+ const frameId = getSelectedFrameId(state, thread);
+ if (!frameId) {
+ return null;
+ }
+
+ return getInlinePreviews(state, thread, frameId);
+}
+
+export function getInlinePreviewExpression(
+ state: State,
+ thread: ThreadId,
+ frameId: string,
+ line: number,
+ expression: string
+) {
+ const previews = getThreadPauseState(state.pause, thread).inlinePreview[
+ getGeneratedFrameId(frameId)
+ ];
+ return previews?.[line]?.[expression];
+}
+
+// NOTE: currently only used for chrome
+export function getChromeScopes(state: State, thread: ThreadId) {
+ const frame: ?ChromeFrame = (getSelectedFrame(state, thread): any);
+ return frame?.scopeChain;
+}
+
+export function getLastExpandedScopes(state: State, thread: ThreadId) {
+ return getThreadPauseState(state.pause, thread).lastExpandedScopes;
+}
+
+export function getPausePreviewLocation(state: State) {
+ return state.pause.previewLocation;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/pending-breakpoints.js b/devtools/client/debugger/src/reducers/pending-breakpoints.js
new file mode 100644
index 0000000000..9b7f87ff21
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/pending-breakpoints.js
@@ -0,0 +1,102 @@
+/* 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
+
+/**
+ * Pending breakpoints reducer
+ * @module reducers/pending-breakpoints
+ */
+
+import {
+ createPendingBreakpoint,
+ makePendingLocationId,
+} from "../utils/breakpoint";
+
+import { isPrettyURL } from "../utils/source";
+
+import type { SourcesState } from "./sources";
+import type { PendingBreakpoint, Source } from "../types";
+import type { Action } from "../actions/types";
+
+export type PendingBreakpointsState = { [string]: PendingBreakpoint };
+
+function update(state: PendingBreakpointsState = {}, action: Action) {
+ switch (action.type) {
+ case "SET_BREAKPOINT":
+ if (action.status === "start") {
+ return setBreakpoint(state, action);
+ }
+ return state;
+
+ case "REMOVE_BREAKPOINT":
+ if (action.status === "start") {
+ return removeBreakpoint(state, action);
+ }
+ return state;
+
+ case "REMOVE_PENDING_BREAKPOINT":
+ return removeBreakpoint(state, action);
+
+ case "REMOVE_BREAKPOINTS": {
+ return {};
+ }
+ }
+
+ return state;
+}
+
+function setBreakpoint(state, { breakpoint }) {
+ if (breakpoint.options.hidden) {
+ return state;
+ }
+ const location =
+ !breakpoint.location.sourceUrl || isPrettyURL(breakpoint.location.sourceUrl)
+ ? breakpoint.generatedLocation
+ : breakpoint.location;
+ const locationId = makePendingLocationId(location);
+ const pendingBreakpoint = createPendingBreakpoint(breakpoint);
+
+ return { ...state, [locationId]: pendingBreakpoint };
+}
+
+function removeBreakpoint(state, { location }) {
+ const locationId = makePendingLocationId(location);
+ state = { ...state };
+
+ delete state[locationId];
+ return state;
+}
+
+// Selectors
+// TODO: these functions should be moved out of the reducer
+
+type OuterState = {
+ pendingBreakpoints: PendingBreakpointsState,
+ sources: SourcesState,
+};
+
+export function getPendingBreakpoints(state: OuterState) {
+ return state.pendingBreakpoints;
+}
+
+export function getPendingBreakpointList(
+ state: OuterState
+): PendingBreakpoint[] {
+ return (Object.values(getPendingBreakpoints(state)): any);
+}
+
+export function getPendingBreakpointsForSource(
+ state: OuterState,
+ source: Source
+): PendingBreakpoint[] {
+ return getPendingBreakpointList(state).filter(pendingBreakpoint => {
+ return (
+ pendingBreakpoint.location.sourceUrl === source.url ||
+ pendingBreakpoint.generatedLocation.sourceUrl == source.url
+ );
+ });
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/preview.js b/devtools/client/debugger/src/reducers/preview.js
new file mode 100644
index 0000000000..7b11d581b2
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/preview.js
@@ -0,0 +1,69 @@
+/* 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
+
+import type { AstLocation } from "../workers/parser";
+
+import type { Action } from "../actions/types";
+import type { Grip, Exception, OINode } from "../types";
+
+export type Preview = {|
+ expression: string,
+ resultGrip: Grip | null,
+ root: OINode,
+ properties: Array<Grip>,
+ location: AstLocation,
+ cursorPos: any,
+ tokenPos: AstLocation,
+ target: HTMLDivElement,
+ exception: ?Exception,
+|};
+
+export type PreviewState = {
+ +preview: ?Preview,
+ previewCount: number,
+};
+
+export function initialPreviewState(): PreviewState {
+ return {
+ preview: null,
+ previewCount: 0,
+ };
+}
+
+function update(
+ state: PreviewState = initialPreviewState(),
+ action: Action
+): PreviewState {
+ switch (action.type) {
+ case "CLEAR_PREVIEW": {
+ return { ...state, preview: null };
+ }
+
+ case "START_PREVIEW": {
+ return { ...state, previewCount: state.previewCount + 1 };
+ }
+
+ case "SET_PREVIEW": {
+ return { ...state, preview: action.value };
+ }
+ }
+
+ return state;
+}
+
+// NOTE: we'd like to have the app state fully typed
+// https://github.com/firefox-devtools/debugger/blob/master/src/reducers/sources.js#L179-L185
+type OuterState = { preview: PreviewState };
+
+export function getPreview(state: OuterState): ?Preview {
+ return state.preview.preview;
+}
+
+export function getPreviewCount(state: OuterState): number {
+ return state.preview.previewCount;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/project-text-search.js b/devtools/client/debugger/src/reducers/project-text-search.js
new file mode 100644
index 0000000000..a1ad6012cc
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/project-text-search.js
@@ -0,0 +1,112 @@
+/* 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
+// @format
+
+/**
+ * Project text search reducer
+ * @module reducers/project-text-search
+ */
+
+import type { Action } from "../actions/types";
+import type { Cancellable, SourceId } from "../types";
+
+export type Search = {
+ +sourceId: SourceId,
+ +filepath: string,
+ +matches: any[],
+};
+
+export type SearchOperation = Cancellable;
+
+export type StatusType =
+ | "INITIAL"
+ | "FETCHING"
+ | "CANCELLED"
+ | "DONE"
+ | "ERROR";
+export const statusType = {
+ initial: "INITIAL",
+ fetching: "FETCHING",
+ cancelled: "CANCELLED",
+ done: "DONE",
+ error: "ERROR",
+};
+
+export type ResultList = Search[];
+export type ProjectTextSearchState = {
+ +query: string,
+ +ongoingSearch: ?SearchOperation,
+ +results: ResultList,
+ +status: StatusType,
+};
+
+export function initialProjectTextSearchState(): ProjectTextSearchState {
+ return {
+ query: "",
+ results: [],
+ ongoingSearch: null,
+ status: statusType.initial,
+ };
+}
+
+function update(
+ state: ProjectTextSearchState = initialProjectTextSearchState(),
+ action: Action
+): ProjectTextSearchState {
+ switch (action.type) {
+ case "ADD_QUERY":
+ return { ...state, query: action.query };
+
+ case "ADD_SEARCH_RESULT":
+ if (action.result.matches.length === 0) {
+ return state;
+ }
+
+ const result = {
+ type: "RESULT",
+ ...action.result,
+ matches: action.result.matches.map(m => ({ type: "MATCH", ...m })),
+ };
+ return { ...state, results: [...state.results, result] };
+
+ case "UPDATE_STATUS":
+ const ongoingSearch =
+ action.status == statusType.fetching ? state.ongoingSearch : null;
+ return { ...state, status: action.status, ongoingSearch };
+
+ case "CLEAR_SEARCH_RESULTS":
+ return { ...state, results: [] };
+
+ case "ADD_ONGOING_SEARCH":
+ return { ...state, ongoingSearch: action.ongoingSearch };
+
+ case "CLEAR_SEARCH":
+ case "CLOSE_PROJECT_SEARCH":
+ case "NAVIGATE":
+ return initialProjectTextSearchState();
+ }
+ return state;
+}
+
+type OuterState = { projectTextSearch: ProjectTextSearchState };
+
+export function getTextSearchOperation(state: OuterState) {
+ return state.projectTextSearch.ongoingSearch;
+}
+
+export function getTextSearchResults(state: OuterState) {
+ return state.projectTextSearch.results;
+}
+
+export function getTextSearchStatus(state: OuterState) {
+ return state.projectTextSearch.status;
+}
+
+export function getTextSearchQuery(state: OuterState) {
+ return state.projectTextSearch.query;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/quick-open.js b/devtools/client/debugger/src/reducers/quick-open.js
new file mode 100644
index 0000000000..11f7072646
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/quick-open.js
@@ -0,0 +1,71 @@
+/* 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
+
+/**
+ * Quick Open reducer
+ * @module reducers/quick-open
+ */
+
+import { parseQuickOpenQuery } from "../utils/quick-open";
+import type { Action } from "../actions/types";
+
+export type QuickOpenType = "sources" | "functions" | "goto" | "gotoSource";
+
+export type QuickOpenState = {
+ enabled: boolean,
+ query: string,
+ searchType: QuickOpenType,
+};
+
+export const initialQuickOpenState = (): QuickOpenState => ({
+ enabled: false,
+ query: "",
+ searchType: "sources",
+});
+
+export default function update(
+ state: QuickOpenState = initialQuickOpenState(),
+ action: Action
+): QuickOpenState {
+ switch (action.type) {
+ case "OPEN_QUICK_OPEN":
+ if (action.query != null) {
+ return {
+ ...state,
+ enabled: true,
+ query: action.query,
+ searchType: parseQuickOpenQuery(action.query),
+ };
+ }
+ return { ...state, enabled: true };
+ case "CLOSE_QUICK_OPEN":
+ return initialQuickOpenState();
+ case "SET_QUICK_OPEN_QUERY":
+ return {
+ ...state,
+ query: action.query,
+ searchType: parseQuickOpenQuery(action.query),
+ };
+ default:
+ return state;
+ }
+}
+
+type OuterState = {
+ quickOpen: QuickOpenState,
+};
+
+export function getQuickOpenEnabled(state: OuterState): boolean {
+ return state.quickOpen.enabled;
+}
+
+export function getQuickOpenQuery(state: OuterState): string {
+ return state.quickOpen.query;
+}
+
+export function getQuickOpenType(state: OuterState): QuickOpenType {
+ return state.quickOpen.searchType;
+}
diff --git a/devtools/client/debugger/src/reducers/source-actors.js b/devtools/client/debugger/src/reducers/source-actors.js
new file mode 100644
index 0000000000..98423328bd
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/source-actors.js
@@ -0,0 +1,296 @@
+/* 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
+
+import type { Action } from "../actions/types";
+import type { SourceId, ThreadId, URL } from "../types";
+import {
+ asSettled,
+ type AsyncValue,
+ type SettledValue,
+} from "../utils/async-value";
+import {
+ createInitial,
+ insertResources,
+ updateResources,
+ removeResources,
+ hasResource,
+ getResource,
+ getMappedResource,
+ makeWeakQuery,
+ makeIdQuery,
+ makeReduceAllQuery,
+ type Resource,
+ type ResourceState,
+ type WeakQuery,
+ type IdQuery,
+ type ReduceAllQuery,
+} from "../utils/resource";
+
+import { asyncActionAsValue } from "../actions/utils/middleware/promise";
+import type {
+ SourceActorBreakpointColumnsAction,
+ SourceActorBreakableLinesAction,
+} from "../actions/types/SourceActorAction";
+
+export opaque type SourceActorId: string = string;
+export type SourceActor = {|
+ +id: SourceActorId,
+ +actor: string,
+ +thread: ThreadId,
+ +source: SourceId,
+
+ +isBlackBoxed: boolean,
+
+ // The URL that the sourcemap should be loaded relative to.
+ +sourceMapBaseURL: URL | null,
+
+ // The URL of the sourcemap for this source if there is one.
+ +sourceMapURL: URL | null,
+
+ // The URL of the actor itself. If the source was from an "eval" or other
+ // string-based source, this will not be known.
+ +url: URL | null,
+
+ // The debugger's Debugger.Source API provides type information for the
+ // cause of this source's creation.
+ +introductionType: string | null,
+|};
+
+type SourceActorResource = Resource<{
+ ...SourceActor,
+
+ // The list of breakpoint positions on each line of the file.
+ breakpointPositions: Map<number, AsyncValue<Array<number>>>,
+
+ // The list of lines that contain breakpoints.
+ breakableLines: AsyncValue<Array<number>> | null,
+}>;
+export type SourceActorsState = ResourceState<SourceActorResource>;
+export type SourceActorOuterState = { sourceActors: SourceActorsState };
+
+export const initial: SourceActorsState = createInitial();
+
+export default function update(
+ state: SourceActorsState = initial,
+ action: Action
+): SourceActorsState {
+ switch (action.type) {
+ case "INSERT_SOURCE_ACTORS": {
+ const { items } = action;
+ state = insertResources(
+ state,
+ items.map(item => ({
+ ...item,
+ breakpointPositions: new Map(),
+ breakableLines: null,
+ }))
+ );
+ break;
+ }
+ case "REMOVE_SOURCE_ACTORS": {
+ state = removeResources(state, action.items);
+ break;
+ }
+
+ case "NAVIGATE": {
+ state = initial;
+ break;
+ }
+
+ case "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS":
+ state = updateBreakpointColumns(state, action);
+ break;
+
+ case "SET_SOURCE_ACTOR_BREAKABLE_LINES":
+ state = updateBreakableLines(state, action);
+ break;
+
+ case "CLEAR_SOURCE_ACTOR_MAP_URL":
+ state = clearSourceActorMapURL(state, action.id);
+ break;
+ }
+
+ return state;
+}
+
+function clearSourceActorMapURL(
+ state: SourceActorsState,
+ id: SourceActorId
+): SourceActorsState {
+ if (!hasResource(state, id)) {
+ return state;
+ }
+
+ return updateResources(state, [
+ {
+ id,
+ sourceMapURL: "",
+ },
+ ]);
+}
+
+function updateBreakpointColumns(
+ state: SourceActorsState,
+ action: SourceActorBreakpointColumnsAction
+): SourceActorsState {
+ const { sourceId, line } = action;
+ const value = asyncActionAsValue(action);
+
+ if (!hasResource(state, sourceId)) {
+ return state;
+ }
+
+ const breakpointPositions = new Map(
+ getResource(state, sourceId).breakpointPositions
+ );
+ breakpointPositions.set(line, value);
+
+ return updateResources(state, [{ id: sourceId, breakpointPositions }]);
+}
+
+function updateBreakableLines(
+ state: SourceActorsState,
+ action: SourceActorBreakableLinesAction
+): SourceActorsState {
+ const value = asyncActionAsValue(action);
+ const { sourceId } = action;
+
+ if (!hasResource(state, sourceId)) {
+ return state;
+ }
+
+ return updateResources(state, [{ id: sourceId, breakableLines: value }]);
+}
+
+export function resourceAsSourceActor({
+ breakpointPositions,
+ breakableLines,
+ ...sourceActor
+}: SourceActorResource): SourceActor {
+ return sourceActor;
+}
+
+// Because we are using an opaque type for our source actor IDs, these
+// functions are required to convert back and forth in order to get a string
+// version of the IDs. That should be super rarely used, but it means that
+// we can very easily see where we're relying on the string version of IDs.
+export function stringToSourceActorId(s: string): SourceActorId {
+ return s;
+}
+
+export function hasSourceActor(
+ state: SourceActorOuterState,
+ id: SourceActorId
+): boolean {
+ return hasResource(state.sourceActors, id);
+}
+
+export function getSourceActor(
+ state: SourceActorOuterState,
+ id: SourceActorId
+): SourceActor {
+ return getMappedResource(state.sourceActors, id, resourceAsSourceActor);
+}
+
+/**
+ * Get all of the source actors for a set of IDs. Caches based on the identity
+ * of "ids" when possible.
+ */
+const querySourceActorsById: IdQuery<
+ SourceActorResource,
+ SourceActor
+> = makeIdQuery(resourceAsSourceActor);
+
+export function getSourceActors(
+ state: SourceActorOuterState,
+ ids: Array<SourceActorId>
+): Array<SourceActor> {
+ return querySourceActorsById(state.sourceActors, ids);
+}
+
+const querySourcesByThreadID: ReduceAllQuery<
+ SourceActorResource,
+ { [ThreadId]: Array<SourceActor> }
+> = makeReduceAllQuery(resourceAsSourceActor, actors => {
+ return actors.reduce((acc, actor) => {
+ acc[actor.thread] = acc[actor.thread] || [];
+ acc[actor.thread].push(actor);
+ return acc;
+ }, {});
+});
+export function getSourceActorsForThread(
+ state: SourceActorOuterState,
+ ids: ThreadId | Array<ThreadId>
+): Array<SourceActor> {
+ const sourcesByThread = querySourcesByThreadID(state.sourceActors);
+
+ let sources = [];
+ for (const id of Array.isArray(ids) ? ids : [ids]) {
+ sources = sources.concat(sourcesByThread[id] || []);
+ }
+ return sources;
+}
+
+const queryThreadsBySourceObject: ReduceAllQuery<
+ SourceActorResource,
+ { [SourceId]: Array<ThreadId> }
+> = makeReduceAllQuery(
+ actor => ({ thread: actor.thread, source: actor.source }),
+ actors =>
+ actors.reduce((acc, { source, thread }) => {
+ let sourceThreads = acc[source];
+ if (!sourceThreads) {
+ sourceThreads = [];
+ acc[source] = sourceThreads;
+ }
+
+ sourceThreads.push(thread);
+ return acc;
+ }, {})
+);
+
+export function getAllThreadsBySource(
+ state: SourceActorOuterState
+): { [SourceId]: Array<ThreadId> } {
+ return queryThreadsBySourceObject(state.sourceActors);
+}
+
+export function getSourceActorBreakableLines(
+ state: SourceActorOuterState,
+ id: SourceActorId
+): SettledValue<Array<number>> | null {
+ const { breakableLines } = getResource(state.sourceActors, id);
+
+ return asSettled(breakableLines);
+}
+
+export function getSourceActorBreakpointColumns(
+ state: SourceActorOuterState,
+ id: SourceActorId,
+ line: number
+): SettledValue<Array<number>> | null {
+ const { breakpointPositions } = getResource(state.sourceActors, id);
+
+ return asSettled(breakpointPositions.get(line) || null);
+}
+
+export const getBreakableLinesForSourceActors: WeakQuery<
+ SourceActorResource,
+ Array<SourceActorId>,
+ Array<number>
+> = makeWeakQuery({
+ filter: (state, ids) => ids,
+ map: ({ breakableLines }) => breakableLines,
+ reduce: items =>
+ Array.from(
+ items.reduce((acc, item) => {
+ if (item && item.state === "fulfilled") {
+ acc = acc.concat(item.value);
+ }
+ return acc;
+ }, [])
+ ),
+});
diff --git a/devtools/client/debugger/src/reducers/source-tree.js b/devtools/client/debugger/src/reducers/source-tree.js
new file mode 100644
index 0000000000..0d8ca49e81
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/source-tree.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/>. */
+
+// @flow
+
+/**
+ * Source tree reducer
+ * @module reducers/source-tree
+ */
+
+import type { SourceTreeAction, FocusItem } from "../actions/types";
+
+export type SourceTreeState = {
+ expanded: Set<string>,
+ focusedItem: ?FocusItem,
+};
+
+export function initialSourcesTreeState(): SourceTreeState {
+ return {
+ expanded: new Set(),
+ focusedItem: null,
+ };
+}
+
+export default function update(
+ state: SourceTreeState = initialSourcesTreeState(),
+ action: SourceTreeAction
+): SourceTreeState {
+ switch (action.type) {
+ case "SET_EXPANDED_STATE":
+ return updateExpanded(state, action);
+
+ case "SET_FOCUSED_SOURCE_ITEM":
+ return { ...state, focusedItem: action.item };
+ }
+
+ return state;
+}
+
+function updateExpanded(state, action) {
+ return {
+ ...state,
+ expanded: new Set(action.expanded),
+ };
+}
+
+type OuterState = {
+ sourceTree: SourceTreeState,
+};
+
+export function getExpandedState(state: OuterState) {
+ return state.sourceTree.expanded;
+}
+
+export function getFocusedSourceItem(state: OuterState): ?FocusItem {
+ return state.sourceTree.focusedItem;
+}
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;
diff --git a/devtools/client/debugger/src/reducers/tabs.js b/devtools/client/debugger/src/reducers/tabs.js
new file mode 100644
index 0000000000..2deaea665a
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tabs.js
@@ -0,0 +1,308 @@
+/* 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
+
+/**
+ * Tabs reducer
+ * @module reducers/tabs
+ */
+
+import { createSelector } from "reselect";
+import { isOriginalId } from "devtools-source-map";
+import move from "lodash-move";
+
+import { isSimilarTab, persistTabs } from "../utils/tabs";
+import { makeShallowQuery } from "../utils/resource";
+import { getPrettySourceURL } from "../utils/source";
+
+import {
+ getSource,
+ getSpecificSourceByURL,
+ getSources,
+ resourceAsSourceBase,
+} from "./sources";
+
+import type { Source, SourceId, URL } from "../types";
+import type { Action } from "../actions/types";
+import type { Selector, State } from "./types";
+import type { SourceBase } from "./sources";
+
+export type PersistedTab = {|
+ url: URL,
+ framework?: string | null,
+ isOriginal: boolean,
+ sourceId: SourceId,
+|};
+
+export type VisibleTab = {| ...Tab, sourceId: SourceId |};
+
+export type Tab = PersistedTab | VisibleTab;
+
+export type TabList = Tab[];
+
+export type TabsSources = $ReadOnlyArray<SourceBase>;
+
+export type TabsState = {
+ tabs: TabList,
+};
+
+export function initialTabState(): TabsState {
+ return { tabs: [] };
+}
+
+function resetTabState(state): TabsState {
+ const tabs = persistTabs(state.tabs);
+ return { tabs };
+}
+
+function update(
+ state: TabsState = initialTabState(),
+ action: Action
+): TabsState {
+ switch (action.type) {
+ case "ADD_TAB":
+ case "UPDATE_TAB":
+ return updateTabList(state, action);
+
+ case "MOVE_TAB":
+ return moveTabInList(state, action);
+ case "MOVE_TAB_BY_SOURCE_ID":
+ return moveTabInListBySourceId(state, action);
+
+ case "CLOSE_TAB":
+ return removeSourceFromTabList(state, action);
+
+ case "CLOSE_TABS":
+ return removeSourcesFromTabList(state, action);
+
+ case "ADD_SOURCE":
+ return addVisibleTabs(state, [action.source]);
+
+ case "ADD_SOURCES":
+ return addVisibleTabs(state, action.sources);
+
+ case "SET_SELECTED_LOCATION": {
+ return addSelectedSource(state, action.source);
+ }
+
+ case "NAVIGATE": {
+ return resetTabState(state);
+ }
+
+ default:
+ return state;
+ }
+}
+
+/**
+ * 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
+ *
+ * @memberof reducers/tabs
+ * @static
+ */
+export function getNewSelectedSourceId(state: State, tabList: TabList): string {
+ const { selectedLocation } = state.sources;
+ const availableTabs = state.tabs.tabs;
+ if (!selectedLocation) {
+ return "";
+ }
+
+ const selectedTab = getSource(state, selectedLocation.sourceId);
+ if (!selectedTab) {
+ return "";
+ }
+
+ const matchingTab = availableTabs.find(tab =>
+ isSimilarTab(tab, selectedTab.url, isOriginalId(selectedLocation.sourceId))
+ );
+
+ if (matchingTab) {
+ const { sources } = state.sources;
+ if (!sources) {
+ return "";
+ }
+
+ const selectedSource = getSpecificSourceByURL(
+ state,
+ selectedTab.url,
+ selectedTab.isOriginal
+ );
+
+ if (selectedSource) {
+ return selectedSource.id;
+ }
+
+ return "";
+ }
+
+ const tabUrls = tabList.map(tab => tab.url);
+ const leftNeighborIndex = Math.max(tabUrls.indexOf(selectedTab.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.id;
+ }
+ }
+
+ return "";
+}
+
+function matchesSource(tab: VisibleTab, source: Source): boolean {
+ return tab.sourceId === source.id || matchesUrl(tab, source);
+}
+
+function matchesUrl(tab: Tab, source: Source): boolean {
+ return tab.url === source.url && tab.isOriginal == isOriginalId(source.id);
+}
+
+function addSelectedSource(state: TabsState, source: Source) {
+ if (
+ state.tabs
+ .filter(({ sourceId }) => sourceId)
+ .map(({ sourceId }) => sourceId)
+ .includes(source.id)
+ ) {
+ return state;
+ }
+
+ const isOriginal = isOriginalId(source.id);
+ return updateTabList(state, {
+ url: source.url,
+ isOriginal,
+ framework: null,
+ sourceId: source.id,
+ });
+}
+
+function addVisibleTabs(state: TabsState, sources) {
+ const tabCount = state.tabs.filter(({ sourceId }) => sourceId).length;
+ const tabs = state.tabs
+ .map(tab => {
+ const source = sources.find(src => matchesUrl(tab, src));
+ if (!source) {
+ return tab;
+ }
+ return { ...tab, sourceId: source.id };
+ })
+ .filter(tab => tab.sourceId);
+
+ if (tabs.length == tabCount) {
+ return state;
+ }
+
+ return { tabs };
+}
+
+function removeSourceFromTabList(state: TabsState, { source }): TabsState {
+ const { tabs } = state;
+ const newTabs = tabs.filter(tab => !matchesSource(tab, source));
+ return { tabs: newTabs };
+}
+
+function removeSourcesFromTabList(state: TabsState, { sources }) {
+ const { tabs } = state;
+
+ const newTabs = sources.reduce(
+ (tabList, source) => tabList.filter(tab => !matchesSource(tab, source)),
+ tabs
+ );
+
+ return { tabs: newTabs };
+}
+
+/**
+ * Adds the new source to the tab list if it is not already there
+ * @memberof reducers/tabs
+ * @static
+ */
+function updateTabList(
+ state: TabsState,
+ { url, framework = null, sourceId, isOriginal = false }
+): TabsState {
+ let { tabs } = state;
+ // Set currentIndex to -1 for URL-less tabs so that they aren't
+ // filtered by isSimilarTab
+ const currentIndex = url
+ ? tabs.findIndex(tab => isSimilarTab(tab, url, isOriginal))
+ : -1;
+
+ if (currentIndex === -1) {
+ const newTab = {
+ url,
+ framework,
+ sourceId,
+ isOriginal,
+ };
+ tabs = [newTab, ...tabs];
+ } else if (framework) {
+ tabs[currentIndex].framework = framework;
+ }
+
+ return { ...state, tabs };
+}
+
+function moveTabInList(
+ state: TabsState,
+ { url, tabIndex: newIndex }
+): TabsState {
+ let { tabs } = state;
+ const currentIndex = tabs.findIndex(tab => tab.url == url);
+ tabs = move(tabs, currentIndex, newIndex);
+ return { tabs };
+}
+
+function moveTabInListBySourceId(
+ state: TabsState,
+ { sourceId, tabIndex: newIndex }
+): TabsState {
+ let { tabs } = state;
+ const currentIndex = tabs.findIndex(tab => tab.sourceId == sourceId);
+ tabs = move(tabs, currentIndex, newIndex);
+ return { tabs };
+}
+
+// Selectors
+
+export const getTabs = (state: State): TabList => state.tabs.tabs;
+
+export const getSourceTabs: Selector<VisibleTab[]> = createSelector(
+ state => state.tabs,
+ ({ tabs }) => tabs.filter(tab => tab.sourceId)
+);
+
+export const getSourcesForTabs: Selector<TabsSources> = state => {
+ const tabs = getSourceTabs(state);
+ const sources = getSources(state);
+ return querySourcesForTabs(sources, tabs);
+};
+
+const querySourcesForTabs = makeShallowQuery({
+ filter: (_, tabs) => tabs.map(({ sourceId }) => sourceId),
+ map: resourceAsSourceBase,
+ reduce: items => items,
+});
+
+export function tabExists(state: State, sourceId: SourceId): boolean {
+ return !!getSourceTabs(state).find(tab => tab.sourceId == sourceId);
+}
+
+export function hasPrettyTab(state: State, sourceUrl: URL): boolean {
+ const prettyUrl = getPrettySourceURL(sourceUrl);
+ return !!getSourceTabs(state).find(tab => tab.url === prettyUrl);
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/tests/breakpoints.spec.js b/devtools/client/debugger/src/reducers/tests/breakpoints.spec.js
new file mode 100644
index 0000000000..0618badb90
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tests/breakpoints.spec.js
@@ -0,0 +1,81 @@
+/* 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
+declare var describe: (name: string, func: () => void) => void;
+declare var it: (desc: string, func: () => void) => void;
+declare var expect: (value: any) => any;
+
+import {
+ getBreakpointsForSource,
+ initialBreakpointsState,
+} from "../breakpoints";
+
+import { makeMockBreakpoint, makeMockSource } from "../../utils/test-mockup";
+
+function initializeStateWith(data) {
+ const state = initialBreakpointsState();
+ state.breakpoints = data;
+ return state;
+}
+
+describe("Breakpoints Selectors", () => {
+ it("it gets a breakpoint for an original source", () => {
+ const sourceId = "server1.conn1.child1/source1/originalSource";
+ const matchingBreakpoints = {
+ id1: makeMockBreakpoint(makeMockSource(undefined, sourceId), 1),
+ };
+
+ const otherBreakpoints = {
+ id2: makeMockBreakpoint(makeMockSource(undefined, "not-this-source"), 1),
+ };
+
+ const data = {
+ ...matchingBreakpoints,
+ ...otherBreakpoints,
+ };
+
+ const breakpoints = initializeStateWith(data);
+ const allBreakpoints = Object.values(matchingBreakpoints);
+ const sourceBreakpoints = getBreakpointsForSource(
+ { breakpoints },
+ sourceId
+ );
+
+ expect(sourceBreakpoints).toEqual(allBreakpoints);
+ expect(sourceBreakpoints[0] === allBreakpoints[0]).toBe(true);
+ });
+
+ it("it gets a breakpoint for a generated source", () => {
+ const generatedSourceId = "random-source";
+ const matchingBreakpoints = {
+ id1: {
+ ...makeMockBreakpoint(makeMockSource(undefined, generatedSourceId), 1),
+ location: { line: 1, sourceId: "original-source-id-1" },
+ },
+ };
+
+ const otherBreakpoints = {
+ id2: {
+ ...makeMockBreakpoint(makeMockSource(undefined, "not-this-source"), 1),
+ location: { line: 1, sourceId: "original-source-id-2" },
+ },
+ };
+
+ const data = {
+ ...matchingBreakpoints,
+ ...otherBreakpoints,
+ };
+
+ const breakpoints = initializeStateWith(data);
+
+ const allBreakpoints = Object.values(matchingBreakpoints);
+ const sourceBreakpoints = getBreakpointsForSource(
+ { breakpoints },
+ generatedSourceId
+ );
+
+ expect(sourceBreakpoints).toEqual(allBreakpoints);
+ });
+});
diff --git a/devtools/client/debugger/src/reducers/tests/quick-open.spec.js b/devtools/client/debugger/src/reducers/tests/quick-open.spec.js
new file mode 100644
index 0000000000..4479708be4
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tests/quick-open.spec.js
@@ -0,0 +1,64 @@
+/* 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
+declare var describe: (name: string, func: () => void) => void;
+declare var test: (desc: string, func: () => void) => void;
+declare var expect: (value: any) => any;
+
+import update, {
+ initialQuickOpenState,
+ getQuickOpenEnabled,
+ getQuickOpenQuery,
+ getQuickOpenType,
+} from "../quick-open";
+import {
+ setQuickOpenQuery,
+ openQuickOpen,
+ closeQuickOpen,
+} from "../../actions/quick-open";
+
+describe("quickOpen reducer", () => {
+ test("initial state", () => {
+ const state = update(undefined, ({ type: "FAKE" }: any));
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+
+ test("opens the quickOpen modal", () => {
+ const state = update(initialQuickOpenState(), openQuickOpen());
+ expect(getQuickOpenEnabled({ quickOpen: state })).toEqual(true);
+ });
+
+ test("closes the quickOpen modal", () => {
+ let state = update(initialQuickOpenState(), openQuickOpen());
+ expect(getQuickOpenEnabled({ quickOpen: state })).toEqual(true);
+ state = update(initialQuickOpenState(), closeQuickOpen());
+ expect(getQuickOpenEnabled({ quickOpen: state })).toEqual(false);
+ });
+
+ test("leaves query alone on open if not provided", () => {
+ const state = update(initialQuickOpenState(), openQuickOpen());
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+
+ test("set query on open if provided", () => {
+ const state = update(initialQuickOpenState(), openQuickOpen("@"));
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("@");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("functions");
+ });
+
+ test("clear query on close", () => {
+ const state = update(initialQuickOpenState(), closeQuickOpen());
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+
+ test("sets the query to the provided string", () => {
+ const state = update(initialQuickOpenState(), setQuickOpenQuery("test"));
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("test");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+});
diff --git a/devtools/client/debugger/src/reducers/tests/sources.spec.js b/devtools/client/debugger/src/reducers/tests/sources.spec.js
new file mode 100644
index 0000000000..d0436b6f76
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tests/sources.spec.js
@@ -0,0 +1,127 @@
+/* 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
+declare var describe: (name: string, func: () => void) => void;
+declare var it: (desc: string, func: () => void) => void;
+declare var expect: (value: any) => any;
+
+import update, { initialSourcesState, getDisplayedSources } from "../sources";
+import { initialThreadsState } from "../threads";
+import updateSourceActors from "../source-actors";
+import type { SourceActor } from "../../types";
+import { prefs } from "../../utils/prefs";
+import { makeMockSource, mockcx, makeMockState } from "../../utils/test-mockup";
+import { getResourceIds } from "../../utils/resource";
+
+const extensionSource = {
+ ...makeMockSource(),
+ id: "extensionId",
+ url: "http://example.com/script.js",
+};
+
+const firefoxExtensionSource = {
+ ...makeMockSource(),
+ id: "firefoxExtension",
+ url: "moz-extension://id/js/content.js",
+ isExtension: true,
+};
+
+const chromeExtensionSource = {
+ ...makeMockSource(),
+ id: "chromeExtension",
+ isExtension: true,
+ url: "chrome-extension://id/js/content.js",
+};
+
+const mockedSources = [
+ extensionSource,
+ firefoxExtensionSource,
+ chromeExtensionSource,
+];
+
+const mockSourceActors: Array<SourceActor> = ([
+ {
+ id: "extensionId-actor",
+ actor: "extensionId-actor",
+ source: "extensionId",
+ thread: "foo",
+ },
+ {
+ id: "firefoxExtension-actor",
+ actor: "firefoxExtension-actor",
+ source: "firefoxExtension",
+ thread: "foo",
+ },
+ {
+ id: "chromeExtension-actor",
+ actor: "chromeExtension-actor",
+ source: "chromeExtension",
+ thread: "foo",
+ },
+]: any);
+
+describe("sources reducer", () => {
+ it("should work", () => {
+ let state = initialSourcesState();
+ state = update(state, {
+ type: "ADD_SOURCE",
+ cx: mockcx,
+ source: makeMockSource(),
+ });
+ expect(getResourceIds(state.sources)).toHaveLength(1);
+ });
+});
+
+describe("sources selectors", () => {
+ it("should return all extensions when chrome preference enabled", () => {
+ prefs.chromeAndExtensionsEnabled = true;
+ let state = initialSourcesState();
+ state = {
+ sources: update(state, {
+ type: "ADD_SOURCES",
+ cx: mockcx,
+ sources: mockedSources,
+ }),
+ sourceActors: undefined,
+ };
+ const insertAction = {
+ type: "INSERT_SOURCE_ACTORS",
+ items: mockSourceActors,
+ };
+ state = makeMockState({
+ sources: update(state.sources, insertAction),
+ sourceActors: updateSourceActors(state.sourceActors, insertAction),
+ threads: initialThreadsState(),
+ });
+ const threadSources = getDisplayedSources(state);
+ expect(Object.values(threadSources.foo)).toHaveLength(3);
+ });
+
+ it("should omit all extensions when chrome preference enabled", () => {
+ prefs.chromeAndExtensionsEnabled = false;
+ let state = initialSourcesState();
+ state = {
+ sources: update(state, {
+ type: "ADD_SOURCES",
+ cx: mockcx,
+ sources: mockedSources,
+ }),
+ sourceActors: undefined,
+ };
+
+ const insertAction = {
+ type: "INSERT_SOURCE_ACTORS",
+ items: mockSourceActors,
+ };
+
+ state = makeMockState({
+ sources: update(state.sources, insertAction),
+ sourceActors: updateSourceActors(state.sourceActors, insertAction),
+ threads: initialThreadsState(),
+ });
+ const threadSources = getDisplayedSources(state);
+ expect(Object.values(threadSources.foo)).toHaveLength(1);
+ });
+});
diff --git a/devtools/client/debugger/src/reducers/tests/ui.spec.js b/devtools/client/debugger/src/reducers/tests/ui.spec.js
new file mode 100644
index 0000000000..a335f0206b
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tests/ui.spec.js
@@ -0,0 +1,35 @@
+/* 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
+declare var describe: (name: string, func: () => void) => void;
+declare var it: (desc: string, func: () => void) => void;
+declare var expect: (value: any) => any;
+
+import { prefs } from "../../utils/prefs";
+import update, { initialUIState } from "../ui";
+
+describe("ui reducer", () => {
+ it("toggle framework grouping to false", () => {
+ const state = initialUIState();
+ const value = false;
+ const updatedState = update(state, {
+ type: "TOGGLE_FRAMEWORK_GROUPING",
+ value,
+ });
+ expect(updatedState.frameworkGroupingOn).toBe(value);
+ expect(prefs.frameworkGroupingOn).toBe(value);
+ });
+
+ it("toggle framework grouping to true", () => {
+ const state = initialUIState();
+ const value = true;
+ const updatedState = update(state, {
+ type: "TOGGLE_FRAMEWORK_GROUPING",
+ value,
+ });
+ expect(updatedState.frameworkGroupingOn).toBe(value);
+ expect(prefs.frameworkGroupingOn).toBe(value);
+ });
+});
diff --git a/devtools/client/debugger/src/reducers/threads.js b/devtools/client/debugger/src/reducers/threads.js
new file mode 100644
index 0000000000..81810595a0
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/threads.js
@@ -0,0 +1,115 @@
+/* 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
+
+/**
+ * Threads reducer
+ * @module reducers/threads
+ */
+
+import { sortBy } from "lodash";
+import { createSelector } from "reselect";
+
+import type { Selector, State } from "./types";
+import type { Thread, ThreadList, Worker } from "../types";
+import type { Action } from "../actions/types";
+
+export type ThreadsState = {
+ threads: ThreadList,
+ traits: Object,
+ isWebExtension: boolean,
+};
+
+export function initialThreadsState(): ThreadsState {
+ return {
+ threads: [],
+ traits: {},
+ isWebExtension: false,
+ };
+}
+
+export default function update(
+ state: ThreadsState = initialThreadsState(),
+ action: Action
+): ThreadsState {
+ switch (action.type) {
+ case "CONNECT":
+ return {
+ ...state,
+ traits: action.traits,
+ isWebExtension: action.isWebExtension,
+ };
+ case "INSERT_THREAD":
+ return {
+ ...state,
+ threads: [...state.threads, action.newThread],
+ };
+
+ case "REMOVE_THREAD":
+ const { oldThread } = action;
+ return {
+ ...state,
+ threads: state.threads.filter(
+ thread => oldThread.actor != thread.actor
+ ),
+ };
+ case "UPDATE_SERVICE_WORKER_STATUS":
+ const { thread, status } = action;
+ return {
+ ...state,
+ threads: state.threads.map(t => {
+ if (t.actor == thread) {
+ return { ...t, serviceWorkerStatus: status };
+ }
+ return t;
+ }),
+ };
+
+ default:
+ return state;
+ }
+}
+
+export const getWorkerCount = (state: State) => getThreads(state).length;
+
+export function getWorkerByThread(state: State, thread: string): ?Worker {
+ return getThreads(state).find(worker => worker.actor == thread);
+}
+
+function isMainThread(thread: Thread) {
+ return thread.isTopLevel;
+}
+
+export function getMainThread(state: State): ?Thread {
+ return state.threads.threads.find(isMainThread);
+}
+
+export function getDebuggeeUrl(state: State): string {
+ return getMainThread(state)?.url || "";
+}
+
+export const getThreads: Selector<Thread[]> = createSelector(
+ state => state.threads.threads,
+ threads => threads.filter(thread => !isMainThread(thread))
+);
+
+export const getAllThreads: Selector<Thread[]> = createSelector(
+ getMainThread,
+ getThreads,
+ (mainThread, threads) =>
+ [mainThread, ...sortBy(threads, thread => thread.name)].filter(Boolean)
+);
+
+export function getThread(state: State, threadActor: string) {
+ return getAllThreads(state).find(thread => thread.actor === threadActor);
+}
+
+// checks if a path begins with a thread actor
+// e.g "server1.conn0.child1/workerTarget22/context1/dbg-workers.glitch.me"
+export function startsWithThreadActor(state: State, path: string): ?string {
+ const threadActors = getAllThreads(state).map(t => t.actor);
+ const match = path.match(new RegExp(`(${threadActors.join("|")})\/(.*)`));
+ return match?.[1];
+}
diff --git a/devtools/client/debugger/src/reducers/types.js b/devtools/client/debugger/src/reducers/types.js
new file mode 100644
index 0000000000..6dc25ea54f
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/types.js
@@ -0,0 +1,71 @@
+/* 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/>. */
+
+/**
+ * Types reducer
+ * @module reducers/types
+ */
+
+// @flow
+
+import type { ASTState } from "./ast";
+import type { BreakpointsState } from "./breakpoints";
+import type { ExpressionState } from "./expressions";
+import type { ThreadsState } from "./threads";
+import type { FileSearchState } from "./file-search";
+import type { PauseState } from "./pause";
+import type { PreviewState } from "./preview";
+import type { PendingBreakpointsState } from "../selectors";
+import type { ProjectTextSearchState } from "./project-text-search";
+import type { SourcesState } from "./sources";
+import type { SourceActorsState } from "./source-actors";
+import type { TabsState } from "./tabs";
+import type { UIState } from "./ui";
+import type { QuickOpenState } from "./quick-open";
+import type { SourceTreeState } from "./source-tree";
+import type { EventListenersState } from "./event-listeners";
+import type { ExceptionState } from "./exceptions";
+import type { URL } from "../types";
+
+export type State = {
+ ast: ASTState,
+ breakpoints: BreakpointsState,
+ exceptions: ExceptionState,
+ expressions: ExpressionState,
+ eventListenerBreakpoints: EventListenersState,
+ threads: ThreadsState,
+ fileSearch: FileSearchState,
+ pause: PauseState,
+ preview: PreviewState,
+ pendingBreakpoints: PendingBreakpointsState,
+ projectTextSearch: ProjectTextSearchState,
+ sources: SourcesState,
+ sourceActors: SourceActorsState,
+ sourceTree: SourceTreeState,
+ tabs: TabsState,
+ ui: UIState,
+ quickOpen: QuickOpenState,
+};
+
+export type Selector<T> = State => T;
+
+export type PendingSelectedLocation = {
+ url: URL,
+ line?: number,
+ column?: number,
+};
+
+export type {
+ SourcesMap,
+ SourcesMapByThread,
+ SourceBase,
+ SourceResourceState,
+ SourceResource,
+} from "./sources";
+export type { ActiveSearchType, OrientationType } from "./ui";
+export type { BreakpointsMap, XHRBreakpointsList } from "./breakpoints";
+export type { Command } from "./pause";
+export type { LoadedSymbols, Symbols } from "./ast";
+export type { Preview } from "./preview";
+export type { Tab, TabList, TabsSources } from "./tabs";
diff --git a/devtools/client/debugger/src/reducers/ui.js b/devtools/client/debugger/src/reducers/ui.js
new file mode 100644
index 0000000000..9f71a6d2dd
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/ui.js
@@ -0,0 +1,234 @@
+/* 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
+/* eslint complexity: ["error", 35]*/
+
+/**
+ * UI reducer
+ * @module reducers/ui
+ */
+
+import { prefs, features } from "../utils/prefs";
+
+import type { Source, SourceLocation, Range } from "../types";
+
+import type { Action, panelPositionType } from "../actions/types";
+
+export type ActiveSearchType = "project" | "file";
+
+export type OrientationType = "horizontal" | "vertical";
+
+export type SelectedPrimaryPaneTabType = "sources" | "outline";
+
+export type UIState = {
+ selectedPrimaryPaneTab: SelectedPrimaryPaneTabType,
+ activeSearch: ?ActiveSearchType,
+ shownSource: ?Source,
+ startPanelCollapsed: boolean,
+ endPanelCollapsed: boolean,
+ frameworkGroupingOn: boolean,
+ orientation: OrientationType,
+ viewport: ?Range,
+ cursorPosition: ?SourceLocation,
+ highlightedLineRange?: {
+ start?: number,
+ end?: number,
+ sourceId?: number,
+ },
+ conditionalPanelLocation: null | SourceLocation,
+ isLogPoint: boolean,
+ inlinePreviewEnabled: boolean,
+ editorWrappingEnabled: boolean,
+ sourceMapsEnabled: boolean,
+ javascriptEnabled: boolean,
+};
+
+export const initialUIState = (): UIState => ({
+ selectedPrimaryPaneTab: "sources",
+ activeSearch: null,
+ shownSource: null,
+ startPanelCollapsed: prefs.startPanelCollapsed,
+ endPanelCollapsed: prefs.endPanelCollapsed,
+ frameworkGroupingOn: prefs.frameworkGroupingOn,
+ highlightedLineRange: undefined,
+ conditionalPanelLocation: null,
+ isLogPoint: false,
+ orientation: "horizontal",
+ viewport: null,
+ cursorPosition: null,
+ inlinePreviewEnabled: features.inlinePreview,
+ editorWrappingEnabled: prefs.editorWrapping,
+ sourceMapsEnabled: prefs.clientSourceMapsEnabled,
+ javascriptEnabled: true,
+});
+
+function update(state: UIState = initialUIState(), action: Action): UIState {
+ switch (action.type) {
+ case "TOGGLE_ACTIVE_SEARCH": {
+ return { ...state, activeSearch: action.value };
+ }
+
+ case "TOGGLE_FRAMEWORK_GROUPING": {
+ prefs.frameworkGroupingOn = action.value;
+ return { ...state, frameworkGroupingOn: action.value };
+ }
+
+ case "TOGGLE_INLINE_PREVIEW": {
+ features.inlinePreview = action.value;
+ return { ...state, inlinePreviewEnabled: action.value };
+ }
+
+ case "TOGGLE_EDITOR_WRAPPING": {
+ prefs.editorWrapping = action.value;
+ return { ...state, editorWrappingEnabled: action.value };
+ }
+
+ case "TOGGLE_JAVASCRIPT_ENABLED": {
+ return { ...state, javascriptEnabled: action.value };
+ }
+
+ case "TOGGLE_SOURCE_MAPS_ENABLED": {
+ prefs.clientSourceMapsEnabled = action.value;
+ return { ...state, sourceMapsEnabled: action.value };
+ }
+
+ case "SET_ORIENTATION": {
+ return { ...state, orientation: action.orientation };
+ }
+
+ case "SHOW_SOURCE": {
+ return { ...state, shownSource: action.source };
+ }
+
+ case "TOGGLE_PANE": {
+ if (action.position == "start") {
+ prefs.startPanelCollapsed = action.paneCollapsed;
+ return { ...state, startPanelCollapsed: action.paneCollapsed };
+ }
+
+ prefs.endPanelCollapsed = action.paneCollapsed;
+ return { ...state, endPanelCollapsed: action.paneCollapsed };
+ }
+
+ case "HIGHLIGHT_LINES":
+ const { start, end, sourceId } = action.location;
+ let lineRange = {};
+
+ if (start && end && sourceId) {
+ lineRange = { start, end, sourceId };
+ }
+
+ return { ...state, highlightedLineRange: lineRange };
+
+ case "CLOSE_QUICK_OPEN":
+ case "CLEAR_HIGHLIGHT_LINES":
+ return { ...state, highlightedLineRange: {} };
+
+ case "OPEN_CONDITIONAL_PANEL":
+ return {
+ ...state,
+ conditionalPanelLocation: action.location,
+ isLogPoint: action.log,
+ };
+
+ case "CLOSE_CONDITIONAL_PANEL":
+ return { ...state, conditionalPanelLocation: null };
+
+ case "SET_PRIMARY_PANE_TAB":
+ return { ...state, selectedPrimaryPaneTab: action.tabName };
+
+ case "CLOSE_PROJECT_SEARCH": {
+ if (state.activeSearch === "project") {
+ return { ...state, activeSearch: null };
+ }
+ return state;
+ }
+
+ case "SET_VIEWPORT": {
+ return { ...state, viewport: action.viewport };
+ }
+
+ case "SET_CURSOR_POSITION": {
+ return { ...state, cursorPosition: action.cursorPosition };
+ }
+
+ case "NAVIGATE": {
+ return { ...state, activeSearch: null, highlightedLineRange: {} };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+// NOTE: we'd like to have the app state fully typed
+// https://github.com/firefox-devtools/debugger/blob/master/src/reducers/sources.js#L179-L185
+type OuterState = { ui: UIState };
+
+export function getSelectedPrimaryPaneTab(
+ state: OuterState
+): SelectedPrimaryPaneTabType {
+ return state.ui.selectedPrimaryPaneTab;
+}
+
+export function getActiveSearch(state: OuterState): ?ActiveSearchType {
+ return state.ui.activeSearch;
+}
+
+export function getFrameworkGroupingState(state: OuterState): boolean {
+ return state.ui.frameworkGroupingOn;
+}
+
+export function getShownSource(state: OuterState): ?Source {
+ return state.ui.shownSource;
+}
+
+export function getPaneCollapse(
+ state: OuterState,
+ position: panelPositionType
+): boolean {
+ if (position == "start") {
+ return state.ui.startPanelCollapsed;
+ }
+
+ return state.ui.endPanelCollapsed;
+}
+
+export function getHighlightedLineRange(state: OuterState) {
+ return state.ui.highlightedLineRange;
+}
+
+export function getConditionalPanelLocation(
+ state: OuterState
+): null | SourceLocation {
+ return state.ui.conditionalPanelLocation;
+}
+
+export function getLogPointStatus(state: OuterState): boolean {
+ return state.ui.isLogPoint;
+}
+
+export function getOrientation(state: OuterState): OrientationType {
+ return state.ui.orientation;
+}
+
+export function getViewport(state: OuterState) {
+ return state.ui.viewport;
+}
+
+export function getCursorPosition(state: OuterState) {
+ return state.ui.cursorPosition;
+}
+
+export function getInlinePreview(state: OuterState) {
+ return state.ui.inlinePreviewEnabled;
+}
+
+export function getEditorWrapping(state: OuterState) {
+ return state.ui.editorWrappingEnabled;
+}
+
+export default update;