summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/debugger/src/actions
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/actions')
-rw-r--r--devtools/client/debugger/src/actions/README.md26
-rw-r--r--devtools/client/debugger/src/actions/ast/index.js7
-rw-r--r--devtools/client/debugger/src/actions/ast/moz.build11
-rw-r--r--devtools/client/debugger/src/actions/ast/setInScopeLines.js89
-rw-r--r--devtools/client/debugger/src/actions/ast/tests/__snapshots__/setInScopeLines.spec.js.snap16
-rw-r--r--devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js69
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js249
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/index.js421
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/modify.js395
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/moz.build14
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/remapLocations.js29
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js194
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap131
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js112
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js487
-rw-r--r--devtools/client/debugger/src/actions/event-listeners.js85
-rw-r--r--devtools/client/debugger/src/actions/exceptions.js46
-rw-r--r--devtools/client/debugger/src/actions/expressions.js210
-rw-r--r--devtools/client/debugger/src/actions/file-search.js215
-rw-r--r--devtools/client/debugger/src/actions/index.js49
-rw-r--r--devtools/client/debugger/src/actions/moz.build30
-rw-r--r--devtools/client/debugger/src/actions/navigation.js77
-rw-r--r--devtools/client/debugger/src/actions/pause/breakOnNext.js24
-rw-r--r--devtools/client/debugger/src/actions/pause/commands.js150
-rw-r--r--devtools/client/debugger/src/actions/pause/continueToHere.js63
-rw-r--r--devtools/client/debugger/src/actions/pause/expandScopes.js25
-rw-r--r--devtools/client/debugger/src/actions/pause/fetchFrames.js27
-rw-r--r--devtools/client/debugger/src/actions/pause/fetchScopes.js34
-rw-r--r--devtools/client/debugger/src/actions/pause/highlightCalls.js99
-rw-r--r--devtools/client/debugger/src/actions/pause/index.js38
-rw-r--r--devtools/client/debugger/src/actions/pause/inlinePreview.js238
-rw-r--r--devtools/client/debugger/src/actions/pause/mapDisplayNames.js60
-rw-r--r--devtools/client/debugger/src/actions/pause/mapFrames.js177
-rw-r--r--devtools/client/debugger/src/actions/pause/mapScopes.js216
-rw-r--r--devtools/client/debugger/src/actions/pause/moz.build27
-rw-r--r--devtools/client/debugger/src/actions/pause/pauseOnExceptions.js37
-rw-r--r--devtools/client/debugger/src/actions/pause/paused.js64
-rw-r--r--devtools/client/debugger/src/actions/pause/previewPausedLocation.js44
-rw-r--r--devtools/client/debugger/src/actions/pause/resumed.js33
-rw-r--r--devtools/client/debugger/src/actions/pause/selectFrame.js43
-rw-r--r--devtools/client/debugger/src/actions/pause/skipPausing.js36
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap10
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/pause.spec.js383
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js26
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js20
-rw-r--r--devtools/client/debugger/src/actions/preview.js231
-rw-r--r--devtools/client/debugger/src/actions/project-text-search.js136
-rw-r--r--devtools/client/debugger/src/actions/quick-open.js25
-rw-r--r--devtools/client/debugger/src/actions/source-actors.js84
-rw-r--r--devtools/client/debugger/src/actions/source-tree.js14
-rw-r--r--devtools/client/debugger/src/actions/sources/blackbox.js106
-rw-r--r--devtools/client/debugger/src/actions/sources/breakableLines.js56
-rw-r--r--devtools/client/debugger/src/actions/sources/index.js12
-rw-r--r--devtools/client/debugger/src/actions/sources/loadSourceText.js176
-rw-r--r--devtools/client/debugger/src/actions/sources/moz.build17
-rw-r--r--devtools/client/debugger/src/actions/sources/newSources.js400
-rw-r--r--devtools/client/debugger/src/actions/sources/prettyPrint.js171
-rw-r--r--devtools/client/debugger/src/actions/sources/select.js240
-rw-r--r--devtools/client/debugger/src/actions/sources/symbols.js63
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/__snapshots__/newSources.spec.js.snap9
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/__snapshots__/prettyPrint.spec.js.snap29
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js39
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js275
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/newSources.spec.js198
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js69
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/querystrings.spec.js38
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/select.spec.js307
-rw-r--r--devtools/client/debugger/src/actions/tabs.js109
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap83
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap11
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap112
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap3
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap180
-rw-r--r--devtools/client/debugger/src/actions/tests/ast.spec.js128
-rw-r--r--devtools/client/debugger/src/actions/tests/expressions.spec.js186
-rw-r--r--devtools/client/debugger/src/actions/tests/file-search.spec.js66
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/immutable.js2
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js7
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js5
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/scopes.js11
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/breakpoints.js64
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js55
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/readFixture.js16
-rw-r--r--devtools/client/debugger/src/actions/tests/navigation.spec.js108
-rw-r--r--devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js439
-rw-r--r--devtools/client/debugger/src/actions/tests/preview.spec.js215
-rw-r--r--devtools/client/debugger/src/actions/tests/project-text-search.spec.js161
-rw-r--r--devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js95
-rw-r--r--devtools/client/debugger/src/actions/tests/source-tree.spec.js19
-rw-r--r--devtools/client/debugger/src/actions/tests/tabs.spec.js138
-rw-r--r--devtools/client/debugger/src/actions/tests/ui.spec.js75
-rw-r--r--devtools/client/debugger/src/actions/threads.js56
-rw-r--r--devtools/client/debugger/src/actions/toolbox.js48
-rw-r--r--devtools/client/debugger/src/actions/types/ASTAction.js25
-rw-r--r--devtools/client/debugger/src/actions/types/BreakpointAction.js65
-rw-r--r--devtools/client/debugger/src/actions/types/PauseAction.js181
-rw-r--r--devtools/client/debugger/src/actions/types/PreviewAction.js22
-rw-r--r--devtools/client/debugger/src/actions/types/SourceAction.js103
-rw-r--r--devtools/client/debugger/src/actions/types/SourceActorAction.js50
-rw-r--r--devtools/client/debugger/src/actions/types/UIAction.js101
-rw-r--r--devtools/client/debugger/src/actions/types/index.js238
-rw-r--r--devtools/client/debugger/src/actions/ui.js282
-rw-r--r--devtools/client/debugger/src/actions/utils/create-store.js83
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/context.js37
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/log.js116
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/moz.build15
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/promise.js122
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/thunk.js26
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/timing.js28
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/wait-service.js65
-rw-r--r--devtools/client/debugger/src/actions/utils/moz.build12
111 files changed, 11494 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/README.md b/devtools/client/debugger/src/actions/README.md
new file mode 100644
index 0000000000..d919247838
--- /dev/null
+++ b/devtools/client/debugger/src/actions/README.md
@@ -0,0 +1,26 @@
+## Actions
+
+### Best Practices
+
+#### Scheduling Async Actions
+
+There are several use-cases with async actions that involve scheduling:
+
+* we do one action and cancel subsequent actions
+* we do one action and subsequent calls wait on the initial call
+* we start an action and show a loading state
+
+If you want to wait on subsequent calls you need to store action promises.
+[ex][req]
+
+If you just want to cancel subsequent calls, you can keep track of a pending
+state in the store. [ex][state]
+
+The advantage of adding the pending state to the store is that we can use that
+in the UI:
+
+* disable/hide the pretty print button
+* show a progress ui
+
+[req]: https://github.com/firefox-devtools/debugger/blob/master/src/actions/sources/loadSourceText.js
+[state]: https://github.com/firefox-devtools/debugger/blob/master/src/reducers/sources.js
diff --git a/devtools/client/debugger/src/actions/ast/index.js b/devtools/client/debugger/src/actions/ast/index.js
new file mode 100644
index 0000000000..0a8d0fee7a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/index.js
@@ -0,0 +1,7 @@
+/* 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
+
+export { setInScopeLines } from "./setInScopeLines";
diff --git a/devtools/client/debugger/src/actions/ast/moz.build b/devtools/client/debugger/src/actions/ast/moz.build
new file mode 100644
index 0000000000..5b0152d2ad
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/moz.build
@@ -0,0 +1,11 @@
+# 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(
+ "index.js",
+ "setInScopeLines.js",
+)
diff --git a/devtools/client/debugger/src/actions/ast/setInScopeLines.js b/devtools/client/debugger/src/actions/ast/setInScopeLines.js
new file mode 100644
index 0000000000..ee87a58766
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/setInScopeLines.js
@@ -0,0 +1,89 @@
+/* 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 {
+ hasInScopeLines,
+ getSourceWithContent,
+ getVisibleSelectedFrame,
+} from "../../selectors";
+
+import { getSourceLineCount } from "../../utils/source";
+
+import { range, flatMap, uniq, without } from "lodash";
+import { isFulfilled } from "../../utils/async-value";
+
+import type { AstLocation } from "../../workers/parser";
+import type { ThunkArgs } from "../types";
+import type { Context, SourceLocation } from "../../types";
+
+function getOutOfScopeLines(
+ outOfScopeLocations: ?(AstLocation[])
+): ?(AstLocation[]) {
+ if (!outOfScopeLocations) {
+ return null;
+ }
+
+ return uniq(
+ flatMap(outOfScopeLocations, location =>
+ range(location.start.line, location.end.line)
+ )
+ );
+}
+
+async function getInScopeLines(
+ cx: Context,
+ location: SourceLocation,
+ { dispatch, getState, parser }: ThunkArgs
+) {
+ const source = getSourceWithContent(getState(), location.sourceId);
+
+ let locations = null;
+ if (location.line && source && !source.isWasm) {
+ locations = await parser.findOutOfScopeLocations(
+ source.id,
+ ((location: any): parser.AstPosition)
+ );
+ }
+
+ const linesOutOfScope = getOutOfScopeLines(locations);
+ const sourceNumLines =
+ !source.content || !isFulfilled(source.content)
+ ? 0
+ : getSourceLineCount(source.content.value);
+
+ const sourceLines = range(1, sourceNumLines + 1);
+
+ return !linesOutOfScope
+ ? sourceLines
+ : without(sourceLines, ...linesOutOfScope);
+}
+
+export function setInScopeLines(cx: Context) {
+ return async (thunkArgs: ThunkArgs) => {
+ const { getState, dispatch } = thunkArgs;
+ const visibleFrame = getVisibleSelectedFrame(getState());
+
+ if (!visibleFrame) {
+ return;
+ }
+
+ const { location } = visibleFrame;
+ const { content } = getSourceWithContent(getState(), location.sourceId);
+
+ if (hasInScopeLines(getState(), location) || !content) {
+ return;
+ }
+
+ const lines = await getInScopeLines(cx, location, thunkArgs);
+
+ dispatch({
+ type: "IN_SCOPE_LINES",
+ cx,
+ location,
+ lines,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/ast/tests/__snapshots__/setInScopeLines.spec.js.snap b/devtools/client/debugger/src/actions/ast/tests/__snapshots__/setInScopeLines.spec.js.snap
new file mode 100644
index 0000000000..1b9befc31b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/tests/__snapshots__/setInScopeLines.spec.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`getInScopeLine with selected line 1`] = `
+Array [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 10,
+ 11,
+ 12,
+]
+`;
diff --git a/devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js b/devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
new file mode 100644
index 0000000000..b3b483222b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
@@ -0,0 +1,69 @@
+/* eslint max-nested-callbacks: ["error", 6] */
+/* 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 readFixture from "../../tests/helpers/readFixture";
+
+import { makeMockFrame, makeMockSource } from "../../../utils/test-mockup";
+import {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ waitForState,
+} from "../../../utils/test-head";
+
+const { getInScopeLines } = selectors;
+
+const sourceTexts = {
+ "scopes.js": readFixture("scopes.js"),
+};
+
+const mockCommandClient = {
+ sourceContents: async ({ source }) => ({
+ source: sourceTexts[source],
+ contentType: "text/javascript",
+ }),
+ evaluateExpressions: async () => {},
+ getFrameScopes: async () => {},
+ getFrames: async () => [],
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+};
+
+describe("getInScopeLine", () => {
+ it("with selected line", async () => {
+ const client = { ...mockCommandClient };
+ const store = createStore(client);
+ const { dispatch, getState } = store;
+ const source = makeMockSource("scopes.js", "scopes.js");
+ const frame = makeMockFrame("scopes-4", source);
+ client.getFrames = async () => [frame];
+
+ await dispatch(actions.newGeneratedSource(makeSource("scopes.js")));
+
+ await dispatch(
+ actions.selectLocation(selectors.getContext(getState()), {
+ sourceId: "scopes.js",
+ line: 5,
+ })
+ );
+
+ await dispatch(
+ actions.paused({
+ thread: "FakeThread",
+ why: { type: "debuggerStatement" },
+ frame,
+ })
+ );
+ await dispatch(actions.setInScopeLines(selectors.getContext(getState())));
+
+ await waitForState(store, state => getInScopeLines(state, frame.location));
+
+ const lines = getInScopeLines(getState(), frame.location);
+
+ expect(lines).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
new file mode 100644
index 0000000000..e28c325f01
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -0,0 +1,249 @@
+/* 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 {
+ isOriginalId,
+ isGeneratedId,
+ originalToGeneratedId,
+} from "devtools-source-map";
+import { uniqBy, zip } from "lodash";
+
+import {
+ getSource,
+ getSourceFromId,
+ getBreakpointPositionsForSource,
+ getSourceActorsForSource,
+} from "../../selectors";
+
+import type {
+ MappedLocation,
+ Range,
+ Source,
+ SourceLocation,
+ SourceId,
+ BreakpointPositions,
+ Context,
+} from "../../types";
+
+import { makeBreakpointId } from "../../utils/breakpoint";
+import {
+ memoizeableAction,
+ type MemoizedAction,
+} from "../../utils/memoizableAction";
+import { fulfilled } from "../../utils/async-value";
+import type { ThunkArgs } from "../../actions/types";
+import { loadSourceActorBreakpointColumns } from "../source-actors";
+
+type LocationsList = {
+ number: ?(number[]),
+};
+
+async function mapLocations(
+ generatedLocations: SourceLocation[],
+ { sourceMaps }: ThunkArgs
+): Promise<MappedLocation[]> {
+ if (generatedLocations.length == 0) {
+ return [];
+ }
+
+ const originalLocations = await sourceMaps.getOriginalLocations(
+ generatedLocations
+ );
+
+ return zip(
+ originalLocations,
+ generatedLocations
+ ).map(([location, generatedLocation]) => ({ location, generatedLocation }));
+}
+
+// Filter out positions, that are not in the original source Id
+function filterBySource(
+ positions: MappedLocation[],
+ sourceId: SourceId
+): MappedLocation[] {
+ if (!isOriginalId(sourceId)) {
+ return positions;
+ }
+ return positions.filter(position => position.location.sourceId == sourceId);
+}
+
+function filterByUniqLocation(positions: MappedLocation[]): MappedLocation[] {
+ return uniqBy(positions, ({ location }) => makeBreakpointId(location));
+}
+
+function convertToList(
+ results: LocationsList,
+ source: Source
+): SourceLocation[] {
+ const { id, url } = source;
+ const positions = [];
+
+ for (const line in results) {
+ for (const column of results[line]) {
+ positions.push({
+ line: Number(line),
+ column,
+ sourceId: id,
+ sourceUrl: url,
+ });
+ }
+ }
+
+ return positions;
+}
+
+function groupByLine(results: MappedLocation[], sourceId: SourceId, line) {
+ const isOriginal = isOriginalId(sourceId);
+ const positions = {};
+
+ // Ensure that we have an entry for the line fetched
+ if (typeof line === "number") {
+ positions[line] = [];
+ }
+
+ for (const result of results) {
+ const location = isOriginal ? result.location : result.generatedLocation;
+
+ if (!positions[location.line]) {
+ positions[location.line] = [];
+ }
+
+ positions[location.line].push(result);
+ }
+
+ return positions;
+}
+
+async function _setBreakpointPositions(
+ cx: Context,
+ sourceId: SourceId,
+ line,
+ thunkArgs: ThunkArgs
+): Promise<void> {
+ const { client, dispatch, getState, sourceMaps } = thunkArgs;
+ let generatedSource = getSource(getState(), sourceId);
+ if (!generatedSource) {
+ return;
+ }
+
+ const results = {};
+ if (isOriginalId(sourceId)) {
+ // Explicitly typing ranges is required to work around the following issue
+ // https://github.com/facebook/flow/issues/5294
+ const ranges: Range[] = await sourceMaps.getGeneratedRangesForOriginal(
+ sourceId,
+ true
+ );
+ const generatedSourceId = originalToGeneratedId(sourceId);
+ generatedSource = getSourceFromId(getState(), generatedSourceId);
+
+ // Note: While looping here may not look ideal, in the vast majority of
+ // cases, the number of ranges here should be very small, and is quite
+ // likely to only be a single range.
+ for (const range of ranges) {
+ // Wrap infinite end positions to the next line to keep things simple
+ // and because we know we don't care about the end-line whitespace
+ // in this case.
+ if (range.end.column === Infinity) {
+ range.end = {
+ line: range.end.line + 1,
+ column: 0,
+ };
+ }
+
+ const actorBps = await Promise.all(
+ getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
+ client.getSourceActorBreakpointPositions(actor, range)
+ )
+ );
+
+ for (const actorPositions of actorBps) {
+ for (const rangeLine of Object.keys(actorPositions)) {
+ let columns = actorPositions[parseInt(rangeLine, 10)];
+ const existing = results[rangeLine];
+ if (existing) {
+ columns = [...new Set([...existing, ...columns])];
+ }
+
+ results[rangeLine] = columns;
+ }
+ }
+ }
+ } else {
+ if (typeof line !== "number") {
+ throw new Error("Line is required for generated sources");
+ }
+
+ const actorColumns = await Promise.all(
+ getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
+ dispatch(loadSourceActorBreakpointColumns({ id: actor.id, line, cx }))
+ )
+ );
+
+ for (const columns of actorColumns) {
+ results[line] = (results[line] || []).concat(columns);
+ }
+ }
+
+ let positions = convertToList(results, generatedSource);
+ positions = await mapLocations(positions, thunkArgs);
+
+ positions = filterBySource(positions, sourceId);
+ positions = filterByUniqLocation(positions);
+ positions = groupByLine(positions, sourceId, line);
+
+ const source = getSource(getState(), sourceId);
+ // NOTE: it's possible that the source was removed during a navigate
+ if (!source) {
+ return;
+ }
+
+ dispatch({
+ type: "ADD_BREAKPOINT_POSITIONS",
+ cx,
+ source,
+ positions,
+ });
+}
+
+function generatedSourceActorKey(state, sourceId: SourceId): string {
+ const generatedSource = getSource(
+ state,
+ isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
+ );
+ const actors = generatedSource
+ ? getSourceActorsForSource(state, generatedSource.id).map(
+ ({ actor }) => actor
+ )
+ : [];
+ return [sourceId, ...actors].join(":");
+}
+
+export const setBreakpointPositions: MemoizedAction<
+ {| cx: Context, sourceId: SourceId, line?: number |},
+ ?BreakpointPositions
+> = memoizeableAction("setBreakpointPositions", {
+ getValue: ({ sourceId, line }, { getState }) => {
+ const positions = getBreakpointPositionsForSource(getState(), sourceId);
+ if (!positions) {
+ return null;
+ }
+
+ if (isGeneratedId(sourceId) && line && !positions[line]) {
+ // We always return the full position dataset, but if a given line is
+ // not available, we treat the whole set as loading.
+ return null;
+ }
+
+ return fulfilled(positions);
+ },
+ createKey({ sourceId, line }, { getState }) {
+ const key = generatedSourceActorKey(getState(), sourceId);
+ return isGeneratedId(sourceId) && line ? `${key}-${line}` : key;
+ },
+ action: async ({ cx, sourceId, line }, thunkArgs) =>
+ _setBreakpointPositions(cx, sourceId, line, thunkArgs),
+});
diff --git a/devtools/client/debugger/src/actions/breakpoints/index.js b/devtools/client/debugger/src/actions/breakpoints/index.js
new file mode 100644
index 0000000000..7d8e2ca737
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/index.js
@@ -0,0 +1,421 @@
+/* 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
+
+/**
+ * Redux actions for breakpoints
+ * @module actions/breakpoints
+ */
+
+import { PROMISE } from "../utils/middleware/promise";
+import {
+ getBreakpointsList,
+ getXHRBreakpoints,
+ getSelectedSource,
+ getBreakpointAtLocation,
+ getBreakpointsForSource,
+ getBreakpointsAtLine,
+} from "../../selectors";
+import { createXHRBreakpoint } from "../../utils/breakpoint";
+import {
+ addBreakpoint,
+ removeBreakpoint,
+ enableBreakpoint,
+ disableBreakpoint,
+} from "./modify";
+import remapLocations from "./remapLocations";
+
+// this will need to be changed so that addCLientBreakpoint is removed
+
+import type { ThunkArgs } from "../types";
+import type {
+ Breakpoint,
+ Source,
+ SourceLocation,
+ SourceId,
+ XHRBreakpoint,
+ Context,
+} from "../../types";
+
+export * from "./breakpointPositions";
+export * from "./modify";
+export * from "./syncBreakpoint";
+
+export function addHiddenBreakpoint(cx: Context, location: SourceLocation) {
+ return ({ dispatch }: ThunkArgs) => {
+ return dispatch(addBreakpoint(cx, location, { hidden: true }));
+ };
+}
+
+/**
+ * Disable all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function disableBreakpointsInSource(cx: Context, source: Source) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ const breakpoints = getBreakpointsForSource(getState(), source.id);
+ for (const breakpoint of breakpoints) {
+ if (!breakpoint.disabled) {
+ dispatch(disableBreakpoint(cx, breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Enable all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function enableBreakpointsInSource(cx: Context, source: Source) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ const breakpoints = getBreakpointsForSource(getState(), source.id);
+ for (const breakpoint of breakpoints) {
+ if (breakpoint.disabled) {
+ dispatch(enableBreakpoint(cx, breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Toggle All Breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function toggleAllBreakpoints(
+ cx: Context,
+ shouldDisableBreakpoints: boolean
+) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ const breakpoints = getBreakpointsList(getState());
+
+ for (const breakpoint of breakpoints) {
+ if (shouldDisableBreakpoints) {
+ dispatch(disableBreakpoint(cx, breakpoint));
+ } else {
+ dispatch(enableBreakpoint(cx, breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Toggle Breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function toggleBreakpoints(
+ cx: Context,
+ shouldDisableBreakpoints: boolean,
+ breakpoints: Breakpoint[]
+) {
+ return async ({ dispatch }: ThunkArgs) => {
+ const promises = breakpoints.map(breakpoint =>
+ shouldDisableBreakpoints
+ ? dispatch(disableBreakpoint(cx, breakpoint))
+ : dispatch(enableBreakpoint(cx, breakpoint))
+ );
+
+ await Promise.all(promises);
+ };
+}
+
+export function toggleBreakpointsAtLine(
+ cx: Context,
+ shouldDisableBreakpoints: boolean,
+ line: number
+) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const breakpoints = getBreakpointsAtLine(getState(), line);
+ return dispatch(
+ toggleBreakpoints(cx, shouldDisableBreakpoints, breakpoints)
+ );
+ };
+}
+
+/**
+ * Removes all breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeAllBreakpoints(cx: Context) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const breakpointList = getBreakpointsList(getState());
+ await Promise.all(
+ breakpointList.map(bp => dispatch(removeBreakpoint(cx, bp)))
+ );
+ dispatch({ type: "REMOVE_BREAKPOINTS" });
+ };
+}
+
+/**
+ * Removes breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpoints(cx: Context, breakpoints: Breakpoint[]) {
+ return async ({ dispatch }: ThunkArgs) => {
+ return Promise.all(
+ breakpoints.map(bp => dispatch(removeBreakpoint(cx, bp)))
+ );
+ };
+}
+
+/**
+ * Removes all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpointsInSource(cx: Context, source: Source) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ const breakpoints = getBreakpointsForSource(getState(), source.id);
+ for (const breakpoint of breakpoints) {
+ dispatch(removeBreakpoint(cx, breakpoint));
+ }
+ };
+}
+
+export function remapBreakpoints(cx: Context, sourceId: SourceId) {
+ return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => {
+ const breakpoints = getBreakpointsForSource(getState(), sourceId);
+ const newBreakpoints = await remapLocations(
+ breakpoints,
+ sourceId,
+ sourceMaps
+ );
+
+ // Normally old breakpoints will be clobbered if we re-add them, but when
+ // remapping we have changed the source maps and the old breakpoints will
+ // have different locations than the new ones. Manually remove the
+ // old breakpoints before adding the new ones.
+ for (const bp of breakpoints) {
+ dispatch(removeBreakpoint(cx, bp));
+ }
+
+ for (const bp of newBreakpoints) {
+ await dispatch(addBreakpoint(cx, bp.location, bp.options, bp.disabled));
+ }
+ };
+}
+
+export function toggleBreakpointAtLine(cx: Context, line: number) {
+ return ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const state = getState();
+ const selectedSource = getSelectedSource(state);
+
+ if (!selectedSource) {
+ return;
+ }
+
+ const bp = getBreakpointAtLocation(state, { line, column: undefined });
+ if (bp) {
+ return dispatch(removeBreakpoint(cx, bp));
+ }
+ return dispatch(
+ addBreakpoint(cx, {
+ sourceId: selectedSource.id,
+ sourceUrl: selectedSource.url,
+ line,
+ })
+ );
+ };
+}
+
+export function addBreakpointAtLine(
+ cx: Context,
+ line: number,
+ shouldLog: boolean = false,
+ disabled: boolean = false
+) {
+ return ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const state = getState();
+ const source = getSelectedSource(state);
+
+ if (!source) {
+ return;
+ }
+ const breakpointLocation = {
+ sourceId: source.id,
+ sourceUrl: source.url,
+ column: undefined,
+ line,
+ };
+
+ const options = {};
+ if (shouldLog) {
+ options.logValue = "displayName";
+ }
+
+ return dispatch(addBreakpoint(cx, breakpointLocation, options, disabled));
+ };
+}
+
+export function removeBreakpointsAtLine(
+ cx: Context,
+ sourceId: SourceId,
+ line: number
+) {
+ return ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const breakpointsAtLine = getBreakpointsForSource(
+ getState(),
+ sourceId,
+ line
+ );
+ return dispatch(removeBreakpoints(cx, breakpointsAtLine));
+ };
+}
+
+export function disableBreakpointsAtLine(
+ cx: Context,
+ sourceId: SourceId,
+ line: number
+) {
+ return ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const breakpointsAtLine = getBreakpointsForSource(
+ getState(),
+ sourceId,
+ line
+ );
+ return dispatch(toggleBreakpoints(cx, true, breakpointsAtLine));
+ };
+}
+
+export function enableBreakpointsAtLine(
+ cx: Context,
+ sourceId: SourceId,
+ line: number
+) {
+ return ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const breakpointsAtLine = getBreakpointsForSource(
+ getState(),
+ sourceId,
+ line
+ );
+ return dispatch(toggleBreakpoints(cx, false, breakpointsAtLine));
+ };
+}
+
+export function toggleDisabledBreakpoint(cx: Context, breakpoint: Breakpoint) {
+ return ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ if (!breakpoint.disabled) {
+ return dispatch(disableBreakpoint(cx, breakpoint));
+ }
+ return dispatch(enableBreakpoint(cx, breakpoint));
+ };
+}
+
+export function enableXHRBreakpoint(index: number, bp?: XHRBreakpoint) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = bp || xhrBreakpoints[index];
+ const enabledBreakpoint = {
+ ...breakpoint,
+ disabled: false,
+ };
+
+ return dispatch({
+ type: "ENABLE_XHR_BREAKPOINT",
+ breakpoint: enabledBreakpoint,
+ index,
+ [PROMISE]: client.setXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
+
+export function disableXHRBreakpoint(index: number, bp?: XHRBreakpoint) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = bp || xhrBreakpoints[index];
+ const disabledBreakpoint = {
+ ...breakpoint,
+ disabled: true,
+ };
+
+ return dispatch({
+ type: "DISABLE_XHR_BREAKPOINT",
+ breakpoint: disabledBreakpoint,
+ index,
+ [PROMISE]: client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
+
+export function updateXHRBreakpoint(
+ index: number,
+ path: string,
+ method: string
+) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = xhrBreakpoints[index];
+
+ const updatedBreakpoint = {
+ ...breakpoint,
+ path,
+ method,
+ text: L10N.getFormatStr("xhrBreakpoints.item.label", path),
+ };
+
+ return dispatch({
+ type: "UPDATE_XHR_BREAKPOINT",
+ breakpoint: updatedBreakpoint,
+ index,
+ [PROMISE]: Promise.all([
+ client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ client.setXHRBreakpoint(path, method),
+ ]),
+ });
+ };
+}
+export function togglePauseOnAny() {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const index = xhrBreakpoints.findIndex(({ path }) => path.length === 0);
+ if (index < 0) {
+ return dispatch(setXHRBreakpoint("", "ANY"));
+ }
+
+ const bp = xhrBreakpoints[index];
+ if (bp.disabled) {
+ return dispatch(enableXHRBreakpoint(index, bp));
+ }
+
+ return dispatch(disableXHRBreakpoint(index, bp));
+ };
+}
+
+export function setXHRBreakpoint(path: string, method: string) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const breakpoint = createXHRBreakpoint(path, method);
+
+ return dispatch({
+ type: "SET_XHR_BREAKPOINT",
+ breakpoint,
+ [PROMISE]: client.setXHRBreakpoint(path, method),
+ });
+ };
+}
+
+export function removeXHRBreakpoint(index: number) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = xhrBreakpoints[index];
+ return dispatch({
+ type: "REMOVE_XHR_BREAKPOINT",
+ breakpoint,
+ index,
+ [PROMISE]: client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/modify.js b/devtools/client/debugger/src/actions/breakpoints/modify.js
new file mode 100644
index 0000000000..9979380d07
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/modify.js
@@ -0,0 +1,395 @@
+/* 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 {
+ makeBreakpointLocation,
+ makeBreakpointId,
+ getASTLocation,
+} from "../../utils/breakpoint";
+
+import {
+ getBreakpoint,
+ getBreakpointPositionsForLocation,
+ getFirstBreakpointPosition,
+ getSymbols,
+ getSource,
+ getSourceContent,
+ getBreakpointsList,
+ getPendingBreakpointList,
+ isMapScopesEnabled,
+} from "../../selectors";
+
+import { setBreakpointPositions } from "./breakpointPositions";
+import { setSkipPausing } from "../pause/skipPausing";
+
+import { PROMISE } from "../utils/middleware/promise";
+import { recordEvent } from "../../utils/telemetry";
+import { comparePosition } from "../../utils/location";
+import { getTextAtPosition } from "../../utils/source";
+import { getMappedScopesForLocation } from "../pause/mapScopes";
+import { isOriginalSource } from "../../utils/source-maps";
+import { validateNavigateContext } from "../../utils/context";
+
+import type { ThunkArgs } from "../types";
+import type {
+ Breakpoint,
+ BreakpointOptions,
+ BreakpointPosition,
+ SourceLocation,
+ Context,
+} from "../../types";
+import type { ParserDispatcher } from "../../workers/parser";
+import type { State } from "../../reducers/types";
+
+// This file has the primitive operations used to modify individual breakpoints
+// and keep them in sync with the breakpoints installed on server threads. These
+// are collected here to make it easier to preserve the following invariant:
+//
+// Breakpoints are included in reducer state iff they are disabled or requests
+// have been dispatched to set them in all server threads.
+//
+// To maintain this property, updates to the reducer and installed breakpoints
+// must happen with no intervening await. Using await allows other operations to
+// modify the breakpoint state in the interim and potentially cause breakpoint
+// state to go out of sync.
+//
+// The reducer is optimistically updated when users set or remove a breakpoint,
+// but it might take a little while before the breakpoints have been set or
+// removed in each thread. Once all outstanding requests sent to a thread have
+// been processed, the reducer and server threads will be in sync.
+//
+// There is another exception to the above invariant when first connecting to
+// the server: breakpoints have been installed on all generated locations in the
+// pending breakpoints, but no breakpoints have been added to the reducer. When
+// a matching source appears, either the server breakpoint will be removed or a
+// breakpoint will be added to the reducer, to restore the above invariant.
+// See syncBreakpoint.js for more.
+
+async function clientSetBreakpoint(
+ client,
+ cx,
+ { getState, dispatch }: ThunkArgs,
+ breakpoint: Breakpoint
+) {
+ const breakpointLocation = makeBreakpointLocation(
+ getState(),
+ breakpoint.generatedLocation
+ );
+ const shouldMapBreakpointExpressions =
+ isMapScopesEnabled(getState()) &&
+ isOriginalSource(getSource(getState(), breakpoint.location?.sourceId)) &&
+ (breakpoint.options.logValue || breakpoint.options.condition);
+
+ if (shouldMapBreakpointExpressions) {
+ breakpoint = await dispatch(updateBreakpointSourceMapping(cx, breakpoint));
+ }
+ return client.setBreakpoint(breakpointLocation, breakpoint.options);
+}
+
+function clientRemoveBreakpoint(
+ client,
+ state: State,
+ generatedLocation: SourceLocation
+) {
+ const breakpointLocation = makeBreakpointLocation(state, generatedLocation);
+ return client.removeBreakpoint(breakpointLocation);
+}
+
+export function enableBreakpoint(cx: Context, initialBreakpoint: Breakpoint) {
+ return (thunkArgs: ThunkArgs) => {
+ const { dispatch, getState, client } = thunkArgs;
+ const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
+ if (!breakpoint || !breakpoint.disabled) {
+ return;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint: { ...breakpoint, disabled: false },
+ [PROMISE]: clientSetBreakpoint(client, cx, thunkArgs, breakpoint),
+ });
+ };
+}
+
+export function addBreakpoint(
+ cx: Context,
+ initialLocation: SourceLocation,
+ options: BreakpointOptions = {},
+ disabled: boolean = false,
+ shouldCancel: () => boolean = () => false
+) {
+ return async (thunkArgs: ThunkArgs) => {
+ const { dispatch, getState, client } = thunkArgs;
+ recordEvent("add_breakpoint");
+
+ const { sourceId, column, line } = initialLocation;
+
+ await dispatch(setBreakpointPositions({ cx, sourceId, line }));
+
+ const position: ?BreakpointPosition = column
+ ? getBreakpointPositionsForLocation(getState(), initialLocation)
+ : getFirstBreakpointPosition(getState(), initialLocation);
+
+ if (!position) {
+ return;
+ }
+
+ const { location, generatedLocation } = position;
+
+ const source = getSource(getState(), location.sourceId);
+ const generatedSource = getSource(getState(), generatedLocation.sourceId);
+
+ if (!source || !generatedSource) {
+ return;
+ }
+
+ const symbols = getSymbols(getState(), source);
+ const astLocation = getASTLocation(source, symbols, location);
+
+ const originalContent = getSourceContent(getState(), source.id);
+ const originalText = getTextAtPosition(
+ source.id,
+ originalContent,
+ location
+ );
+
+ const content = getSourceContent(getState(), generatedSource.id);
+ const text = getTextAtPosition(
+ generatedSource.id,
+ content,
+ generatedLocation
+ );
+
+ const id = makeBreakpointId(location);
+ const breakpoint = {
+ id,
+ disabled,
+ options,
+ location,
+ astLocation,
+ generatedLocation,
+ text,
+ originalText,
+ };
+
+ if (shouldCancel()) {
+ return;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint,
+ // If we just clobbered an enabled breakpoint with a disabled one, we need
+ // to remove any installed breakpoint in the server.
+ [PROMISE]: disabled
+ ? clientRemoveBreakpoint(client, getState(), generatedLocation)
+ : clientSetBreakpoint(client, cx, thunkArgs, breakpoint),
+ });
+ };
+}
+
+/**
+ * Remove a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpoint(cx: Context, initialBreakpoint: Breakpoint) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ recordEvent("remove_breakpoint");
+
+ const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
+ if (!breakpoint) {
+ return;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "REMOVE_BREAKPOINT",
+ cx,
+ location: breakpoint.location,
+ // If the breakpoint is disabled then it is not installed in the server.
+ [PROMISE]: breakpoint.disabled
+ ? Promise.resolve()
+ : clientRemoveBreakpoint(
+ client,
+ getState(),
+ breakpoint.generatedLocation
+ ),
+ });
+ };
+}
+
+/**
+ * Remove all installed, pending, and client breakpoints associated with a
+ * target generated location.
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpointAtGeneratedLocation(
+ cx: Context,
+ target: SourceLocation
+) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ // remove breakpoint from the server
+ const onBreakpointRemoved = clientRemoveBreakpoint(
+ client,
+ getState(),
+ target
+ );
+ // Remove any breakpoints matching the generated location.
+ const breakpoints = getBreakpointsList(getState());
+ for (const { location, generatedLocation } of breakpoints) {
+ if (
+ generatedLocation.sourceId == target.sourceId &&
+ comparePosition(generatedLocation, target)
+ ) {
+ dispatch({
+ type: "REMOVE_BREAKPOINT",
+ cx,
+ location,
+ [PROMISE]: onBreakpointRemoved,
+ });
+ }
+ }
+
+ // Remove any remaining pending breakpoints matching the generated location.
+ const pending = getPendingBreakpointList(getState());
+ for (const { location, generatedLocation } of pending) {
+ if (
+ generatedLocation.sourceUrl == target.sourceUrl &&
+ comparePosition(generatedLocation, target)
+ ) {
+ dispatch({
+ type: "REMOVE_PENDING_BREAKPOINT",
+ cx,
+ location,
+ });
+ }
+ }
+ return onBreakpointRemoved;
+ };
+}
+
+/**
+ * Disable a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function disableBreakpoint(cx: Context, initialBreakpoint: Breakpoint) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
+ if (!breakpoint || breakpoint.disabled) {
+ return;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint: { ...breakpoint, disabled: true },
+ [PROMISE]: clientRemoveBreakpoint(
+ client,
+ getState(),
+ breakpoint.generatedLocation
+ ),
+ });
+ };
+}
+
+/**
+ * Update the options of a breakpoint.
+ *
+ * @throws {Error} "not implemented"
+ * @memberof actions/breakpoints
+ * @static
+ * @param {SourceLocation} location
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param {Object} options
+ * Any options to set on the breakpoint
+ */
+export function setBreakpointOptions(
+ cx: Context,
+ location: SourceLocation,
+ options: BreakpointOptions = {}
+) {
+ return (thunkArgs: ThunkArgs) => {
+ const { dispatch, getState, client } = thunkArgs;
+ let breakpoint = getBreakpoint(getState(), location);
+ if (!breakpoint) {
+ return dispatch(addBreakpoint(cx, location, options));
+ }
+
+ // Note: setting a breakpoint's options implicitly enables it.
+ breakpoint = { ...breakpoint, disabled: false, options };
+
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint,
+ [PROMISE]: clientSetBreakpoint(client, cx, thunkArgs, breakpoint),
+ });
+ };
+}
+
+async function updateExpression(
+ evaluationsParser: ParserDispatcher,
+ mappings,
+ originalExpression: string
+) {
+ const mapped = await evaluationsParser.mapExpression(
+ originalExpression,
+ mappings,
+ [],
+ false,
+ false
+ );
+ if (!mapped) {
+ return originalExpression;
+ }
+ if (!originalExpression.trimEnd().endsWith(";")) {
+ return mapped.expression.replace(/;$/, "");
+ }
+ return mapped.expression;
+}
+
+function updateBreakpointSourceMapping(cx: Context, breakpoint: Breakpoint) {
+ return async ({ getState, dispatch, evaluationsParser }: ThunkArgs) => {
+ const options = { ...breakpoint.options };
+
+ const mappedScopes = await dispatch(
+ getMappedScopesForLocation(breakpoint.location)
+ );
+ if (!mappedScopes) {
+ return breakpoint;
+ }
+ const { mappings } = mappedScopes;
+
+ if (options.condition) {
+ options.condition = await updateExpression(
+ evaluationsParser,
+ mappings,
+ options.condition
+ );
+ }
+ if (options.logValue) {
+ options.logValue = await updateExpression(
+ evaluationsParser,
+ mappings,
+ options.logValue
+ );
+ }
+
+ validateNavigateContext(getState(), cx);
+ return { ...breakpoint, options };
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/moz.build b/devtools/client/debugger/src/actions/breakpoints/moz.build
new file mode 100644
index 0000000000..b686529199
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/moz.build
@@ -0,0 +1,14 @@
+# 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(
+ "breakpointPositions.js",
+ "index.js",
+ "modify.js",
+ "remapLocations.js",
+ "syncBreakpoint.js",
+)
diff --git a/devtools/client/debugger/src/actions/breakpoints/remapLocations.js b/devtools/client/debugger/src/actions/breakpoints/remapLocations.js
new file mode 100644
index 0000000000..0afbc640bc
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/remapLocations.js
@@ -0,0 +1,29 @@
+/* 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 typeof SourceMaps from "devtools-source-map";
+
+import type { Breakpoint, SourceId } from "../../types";
+
+export default function remapLocations(
+ breakpoints: Breakpoint[],
+ sourceId: SourceId,
+ sourceMaps: SourceMaps
+): Promise<Breakpoint[]> {
+ const sourceBreakpoints: Promise<Breakpoint>[] = breakpoints.map(
+ async breakpoint => {
+ if (breakpoint.location.sourceId !== sourceId) {
+ return breakpoint;
+ }
+ const location = await sourceMaps.getOriginalLocation(
+ breakpoint.location
+ );
+ return { ...breakpoint, location };
+ }
+ );
+
+ return Promise.all(sourceBreakpoints);
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js b/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
new file mode 100644
index 0000000000..bed0193313
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
@@ -0,0 +1,194 @@
+/* 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 { setBreakpointPositions } from "./breakpointPositions";
+import { setSymbols } from "../sources/symbols";
+import {
+ assertPendingBreakpoint,
+ findFunctionByName,
+ findPosition,
+ makeBreakpointLocation,
+} from "../../utils/breakpoint";
+
+import { comparePosition, createLocation } from "../../utils/location";
+
+import { originalToGeneratedId, isOriginalId } from "devtools-source-map";
+import { getSource } from "../../selectors";
+import { addBreakpoint, removeBreakpointAtGeneratedLocation } from ".";
+
+import type { ThunkArgs } from "../types";
+import type { LoadedSymbols } from "../../reducers/types";
+
+import type {
+ SourceLocation,
+ ASTLocation,
+ PendingBreakpoint,
+ SourceId,
+ Source,
+ BreakpointPositions,
+ Context,
+} from "../../types";
+
+async function findBreakpointPosition(
+ cx: Context,
+ { getState, dispatch }: ThunkArgs,
+ location: SourceLocation
+) {
+ const { sourceId, line } = location;
+ const positions: BreakpointPositions = await dispatch(
+ setBreakpointPositions({ cx, sourceId, line })
+ );
+
+ const position = findPosition(positions, location);
+ return position?.generatedLocation;
+}
+
+async function findNewLocation(
+ cx: Context,
+ { name, offset, index }: ASTLocation,
+ location: SourceLocation,
+ source: Source,
+ thunkArgs: ThunkArgs
+) {
+ const symbols: LoadedSymbols = await thunkArgs.dispatch(
+ setSymbols({ cx, source })
+ );
+ const func = symbols ? findFunctionByName(symbols, name, index) : null;
+
+ // Fallback onto the location line, if we do not find a function.
+ let { line } = location;
+ if (func) {
+ line = func.location.start.line + offset.line;
+ }
+
+ return {
+ line,
+ column: location.column,
+ sourceUrl: source.url,
+ sourceId: source.id,
+ };
+}
+
+// Breakpoint syncing occurs when a source is found that matches either the
+// original or generated URL of a pending breakpoint. A new breakpoint is
+// constructed that might have a different original and/or generated location,
+// if the original source has changed since the pending breakpoint was created.
+// There are a couple subtle aspects to syncing:
+//
+// - We handle both the original and generated source because there is no
+// guarantee that seeing the generated source means we will also see the
+// original source. When connecting, a breakpoint will be installed in the
+// client for the generated location in the pending breakpoint, and we need
+// to make sure that either a breakpoint is added to the reducer or that this
+// client breakpoint is deleted.
+//
+// - If we see both the original and generated sources and the source mapping
+// has changed, we need to make sure that only a single breakpoint is added
+// to the reducer for the new location corresponding to the original location
+// in the pending breakpoint.
+export function syncBreakpoint(
+ cx: Context,
+ sourceId: SourceId,
+ pendingBreakpoint: PendingBreakpoint
+) {
+ return async (thunkArgs: ThunkArgs) => {
+ const { getState, client, dispatch } = thunkArgs;
+ assertPendingBreakpoint(pendingBreakpoint);
+
+ const source = getSource(getState(), sourceId);
+
+ const generatedSourceId = isOriginalId(sourceId)
+ ? originalToGeneratedId(sourceId)
+ : sourceId;
+
+ const generatedSource = getSource(getState(), generatedSourceId);
+
+ if (!source || !generatedSource) {
+ return;
+ }
+
+ const { location, generatedLocation, astLocation } = pendingBreakpoint;
+ const sourceGeneratedLocation = createLocation({
+ ...generatedLocation,
+ sourceId: generatedSourceId,
+ });
+
+ if (
+ source == generatedSource &&
+ location.sourceUrl != generatedLocation.sourceUrl
+ ) {
+ // We are handling the generated source and the pending breakpoint has a
+ // source mapping. Supply a cancellation callback that will abort the
+ // breakpoint if the original source was synced to a different location,
+ // in which case the client breakpoint has been removed.
+ const breakpointLocation = makeBreakpointLocation(
+ getState(),
+ sourceGeneratedLocation
+ );
+ return dispatch(
+ addBreakpoint(
+ cx,
+ sourceGeneratedLocation,
+ pendingBreakpoint.options,
+ pendingBreakpoint.disabled,
+ () => !client.hasBreakpoint(breakpointLocation)
+ )
+ );
+ }
+
+ const previousLocation = { ...location, sourceId };
+
+ const newLocation = await findNewLocation(
+ cx,
+ astLocation,
+ previousLocation,
+ source,
+ thunkArgs
+ );
+
+ const newGeneratedLocation = await findBreakpointPosition(
+ cx,
+ thunkArgs,
+ newLocation
+ );
+
+ if (!newGeneratedLocation) {
+ // We couldn't find a new mapping for the breakpoint. If there is a source
+ // mapping, remove any breakpoints for the generated location, as if the
+ // breakpoint moved. If the old generated location still maps to an
+ // original location then we don't want to add a breakpoint for it.
+ if (location.sourceUrl != generatedLocation.sourceUrl) {
+ dispatch(
+ removeBreakpointAtGeneratedLocation(cx, sourceGeneratedLocation)
+ );
+ }
+ return;
+ }
+
+ const isSameLocation = comparePosition(
+ generatedLocation,
+ newGeneratedLocation
+ );
+
+ // If the new generated location has changed from that in the pending
+ // breakpoint, remove any breakpoint associated with the old generated
+ // location.
+ if (!isSameLocation) {
+ dispatch(
+ removeBreakpointAtGeneratedLocation(cx, sourceGeneratedLocation)
+ );
+ }
+
+ return dispatch(
+ addBreakpoint(
+ cx,
+ newLocation,
+ pendingBreakpoint.options,
+ pendingBreakpoint.disabled
+ )
+ );
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap
new file mode 100644
index 0000000000..c55e71c546
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap
@@ -0,0 +1,131 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`breakpoints should add a breakpoint 1`] = `
+Array [
+ Object {
+ "breakpoints": Array [
+ Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": undefined,
+ "offset": Object {
+ "column": 1,
+ "line": 2,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ },
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 2,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "id": "a:2:1",
+ "location": Object {
+ "column": 1,
+ "line": 2,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "options": Object {},
+ "originalText": "return a",
+ "text": "return a",
+ },
+ ],
+ "source": Object {
+ "extensionName": null,
+ "id": "a",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "/examples/a",
+ "url": "http://localhost:8000/examples/a",
+ },
+ },
+]
+`;
+
+exports[`breakpoints should not show a breakpoint that does not have text 1`] = `Array []`;
+
+exports[`breakpoints should remap breakpoints on pretty print 1`] = `
+Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": "a",
+ "offset": Object {
+ "column": undefined,
+ "line": 0,
+ },
+ },
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "a.js",
+ "sourceUrl": "http://localhost:8000/examples/a.js",
+ },
+ "id": "a.js/originalSource-d6d70368d5c252598541e693a7ad6c27:1:",
+ "location": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "a.js/originalSource-d6d70368d5c252598541e693a7ad6c27",
+ "sourceUrl": "http://localhost:8000/examples/a.js:formatted",
+ },
+ "options": Object {},
+ "originalText": "function a() {",
+ "text": "function a() {",
+}
+`;
+
+exports[`breakpoints should show a disabled breakpoint that does not have text 1`] = `
+Array [
+ Object {
+ "breakpoints": Array [
+ Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": undefined,
+ "offset": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ },
+ "disabled": true,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "id": "a:5:1",
+ "location": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "options": Object {},
+ "originalText": "",
+ "text": "",
+ },
+ ],
+ "source": Object {
+ "extensionName": null,
+ "id": "a",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "/examples/a",
+ "url": "http://localhost:8000/examples/a",
+ },
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js b/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
new file mode 100644
index 0000000000..6798cf419b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
@@ -0,0 +1,112 @@
+// @flow
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ actions,
+ selectors,
+ createStore,
+ makeSource,
+ waitForState,
+} from "../../../utils/test-head";
+import { createSource } from "../../tests/helpers/mockCommandClient";
+
+describe("breakpointPositions", () => {
+ it("fetches positions", async () => {
+ const fooContent = createSource("foo", "");
+
+ const store = createStore({
+ getSourceActorBreakpointPositions: async () => ({ "9": [1] }),
+ getSourceActorBreakableLines: async () => [],
+ sourceContents: async () => fooContent,
+ });
+
+ const { dispatch, getState, cx } = store;
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ await dispatch(actions.loadSourceById(cx, source.id));
+
+ dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo", line: 9 }));
+
+ await waitForState(store, state =>
+ selectors.hasBreakpointPositions(state, "foo")
+ );
+
+ expect(
+ selectors.getBreakpointPositionsForSource(getState(), "foo")
+ ).toEqual({
+ [9]: [
+ {
+ location: {
+ line: 9,
+ column: 1,
+ sourceId: "foo",
+ sourceUrl: "http://localhost:8000/examples/foo",
+ },
+ generatedLocation: {
+ line: 9,
+ column: 1,
+ sourceId: "foo",
+ sourceUrl: "http://localhost:8000/examples/foo",
+ },
+ },
+ ],
+ });
+ });
+
+ it("doesn't re-fetch positions", async () => {
+ const fooContent = createSource("foo", "");
+
+ let resolve = _ => {};
+ let count = 0;
+ const store = createStore({
+ getSourceActorBreakpointPositions: () =>
+ new Promise(r => {
+ count++;
+ resolve = r;
+ }),
+ getSourceActorBreakableLines: async () => [],
+ sourceContents: async () => fooContent,
+ });
+
+ const { dispatch, getState, cx } = store;
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ await dispatch(actions.loadSourceById(cx, source.id));
+
+ dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo", line: 9 }));
+ dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo", line: 9 }));
+
+ resolve({ "9": [1] });
+ await waitForState(store, state =>
+ selectors.hasBreakpointPositions(state, "foo")
+ );
+
+ expect(
+ selectors.getBreakpointPositionsForSource(getState(), "foo")
+ ).toEqual({
+ [9]: [
+ {
+ location: {
+ line: 9,
+ column: 1,
+ sourceId: "foo",
+ sourceUrl: "http://localhost:8000/examples/foo",
+ },
+ generatedLocation: {
+ line: 9,
+ column: 1,
+ sourceId: "foo",
+ sourceUrl: "http://localhost:8000/examples/foo",
+ },
+ },
+ ],
+ });
+
+ expect(count).toEqual(1);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
new file mode 100644
index 0000000000..4a1e68a0bd
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -0,0 +1,487 @@
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ getTelemetryEvents,
+} from "../../../utils/test-head";
+
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { mockPendingBreakpoint } from "../../tests/helpers/breakpoints.js";
+import { makePendingLocationId } from "../../../utils/breakpoint";
+
+function mockClient(positionsResponse = {}) {
+ return {
+ ...mockCommandClient,
+ getSourceActorBreakpointPositions: async () => positionsResponse,
+ getSourceActorBreakableLines: async () => [],
+ };
+}
+
+describe("breakpoints", () => {
+ it("should add a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ "2": [1] }));
+ const loc1 = {
+ sourceId: "a",
+ line: 2,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+ await dispatch(
+ actions.setSelectedLocation(cx, source, {
+ line: 1,
+ column: 1,
+ sourceId: source.id,
+ })
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(getTelemetryEvents("add_breakpoint")).toHaveLength(1);
+
+ const bpSources = selectors.getBreakpointSources(getState());
+ expect(bpSources).toMatchSnapshot();
+ });
+
+ it("should not show a breakpoint that does not have text", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ "5": [1] }));
+ const loc1 = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+ await dispatch(
+ actions.setSelectedLocation(cx, source, {
+ line: 1,
+ column: 1,
+ sourceId: source.id,
+ })
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(selectors.getBreakpointSources(getState())).toMatchSnapshot();
+ });
+
+ it("should show a disabled breakpoint that does not have text", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ "5": [1] }));
+ const loc1 = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+ await dispatch(
+ actions.setSelectedLocation(cx, source, {
+ line: 1,
+ column: 1,
+ sourceId: source.id,
+ })
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ const breakpoint = selectors.getBreakpoint(getState(), loc1);
+ if (!breakpoint) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, breakpoint));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(selectors.getBreakpointSources(getState())).toMatchSnapshot();
+ });
+
+ it("should not re-add a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ "5": [1] }));
+ const loc1 = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+ await dispatch(
+ actions.setSelectedLocation(cx, source, {
+ line: 1,
+ column: 1,
+ sourceId: source.id,
+ })
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("should remove a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [1], "6": [2] })
+ );
+
+ const loc1 = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const loc2 = {
+ sourceId: "b",
+ line: 6,
+ column: 2,
+ sourceUrl: "http://localhost:8000/examples/b",
+ };
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source: aSource }));
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ await dispatch(actions.loadSourceText({ cx, source: bSource }));
+
+ await dispatch(
+ actions.setSelectedLocation(cx, aSource, {
+ line: 1,
+ column: 1,
+ sourceId: aSource.id,
+ })
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ await dispatch(actions.addBreakpoint(cx, loc2));
+
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ await dispatch(actions.removeBreakpoint(cx, bp));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("should disable a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [1], "6": [2] })
+ );
+
+ const loc1 = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const loc2 = {
+ sourceId: "b",
+ line: 6,
+ column: 2,
+ sourceUrl: "http://localhost:8000/examples/b",
+ };
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source: aSource }));
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ await dispatch(actions.loadSourceText({ cx, source: bSource }));
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ await dispatch(actions.addBreakpoint(cx, loc2));
+
+ const breakpoint = selectors.getBreakpoint(getState(), loc1);
+ if (!breakpoint) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, breakpoint));
+
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.disabled).toBe(true);
+ });
+
+ it("should enable breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [1], "6": [2] })
+ );
+ const loc = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source: aSource }));
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+ let bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ expect(bp && bp.disabled).toBe(true);
+
+ await dispatch(actions.enableBreakpoint(cx, bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && !bp.disabled).toBe(true);
+ });
+
+ it("should toggle all the breakpoints", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [1], "6": [2] })
+ );
+
+ const loc1 = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const loc2 = {
+ sourceId: "b",
+ line: 6,
+ column: 2,
+ sourceUrl: "http://localhost:8000/examples/b",
+ };
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source: aSource }));
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ await dispatch(actions.loadSourceText({ cx, source: bSource }));
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ await dispatch(actions.addBreakpoint(cx, loc2));
+
+ await dispatch(actions.toggleAllBreakpoints(cx, true));
+
+ let bp1 = selectors.getBreakpoint(getState(), loc1);
+ let bp2 = selectors.getBreakpoint(getState(), loc2);
+
+ expect(bp1 && bp1.disabled).toBe(true);
+ expect(bp2 && bp2.disabled).toBe(true);
+
+ await dispatch(actions.toggleAllBreakpoints(cx, false));
+
+ bp1 = selectors.getBreakpoint(getState(), loc1);
+ bp2 = selectors.getBreakpoint(getState(), loc2);
+ expect(bp1 && bp1.disabled).toBe(false);
+ expect(bp2 && bp2.disabled).toBe(false);
+ });
+
+ it("should remove all the breakpoints", async () => {
+ const mockedPendingBreakpoint = mockPendingBreakpoint({ column: 2 });
+ const id = makePendingLocationId(mockedPendingBreakpoint.location);
+ const pendingBreakpoints = { [id]: mockedPendingBreakpoint };
+
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [1], "6": [2] }),
+ { pendingBreakpoints }
+ );
+
+ const loc1 = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const loc2 = {
+ sourceId: "b",
+ line: 6,
+ column: 2,
+ sourceUrl: "http://localhost:8000/examples/b",
+ };
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source: aSource }));
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ await dispatch(actions.loadSourceText({ cx, source: bSource }));
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ await dispatch(actions.addBreakpoint(cx, loc2));
+
+ await dispatch(actions.removeAllBreakpoints(cx));
+
+ const bps = selectors.getBreakpointsList(getState());
+ const pendingBps = selectors.getPendingBreakpointList(getState());
+
+ expect(bps).toHaveLength(0);
+ expect(pendingBps).toHaveLength(0);
+ });
+
+ it("should toggle a breakpoint at a location", async () => {
+ const loc = { sourceId: "foo1", line: 5, column: 1 };
+ const getBp = () => selectors.getBreakpoint(getState(), loc);
+
+ const { dispatch, getState, cx } = createStore(mockClient({ "5": [1] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.selectLocation(cx, loc));
+
+ await dispatch(actions.toggleBreakpointAtLine(cx, 5));
+ const bp = getBp();
+ expect(bp && !bp.disabled).toBe(true);
+
+ await dispatch(actions.toggleBreakpointAtLine(cx, 5));
+ expect(getBp()).toBe(undefined);
+ });
+
+ it("should disable/enable a breakpoint at a location", async () => {
+ const location = { sourceId: "foo1", line: 5, column: 1 };
+ const getBp = () => selectors.getBreakpoint(getState(), location);
+
+ const { dispatch, getState, cx } = createStore(mockClient({ "5": [1] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo1", line: 1 }));
+
+ await dispatch(actions.toggleBreakpointAtLine(cx, 5));
+ let bp = getBp();
+ expect(bp && !bp.disabled).toBe(true);
+ bp = getBp();
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ await dispatch(actions.toggleDisabledBreakpoint(cx, bp));
+ bp = getBp();
+ expect(bp && bp.disabled).toBe(true);
+ });
+
+ it("should set the breakpoint condition", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ "5": [1] }));
+
+ const loc = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+
+ let bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe(undefined);
+
+ await dispatch(
+ actions.setBreakpointOptions(cx, loc, {
+ condition: "const foo = 0",
+ getTextForLine: () => {},
+ })
+ );
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe("const foo = 0");
+ });
+
+ it("should set the condition and enable a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ "5": [1] }));
+
+ const loc = {
+ sourceId: "a",
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ };
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+ let bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe(undefined);
+
+ await dispatch(
+ actions.setBreakpointOptions(cx, loc, {
+ condition: "const foo = 0",
+ getTextForLine: () => {},
+ })
+ );
+ const newBreakpoint = selectors.getBreakpoint(getState(), loc);
+ expect(newBreakpoint && !newBreakpoint.disabled).toBe(true);
+ expect(newBreakpoint && newBreakpoint.options.condition).toBe(
+ "const foo = 0"
+ );
+ });
+
+ it("should remap breakpoints on pretty print", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ "1": [0] }));
+
+ const loc = {
+ sourceId: "a.js",
+ line: 1,
+ column: 0,
+ sourceUrl: "http://localhost:8000/examples/a.js",
+ };
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("a.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+ await dispatch(actions.togglePrettyPrint(cx, "a.js"));
+
+ const breakpoint = selectors.getBreakpointsList(getState())[0];
+
+ expect(
+ breakpoint.location.sourceUrl &&
+ breakpoint.location.sourceUrl.includes("formatted")
+ ).toBe(true);
+ expect(breakpoint).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/actions/event-listeners.js b/devtools/client/debugger/src/actions/event-listeners.js
new file mode 100644
index 0000000000..e2112ff763
--- /dev/null
+++ b/devtools/client/debugger/src/actions/event-listeners.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { uniq, remove } from "lodash";
+
+import {
+ getActiveEventListeners,
+ getEventListenerExpanded,
+ shouldLogEventBreakpoints,
+} from "../selectors";
+
+import type { ThunkArgs } from "./types";
+
+async function updateBreakpoints(dispatch, client, newEvents: string[]) {
+ dispatch({ type: "UPDATE_EVENT_LISTENERS", active: newEvents });
+ await client.setEventListenerBreakpoints(newEvents);
+}
+
+async function updateExpanded(dispatch, newExpanded: string[]) {
+ dispatch({
+ type: "UPDATE_EVENT_LISTENER_EXPANDED",
+ expanded: newExpanded,
+ });
+}
+
+export function addEventListenerBreakpoints(eventsToAdd: string[]) {
+ return async ({ dispatch, client, getState }: ThunkArgs) => {
+ const activeListenerBreakpoints = await getActiveEventListeners(getState());
+
+ const newEvents = uniq([...eventsToAdd, ...activeListenerBreakpoints]);
+
+ await updateBreakpoints(dispatch, client, newEvents);
+ };
+}
+
+export function removeEventListenerBreakpoints(eventsToRemove: string[]) {
+ return async ({ dispatch, client, getState }: ThunkArgs) => {
+ const activeListenerBreakpoints = await getActiveEventListeners(getState());
+
+ const newEvents = remove(
+ activeListenerBreakpoints,
+ event => !eventsToRemove.includes(event)
+ );
+
+ await updateBreakpoints(dispatch, client, newEvents);
+ };
+}
+
+export function toggleEventLogging() {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ const logEventBreakpoints = !shouldLogEventBreakpoints(getState());
+ await client.toggleEventLogging(logEventBreakpoints);
+ dispatch({ type: "TOGGLE_EVENT_LISTENERS", logEventBreakpoints });
+ };
+}
+
+export function addEventListenerExpanded(category: string) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const expanded = await getEventListenerExpanded(getState());
+
+ const newExpanded = uniq([...expanded, category]);
+
+ await updateExpanded(dispatch, newExpanded);
+ };
+}
+
+export function removeEventListenerExpanded(category: string) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const expanded = await getEventListenerExpanded(getState());
+
+ const newExpanded = expanded.filter(expand => expand != category);
+
+ updateExpanded(dispatch, newExpanded);
+ };
+}
+
+export function getEventListenerBreakpointTypes() {
+ return async ({ dispatch, client }: ThunkArgs) => {
+ const categories = await client.getEventListenerBreakpointTypes();
+ dispatch({ type: "RECEIVE_EVENT_LISTENER_TYPES", categories });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/exceptions.js b/devtools/client/debugger/src/actions/exceptions.js
new file mode 100644
index 0000000000..f054715041
--- /dev/null
+++ b/devtools/client/debugger/src/actions/exceptions.js
@@ -0,0 +1,46 @@
+/* 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 { hasException } from "../selectors";
+
+import type { ThunkArgs } from "./types";
+import type { Exception } from "../types";
+
+export function addExceptionFromResources(resources: Array<Object>) {
+ return async function({ dispatch }: ThunkArgs) {
+ for (const resource of resources) {
+ const { pageError } = resource;
+ if (!pageError.error) {
+ continue;
+ }
+ const { columnNumber, lineNumber, sourceId, errorMessage } = pageError;
+ const stacktrace = pageError.stacktrace || [];
+
+ const exception = {
+ columnNumber,
+ lineNumber,
+ sourceActorId: sourceId,
+ errorMessage,
+ stacktrace,
+ };
+
+ dispatch(addException(exception));
+ }
+ };
+}
+
+export function addException(exception: Exception) {
+ return async function({ dispatch, getState }: ThunkArgs) {
+ const { columnNumber, lineNumber } = exception;
+
+ if (!hasException(getState(), lineNumber, columnNumber)) {
+ dispatch({
+ type: "ADD_EXCEPTION",
+ exception,
+ });
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/expressions.js b/devtools/client/debugger/src/actions/expressions.js
new file mode 100644
index 0000000000..0f3ea67cae
--- /dev/null
+++ b/devtools/client/debugger/src/actions/expressions.js
@@ -0,0 +1,210 @@
+/* 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 {
+ getExpression,
+ getExpressions,
+ getSelectedFrame,
+ getSelectedFrameId,
+ getSourceFromId,
+ getSelectedSource,
+ getSelectedScopeMappings,
+ getSelectedFrameBindings,
+ getCurrentThread,
+ getIsPaused,
+ isMapScopesEnabled,
+} from "../selectors";
+import { PROMISE } from "./utils/middleware/promise";
+import { wrapExpression } from "../utils/expressions";
+import { features } from "../utils/prefs";
+import { isOriginal } from "../utils/source";
+
+import type { Expression, ThreadContext } from "../types";
+import type { ThunkArgs } from "./types";
+
+/**
+ * Add expression for debugger to watch
+ *
+ * @param {object} expression
+ * @param {number} expression.id
+ * @memberof actions/pause
+ * @static
+ */
+export function addExpression(cx: ThreadContext, input: string) {
+ return async ({ dispatch, getState, evaluationsParser }: ThunkArgs) => {
+ if (!input) {
+ return;
+ }
+
+ const expressionError = await evaluationsParser.hasSyntaxError(input);
+
+ const expression = getExpression(getState(), input);
+ if (expression) {
+ return dispatch(evaluateExpression(cx, expression));
+ }
+
+ dispatch({ type: "ADD_EXPRESSION", cx, input, expressionError });
+
+ const newExpression = getExpression(getState(), input);
+ if (newExpression) {
+ return dispatch(evaluateExpression(cx, newExpression));
+ }
+ };
+}
+
+export function autocomplete(cx: ThreadContext, input: string, cursor: number) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ if (!input) {
+ return;
+ }
+ const frameId = getSelectedFrameId(getState(), cx.thread);
+ const result = await client.autocomplete(input, cursor, frameId);
+ await dispatch({ type: "AUTOCOMPLETE", cx, input, result });
+ };
+}
+
+export function clearAutocomplete() {
+ return { type: "CLEAR_AUTOCOMPLETE" };
+}
+
+export function clearExpressionError() {
+ return { type: "CLEAR_EXPRESSION_ERROR" };
+}
+
+export function updateExpression(
+ cx: ThreadContext,
+ input: string,
+ expression: Expression
+) {
+ return async ({ dispatch, getState, parser }: ThunkArgs) => {
+ if (!input) {
+ return;
+ }
+
+ const expressionError = await parser.hasSyntaxError(input);
+ dispatch({
+ type: "UPDATE_EXPRESSION",
+ cx,
+ expression,
+ input: expressionError ? expression.input : input,
+ expressionError,
+ });
+
+ dispatch(evaluateExpressions(cx));
+ };
+}
+
+/**
+ *
+ * @param {object} expression
+ * @param {number} expression.id
+ * @memberof actions/pause
+ * @static
+ */
+export function deleteExpression(expression: Expression) {
+ return ({ dispatch }: ThunkArgs) => {
+ dispatch({
+ type: "DELETE_EXPRESSION",
+ input: expression.input,
+ });
+ };
+}
+
+/**
+ *
+ * @memberof actions/pause
+ * @param {number} selectedFrameId
+ * @static
+ */
+export function evaluateExpressions(cx: ThreadContext) {
+ return async function({ dispatch, getState, client }: ThunkArgs) {
+ const expressions = getExpressions(getState());
+ const inputs = expressions.map(({ input }) => input);
+ const frameId = getSelectedFrameId(getState(), cx.thread);
+ const results = await client.evaluateExpressions(inputs, {
+ frameId,
+ thread: cx.thread,
+ });
+ dispatch({ type: "EVALUATE_EXPRESSIONS", cx, inputs, results });
+ };
+}
+
+function evaluateExpression(cx: ThreadContext, expression: Expression) {
+ return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+ if (!expression.input) {
+ console.warn("Expressions should not be empty");
+ return;
+ }
+
+ let { input } = expression;
+ const frame = getSelectedFrame(getState(), cx.thread);
+
+ if (frame) {
+ const { location } = frame;
+ const source = getSourceFromId(getState(), location.sourceId);
+
+ const selectedSource = getSelectedSource(getState());
+
+ if (selectedSource && isOriginal(source) && isOriginal(selectedSource)) {
+ const mapResult = await dispatch(getMappedExpression(input));
+ if (mapResult) {
+ input = mapResult.expression;
+ }
+ }
+ }
+
+ const frameId = getSelectedFrameId(getState(), cx.thread);
+
+ return dispatch({
+ type: "EVALUATE_EXPRESSION",
+ cx,
+ thread: cx.thread,
+ input: expression.input,
+ [PROMISE]: client.evaluateInFrame(wrapExpression(input), {
+ frameId,
+ thread: cx.thread,
+ }),
+ });
+ };
+}
+
+/**
+ * Gets information about original variable names from the source map
+ * and replaces all posible generated names.
+ */
+export function getMappedExpression(expression: string) {
+ return async function({
+ dispatch,
+ getState,
+ client,
+ sourceMaps,
+ evaluationsParser,
+ }: ThunkArgs) {
+ const thread = getCurrentThread(getState());
+ const mappings = getSelectedScopeMappings(getState(), thread);
+ const bindings = getSelectedFrameBindings(getState(), thread);
+
+ // We bail early if we do not need to map the expression. This is important
+ // because mapping an expression can be slow if the evaluationsParser
+ // worker is busy doing other work.
+ //
+ // 1. there are no mappings - we do not need to map original expressions
+ // 2. does not contain `await` - we do not need to map top level awaits
+ // 3. does not contain `=` - we do not need to map assignments
+ const shouldMapScopes = isMapScopesEnabled(getState()) && mappings;
+ if (!shouldMapScopes && !expression.match(/(await|=)/)) {
+ return null;
+ }
+
+ return evaluationsParser.mapExpression(
+ expression,
+ mappings,
+ bindings || [],
+ features.mapExpressionBindings && getIsPaused(getState(), thread),
+ features.mapAwaitExpression
+ );
+ };
+}
diff --git a/devtools/client/debugger/src/actions/file-search.js b/devtools/client/debugger/src/actions/file-search.js
new file mode 100644
index 0000000000..92205f9fdc
--- /dev/null
+++ b/devtools/client/debugger/src/actions/file-search.js
@@ -0,0 +1,215 @@
+/* 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 {
+ clearSearch,
+ find,
+ findNext,
+ findPrev,
+ removeOverlay,
+ searchSourceForHighlight,
+} from "../utils/editor";
+import { renderWasmText } from "../utils/wasm";
+import { getMatches } from "../workers/search";
+import type { Action, FileTextSearchModifier, ThunkArgs } from "./types";
+import type { Context } from "../types";
+
+import {
+ getSelectedSourceWithContent,
+ getFileSearchModifiers,
+ getFileSearchQuery,
+ getFileSearchResults,
+} from "../selectors";
+
+import {
+ closeActiveSearch,
+ clearHighlightLineRange,
+ setActiveSearch,
+} from "./ui";
+import { isFulfilled } from "../utils/async-value";
+type Editor = Object;
+type Match = Object;
+
+export function doSearch(cx: Context, query: string, editor: Editor) {
+ return ({ getState, dispatch }: ThunkArgs) => {
+ const selectedSource = getSelectedSourceWithContent(getState());
+ if (!selectedSource || !selectedSource.content) {
+ return;
+ }
+
+ dispatch(setFileSearchQuery(cx, query));
+ dispatch(searchContents(cx, query, editor));
+ };
+}
+
+export function doSearchForHighlight(
+ query: string,
+ editor: Editor,
+ line: number,
+ ch: number
+) {
+ return async ({ getState, dispatch }: ThunkArgs) => {
+ const selectedSource = getSelectedSourceWithContent(getState());
+ if (!selectedSource?.content) {
+ return;
+ }
+ dispatch(searchContentsForHighlight(query, editor, line, ch));
+ };
+}
+
+export function setFileSearchQuery(cx: Context, query: string): Action {
+ return {
+ type: "UPDATE_FILE_SEARCH_QUERY",
+ cx,
+ query,
+ };
+}
+
+export function toggleFileSearchModifier(
+ cx: Context,
+ modifier: FileTextSearchModifier
+): Action {
+ return { type: "TOGGLE_FILE_SEARCH_MODIFIER", cx, modifier };
+}
+
+export function updateSearchResults(
+ cx: Context,
+ characterIndex: number,
+ line: number,
+ matches: Match[]
+): Action {
+ const matchIndex = matches.findIndex(
+ elm => elm.line === line && elm.ch === characterIndex
+ );
+
+ return {
+ type: "UPDATE_SEARCH_RESULTS",
+ cx,
+ results: {
+ matches,
+ matchIndex,
+ count: matches.length,
+ index: characterIndex,
+ },
+ };
+}
+
+export function searchContents(
+ cx: Context,
+ query: string,
+ editor: Object,
+ focusFirstResult?: boolean = true
+) {
+ return async ({ getState, dispatch }: ThunkArgs) => {
+ const modifiers = getFileSearchModifiers(getState());
+ const selectedSource = getSelectedSourceWithContent(getState());
+
+ if (
+ !editor ||
+ !selectedSource ||
+ !selectedSource.content ||
+ !isFulfilled(selectedSource.content) ||
+ !modifiers
+ ) {
+ return;
+ }
+ const selectedContent = selectedSource.content.value;
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+
+ if (!query) {
+ clearSearch(ctx.cm, query);
+ return;
+ }
+
+ let text;
+ if (selectedContent.type === "wasm") {
+ text = renderWasmText(selectedSource.id, selectedContent).join("\n");
+ } else {
+ text = selectedContent.value;
+ }
+
+ const matches = await getMatches(query, text, modifiers);
+
+ const res = find(ctx, query, true, modifiers, focusFirstResult);
+ if (!res) {
+ return;
+ }
+
+ const { ch, line } = res;
+
+ dispatch(updateSearchResults(cx, ch, line, matches));
+ };
+}
+
+export function searchContentsForHighlight(
+ query: string,
+ editor: Object,
+ line: number,
+ ch: number
+) {
+ return async ({ getState, dispatch }: ThunkArgs) => {
+ const modifiers = getFileSearchModifiers(getState());
+ const selectedSource = getSelectedSourceWithContent(getState());
+
+ if (
+ !query ||
+ !editor ||
+ !selectedSource ||
+ !selectedSource.content ||
+ !modifiers
+ ) {
+ return;
+ }
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+ searchSourceForHighlight(ctx, false, query, true, modifiers, line, ch);
+ };
+}
+
+export function traverseResults(cx: Context, rev: boolean, editor: Editor) {
+ return async ({ getState, dispatch }: ThunkArgs) => {
+ if (!editor) {
+ return;
+ }
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+
+ const query = getFileSearchQuery(getState());
+ const modifiers = getFileSearchModifiers(getState());
+ const { matches } = getFileSearchResults(getState());
+
+ if (query === "") {
+ dispatch(setActiveSearch("file"));
+ }
+
+ if (modifiers) {
+ const matchedLocations = matches || [];
+ const findArgs = [ctx, query, true, modifiers];
+ const results = rev ? findPrev(...findArgs) : findNext(...findArgs);
+
+ if (!results) {
+ return;
+ }
+ const { ch, line } = results;
+ dispatch(updateSearchResults(cx, ch, line, matchedLocations));
+ }
+ };
+}
+
+export function closeFileSearch(cx: Context, editor: Editor) {
+ return ({ getState, dispatch }: ThunkArgs) => {
+ if (editor) {
+ const query = getFileSearchQuery(getState());
+ const ctx = { ed: editor, cm: editor.codeMirror };
+ removeOverlay(ctx, query);
+ }
+
+ dispatch(setFileSearchQuery(cx, ""));
+ dispatch(closeActiveSearch());
+ dispatch(clearHighlightLineRange());
+ };
+}
diff --git a/devtools/client/debugger/src/actions/index.js b/devtools/client/debugger/src/actions/index.js
new file mode 100644
index 0000000000..205f70114f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/index.js
@@ -0,0 +1,49 @@
+/* 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 * as ast from "./ast";
+import * as breakpoints from "./breakpoints";
+import * as exceptions from "./exceptions";
+import * as expressions from "./expressions";
+import * as eventListeners from "./event-listeners";
+import * as pause from "./pause";
+import * as navigation from "./navigation";
+import * as ui from "./ui";
+import * as fileSearch from "./file-search";
+import * as projectTextSearch from "./project-text-search";
+import * as quickOpen from "./quick-open";
+import * as sourceTree from "./source-tree";
+import * as sources from "./sources";
+import * as sourcesActors from "./source-actors";
+import * as tabs from "./tabs";
+import * as threads from "./threads";
+import * as toolbox from "./toolbox";
+import * as preview from "./preview";
+
+// $FlowIgnore
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+export default {
+ ...ast,
+ ...navigation,
+ ...breakpoints,
+ ...exceptions,
+ ...expressions,
+ ...eventListeners,
+ ...sources,
+ ...sourcesActors,
+ ...tabs,
+ ...pause,
+ ...ui,
+ ...fileSearch,
+ ...objectInspector.actions,
+ ...projectTextSearch,
+ ...quickOpen,
+ ...sourceTree,
+ ...threads,
+ ...toolbox,
+ ...preview,
+};
diff --git a/devtools/client/debugger/src/actions/moz.build b/devtools/client/debugger/src/actions/moz.build
new file mode 100644
index 0000000000..32978f266b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/moz.build
@@ -0,0 +1,30 @@
+# 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 += [
+ "ast",
+ "breakpoints",
+ "pause",
+ "sources",
+ "utils",
+]
+
+CompiledModules(
+ "event-listeners.js",
+ "exceptions.js",
+ "expressions.js",
+ "file-search.js",
+ "index.js",
+ "navigation.js",
+ "preview.js",
+ "project-text-search.js",
+ "quick-open.js",
+ "source-actors.js",
+ "source-tree.js",
+ "tabs.js",
+ "toolbox.js",
+ "threads.js",
+ "ui.js",
+)
diff --git a/devtools/client/debugger/src/actions/navigation.js b/devtools/client/debugger/src/actions/navigation.js
new file mode 100644
index 0000000000..555efb719b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/navigation.js
@@ -0,0 +1,77 @@
+/* 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 { clearDocuments } from "../utils/editor";
+import sourceQueue from "../utils/source-queue";
+
+import { evaluateExpressions } from "./expressions";
+
+import { clearWasmStates } from "../utils/wasm";
+import { getMainThread, getThreadContext } from "../selectors";
+import type { Action, ThunkArgs } from "./types";
+import type { ActorId, URL } from "../types";
+
+/**
+ * Redux actions for the navigation state
+ * @module actions/navigation
+ */
+
+/**
+ * @memberof actions/navigation
+ * @static
+ */
+export function willNavigate(event: Object) {
+ return async function({
+ dispatch,
+ getState,
+ client,
+ sourceMaps,
+ parser,
+ }: ThunkArgs) {
+ sourceQueue.clear();
+ sourceMaps.clearSourceMaps();
+ clearWasmStates();
+ clearDocuments();
+ parser.clear();
+ const thread = getMainThread(getState());
+
+ dispatch({
+ type: "NAVIGATE",
+ mainThread: { ...thread, url: event.url },
+ });
+ };
+}
+
+export function connect(
+ url: URL,
+ actor: ActorId,
+ traits: Object,
+ isWebExtension: boolean
+) {
+ return async function({ dispatch, getState }: ThunkArgs) {
+ await dispatch(
+ ({
+ type: "CONNECT",
+ traits,
+ mainThreadActorID: actor,
+ isWebExtension,
+ }: Action)
+ );
+
+ const cx = getThreadContext(getState());
+ dispatch(evaluateExpressions(cx));
+ };
+}
+
+/**
+ * @memberof actions/navigation
+ * @static
+ */
+export function navigated() {
+ return async function({ dispatch, panel }: ThunkArgs) {
+ panel.emit("reloaded");
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/breakOnNext.js b/devtools/client/debugger/src/actions/pause/breakOnNext.js
new file mode 100644
index 0000000000..f82fdb7c20
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/breakOnNext.js
@@ -0,0 +1,24 @@
+/* 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 { ThunkArgs } from "../types";
+import type { PauseAction } from "../types/PauseAction";
+import type { ThreadContext } from "../../types";
+
+/**
+ * Debugger breakOnNext command.
+ * It's different from the comand action because we also want to
+ * highlight the pause icon.
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function breakOnNext(cx: ThreadContext): PauseAction {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ await client.breakOnNext(cx.thread);
+ return dispatch({ type: "BREAK_ON_NEXT", thread: cx.thread });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/commands.js b/devtools/client/debugger/src/actions/pause/commands.js
new file mode 100644
index 0000000000..bf39687de6
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/commands.js
@@ -0,0 +1,150 @@
+/* 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 {
+ getSelectedFrame,
+ getThreadContext,
+ getCurrentThread,
+} from "../../selectors";
+import { PROMISE } from "../utils/middleware/promise";
+import { evaluateExpressions } from "../expressions";
+import { selectLocation } from "../sources";
+import { fetchScopes } from "./fetchScopes";
+import { fetchFrames } from "./fetchFrames";
+import { recordEvent } from "../../utils/telemetry";
+import { features } from "../../utils/prefs";
+import assert from "../../utils/assert";
+
+import type { ThreadId, Context, ThreadContext, Frame } from "../../types";
+
+import type { ThunkArgs } from "../types";
+import type { Command } from "../../reducers/types";
+
+export function selectThread(cx: Context, thread: ThreadId) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ if (getCurrentThread(getState()) === thread) {
+ return;
+ }
+
+ await dispatch({ cx, type: "SELECT_THREAD", thread });
+
+ // Get a new context now that the current thread has changed.
+ const threadcx = getThreadContext(getState());
+ assert(threadcx.thread == thread, "Thread mismatch");
+
+ const serverRequests = [];
+ serverRequests.push(dispatch(evaluateExpressions(threadcx)));
+
+ const frame = getSelectedFrame(getState(), thread);
+ if (frame) {
+ serverRequests.push(dispatch(selectLocation(threadcx, frame.location)));
+ serverRequests.push(dispatch(fetchFrames(threadcx)));
+ serverRequests.push(dispatch(fetchScopes(threadcx)));
+ }
+ await Promise.all(serverRequests);
+ };
+}
+
+/**
+ * Debugger commands like stepOver, stepIn, stepUp
+ *
+ * @param string $0.type
+ * @memberof actions/pause
+ * @static
+ */
+export function command(cx: ThreadContext, type: Command) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ if (!type) {
+ return;
+ }
+
+ const frame = features.frameStep && getSelectedFrame(getState(), cx.thread);
+
+ return dispatch({
+ type: "COMMAND",
+ command: type,
+ cx,
+ thread: cx.thread,
+ [PROMISE]: client[type](cx.thread, frame?.id),
+ });
+ };
+}
+
+/**
+ * StepIn
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function stepIn(cx: ThreadContext) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ if (cx.isPaused) {
+ return dispatch(command(cx, "stepIn"));
+ }
+ };
+}
+
+/**
+ * stepOver
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function stepOver(cx: ThreadContext) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ if (cx.isPaused) {
+ return dispatch(command(cx, "stepOver"));
+ }
+ };
+}
+
+/**
+ * stepOut
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function stepOut(cx: ThreadContext) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ if (cx.isPaused) {
+ return dispatch(command(cx, "stepOut"));
+ }
+ };
+}
+
+/**
+ * resume
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function resume(cx: ThreadContext) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ if (cx.isPaused) {
+ recordEvent("continue");
+ return dispatch(command(cx, "resume"));
+ }
+ };
+}
+
+/**
+ * restart frame
+ * @memberof actions/pause
+ * @static
+ */
+export function restart(cx: ThreadContext, frame: Frame) {
+ return async ({ dispatch, getState, client }: ThunkArgs) => {
+ if (cx.isPaused) {
+ return dispatch({
+ type: "COMMAND",
+ command: "restart",
+ cx,
+ thread: cx.thread,
+ [PROMISE]: client.restart(cx.thread, frame.id),
+ });
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/continueToHere.js b/devtools/client/debugger/src/actions/pause/continueToHere.js
new file mode 100644
index 0000000000..70e6192d19
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/continueToHere.js
@@ -0,0 +1,63 @@
+/* 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 {
+ getSelectedSource,
+ getSelectedFrame,
+ getClosestBreakpointPosition,
+ getBreakpoint,
+} from "../../selectors";
+import { addHiddenBreakpoint } from "../breakpoints";
+import { setBreakpointPositions } from "../breakpoints/breakpointPositions";
+
+import { resume } from "./commands";
+
+import type { ThunkArgs } from "../types";
+import type { ThreadContext, SourceLocation } from "../../types";
+
+export function continueToHere(cx: ThreadContext, location: SourceLocation) {
+ return async function({ dispatch, getState }: ThunkArgs) {
+ const { line, column, sourceId } = location;
+ const selectedSource = getSelectedSource(getState());
+ const selectedFrame = getSelectedFrame(getState(), cx.thread);
+
+ if (!selectedFrame || !selectedSource) {
+ return;
+ }
+
+ const debugLine = selectedFrame.location.line;
+ // If the user selects a line to continue to,
+ // it must be different than the currently paused line.
+ if (!column && debugLine == line) {
+ return;
+ }
+
+ await dispatch(setBreakpointPositions({ cx, sourceId, line }));
+ const position = getClosestBreakpointPosition(getState(), location);
+
+ // If the user selects a location in the editor,
+ // there must be a place we can pause on that line.
+ if (column && !position) {
+ return;
+ }
+
+ const pauseLocation = column && position ? position.location : location;
+
+ // Set a hidden breakpoint if we do not already have a breakpoint
+ // at the closest position
+ if (!getBreakpoint(getState(), pauseLocation)) {
+ await dispatch(
+ addHiddenBreakpoint(cx, {
+ sourceId: selectedSource.id,
+ line: pauseLocation.line,
+ column: pauseLocation.column,
+ })
+ );
+ }
+
+ dispatch(resume(cx));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/expandScopes.js b/devtools/client/debugger/src/actions/pause/expandScopes.js
new file mode 100644
index 0000000000..034d809858
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/expandScopes.js
@@ -0,0 +1,25 @@
+/* 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 { getScopeItemPath } from "../../utils/pause/scopes/utils";
+import type { ThunkArgs } from "../types";
+import type { ThreadContext } from "../../types";
+
+export function setExpandedScope(
+ cx: ThreadContext,
+ item: Object,
+ expanded: boolean
+) {
+ return function({ dispatch, getState }: ThunkArgs) {
+ return dispatch({
+ type: "SET_EXPANDED_SCOPE",
+ cx,
+ thread: cx.thread,
+ path: getScopeItemPath(item),
+ expanded,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/fetchFrames.js b/devtools/client/debugger/src/actions/pause/fetchFrames.js
new file mode 100644
index 0000000000..a6b448bf26
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/fetchFrames.js
@@ -0,0 +1,27 @@
+/* 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 { ThreadContext } from "../../types";
+import type { ThunkArgs } from "../types";
+import { isValidThreadContext } from "../../utils/context";
+
+export function fetchFrames(cx: ThreadContext) {
+ return async function({ dispatch, client, getState }: ThunkArgs) {
+ const { thread } = cx;
+ let frames;
+ try {
+ frames = await client.getFrames(thread);
+ } catch (e) {
+ // getFrames will fail if the thread has resumed. In this case the thread
+ // should no longer be valid and the frames we would have fetched would be
+ // discarded anyways.
+ if (isValidThreadContext(getState(), cx)) {
+ throw e;
+ }
+ }
+ dispatch({ type: "FETCHED_FRAMES", thread, frames, cx });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/fetchScopes.js b/devtools/client/debugger/src/actions/pause/fetchScopes.js
new file mode 100644
index 0000000000..801330a934
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/fetchScopes.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { getSelectedFrame, getGeneratedFrameScope } from "../../selectors";
+import { mapScopes } from "./mapScopes";
+import { generateInlinePreview } from "./inlinePreview";
+import { PROMISE } from "../utils/middleware/promise";
+import type { ThreadContext } from "../../types";
+import type { ThunkArgs } from "../types";
+
+export function fetchScopes(cx: ThreadContext) {
+ return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+ const frame = getSelectedFrame(getState(), cx.thread);
+ if (!frame || getGeneratedFrameScope(getState(), frame.id)) {
+ return;
+ }
+
+ const scopes = dispatch({
+ type: "ADD_SCOPES",
+ cx,
+ thread: cx.thread,
+ frame,
+ [PROMISE]: client.getFrameScopes(frame),
+ });
+
+ scopes.then(() => {
+ dispatch(generateInlinePreview(cx, frame));
+ });
+ await dispatch(mapScopes(cx, scopes, frame));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/highlightCalls.js b/devtools/client/debugger/src/actions/pause/highlightCalls.js
new file mode 100644
index 0000000000..0c52a2ce16
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/highlightCalls.js
@@ -0,0 +1,99 @@
+/* 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 {
+ getSymbols,
+ getSource,
+ getSelectedFrame,
+ getCurrentThread,
+} from "../../selectors";
+import type { ThreadContext } from "../../types";
+import type { ThunkArgs } from "../types";
+
+// a is an ast location with start and end positions (line and column).
+// b is a single position (line and column).
+// This function tests to see if the b position
+// falls within the range given in a.
+function inHouseContainsPosition(a: Object, b: Object): boolean {
+ const bColumn = b.column || 0;
+ const startsBefore =
+ a.start.line < b.line ||
+ (a.start.line === b.line && a.start.column <= bColumn);
+ const endsAfter =
+ a.end.line > b.line || (a.end.line === b.line && a.end.column >= bColumn);
+
+ return startsBefore && endsAfter;
+}
+
+export function highlightCalls(cx: ThreadContext) {
+ return async function({ dispatch, getState, parser, client }: ThunkArgs) {
+ if (!cx) {
+ return;
+ }
+
+ const frame = await getSelectedFrame(
+ getState(),
+ getCurrentThread(getState())
+ );
+
+ if (!frame) {
+ return;
+ }
+
+ const { thread } = cx;
+
+ const originalAstScopes = await parser.getScopes(frame.location);
+ if (!originalAstScopes) {
+ return;
+ }
+
+ const source = getSource(getState(), frame.location.sourceId);
+ if (!source) {
+ return;
+ }
+
+ const symbols = getSymbols(getState(), source);
+
+ if (!symbols || symbols.loading) {
+ return;
+ }
+
+ if (!symbols.callExpressions) {
+ return;
+ }
+
+ const localAstScope = originalAstScopes[0];
+ const allFunctionCalls = symbols.callExpressions;
+
+ const highlightedCalls = allFunctionCalls.filter(function(call) {
+ const containsStart = inHouseContainsPosition(
+ localAstScope,
+ call.location.start
+ );
+ const containsEnd = inHouseContainsPosition(
+ localAstScope,
+ call.location.end
+ );
+ return containsStart && containsEnd;
+ });
+
+ return dispatch({
+ type: "HIGHLIGHT_CALLS",
+ thread,
+ highlightedCalls,
+ });
+ };
+}
+
+export function unhighlightCalls(cx: ThreadContext) {
+ return async function({ dispatch, getState, parser, client }: ThunkArgs) {
+ const { thread } = cx;
+ return dispatch({
+ type: "UNHIGHLIGHT_CALLS",
+ thread,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/index.js b/devtools/client/debugger/src/actions/pause/index.js
new file mode 100644
index 0000000000..621918b0f4
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/index.js
@@ -0,0 +1,38 @@
+/* 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
+
+/**
+ * Redux actions for the pause state
+ * @module actions/pause
+ */
+
+export {
+ selectThread,
+ stepIn,
+ stepOver,
+ stepOut,
+ resume,
+ restart,
+} from "./commands";
+export { fetchFrames } from "./fetchFrames";
+export { fetchScopes } from "./fetchScopes";
+export { paused } from "./paused";
+export { resumed } from "./resumed";
+export { continueToHere } from "./continueToHere";
+export { breakOnNext } from "./breakOnNext";
+export { mapFrames } from "./mapFrames";
+export { mapDisplayNames } from "./mapDisplayNames";
+export { pauseOnExceptions } from "./pauseOnExceptions";
+export { selectFrame } from "./selectFrame";
+export { toggleSkipPausing, setSkipPausing } from "./skipPausing";
+export { toggleMapScopes } from "./mapScopes";
+export { setExpandedScope } from "./expandScopes";
+export { generateInlinePreview } from "./inlinePreview";
+export { highlightCalls, unhighlightCalls } from "./highlightCalls";
+export {
+ previewPausedLocation,
+ clearPreviewPausedLocation,
+} from "./previewPausedLocation";
diff --git a/devtools/client/debugger/src/actions/pause/inlinePreview.js b/devtools/client/debugger/src/actions/pause/inlinePreview.js
new file mode 100644
index 0000000000..2dd7d36031
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/inlinePreview.js
@@ -0,0 +1,238 @@
+/* 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 { sortBy } from "lodash";
+import {
+ getOriginalFrameScope,
+ getGeneratedFrameScope,
+ getInlinePreviews,
+ getSelectedLocation,
+} from "../../selectors";
+import { features } from "../../utils/prefs";
+import { validateThreadContext } from "../../utils/context";
+
+import type { OriginalScope } from "../../utils/pause/mapScopes";
+import type { ThreadContext, Frame, Scope, Preview } from "../../types";
+import type { ThunkArgs } from "../types";
+import type { SourceScope } from "../../workers/parser/getScopes";
+
+// We need to display all variables in the current functional scope so
+// include all data for block scopes until the first functional scope
+function getLocalScopeLevels(originalAstScopes: SourceScope[]): number {
+ let levels = 0;
+ while (
+ originalAstScopes[levels] &&
+ originalAstScopes[levels].type === "block"
+ ) {
+ levels++;
+ }
+ return levels;
+}
+
+export function generateInlinePreview(cx: ThreadContext, frame: ?Frame) {
+ return async function({ dispatch, getState, parser, client }: ThunkArgs) {
+ if (!frame || !features.inlinePreview) {
+ return;
+ }
+
+ const { thread } = cx;
+
+ // Avoid regenerating inline previews when we already have preview data
+ if (getInlinePreviews(getState(), thread, frame.id)) {
+ return;
+ }
+
+ const originalFrameScopes = getOriginalFrameScope(
+ getState(),
+ thread,
+ frame.location.sourceId,
+ frame.id
+ );
+
+ const generatedFrameScopes = getGeneratedFrameScope(
+ getState(),
+ thread,
+ frame.id
+ );
+
+ let scopes: ?OriginalScope | Scope | null =
+ originalFrameScopes?.scope || generatedFrameScopes?.scope;
+
+ if (!scopes || !scopes.bindings) {
+ return;
+ }
+
+ // It's important to use selectedLocation, because we don't know
+ // if we'll be viewing the original or generated frame location
+ const selectedLocation = getSelectedLocation(getState());
+ if (!selectedLocation) {
+ return;
+ }
+
+ const originalAstScopes = await parser.getScopes(selectedLocation);
+ validateThreadContext(getState(), cx);
+ if (!originalAstScopes) {
+ return;
+ }
+
+ const allPreviews = [];
+ const pausedOnLine: number = selectedLocation.line;
+ const levels: number = getLocalScopeLevels(originalAstScopes);
+
+ for (
+ let curLevel = 0;
+ curLevel <= levels && scopes && scopes.bindings;
+ curLevel++
+ ) {
+ const bindings = { ...scopes.bindings.variables };
+ scopes.bindings.arguments.forEach(argument => {
+ Object.keys(argument).forEach(key => {
+ bindings[key] = argument[key];
+ });
+ });
+
+ const previewBindings = Object.keys(bindings).map(async name => {
+ // We want to show values of properties of objects only and not
+ // function calls on other data types like someArr.forEach etc..
+ let properties = null;
+ const objectFront = bindings[name].value;
+ if (objectFront.actorID && objectFront.class === "Object") {
+ properties = await client.loadObjectProperties({
+ name,
+ path: name,
+ contents: { value: objectFront },
+ });
+ }
+
+ const previewsFromBindings: Array<Preview> = getBindingValues(
+ originalAstScopes,
+ pausedOnLine,
+ name,
+ bindings[name].value,
+ curLevel,
+ properties
+ );
+
+ allPreviews.push(...previewsFromBindings);
+ });
+ await Promise.all(previewBindings);
+
+ scopes = scopes.parent;
+ }
+
+ const previews = {};
+ const sortedPreviews = sortBy(allPreviews, ["line", "column"]);
+
+ sortedPreviews.forEach(preview => {
+ const { line } = preview;
+ if (!previews[line]) {
+ previews[line] = [preview];
+ } else {
+ previews[line].push(preview);
+ }
+ });
+
+ return dispatch({
+ type: "ADD_INLINE_PREVIEW",
+ thread,
+ frame,
+ previews,
+ });
+ };
+}
+
+function getBindingValues(
+ originalAstScopes: Object,
+ pausedOnLine: number,
+ name: string,
+ value: any,
+ curLevel: number,
+ properties: Array<Object> | null
+): Array<Preview> {
+ const previews = [];
+
+ const binding = originalAstScopes[curLevel]?.bindings[name];
+ if (!binding) {
+ return previews;
+ }
+
+ // Show a variable only once ( an object and it's child property are
+ // counted as different )
+ const identifiers = new Set();
+
+ // We start from end as we want to show values besides variable
+ // located nearest to the breakpoint
+ for (let i = binding.refs.length - 1; i >= 0; i--) {
+ const ref = binding.refs[i];
+ // Subtracting 1 from line as codemirror lines are 0 indexed
+ const line = ref.start.line - 1;
+ const column: number = ref.start.column;
+ // We don't want to render inline preview below the paused line
+ if (line >= pausedOnLine - 1) {
+ continue;
+ }
+
+ const { displayName, displayValue } = getExpressionNameAndValue(
+ name,
+ value,
+ ref,
+ properties
+ );
+
+ // Variable with same name exists, display value of current or
+ // closest to the current scope's variable
+ if (identifiers.has(displayName)) {
+ continue;
+ }
+ identifiers.add(displayName);
+
+ previews.push({
+ line,
+ column,
+ name: displayName,
+ value: displayValue,
+ });
+ }
+ return previews;
+}
+
+function getExpressionNameAndValue(
+ name: string,
+ value: any,
+ // TODO: Add data type to ref
+ ref: Object,
+ properties: Array<Object> | null
+) {
+ let displayName = name;
+ let displayValue = value;
+
+ // Only variables of type Object will have properties
+ if (properties) {
+ let { meta } = ref;
+ // Presence of meta property means expression contains child property
+ // reference eg: objName.propName
+ while (meta) {
+ // Initially properties will be an array, after that it will be an object
+ if (displayValue === value) {
+ const property: Object = properties.find(
+ prop => prop.name === meta.property
+ );
+ displayValue = property?.contents.value;
+ displayName += `.${meta.property}`;
+ } else if (displayValue?.preview?.ownProperties) {
+ const { ownProperties } = displayValue.preview;
+ Object.keys(ownProperties).forEach(prop => {
+ if (prop === meta.property) {
+ displayValue = ownProperties[prop].value;
+ displayName += `.${meta.property}`;
+ }
+ });
+ }
+ meta = meta.parent;
+ }
+ }
+
+ return { displayName, displayValue };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapDisplayNames.js b/devtools/client/debugger/src/actions/pause/mapDisplayNames.js
new file mode 100644
index 0000000000..f4b916afda
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapDisplayNames.js
@@ -0,0 +1,60 @@
+/* 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 { getFrames, getSymbols, getSource } from "../../selectors";
+
+import { findClosestFunction } from "../../utils/ast";
+
+import type { Frame, ThreadContext } from "../../types";
+import type { ThunkArgs } from "../types";
+
+function mapDisplayName(frame: Frame, { getState }) {
+ if (frame.isOriginal) {
+ return frame;
+ }
+
+ const source = getSource(getState(), frame.location.sourceId);
+
+ if (!source) {
+ return frame;
+ }
+
+ const symbols = getSymbols(getState(), source);
+
+ if (!symbols || !symbols.functions) {
+ return frame;
+ }
+
+ const originalFunction = findClosestFunction(symbols, frame.location);
+
+ if (!originalFunction) {
+ return frame;
+ }
+
+ const originalDisplayName = originalFunction.name;
+ return { ...frame, originalDisplayName };
+}
+
+export function mapDisplayNames(cx: ThreadContext) {
+ return function({ dispatch, getState }: ThunkArgs) {
+ const frames = getFrames(getState(), cx.thread);
+
+ if (!frames) {
+ return;
+ }
+
+ const mappedFrames = frames.map(frame =>
+ mapDisplayName(frame, { getState })
+ );
+
+ dispatch({
+ type: "MAP_FRAME_DISPLAY_NAMES",
+ cx,
+ thread: cx.thread,
+ frames: mappedFrames,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapFrames.js b/devtools/client/debugger/src/actions/pause/mapFrames.js
new file mode 100644
index 0000000000..8de3e09f3d
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapFrames.js
@@ -0,0 +1,177 @@
+/* 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 { getFrames, getSource, getSelectedFrame } from "../../selectors";
+
+import assert from "../../utils/assert";
+
+import type {
+ Frame,
+ FrameId,
+ OriginalFrame,
+ ThreadContext,
+ ThreadId,
+} from "../../types";
+import type { State } from "../../reducers/types";
+import type { ThunkArgs } from "../types";
+
+import SourceMaps, { isGeneratedId } from "devtools-source-map";
+
+function isFrameBlackboxed(state: State, frame: Frame): boolean {
+ const source = getSource(state, frame.location.sourceId);
+ return !!source?.isBlackBoxed;
+}
+
+function getSelectedFrameId(
+ state: State,
+ thread: ThreadId,
+ frames: Frame[]
+): ?FrameId {
+ let selectedFrame = getSelectedFrame(state, thread);
+ if (selectedFrame && !isFrameBlackboxed(state, selectedFrame)) {
+ return selectedFrame.id;
+ }
+
+ selectedFrame = frames.find(frame => !isFrameBlackboxed(state, frame));
+ return selectedFrame?.id;
+}
+
+export function updateFrameLocation(
+ frame: Frame,
+ sourceMaps: typeof SourceMaps
+): Promise<Frame> {
+ if (frame.isOriginal) {
+ return Promise.resolve(frame);
+ }
+ return sourceMaps.getOriginalLocation(frame.location).then(loc => ({
+ ...frame,
+ location: loc,
+ generatedLocation: frame.generatedLocation || frame.location,
+ }));
+}
+
+function updateFrameLocations(
+ frames: Frame[],
+ sourceMaps: typeof SourceMaps
+): Promise<Frame[]> {
+ if (!frames || frames.length == 0) {
+ return Promise.resolve(frames);
+ }
+
+ return Promise.all(
+ frames.map(frame => updateFrameLocation(frame, sourceMaps))
+ );
+}
+
+function isWasmOriginalSourceFrame(
+ frame: Frame,
+ getState: () => State
+): boolean {
+ if (isGeneratedId(frame.location.sourceId)) {
+ return false;
+ }
+ const generatedSource = getSource(
+ getState(),
+ frame.generatedLocation.sourceId
+ );
+
+ return Boolean(generatedSource?.isWasm);
+}
+
+async function expandFrames(
+ frames: Frame[],
+ sourceMaps: typeof SourceMaps,
+ getState: () => State
+): Promise<Frame[]> {
+ const result = [];
+ for (let i = 0; i < frames.length; ++i) {
+ const frame = frames[i];
+ if (frame.isOriginal || !isWasmOriginalSourceFrame(frame, getState)) {
+ result.push(frame);
+ continue;
+ }
+ const originalFrames: ?Array<OriginalFrame> = await sourceMaps.getOriginalStackFrames(
+ frame.generatedLocation
+ );
+ if (!originalFrames) {
+ result.push(frame);
+ continue;
+ }
+
+ assert(originalFrames.length > 0, "Expected at least one original frame");
+ // First entry has not specific location -- use one from original frame.
+ originalFrames[0] = {
+ ...originalFrames[0],
+ location: frame.location,
+ };
+
+ originalFrames.forEach((originalFrame, j) => {
+ if (!originalFrame.location) {
+ return;
+ }
+
+ // Keep outer most frame with true actor ID, and generate uniquie
+ // one for the nested frames.
+ const id = j == 0 ? frame.id : `${frame.id}-originalFrame${j}`;
+ result.push({
+ id,
+ displayName: originalFrame.displayName,
+ location: originalFrame.location,
+ index: frame.index,
+ source: null,
+ thread: frame.thread,
+ scope: frame.scope,
+ this: frame.this,
+ isOriginal: true,
+ // More fields that will be added by the mapDisplayNames and
+ // updateFrameLocation.
+ generatedLocation: frame.generatedLocation,
+ originalDisplayName: originalFrame.displayName,
+ originalVariables: originalFrame.variables,
+ asyncCause: frame.asyncCause,
+ state: frame.state,
+ });
+ });
+ }
+ return result;
+}
+
+/**
+ * Map call stack frame locations and display names to originals.
+ * e.g.
+ * 1. When the debuggee pauses
+ * 2. When a source is pretty printed
+ * 3. When symbols are loaded
+ * @memberof actions/pause
+ * @static
+ */
+export function mapFrames(cx: ThreadContext) {
+ return async function(thunkArgs: ThunkArgs) {
+ const { dispatch, getState, sourceMaps } = thunkArgs;
+ const frames = getFrames(getState(), cx.thread);
+ if (!frames) {
+ return;
+ }
+
+ let mappedFrames = await updateFrameLocations(frames, sourceMaps);
+
+ mappedFrames = await expandFrames(mappedFrames, sourceMaps, getState);
+
+ const selectedFrameId = getSelectedFrameId(
+ getState(),
+ cx.thread,
+ mappedFrames
+ );
+
+ dispatch({
+ type: "MAP_FRAMES",
+ cx,
+ thread: cx.thread,
+ frames: mappedFrames,
+ selectedFrameId,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapScopes.js b/devtools/client/debugger/src/actions/pause/mapScopes.js
new file mode 100644
index 0000000000..9e5e249e2d
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapScopes.js
@@ -0,0 +1,216 @@
+/* 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 {
+ getSelectedFrameId,
+ getSource,
+ getSourceContent,
+ isMapScopesEnabled,
+ getSelectedFrame,
+ getSelectedGeneratedScope,
+ getSelectedOriginalScope,
+ getThreadContext,
+} from "../../selectors";
+import { loadSourceText } from "../sources/loadSourceText";
+import { PROMISE } from "../utils/middleware/promise";
+import assert from "../../utils/assert";
+
+import { log } from "../../utils/log";
+import { isGenerated, isOriginal } from "../../utils/source";
+import type {
+ Frame,
+ Scope,
+ ThreadContext,
+ XScopeVariables,
+ SourceLocation,
+} from "../../types";
+
+import type { ThunkArgs } from "../types";
+
+import {
+ buildMappedScopes,
+ type MappedFrameLocation,
+} from "../../utils/pause/mapScopes";
+import { isFulfilled } from "../../utils/async-value";
+
+import type { OriginalScope } from "../../utils/pause/mapScopes";
+import { getMappedLocation } from "../../utils/source-maps";
+
+const expressionRegex = /\bfp\(\)/g;
+
+export async function buildOriginalScopes(
+ frame: Frame,
+ client: any,
+ cx: ThreadContext,
+ frameId: any,
+ generatedScopes: Promise<Scope>
+): Promise<?{
+ mappings: {
+ [string]: string,
+ },
+ scope: OriginalScope,
+}> {
+ if (!frame.originalVariables) {
+ throw new TypeError("(frame.originalVariables: XScopeVariables)");
+ }
+ const originalVariables: XScopeVariables = frame.originalVariables;
+ const frameBase = originalVariables.frameBase || "";
+
+ const inputs = [];
+ for (let i = 0; i < originalVariables.vars.length; i++) {
+ const { expr } = originalVariables.vars[i];
+ const expression = expr
+ ? expr.replace(expressionRegex, frameBase)
+ : "void 0";
+
+ inputs[i] = expression;
+ }
+
+ const results = await client.evaluateExpressions(inputs, {
+ frameId,
+ thread: cx.thread,
+ });
+
+ const variables = {};
+ for (let i = 0; i < originalVariables.vars.length; i++) {
+ const { name } = originalVariables.vars[i];
+ variables[name] = { value: results[i].result };
+ }
+
+ const bindings = {
+ arguments: [],
+ variables,
+ };
+
+ const { actor } = await generatedScopes;
+ const scope = {
+ type: "function",
+ scopeKind: "",
+ actor,
+ bindings,
+ parent: null,
+ function: null,
+ block: null,
+ };
+ return {
+ mappings: {},
+ scope,
+ };
+}
+
+export function toggleMapScopes() {
+ return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+ if (isMapScopesEnabled(getState())) {
+ return dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: false });
+ }
+
+ dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: true });
+
+ const cx = getThreadContext(getState());
+
+ if (getSelectedOriginalScope(getState(), cx.thread)) {
+ return;
+ }
+
+ const scopes = getSelectedGeneratedScope(getState(), cx.thread);
+ const frame = getSelectedFrame(getState(), cx.thread);
+ if (!scopes || !frame) {
+ return;
+ }
+
+ dispatch(mapScopes(cx, Promise.resolve(scopes.scope), frame));
+ };
+}
+
+export function mapScopes(
+ cx: ThreadContext,
+ scopes: Promise<Scope>,
+ frame: Frame
+) {
+ return async function(thunkArgs: ThunkArgs) {
+ const { dispatch, client, getState } = thunkArgs;
+ assert(cx.thread == frame.thread, "Thread mismatch");
+
+ await dispatch({
+ type: "MAP_SCOPES",
+ cx,
+ thread: cx.thread,
+ frame,
+ [PROMISE]: (async function() {
+ if (frame.isOriginal && frame.originalVariables) {
+ const frameId = getSelectedFrameId(getState(), cx.thread);
+ return buildOriginalScopes(frame, client, cx, frameId, scopes);
+ }
+
+ return dispatch(getMappedScopes(cx, scopes, frame));
+ })(),
+ });
+ };
+}
+
+export function getMappedScopes(
+ cx: ThreadContext,
+ scopes: ?Promise<Scope>,
+ frame: MappedFrameLocation
+) {
+ return async function(thunkArgs: ThunkArgs) {
+ const { getState, dispatch } = thunkArgs;
+ const generatedSource = getSource(
+ getState(),
+ frame.generatedLocation.sourceId
+ );
+
+ const source = getSource(getState(), frame.location.sourceId);
+
+ if (
+ !isMapScopesEnabled(getState()) ||
+ !source ||
+ !generatedSource ||
+ generatedSource.isWasm ||
+ source.isPrettyPrinted ||
+ isGenerated(source)
+ ) {
+ return null;
+ }
+
+ await dispatch(loadSourceText({ cx, source }));
+ if (isOriginal(source)) {
+ await dispatch(loadSourceText({ cx, source: generatedSource }));
+ }
+
+ try {
+ const content =
+ getSource(getState(), source.id) &&
+ getSourceContent(getState(), source.id);
+
+ return await buildMappedScopes(
+ source,
+ content && isFulfilled(content)
+ ? content.value
+ : { type: "text", value: "", contentType: undefined },
+ frame,
+ await scopes,
+ thunkArgs
+ );
+ } catch (e) {
+ log(e);
+ return null;
+ }
+ };
+}
+
+export function getMappedScopesForLocation(location: SourceLocation) {
+ return async function(thunkArgs: ThunkArgs) {
+ const { dispatch, getState, sourceMaps } = thunkArgs;
+ const cx = getThreadContext(getState());
+ const mappedLocation: $Shape<MappedFrameLocation> = await getMappedLocation(
+ getState(),
+ sourceMaps,
+ location
+ );
+ return dispatch(getMappedScopes(cx, null, mappedLocation));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/moz.build b/devtools/client/debugger/src/actions/pause/moz.build
new file mode 100644
index 0000000000..ab509b9d16
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/moz.build
@@ -0,0 +1,27 @@
+# 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(
+ "breakOnNext.js",
+ "commands.js",
+ "continueToHere.js",
+ "expandScopes.js",
+ "fetchFrames.js",
+ "fetchScopes.js",
+ "index.js",
+ "inlinePreview.js",
+ "mapDisplayNames.js",
+ "mapFrames.js",
+ "mapScopes.js",
+ "paused.js",
+ "pauseOnExceptions.js",
+ "previewPausedLocation.js",
+ "resumed.js",
+ "selectFrame.js",
+ "highlightCalls.js",
+ "skipPausing.js",
+)
diff --git a/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js b/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js
new file mode 100644
index 0000000000..185c4f2437
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js
@@ -0,0 +1,37 @@
+/* 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 { PROMISE } from "../utils/middleware/promise";
+import { recordEvent } from "../../utils/telemetry";
+import type { ThunkArgs } from "../types";
+
+/**
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function pauseOnExceptions(
+ shouldPauseOnExceptions: boolean,
+ shouldPauseOnCaughtExceptions: boolean
+) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ recordEvent("pause_on_exceptions", {
+ exceptions: shouldPauseOnExceptions,
+ // There's no "n" in the key below (#1463117)
+ ["caught_exceptio"]: shouldPauseOnCaughtExceptions,
+ });
+
+ return dispatch({
+ type: "PAUSE_ON_EXCEPTIONS",
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ [PROMISE]: client.pauseOnExceptions(
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions
+ ),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/paused.js b/devtools/client/debugger/src/actions/pause/paused.js
new file mode 100644
index 0000000000..84cc54d2c3
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/paused.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
+import {
+ getHiddenBreakpoint,
+ isEvaluatingExpression,
+ getSelectedFrame,
+ getThreadContext,
+} from "../../selectors";
+
+import { mapFrames, fetchFrames } from ".";
+import { removeBreakpoint } from "../breakpoints";
+import { evaluateExpressions } from "../expressions";
+import { selectSpecificLocation } from "../sources";
+import assert from "../../utils/assert";
+
+import { fetchScopes } from "./fetchScopes";
+
+import type { Pause } from "../../types";
+import type { ThunkArgs } from "../types";
+
+/**
+ * Debugger has just paused
+ *
+ * @param {object} pauseInfo
+ * @memberof actions/pause
+ * @static
+ */
+export function paused(pauseInfo: Pause) {
+ return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+ const { thread, frame, why } = pauseInfo;
+
+ dispatch({ type: "PAUSED", thread, why, frame });
+
+ // Get a context capturing the newly paused and selected thread.
+ const cx = getThreadContext(getState());
+ assert(cx.thread == thread, "Thread mismatch");
+
+ await dispatch(fetchFrames(cx));
+
+ const hiddenBreakpoint = getHiddenBreakpoint(getState());
+ if (hiddenBreakpoint) {
+ dispatch(removeBreakpoint(cx, hiddenBreakpoint));
+ }
+
+ await dispatch(mapFrames(cx));
+
+ const selectedFrame = getSelectedFrame(getState(), thread);
+ if (selectedFrame) {
+ await dispatch(selectSpecificLocation(cx, selectedFrame.location));
+ }
+
+ await dispatch(fetchScopes(cx));
+
+ // Run after fetching scoping data so that it may make use of the sourcemap
+ // expression mappings for local variables.
+ const atException = why.type == "exception";
+ if (!atException || !isEvaluatingExpression(getState(), thread)) {
+ await dispatch(evaluateExpressions(cx));
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/previewPausedLocation.js b/devtools/client/debugger/src/actions/pause/previewPausedLocation.js
new file mode 100644
index 0000000000..7be462abfb
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/previewPausedLocation.js
@@ -0,0 +1,44 @@
+/* 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 { selectLocation } from "../sources";
+import { getContext, getSourceByURL } from "../../selectors";
+import type { ThunkArgs } from "../types";
+import type { URL } from "../../types";
+
+type Location = {
+ sourceUrl: URL,
+ column: number,
+ line: number,
+};
+
+export function previewPausedLocation(location: Location) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const cx = getContext(getState());
+ const source = getSourceByURL(getState(), location.sourceUrl);
+ if (!source) {
+ return;
+ }
+
+ const sourceLocation = {
+ line: location.line,
+ column: location.column,
+ sourceId: source.id,
+ };
+ dispatch(selectLocation(cx, sourceLocation));
+
+ dispatch({
+ type: "PREVIEW_PAUSED_LOCATION",
+ location: sourceLocation,
+ });
+ };
+}
+
+export function clearPreviewPausedLocation() {
+ return {
+ type: "CLEAR_PREVIEW_PAUSED_LOCATION",
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/resumed.js b/devtools/client/debugger/src/actions/pause/resumed.js
new file mode 100644
index 0000000000..12b2492f5b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/resumed.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
+
+import { isStepping, getPauseReason, getThreadContext } from "../../selectors";
+import { evaluateExpressions } from "../expressions";
+import { inDebuggerEval } from "../../utils/pause";
+
+import type { ThunkArgs } from "../types";
+import type { ActorId } from "../../types";
+
+/**
+ * Debugger has just resumed
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function resumed(thread: ActorId) {
+ return async ({ dispatch, client, getState }: ThunkArgs) => {
+ const why = getPauseReason(getState(), thread);
+ const wasPausedInEval = inDebuggerEval(why);
+ const wasStepping = isStepping(getState(), thread);
+
+ dispatch({ type: "RESUME", thread, wasStepping });
+
+ const cx = getThreadContext(getState());
+ if (!wasStepping && !wasPausedInEval && cx.thread == thread) {
+ await dispatch(evaluateExpressions(cx));
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/selectFrame.js b/devtools/client/debugger/src/actions/pause/selectFrame.js
new file mode 100644
index 0000000000..4344a1c00b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/selectFrame.js
@@ -0,0 +1,43 @@
+/* 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 { selectLocation } from "../sources";
+import { evaluateExpressions } from "../expressions";
+import { fetchScopes } from "./fetchScopes";
+import assert from "../../utils/assert";
+
+import type { Frame, ThreadContext } from "../../types";
+import type { ThunkArgs } from "../types";
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function selectFrame(cx: ThreadContext, frame: Frame) {
+ return async ({ dispatch, client, getState, sourceMaps }: ThunkArgs) => {
+ assert(cx.thread == frame.thread, "Thread mismatch");
+
+ // Frames that aren't on-stack do not support evalling and may not
+ // have live inspectable scopes, so we do not allow selecting them.
+ if (frame.state !== "on-stack") {
+ return dispatch(selectLocation(cx, frame.location));
+ }
+
+ dispatch({
+ type: "SELECT_FRAME",
+ cx,
+ thread: cx.thread,
+ frame,
+ });
+
+ // It's important that we wait for selectLocation to finish because
+ // we rely on the source being loaded and symbols fetched below.
+ await dispatch(selectLocation(cx, frame.location));
+
+ dispatch(evaluateExpressions(cx));
+ dispatch(fetchScopes(cx));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/skipPausing.js b/devtools/client/debugger/src/actions/pause/skipPausing.js
new file mode 100644
index 0000000000..e5d3a9ef40
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/skipPausing.js
@@ -0,0 +1,36 @@
+/* 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 { ThunkArgs } from "../types";
+import { getSkipPausing } from "../../selectors";
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function toggleSkipPausing() {
+ return async ({ dispatch, client, getState, sourceMaps }: ThunkArgs) => {
+ const skipPausing = !getSkipPausing(getState());
+ await client.setSkipPausing(skipPausing);
+ dispatch({ type: "TOGGLE_SKIP_PAUSING", skipPausing });
+ };
+}
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function setSkipPausing(skipPausing: boolean) {
+ return async ({ dispatch, client, getState, sourceMaps }: ThunkArgs) => {
+ const currentlySkipping = getSkipPausing(getState());
+ if (currentlySkipping === skipPausing) {
+ return;
+ }
+
+ await client.setSkipPausing(skipPausing);
+ dispatch({ type: "TOGGLE_SKIP_PAUSING", skipPausing });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap b/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap
new file mode 100644
index 0000000000..55b8d3e724
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`pauseOnExceptions should track telemetry for pauseOnException changes 1`] = `
+Array [
+ Object {
+ "caught_exceptio": false,
+ "exceptions": true,
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/actions/pause/tests/pause.spec.js b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
new file mode 100644
index 0000000000..3873810de3
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
@@ -0,0 +1,383 @@
+/* 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 {
+ actions,
+ selectors,
+ createStore,
+ waitForState,
+ makeSource,
+ makeOriginalSource,
+ makeFrame,
+} from "../../../utils/test-head";
+
+import { makeWhyNormal } from "../../../utils/test-mockup";
+
+const { isStepping } = selectors;
+
+let stepInResolve = null;
+const mockCommandClient = {
+ stepIn: () =>
+ new Promise(_resolve => {
+ stepInResolve = _resolve;
+ }),
+ stepOver: () => new Promise(_resolve => _resolve),
+ evaluate: async () => {},
+ evaluateInFrame: async () => {},
+ evaluateExpressions: async () => [],
+ resume: async () => {},
+ getFrameScopes: async frame => frame.scope,
+ getFrames: async () => [],
+ setBreakpoint: () => new Promise(_resolve => {}),
+ sourceContents: ({ source }) => {
+ return new Promise((resolve, reject) => {
+ switch (source) {
+ case "foo1":
+ return resolve({
+ source: "function foo1() {\n return 5;\n}",
+ contentType: "text/javascript",
+ });
+ case "await":
+ return resolve({
+ source: "async function aWait() {\n await foo(); return 5;\n}",
+ contentType: "text/javascript",
+ });
+
+ case "foo":
+ return resolve({
+ source: "function foo() {\n return -5;\n}",
+ contentType: "text/javascript",
+ });
+ case "foo-original":
+ return resolve({
+ source: "\n\nfunction fooOriginal() {\n return -5;\n}",
+ contentType: "text/javascript",
+ });
+ case "foo-wasm":
+ return resolve({
+ source: { binary: new ArrayBuffer(0) },
+ contentType: "application/wasm",
+ });
+ case "foo-wasm/originalSource":
+ return resolve({
+ source: "fn fooBar() {}\nfn barZoo() { fooBar() }",
+ contentType: "text/rust",
+ });
+ }
+ });
+ },
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ actorID: "threadActorID",
+};
+
+const mockFrameId = "1";
+
+function createPauseInfo(
+ frameLocation = { sourceId: "foo1", line: 2 },
+ frameOpts = {}
+) {
+ const frames = [
+ makeFrame(
+ { id: mockFrameId, sourceId: frameLocation.sourceId },
+ {
+ location: frameLocation,
+ generatedLocation: frameLocation,
+ ...frameOpts,
+ }
+ ),
+ ];
+ return {
+ thread: "FakeThread",
+ frame: frames[0],
+ frames,
+ loadedObjects: [],
+ why: makeWhyNormal(),
+ };
+}
+
+describe("pause", () => {
+ describe("stepping", () => {
+ it("should set and clear the command", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.paused(mockPauseInfo));
+ const cx = selectors.getThreadContext(getState());
+ const stepped = dispatch(actions.stepIn(cx));
+ expect(isStepping(getState(), "FakeThread")).toBeTruthy();
+ if (!stepInResolve) {
+ throw new Error("no stepInResolve");
+ }
+ await stepInResolve();
+ await stepped;
+ expect(isStepping(getState(), "FakeThread")).toBeFalsy();
+ });
+
+ it("should only step when paused", async () => {
+ const client = { stepIn: jest.fn() };
+ const { dispatch, cx } = createStore(client);
+
+ dispatch(actions.stepIn(cx));
+ expect(client.stepIn.mock.calls).toHaveLength(0);
+ });
+
+ it("should step when paused", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.paused(mockPauseInfo));
+ const cx = selectors.getThreadContext(getState());
+ dispatch(actions.stepIn(cx));
+ expect(isStepping(getState(), "FakeThread")).toBeTruthy();
+ });
+
+ it("getting frame scopes with bindings", async () => {
+ const generatedLocation = {
+ sourceId: "foo",
+ line: 1,
+ column: 0,
+ };
+
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {});
+ const { dispatch, getState } = store;
+ const mockPauseInfo = createPauseInfo(generatedLocation, {
+ scope: {
+ bindings: {
+ variables: { b: { value: {} } },
+ arguments: [{ a: { value: {} } }],
+ },
+ },
+ });
+
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ await dispatch(actions.newOriginalSource(makeOriginalSource(source)));
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ generatedLocation: { column: 0, line: 1, sourceId: "foo" },
+ id: mockFrameId,
+ location: { column: 0, line: 1, sourceId: "foo" },
+ originalDisplayName: "foo",
+ scope: {
+ bindings: {
+ arguments: [{ a: { value: {} } }],
+ variables: { b: { value: {} } },
+ },
+ },
+ thread: "FakeThread",
+ },
+ ]);
+
+ expect(selectors.getFrameScopes(getState(), "FakeThread")).toEqual({
+ generated: {
+ "1": {
+ pending: false,
+ scope: {
+ bindings: {
+ arguments: [{ a: { value: {} } }],
+ variables: { b: { value: {} } },
+ },
+ },
+ },
+ },
+ mappings: { "1": undefined },
+ original: { "1": { pending: false, scope: undefined } },
+ });
+
+ expect(
+ selectors.getSelectedFrameBindings(getState(), "FakeThread")
+ ).toEqual(["b", "a"]);
+ });
+
+ it("maps frame locations and names to original source", async () => {
+ const generatedLocation = {
+ sourceId: "foo",
+ line: 1,
+ column: 0,
+ };
+
+ const originalLocation = {
+ sourceId: "foo-original",
+ line: 3,
+ column: 0,
+ };
+
+ const sourceMapsMock = {
+ getOriginalLocation: () => Promise.resolve(originalLocation),
+ getOriginalLocations: async items => items,
+ getOriginalSourceText: async () => ({
+ text: "\n\nfunction fooOriginal() {\n return -5;\n}",
+ contentType: "text/javascript",
+ }),
+ getGeneratedLocation: async location => location,
+ };
+
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {}, sourceMapsMock);
+ const { dispatch, getState } = store;
+ const mockPauseInfo = createPauseInfo(generatedLocation);
+
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+ await dispatch(actions.newGeneratedSource(makeSource("foo-original")));
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ generatedLocation: { column: 0, line: 1, sourceId: "foo" },
+ id: mockFrameId,
+ location: { column: 0, line: 3, sourceId: "foo-original" },
+ originalDisplayName: "fooOriginal",
+ scope: { bindings: { arguments: [], variables: {} } },
+ thread: "FakeThread",
+ },
+ ]);
+ });
+
+ it("maps frame to original frames", async () => {
+ const generatedLocation = {
+ sourceId: "foo-wasm",
+ line: 1,
+ column: 0,
+ };
+
+ const originalLocation = {
+ sourceId: "foo-wasm/originalSource",
+ line: 1,
+ column: 1,
+ };
+ const originalLocation2 = {
+ sourceId: "foo-wasm/originalSource",
+ line: 2,
+ column: 14,
+ };
+
+ const originStackFrames = [
+ {
+ displayName: "fooBar",
+ thread: "FakeThread",
+ },
+ {
+ displayName: "barZoo",
+ location: originalLocation2,
+ thread: "FakeThread",
+ },
+ ];
+
+ const sourceMapsMock = {
+ getOriginalStackFrames: loc => Promise.resolve(originStackFrames),
+ getOriginalLocation: () => Promise.resolve(originalLocation),
+ getOriginalLocations: async items => items,
+ getOriginalSourceText: async () => ({
+ text: "fn fooBar() {}\nfn barZoo() { fooBar() }",
+ contentType: "text/rust",
+ }),
+ getGeneratedRangesForOriginal: async () => [],
+ };
+
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {}, sourceMapsMock);
+ const { dispatch, getState } = store;
+ const mockPauseInfo = createPauseInfo(generatedLocation);
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+
+ const source = await dispatch(
+ actions.newGeneratedSource(
+ makeSource("foo-wasm", { introductionType: "wasm" })
+ )
+ );
+ await dispatch(actions.newOriginalSource(makeOriginalSource(source)));
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ asyncCause: undefined,
+ displayName: "fooBar",
+ generatedLocation: { column: 0, line: 1, sourceId: "foo-wasm" },
+ id: "1",
+ index: undefined,
+ isOriginal: true,
+ location: { column: 1, line: 1, sourceId: "foo-wasm/originalSource" },
+ originalDisplayName: "fooBar",
+ originalVariables: undefined,
+ scope: { bindings: { arguments: [], variables: {} } },
+ source: null,
+ state: undefined,
+ this: undefined,
+ thread: "FakeThread",
+ },
+ {
+ asyncCause: undefined,
+ displayName: "barZoo",
+ generatedLocation: { column: 0, line: 1, sourceId: "foo-wasm" },
+ id: "1-originalFrame1",
+ index: undefined,
+ isOriginal: true,
+ location: {
+ column: 14,
+ line: 2,
+ sourceId: "foo-wasm/originalSource",
+ },
+ originalDisplayName: "barZoo",
+ originalVariables: undefined,
+ scope: { bindings: { arguments: [], variables: {} } },
+ source: null,
+ state: undefined,
+ this: undefined,
+ thread: "FakeThread",
+ },
+ ]);
+ });
+ });
+
+ describe("resumed", () => {
+ it("should not evaluate expression while stepping", async () => {
+ const client = { ...mockCommandClient, evaluateExpressions: jest.fn() };
+ const { dispatch, getState } = createStore(client);
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.paused(mockPauseInfo));
+
+ const cx = selectors.getThreadContext(getState());
+ dispatch(actions.stepIn(cx));
+ await dispatch(actions.resumed(mockCommandClient.actorID));
+ expect(client.evaluateExpressions.mock.calls).toHaveLength(1);
+ });
+
+ it("resuming - will re-evaluate watch expressions", async () => {
+ const client = { ...mockCommandClient, evaluateExpressions: jest.fn() };
+ const store = createStore(client);
+ const { dispatch, getState, cx } = store;
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+ await dispatch(actions.addExpression(cx, "foo"));
+ await waitForState(store, state => selectors.getExpression(state, "foo"));
+
+ client.evaluateExpressions.mockReturnValue(Promise.resolve(["YAY"]));
+ await dispatch(actions.paused(mockPauseInfo));
+
+ await dispatch(actions.resumed(mockCommandClient.actorID));
+ const expression = selectors.getExpression(getState(), "foo");
+ expect(expression && expression.value).toEqual("YAY");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js b/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js
new file mode 100644
index 0000000000..d6c265c56c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import {
+ actions,
+ createStore,
+ getTelemetryEvents,
+} from "../../../utils/test-head";
+
+import {
+ getShouldPauseOnExceptions,
+ getShouldPauseOnCaughtExceptions,
+} from "../../../reducers/pause";
+
+describe("pauseOnExceptions", () => {
+ it("should track telemetry for pauseOnException changes", async () => {
+ const { dispatch, getState } = createStore({ pauseOnExceptions: () => {} });
+ dispatch(actions.pauseOnExceptions(true, false));
+ expect(getTelemetryEvents("pause_on_exceptions")).toMatchSnapshot();
+ expect(getShouldPauseOnExceptions(getState())).toBe(true);
+ expect(getShouldPauseOnCaughtExceptions(getState())).toBe(false);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js b/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js
new file mode 100644
index 0000000000..7f915d478d
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { actions, selectors, createStore } from "../../../utils/test-head";
+
+describe("sources - pretty print", () => {
+ it("returns a pretty source for a minified file", async () => {
+ const client = { setSkipPausing: jest.fn() };
+ const { dispatch, getState } = createStore(client);
+
+ await dispatch(actions.toggleSkipPausing());
+ expect(selectors.getSkipPausing(getState())).toBe(true);
+
+ await dispatch(actions.toggleSkipPausing());
+ expect(selectors.getSkipPausing(getState())).toBe(false);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/preview.js b/devtools/client/debugger/src/actions/preview.js
new file mode 100644
index 0000000000..b07ae69d1f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/preview.js
@@ -0,0 +1,231 @@
+/* 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 { isConsole } from "../utils/preview";
+import { findBestMatchExpression } from "../utils/ast";
+import { getGrip, getFront } from "../utils/evaluation-result";
+import { getExpressionFromCoords } from "../utils/editor/get-expression";
+import { isOriginal } from "../utils/source";
+import { isNodeTest } from "../utils/environment";
+
+import {
+ getPreview,
+ isLineInScope,
+ isSelectedFrameVisible,
+ getSelectedSource,
+ getSelectedFrame,
+ getSymbols,
+ getCurrentThread,
+ getPreviewCount,
+ getSelectedException,
+} from "../selectors";
+
+import { getMappedExpression } from "./expressions";
+
+import type { Action, ThunkArgs } from "./types";
+import type { Position, Context } from "../types";
+import type { AstLocation } from "../workers/parser";
+
+function findExpressionMatch(state, codeMirror: any, tokenPos: Object) {
+ const source = getSelectedSource(state);
+ if (!source) {
+ return;
+ }
+
+ const symbols = getSymbols(state, source);
+
+ let match;
+ if (!symbols || symbols.loading) {
+ match = getExpressionFromCoords(codeMirror, tokenPos);
+ } else {
+ match = findBestMatchExpression(symbols, tokenPos);
+ }
+ return match;
+}
+
+export function updatePreview(
+ cx: Context,
+ target: HTMLElement,
+ tokenPos: Object,
+ codeMirror: any
+) {
+ return ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const cursorPos = target.getBoundingClientRect();
+
+ if (
+ !isSelectedFrameVisible(getState()) ||
+ !isLineInScope(getState(), tokenPos.line)
+ ) {
+ return;
+ }
+
+ const match = findExpressionMatch(getState(), codeMirror, tokenPos);
+ if (!match) {
+ return;
+ }
+
+ const { expression, location } = match;
+
+ if (isConsole(expression)) {
+ return;
+ }
+
+ dispatch(setPreview(cx, expression, location, tokenPos, cursorPos, target));
+ };
+}
+
+export function setPreview(
+ cx: Context,
+ expression: string,
+ location: AstLocation,
+ tokenPos: Position,
+ cursorPos: ClientRect,
+ target: HTMLElement
+) {
+ return async ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ dispatch({ type: "START_PREVIEW" });
+ const previewCount = getPreviewCount(getState());
+ if (getPreview(getState())) {
+ dispatch(clearPreview(cx));
+ }
+
+ const source = getSelectedSource(getState());
+ if (!source) {
+ return;
+ }
+
+ const thread = getCurrentThread(getState());
+ const selectedFrame = getSelectedFrame(getState(), thread);
+
+ if (location && isOriginal(source)) {
+ const mapResult = await dispatch(getMappedExpression(expression));
+ if (mapResult) {
+ expression = mapResult.expression;
+ }
+ }
+
+ if (!selectedFrame) {
+ return;
+ }
+
+ const { result } = await client.evaluateInFrame(expression, {
+ frameId: selectedFrame.id,
+ thread,
+ });
+
+ const resultGrip = getGrip(result);
+
+ // Error case occurs for a token that follows an errored evaluation
+ // https://github.com/firefox-devtools/debugger/pull/8056
+ // Accommodating for null allows us to show preview for falsy values
+ // line "", false, null, Nan, and more
+ if (resultGrip === null) {
+ return;
+ }
+
+ // Handle cases where the result is invisible to the debugger
+ // and not possible to preview. Bug 1548256
+ if (
+ resultGrip &&
+ resultGrip.class &&
+ typeof resultGrip.class === "string" &&
+ resultGrip.class.includes("InvisibleToDebugger")
+ ) {
+ return;
+ }
+
+ const root = {
+ name: expression,
+ path: expression,
+ contents: {
+ value: resultGrip,
+ front: getFront(result),
+ },
+ };
+ const properties = await client.loadObjectProperties(root);
+
+ // The first time a popup is rendered, the mouse should be hovered
+ // on the token. If it happens to be hovered on whitespace, it should
+ // not render anything
+ if (!target.matches(":hover") && !isNodeTest()) {
+ return;
+ }
+
+ // Don't finish dispatching if another setPreview was started
+ if (previewCount != getPreviewCount(getState())) {
+ return;
+ }
+
+ dispatch({
+ type: "SET_PREVIEW",
+ cx,
+ value: {
+ expression,
+ resultGrip,
+ properties,
+ root,
+ location,
+ tokenPos,
+ cursorPos,
+ target,
+ },
+ });
+ };
+}
+
+export function clearPreview(cx: Context) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const currentSelection = getPreview(getState());
+ if (!currentSelection) {
+ return;
+ }
+
+ return dispatch(
+ ({
+ type: "CLEAR_PREVIEW",
+ cx,
+ }: Action)
+ );
+ };
+}
+
+export function setExceptionPreview(
+ cx: Context,
+ target: HTMLElement,
+ tokenPos: Object,
+ codeMirror: any
+) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const cursorPos = target.getBoundingClientRect();
+
+ const match = findExpressionMatch(getState(), codeMirror, tokenPos);
+ if (!match) {
+ return;
+ }
+
+ const tokenColumnStart = match.location.start.column + 1;
+ const exception = getSelectedException(
+ getState(),
+ tokenPos.line,
+ tokenColumnStart
+ );
+ if (!exception) {
+ return;
+ }
+
+ dispatch({
+ type: "SET_PREVIEW",
+ cx,
+ value: {
+ exception,
+ location: match.location,
+ tokenPos,
+ cursorPos,
+ target,
+ },
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/project-text-search.js b/devtools/client/debugger/src/actions/project-text-search.js
new file mode 100644
index 0000000000..327dbccda5
--- /dev/null
+++ b/devtools/client/debugger/src/actions/project-text-search.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
+
+/**
+ * Redux actions for the search state
+ * @module actions/search
+ */
+
+import { isFulfilled } from "../utils/async-value";
+import { findSourceMatches } from "../workers/search";
+import {
+ getSource,
+ hasPrettySource,
+ getSourceList,
+ getSourceContent,
+} from "../selectors";
+import { isThirdParty } from "../utils/source";
+import { loadSourceText } from "./sources/loadSourceText";
+import {
+ statusType,
+ getTextSearchOperation,
+ getTextSearchStatus,
+} from "../reducers/project-text-search";
+
+import type { Action, ThunkArgs } from "./types";
+import type { Context, SourceId } from "../types";
+import type {
+ SearchOperation,
+ StatusType,
+} from "../reducers/project-text-search";
+
+export function addSearchQuery(cx: Context, query: string): Action {
+ return { type: "ADD_QUERY", cx, query };
+}
+
+export function addOngoingSearch(
+ cx: Context,
+ ongoingSearch: SearchOperation
+): Action {
+ return { type: "ADD_ONGOING_SEARCH", cx, ongoingSearch };
+}
+
+export function addSearchResult(
+ cx: Context,
+ sourceId: SourceId,
+ filepath: string,
+ matches: Object[]
+): Action {
+ return {
+ type: "ADD_SEARCH_RESULT",
+ cx,
+ result: { sourceId, filepath, matches },
+ };
+}
+
+export function clearSearchResults(cx: Context): Action {
+ return { type: "CLEAR_SEARCH_RESULTS", cx };
+}
+
+export function clearSearch(cx: Context): Action {
+ return { type: "CLEAR_SEARCH", cx };
+}
+
+export function updateSearchStatus(cx: Context, status: StatusType): Action {
+ return { type: "UPDATE_STATUS", cx, status };
+}
+
+export function closeProjectSearch(cx: Context) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ dispatch(stopOngoingSearch(cx));
+ dispatch({ type: "CLOSE_PROJECT_SEARCH" });
+ };
+}
+
+export function stopOngoingSearch(cx: Context) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const state = getState();
+ const ongoingSearch = getTextSearchOperation(state);
+ const status = getTextSearchStatus(state);
+ if (ongoingSearch && status !== statusType.done) {
+ ongoingSearch.cancel();
+ dispatch(updateSearchStatus(cx, statusType.cancelled));
+ }
+ };
+}
+
+export function searchSources(cx: Context, query: string) {
+ let cancelled = false;
+
+ const search = async ({ dispatch, getState }: ThunkArgs) => {
+ dispatch(stopOngoingSearch(cx));
+ await dispatch(addOngoingSearch(cx, search));
+ await dispatch(clearSearchResults(cx));
+ await dispatch(addSearchQuery(cx, query));
+ dispatch(updateSearchStatus(cx, statusType.fetching));
+ const validSources = getSourceList(getState()).filter(
+ source => !hasPrettySource(getState(), source.id) && !isThirdParty(source)
+ );
+ for (const source of validSources) {
+ if (cancelled) {
+ return;
+ }
+ await dispatch(loadSourceText({ cx, source }));
+ await dispatch(searchSource(cx, source.id, query));
+ }
+ dispatch(updateSearchStatus(cx, statusType.done));
+ };
+
+ search.cancel = () => {
+ cancelled = true;
+ };
+
+ return search;
+}
+
+export function searchSource(cx: Context, sourceId: SourceId, query: string) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const source = getSource(getState(), sourceId);
+ if (!source) {
+ return;
+ }
+
+ const content = getSourceContent(getState(), source.id);
+ let matches = [];
+ if (content && isFulfilled(content) && content.value.type === "text") {
+ matches = await findSourceMatches(source.id, content.value, query);
+ }
+ if (!matches.length) {
+ return;
+ }
+ dispatch(addSearchResult(cx, source.id, source.url, matches));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/quick-open.js b/devtools/client/debugger/src/actions/quick-open.js
new file mode 100644
index 0000000000..26f1cc91cf
--- /dev/null
+++ b/devtools/client/debugger/src/actions/quick-open.js
@@ -0,0 +1,25 @@
+/* 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 { QuickOpenAction } from "./types";
+
+export function setQuickOpenQuery(query: string): QuickOpenAction {
+ return {
+ type: "SET_QUICK_OPEN_QUERY",
+ query,
+ };
+}
+
+export function openQuickOpen(query?: string): QuickOpenAction {
+ if (query != null) {
+ return { type: "OPEN_QUICK_OPEN", query };
+ }
+ return { type: "OPEN_QUICK_OPEN" };
+}
+
+export function closeQuickOpen(): QuickOpenAction {
+ return { type: "CLOSE_QUICK_OPEN" };
+}
diff --git a/devtools/client/debugger/src/actions/source-actors.js b/devtools/client/debugger/src/actions/source-actors.js
new file mode 100644
index 0000000000..2def429a21
--- /dev/null
+++ b/devtools/client/debugger/src/actions/source-actors.js
@@ -0,0 +1,84 @@
+/* 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 {
+ getSourceActor,
+ getSourceActorBreakableLines,
+ getSourceActorBreakpointColumns,
+ type SourceActorId,
+ type SourceActor,
+} from "../reducers/source-actors";
+import {
+ memoizeableAction,
+ type MemoizedAction,
+} from "../utils/memoizableAction";
+import { PROMISE } from "./utils/middleware/promise";
+
+import type { ThunkArgs } from "./types";
+import type { Context } from "../utils/context";
+
+export function insertSourceActors(items: Array<SourceActor>) {
+ return function({ dispatch }: ThunkArgs) {
+ dispatch({
+ type: "INSERT_SOURCE_ACTORS",
+ items,
+ });
+ };
+}
+
+export function removeSourceActor(item: SourceActor) {
+ return removeSourceActors([item]);
+}
+export function removeSourceActors(items: Array<SourceActor>) {
+ return function({ dispatch }: ThunkArgs) {
+ dispatch({ type: "REMOVE_SOURCE_ACTORS", items });
+ };
+}
+
+export const loadSourceActorBreakpointColumns: MemoizedAction<
+ {| id: SourceActorId, line: number, cx: Context |},
+ Array<number>
+> = memoizeableAction("loadSourceActorBreakpointColumns", {
+ createKey: ({ id, line }) => `${id}:${line}`,
+ getValue: ({ id, line }, { getState }) =>
+ getSourceActorBreakpointColumns(getState(), id, line),
+ action: async ({ id, line }, { dispatch, getState, client }) => {
+ await dispatch({
+ type: "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS",
+ sourceId: id,
+ line,
+ [PROMISE]: (async () => {
+ const positions = await client.getSourceActorBreakpointPositions(
+ getSourceActor(getState(), id),
+ {
+ start: { line, column: 0 },
+ end: { line: line + 1, column: 0 },
+ }
+ );
+
+ return positions[line] || [];
+ })(),
+ });
+ },
+});
+
+export const loadSourceActorBreakableLines: MemoizedAction<
+ {| id: SourceActorId, cx: Context |},
+ Array<number>
+> = memoizeableAction("loadSourceActorBreakableLines", {
+ createKey: args => args.id,
+ getValue: ({ id }, { getState }) =>
+ getSourceActorBreakableLines(getState(), id),
+ action: async ({ id }, { dispatch, getState, client }) => {
+ await dispatch({
+ type: "SET_SOURCE_ACTOR_BREAKABLE_LINES",
+ sourceId: id,
+ [PROMISE]: client.getSourceActorBreakableLines(
+ getSourceActor(getState(), id)
+ ),
+ });
+ },
+});
diff --git a/devtools/client/debugger/src/actions/source-tree.js b/devtools/client/debugger/src/actions/source-tree.js
new file mode 100644
index 0000000000..b2cfa0c44b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/source-tree.js
@@ -0,0 +1,14 @@
+/* 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 { TreeNode } from "../utils/sources-tree/types";
+
+export function setExpandedState(expanded: Set<string>) {
+ return { type: "SET_EXPANDED_STATE", expanded };
+}
+
+export function focusItem(item: TreeNode) {
+ return { type: "SET_FOCUSED_SOURCE_ITEM", item };
+}
diff --git a/devtools/client/debugger/src/actions/sources/blackbox.js b/devtools/client/debugger/src/actions/sources/blackbox.js
new file mode 100644
index 0000000000..3561b5f77f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/blackbox.js
@@ -0,0 +1,106 @@
+/* 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
+
+/**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+
+import SourceMaps, {
+ isOriginalId,
+ originalToGeneratedId,
+} from "devtools-source-map";
+import { recordEvent } from "../../utils/telemetry";
+import { features } from "../../utils/prefs";
+import { getSourceActorsForSource } from "../../selectors";
+
+import { PROMISE } from "../utils/middleware/promise";
+
+import type { Source, Context, SourceId } from "../../types";
+import type { ThunkArgs } from "../types";
+import type { State } from "../../reducers/types";
+
+async function blackboxActors(
+ state: State,
+ client,
+ sourceId: SourceId,
+ isBlackBoxed: boolean,
+ range?
+): Promise<{ isBlackBoxed: boolean }> {
+ for (const actor of getSourceActorsForSource(state, sourceId)) {
+ await client.blackBox(actor, isBlackBoxed, range);
+ }
+ return { isBlackBoxed: !isBlackBoxed };
+}
+
+async function getSourceId(source: Source, sourceMaps: typeof SourceMaps) {
+ let sourceId = source.id,
+ range;
+ if (features.originalBlackbox && isOriginalId(source.id)) {
+ range = await sourceMaps.getFileGeneratedRange(source.id);
+ sourceId = originalToGeneratedId(source.id);
+ }
+ return { sourceId, range };
+}
+
+export function toggleBlackBox(cx: Context, source: Source) {
+ return async ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const { isBlackBoxed } = source;
+
+ if (!isBlackBoxed) {
+ recordEvent("blackbox");
+ }
+
+ const { sourceId, range } = await getSourceId(source, sourceMaps);
+
+ return dispatch({
+ type: "BLACKBOX",
+ cx,
+ source,
+ [PROMISE]: blackboxActors(
+ getState(),
+ client,
+ sourceId,
+ isBlackBoxed,
+ range
+ ),
+ });
+ };
+}
+
+export function blackBoxSources(
+ cx: Context,
+ sourcesToBlackBox: Source[],
+ shouldBlackBox: boolean
+) {
+ return async ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const state = getState();
+ const sources = sourcesToBlackBox.filter(
+ source => source.isBlackBoxed !== shouldBlackBox
+ );
+
+ if (shouldBlackBox) {
+ recordEvent("blackbox");
+ }
+
+ const promises = [
+ ...sources.map(async source => {
+ const { sourceId, range } = await getSourceId(source, sourceMaps);
+
+ return getSourceActorsForSource(state, sourceId).map(actor =>
+ client.blackBox(actor, source.isBlackBoxed, range)
+ );
+ }),
+ ];
+
+ return dispatch({
+ type: "BLACKBOX_SOURCES",
+ cx,
+ shouldBlackBox,
+ [PROMISE]: Promise.all(promises).then(() => ({ sources })),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/breakableLines.js b/devtools/client/debugger/src/actions/sources/breakableLines.js
new file mode 100644
index 0000000000..f7931ad622
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/breakableLines.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { isOriginalId } from "devtools-source-map";
+import { getSourceActorsForSource, getBreakableLines } from "../../selectors";
+import { setBreakpointPositions } from "../breakpoints/breakpointPositions";
+import { union } from "lodash";
+import type { Context, SourceId } from "../../types";
+import type { ThunkArgs } from "../../actions/types";
+import { loadSourceActorBreakableLines } from "../source-actors";
+
+function calculateBreakableLines(positions): number[] {
+ const lines = [];
+ for (const line in positions) {
+ if (positions[line].length > 0) {
+ lines.push(Number(line));
+ }
+ }
+
+ return lines;
+}
+
+export function setBreakableLines(cx: Context, sourceId: SourceId) {
+ return async ({ getState, dispatch, client }: ThunkArgs) => {
+ let breakableLines;
+ if (isOriginalId(sourceId)) {
+ const positions = await dispatch(
+ setBreakpointPositions({ cx, sourceId })
+ );
+ breakableLines = calculateBreakableLines(positions);
+
+ const existingBreakableLines = getBreakableLines(getState(), sourceId);
+ if (existingBreakableLines) {
+ breakableLines = union(existingBreakableLines, breakableLines);
+ }
+
+ dispatch({
+ type: "SET_ORIGINAL_BREAKABLE_LINES",
+ cx,
+ sourceId,
+ breakableLines,
+ });
+ } else {
+ const actors = getSourceActorsForSource(getState(), sourceId);
+
+ await Promise.all(
+ actors.map(({ id }) =>
+ dispatch(loadSourceActorBreakableLines({ id, cx }))
+ )
+ );
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/index.js b/devtools/client/debugger/src/actions/sources/index.js
new file mode 100644
index 0000000000..48767a107c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/index.js
@@ -0,0 +1,12 @@
+/* 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
+export * from "./blackbox";
+export * from "./breakableLines";
+export * from "./loadSourceText";
+export * from "./newSources";
+export * from "./prettyPrint";
+export * from "./select";
+export { setSymbols } from "./symbols";
diff --git a/devtools/client/debugger/src/actions/sources/loadSourceText.js b/devtools/client/debugger/src/actions/sources/loadSourceText.js
new file mode 100644
index 0000000000..8f34cc3525
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/loadSourceText.js
@@ -0,0 +1,176 @@
+/* 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 { PROMISE } from "../utils/middleware/promise";
+import {
+ getSource,
+ getSourceFromId,
+ getSourceWithContent,
+ getSourceContent,
+ getGeneratedSource,
+ getSourcesEpoch,
+ getBreakpointsForSource,
+ getSourceActorsForSource,
+} from "../../selectors";
+import { addBreakpoint } from "../breakpoints";
+
+import { prettyPrintSource } from "./prettyPrint";
+import { isFulfilled, fulfilled } from "../../utils/async-value";
+
+import { isOriginal, isPretty } from "../../utils/source";
+import {
+ memoizeableAction,
+ type MemoizedAction,
+} from "../../utils/memoizableAction";
+
+// $FlowIgnore
+const Telemetry = require("devtools/client/shared/telemetry");
+
+import type { ThunkArgs } from "../types";
+import type { Source, Context, SourceId } from "../../types";
+import type { State } from "../../reducers/types";
+
+// Measures the time it takes for a source to load
+const loadSourceHistogram = "DEVTOOLS_DEBUGGER_LOAD_SOURCE_MS";
+const telemetry = new Telemetry();
+
+async function loadSource(
+ state: State,
+ source: Source,
+ { sourceMaps, client, getState }
+): Promise<?{
+ text: string,
+ contentType: string,
+}> {
+ if (isPretty(source) && isOriginal(source)) {
+ const generatedSource = getGeneratedSource(state, source);
+ if (!generatedSource) {
+ throw new Error("Unable to find minified original.");
+ }
+ const content = getSourceContent(state, generatedSource.id);
+ if (!content || !isFulfilled(content)) {
+ throw new Error("Cannot pretty-print a file that has not loaded");
+ }
+
+ return prettyPrintSource(
+ sourceMaps,
+ generatedSource,
+ content.value,
+ getSourceActorsForSource(state, generatedSource.id)
+ );
+ }
+
+ if (isOriginal(source)) {
+ const result = await sourceMaps.getOriginalSourceText(source.id);
+ if (!result) {
+ // The way we currently try to load and select a pending
+ // selected location, it is possible that we will try to fetch the
+ // original source text right after the source map has been cleared
+ // after a navigation event.
+ throw new Error("Original source text unavailable");
+ }
+ return result;
+ }
+
+ // We only need the source text from one actor, but messages sent to retrieve
+ // the source might fail if the actor has or is about to shut down. Keep
+ // trying with different actors until one request succeeds.
+ let response;
+ const handledActors = new Set();
+ while (true) {
+ const actors = getSourceActorsForSource(state, source.id);
+ const actor = actors.find(({ actor: a }) => !handledActors.has(a));
+ if (!actor) {
+ throw new Error("Unknown source");
+ }
+ handledActors.add(actor.actor);
+
+ try {
+ telemetry.start(loadSourceHistogram, source);
+ response = await client.sourceContents(actor);
+ telemetry.finish(loadSourceHistogram, source);
+ break;
+ } catch (e) {
+ console.warn(`sourceContents failed: ${e}`);
+ }
+ }
+
+ return {
+ text: (response: any).source,
+ contentType: (response: any).contentType || "text/javascript",
+ };
+}
+
+async function loadSourceTextPromise(
+ cx: Context,
+ source: Source,
+ { dispatch, getState, client, sourceMaps, parser }: ThunkArgs
+): Promise<?Source> {
+ const epoch = getSourcesEpoch(getState());
+ await dispatch({
+ type: "LOAD_SOURCE_TEXT",
+ sourceId: source.id,
+ epoch,
+ [PROMISE]: loadSource(getState(), source, { sourceMaps, client, getState }),
+ });
+
+ const newSource = getSource(getState(), source.id);
+
+ if (!newSource) {
+ return;
+ }
+ const content = getSourceContent(getState(), newSource.id);
+
+ if (!newSource.isWasm && content) {
+ parser.setSource(
+ newSource.id,
+ isFulfilled(content)
+ ? content.value
+ : { type: "text", value: "", contentType: undefined }
+ );
+
+ // Update the text in any breakpoints for this source by re-adding them.
+ const breakpoints = getBreakpointsForSource(getState(), source.id);
+ for (const { location, options, disabled } of breakpoints) {
+ await dispatch(addBreakpoint(cx, location, options, disabled));
+ }
+ }
+}
+
+export function loadSourceById(cx: Context, sourceId: SourceId) {
+ return ({ getState, dispatch }: ThunkArgs) => {
+ const source = getSourceFromId(getState(), sourceId);
+ return dispatch(loadSourceText({ cx, source }));
+ };
+}
+
+export const loadSourceText: MemoizedAction<
+ {| cx: Context, source: Source |},
+ ?Source
+> = memoizeableAction("loadSourceText", {
+ getValue: ({ source }, { getState }) => {
+ source = source ? getSource(getState(), source.id) : null;
+ if (!source) {
+ return null;
+ }
+
+ const { content } = getSourceWithContent(getState(), source.id);
+ if (!content || content.state === "pending") {
+ return content;
+ }
+
+ // This currently swallows source-load-failure since we return fulfilled
+ // here when content.state === "rejected". In an ideal world we should
+ // propagate that error upward.
+ return fulfilled(source);
+ },
+ createKey: ({ source }, { getState }) => {
+ const epoch = getSourcesEpoch(getState());
+ return `${epoch}:${source.id}`;
+ },
+ action: ({ cx, source }, thunkArgs) =>
+ loadSourceTextPromise(cx, source, thunkArgs),
+});
diff --git a/devtools/client/debugger/src/actions/sources/moz.build b/devtools/client/debugger/src/actions/sources/moz.build
new file mode 100644
index 0000000000..9972e9f09b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/moz.build
@@ -0,0 +1,17 @@
+# 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(
+ "blackbox.js",
+ "breakableLines.js",
+ "index.js",
+ "loadSourceText.js",
+ "newSources.js",
+ "prettyPrint.js",
+ "select.js",
+ "symbols.js",
+)
diff --git a/devtools/client/debugger/src/actions/sources/newSources.js b/devtools/client/debugger/src/actions/sources/newSources.js
new file mode 100644
index 0000000000..6cd0516e5a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/newSources.js
@@ -0,0 +1,400 @@
+/* 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
+
+/**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+
+import { flatten } from "lodash";
+
+import {
+ stringToSourceActorId,
+ type SourceActor,
+} from "../../reducers/source-actors";
+import { insertSourceActors } from "../../actions/source-actors";
+import { makeSourceId } from "../../client/firefox/create";
+import { toggleBlackBox } from "./blackbox";
+import { syncBreakpoint } from "../breakpoints";
+import { loadSourceText } from "./loadSourceText";
+import { togglePrettyPrint } from "./prettyPrint";
+import { selectLocation, setBreakableLines } from "../sources";
+
+import {
+ getRawSourceURL,
+ isPrettyURL,
+ isUrlExtension,
+ isInlineScript,
+} from "../../utils/source";
+import {
+ getBlackBoxList,
+ getSource,
+ getSourceFromId,
+ hasSourceActor,
+ getSourceByActorId,
+ getPendingSelectedLocation,
+ getPendingBreakpointsForSource,
+ getContext,
+ isSourceLoadingOrLoaded,
+} from "../../selectors";
+
+import { prefs, features } from "../../utils/prefs";
+import sourceQueue from "../../utils/source-queue";
+import { validateNavigateContext, ContextError } from "../../utils/context";
+
+import type {
+ Source,
+ SourceId,
+ Context,
+ OriginalSourceData,
+ GeneratedSourceData,
+ QueuedSourceData,
+} from "../../types";
+import type { Action, ThunkArgs } from "../types";
+
+function loadSourceMaps(cx: Context, sources: SourceActor[]) {
+ return async function({
+ dispatch,
+ sourceMaps,
+ }: ThunkArgs): Promise<?(Promise<Source>[])> {
+ try {
+ const sourceList = await Promise.all(
+ sources.map(async sourceActor => {
+ const originalSources = await dispatch(
+ loadSourceMap(cx, sourceActor)
+ );
+ sourceQueue.queueSources(
+ originalSources.map(data => ({
+ type: "original",
+ data,
+ }))
+ );
+ return originalSources;
+ })
+ );
+
+ await sourceQueue.flush();
+
+ return flatten(sourceList);
+ } catch (error) {
+ if (!(error instanceof ContextError)) {
+ throw error;
+ }
+ }
+ };
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+function loadSourceMap(cx: Context, sourceActor: SourceActor) {
+ return async function({
+ dispatch,
+ getState,
+ sourceMaps,
+ }: ThunkArgs): Promise<OriginalSourceData[]> {
+ if (!prefs.clientSourceMapsEnabled || !sourceActor.sourceMapURL) {
+ return [];
+ }
+
+ let data = null;
+ try {
+ // Ignore sourceMapURL on scripts that are part of HTML files, since
+ // we currently treat sourcemaps as Source-wide, not SourceActor-specific.
+ const source = getSourceByActorId(getState(), sourceActor.id);
+ if (source) {
+ data = await sourceMaps.getOriginalURLs({
+ // Using source ID here is historical and eventually we'll want to
+ // switch to all of this being per-source-actor.
+ id: source.id,
+ url: sourceActor.url || "",
+ sourceMapBaseURL: sourceActor.sourceMapBaseURL || "",
+ sourceMapURL: sourceActor.sourceMapURL || "",
+ isWasm: sourceActor.introductionType === "wasm",
+ });
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (!data) {
+ // If this source doesn't have a sourcemap, enable it for pretty printing
+ dispatch(
+ ({
+ type: "CLEAR_SOURCE_ACTOR_MAP_URL",
+ cx,
+ id: sourceActor.id,
+ }: Action)
+ );
+ return [];
+ }
+
+ validateNavigateContext(getState(), cx);
+ return data;
+ };
+}
+
+// If a request has been made to show this source, go ahead and
+// select it.
+function checkSelectedSource(cx: Context, sourceId: SourceId) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const state = getState();
+ const pendingLocation = getPendingSelectedLocation(state);
+
+ if (!pendingLocation || !pendingLocation.url) {
+ return;
+ }
+
+ const source = getSource(state, sourceId);
+
+ if (!source || !source.url) {
+ return;
+ }
+
+ const pendingUrl = pendingLocation.url;
+ const rawPendingUrl = getRawSourceURL(pendingUrl);
+
+ if (rawPendingUrl === source.url) {
+ if (isPrettyURL(pendingUrl)) {
+ const prettySource = await dispatch(togglePrettyPrint(cx, source.id));
+ return dispatch(checkPendingBreakpoints(cx, prettySource.id));
+ }
+
+ await dispatch(
+ selectLocation(cx, {
+ sourceId: source.id,
+ line:
+ typeof pendingLocation.line === "number" ? pendingLocation.line : 0,
+ column: pendingLocation.column,
+ })
+ );
+ }
+ };
+}
+
+function checkPendingBreakpoints(cx: Context, sourceId: SourceId) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ // source may have been modified by selectLocation
+ const source = getSource(getState(), sourceId);
+ if (!source) {
+ return;
+ }
+
+ const pendingBreakpoints = getPendingBreakpointsForSource(
+ getState(),
+ source
+ );
+
+ if (pendingBreakpoints.length === 0) {
+ return;
+ }
+
+ // load the source text if there is a pending breakpoint for it
+ await dispatch(loadSourceText({ cx, source }));
+
+ await dispatch(setBreakableLines(cx, source.id));
+
+ await Promise.all(
+ pendingBreakpoints.map(bp => {
+ return dispatch(syncBreakpoint(cx, sourceId, bp));
+ })
+ );
+ };
+}
+
+function restoreBlackBoxedSources(cx: Context, sources: Source[]) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const tabs = getBlackBoxList(getState());
+ if (tabs.length == 0) {
+ return;
+ }
+ for (const source of sources) {
+ if (tabs.includes(source.url) && !source.isBlackBoxed) {
+ dispatch(toggleBlackBox(cx, source));
+ }
+ }
+ };
+}
+
+export function newQueuedSources(sourceInfo: Array<QueuedSourceData>) {
+ return async ({ dispatch }: ThunkArgs) => {
+ const generated = [];
+ const original = [];
+ for (const source of sourceInfo) {
+ if (source.type === "generated") {
+ generated.push(source.data);
+ } else {
+ original.push(source.data);
+ }
+ }
+
+ if (generated.length > 0) {
+ await dispatch(newGeneratedSources(generated));
+ }
+ if (original.length > 0) {
+ await dispatch(newOriginalSources(original));
+ }
+ };
+}
+
+export function newOriginalSource(sourceInfo: OriginalSourceData) {
+ return async ({ dispatch }: ThunkArgs) => {
+ const sources = await dispatch(newOriginalSources([sourceInfo]));
+ return sources[0];
+ };
+}
+export function newOriginalSources(sourceInfo: Array<OriginalSourceData>) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ const state = getState();
+ const seen: Set<string> = new Set();
+ const sources: Array<Source> = [];
+
+ for (const { id, url } of sourceInfo) {
+ if (seen.has(id) || getSource(state, id)) {
+ continue;
+ }
+
+ seen.add(id);
+
+ sources.push({
+ id,
+ url,
+ relativeUrl: url,
+ isPrettyPrinted: false,
+ isWasm: false,
+ isBlackBoxed: false,
+ isExtension: false,
+ extensionName: null,
+ isOriginal: true,
+ });
+ }
+
+ const cx = getContext(state);
+ dispatch(addSources(cx, sources));
+
+ await dispatch(checkNewSources(cx, sources));
+
+ for (const source of sources) {
+ dispatch(checkPendingBreakpoints(cx, source.id));
+ }
+
+ return sources;
+ };
+}
+
+export function newGeneratedSource(sourceInfo: GeneratedSourceData) {
+ return async ({ dispatch }: ThunkArgs) => {
+ const sources = await dispatch(newGeneratedSources([sourceInfo]));
+ return sources[0];
+ };
+}
+export function newGeneratedSources(sourceInfo: Array<GeneratedSourceData>) {
+ return async ({
+ dispatch,
+ getState,
+ client,
+ }: ThunkArgs): Promise<Array<Source>> => {
+ if (sourceInfo.length == 0) {
+ return [];
+ }
+
+ const resultIds = [];
+ const newSourcesObj = {};
+ const newSourceActors: Array<SourceActor> = [];
+
+ for (const { thread, isServiceWorker, source, id } of sourceInfo) {
+ const newId = id || makeSourceId(source, isServiceWorker);
+
+ if (!getSource(getState(), newId) && !newSourcesObj[newId]) {
+ newSourcesObj[newId] = {
+ id: newId,
+ url: source.url,
+ relativeUrl: source.url,
+ isPrettyPrinted: false,
+ extensionName: source.extensionName,
+ isBlackBoxed: false,
+ isWasm: !!features.wasm && source.introductionType === "wasm",
+ isExtension: (source.url && isUrlExtension(source.url)) || false,
+ isOriginal: false,
+ };
+ }
+
+ const actorId = stringToSourceActorId(source.actor);
+
+ // We are sometimes notified about a new source multiple times if we
+ // request a new source list and also get a source event from the server.
+ if (!hasSourceActor(getState(), actorId)) {
+ newSourceActors.push({
+ id: actorId,
+ actor: source.actor,
+ thread,
+ source: newId,
+ isBlackBoxed: source.isBlackBoxed,
+ sourceMapBaseURL: source.sourceMapBaseURL,
+ sourceMapURL: source.sourceMapURL,
+ url: source.url,
+ introductionType: source.introductionType,
+ });
+ }
+
+ resultIds.push(newId);
+ }
+
+ const newSources: Array<Source> = (Object.values(newSourcesObj): any[]);
+
+ const cx = getContext(getState());
+ dispatch(addSources(cx, newSources));
+ dispatch(insertSourceActors(newSourceActors));
+
+ for (const newSourceActor of newSourceActors) {
+ // Fetch breakable lines for new HTML scripts
+ // when the HTML file has started loading
+ if (
+ isInlineScript(newSourceActor) &&
+ isSourceLoadingOrLoaded(getState(), newSourceActor.source)
+ ) {
+ dispatch(setBreakableLines(cx, newSourceActor.source)).catch(error => {
+ if (!(error instanceof ContextError)) {
+ throw error;
+ }
+ });
+ }
+ }
+ await dispatch(checkNewSources(cx, newSources));
+
+ (async () => {
+ await dispatch(loadSourceMaps(cx, newSourceActors));
+
+ // We would like to sync breakpoints after we are done
+ // loading source maps as sometimes generated and original
+ // files share the same paths.
+ for (const { source } of newSourceActors) {
+ dispatch(checkPendingBreakpoints(cx, source));
+ }
+ })();
+
+ return resultIds.map(id => getSourceFromId(getState(), id));
+ };
+}
+
+function addSources(cx: Context, sources: Array<Source>) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ dispatch({ type: "ADD_SOURCES", cx, sources });
+ };
+}
+
+function checkNewSources(cx: Context, sources: Source[]) {
+ return async ({ dispatch, getState }: ThunkArgs) => {
+ for (const source of sources) {
+ dispatch(checkSelectedSource(cx, source.id));
+ }
+
+ dispatch(restoreBlackBoxedSources(cx, sources));
+
+ return sources;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/prettyPrint.js b/devtools/client/debugger/src/actions/sources/prettyPrint.js
new file mode 100644
index 0000000000..ef5036fb8c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/prettyPrint.js
@@ -0,0 +1,171 @@
+/* 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 SourceMaps, { generatedToOriginalId } from "devtools-source-map";
+
+import assert from "../../utils/assert";
+import { recordEvent } from "../../utils/telemetry";
+import { remapBreakpoints } from "../breakpoints";
+
+import { setSymbols } from "./symbols";
+import { prettyPrint } from "../../workers/pretty-print";
+import {
+ getPrettySourceURL,
+ isGenerated,
+ isJavaScript,
+} from "../../utils/source";
+import { loadSourceText } from "./loadSourceText";
+import { mapFrames } from "../pause";
+import { selectSpecificLocation } from "../sources";
+
+import {
+ getSource,
+ getSourceFromId,
+ getSourceByURL,
+ getSelectedLocation,
+ getThreadContext,
+} from "../../selectors";
+
+import type { Action, ThunkArgs } from "../types";
+import { selectSource } from "./select";
+import type {
+ Source,
+ SourceId,
+ SourceContent,
+ SourceActor,
+ Context,
+ SourceLocation,
+} from "../../types";
+
+function getPrettyOriginalSourceURL(generatedSource: Source) {
+ return getPrettySourceURL(generatedSource.url || generatedSource.id);
+}
+
+export async function prettyPrintSource(
+ sourceMaps: typeof SourceMaps,
+ generatedSource: Source,
+ content: SourceContent,
+ actors: Array<SourceActor>
+) {
+ if (!isJavaScript(generatedSource, content) || content.type !== "text") {
+ throw new Error("Can't prettify non-javascript files.");
+ }
+
+ const url = getPrettyOriginalSourceURL(generatedSource);
+ const { code, mappings } = await prettyPrint({
+ text: content.value,
+ url,
+ });
+ await sourceMaps.applySourceMap(generatedSource.id, url, code, mappings);
+
+ // The source map URL service used by other devtools listens to changes to
+ // sources based on their actor IDs, so apply the mapping there too.
+ for (const { actor } of actors) {
+ await sourceMaps.applySourceMap(actor, url, code, mappings);
+ }
+ return {
+ text: code,
+ contentType: "text/javascript",
+ };
+}
+
+export function createPrettySource(cx: Context, sourceId: SourceId) {
+ return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => {
+ const source = getSourceFromId(getState(), sourceId);
+ const url = getPrettyOriginalSourceURL(source);
+ const id = generatedToOriginalId(sourceId, url);
+
+ const prettySource = {
+ id,
+ url,
+ relativeUrl: url,
+ isBlackBoxed: false,
+ isPrettyPrinted: true,
+ isWasm: false,
+ isExtension: false,
+ extensionName: null,
+ isOriginal: true,
+ };
+
+ dispatch(({ type: "ADD_SOURCE", cx, source: prettySource }: Action));
+
+ await dispatch(selectSource(cx, id));
+
+ return prettySource;
+ };
+}
+
+function selectPrettyLocation(
+ cx: Context,
+ prettySource: Source,
+ generatedLocation: ?SourceLocation
+) {
+ return async ({ dispatch, sourceMaps, getState }: ThunkArgs) => {
+ let location = generatedLocation
+ ? generatedLocation
+ : getSelectedLocation(getState());
+
+ if (location && location.line >= 1) {
+ location = await sourceMaps.getOriginalLocation(location);
+
+ return dispatch(
+ selectSpecificLocation(cx, { ...location, sourceId: prettySource.id })
+ );
+ }
+
+ return dispatch(selectSource(cx, prettySource.id));
+ };
+}
+
+/**
+ * Toggle the pretty printing of a source's text. All subsequent calls to
+ * |getText| will return the pretty-toggled text. Nothing will happen for
+ * non-javascript files.
+ *
+ * @memberof actions/sources
+ * @static
+ * @param string id The source form from the RDP.
+ * @returns Promise
+ * A promise that resolves to [aSource, prettyText] or rejects to
+ * [aSource, error].
+ */
+export function togglePrettyPrint(cx: Context, sourceId: SourceId) {
+ return async ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => {
+ const source = getSource(getState(), sourceId);
+ if (!source) {
+ return {};
+ }
+
+ if (!source.isPrettyPrinted) {
+ recordEvent("pretty_print");
+ }
+
+ await dispatch(loadSourceText({ cx, source }));
+ assert(
+ isGenerated(source),
+ "Pretty-printing only allowed on generated sources"
+ );
+
+ const url = getPrettySourceURL(source.url);
+ const prettySource = getSourceByURL(getState(), url);
+
+ if (prettySource) {
+ return dispatch(selectPrettyLocation(cx, prettySource));
+ }
+
+ const selectedLocation = getSelectedLocation(getState());
+ const newPrettySource = await dispatch(createPrettySource(cx, sourceId));
+ dispatch(selectPrettyLocation(cx, newPrettySource, selectedLocation));
+
+ const threadcx = getThreadContext(getState());
+ await dispatch(mapFrames(threadcx));
+
+ await dispatch(setSymbols({ cx, source: newPrettySource }));
+
+ await dispatch(remapBreakpoints(cx, sourceId));
+
+ return newPrettySource;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/select.js b/devtools/client/debugger/src/actions/sources/select.js
new file mode 100644
index 0000000000..802c8cf021
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/select.js
@@ -0,0 +1,240 @@
+/* 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
+
+/**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+
+import { isOriginalId } from "devtools-source-map";
+
+import { getSourceFromId, getSourceWithContent } from "../../reducers/sources";
+import { tabExists } from "../../reducers/tabs";
+import { setSymbols } from "./symbols";
+import { setInScopeLines } from "../ast";
+import { closeActiveSearch, updateActiveFileSearch } from "../ui";
+import { togglePrettyPrint } from "./prettyPrint";
+import { addTab, closeTab } from "../tabs";
+import { loadSourceText } from "./loadSourceText";
+import { mapDisplayNames } from "../pause";
+import { setBreakableLines } from ".";
+
+import { prefs } from "../../utils/prefs";
+import { isMinified } from "../../utils/source";
+import { createLocation } from "../../utils/location";
+import { mapLocation } from "../../utils/source-maps";
+
+import {
+ getSource,
+ getSourceByURL,
+ getPrettySource,
+ getActiveSearch,
+ getSelectedLocation,
+ getSelectedSource,
+ canPrettyPrintSource,
+} from "../../selectors";
+
+import type {
+ SourceLocation,
+ PartialPosition,
+ SourceId,
+ Source,
+ Context,
+ URL,
+} from "../../types";
+import type { ThunkArgs } from "../types";
+import type { SourceAction } from "../types/SourceAction";
+
+export const setSelectedLocation = (
+ cx: Context,
+ source: Source,
+ location: SourceLocation
+): SourceAction => ({
+ type: "SET_SELECTED_LOCATION",
+ cx,
+ source,
+ location,
+});
+
+export const setPendingSelectedLocation = (
+ cx: Context,
+ url: URL,
+ options?: PartialPosition
+): SourceAction => ({
+ type: "SET_PENDING_SELECTED_LOCATION",
+ cx,
+ url,
+ line: options?.line,
+ column: options?.column,
+});
+
+export const clearSelectedLocation = (cx: Context): SourceAction => ({
+ type: "CLEAR_SELECTED_LOCATION",
+ cx,
+});
+
+/**
+ * Deterministically select a source that has a given URL. This will
+ * work regardless of the connection status or if the source exists
+ * yet.
+ *
+ * This exists mostly for external things to interact with the
+ * debugger.
+ *
+ * @memberof actions/sources
+ * @static
+ */
+export function selectSourceURL(
+ cx: Context,
+ url: URL,
+ options?: PartialPosition
+) {
+ return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => {
+ const source = getSourceByURL(getState(), url);
+ if (!source) {
+ return dispatch(setPendingSelectedLocation(cx, url, options));
+ }
+
+ const sourceId = source.id;
+ const location = createLocation({ ...options, sourceId });
+ return dispatch(selectLocation(cx, location));
+ };
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+export function selectSource(
+ cx: Context,
+ sourceId: SourceId,
+ options: PartialPosition = {}
+) {
+ return async ({ dispatch }: ThunkArgs) => {
+ const location = createLocation({ ...options, sourceId });
+ return dispatch(selectSpecificLocation(cx, location));
+ };
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+export function selectLocation(
+ cx: Context,
+ location: SourceLocation,
+ { keepContext = true }: Object = {}
+) {
+ return async ({ dispatch, getState, sourceMaps, client }: ThunkArgs) => {
+ const currentSource = getSelectedSource(getState());
+
+ if (!client) {
+ // No connection, do nothing. This happens when the debugger is
+ // shut down too fast and it tries to display a default source.
+ return;
+ }
+
+ let source = getSource(getState(), location.sourceId);
+ if (!source) {
+ // If there is no source we deselect the current selected source
+ return dispatch(clearSelectedLocation(cx));
+ }
+
+ const activeSearch = getActiveSearch(getState());
+ if (activeSearch && activeSearch !== "file") {
+ dispatch(closeActiveSearch());
+ }
+
+ // Preserve the current source map context (original / generated)
+ // when navigting to a new location.
+ const selectedSource = getSelectedSource(getState());
+ if (
+ keepContext &&
+ selectedSource &&
+ selectedSource.isOriginal != isOriginalId(location.sourceId)
+ ) {
+ location = await mapLocation(getState(), sourceMaps, location);
+ source = getSourceFromId(getState(), location.sourceId);
+ }
+
+ if (!tabExists(getState(), source.id)) {
+ dispatch(addTab(source));
+ }
+
+ dispatch(setSelectedLocation(cx, source, location));
+
+ await dispatch(loadSourceText({ cx, source }));
+ await dispatch(setBreakableLines(cx, source.id));
+
+ const loadedSource = getSource(getState(), source.id);
+
+ if (!loadedSource) {
+ // If there was a navigation while we were loading the loadedSource
+ return;
+ }
+
+ const sourceWithContent = getSourceWithContent(getState(), source.id);
+
+ if (
+ keepContext &&
+ prefs.autoPrettyPrint &&
+ !getPrettySource(getState(), loadedSource.id) &&
+ canPrettyPrintSource(getState(), loadedSource.id) &&
+ isMinified(sourceWithContent)
+ ) {
+ await dispatch(togglePrettyPrint(cx, loadedSource.id));
+ dispatch(closeTab(cx, loadedSource));
+ }
+
+ await dispatch(setSymbols({ cx, source: loadedSource }));
+ dispatch(setInScopeLines(cx));
+
+ if (cx.isPaused) {
+ await dispatch(mapDisplayNames(cx));
+ }
+
+ // If a new source is selected update the file search results
+ const newSource = getSelectedSource(getState());
+ if (currentSource && currentSource !== newSource) {
+ dispatch(updateActiveFileSearch(cx));
+ }
+ };
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+export function selectSpecificLocation(cx: Context, location: SourceLocation) {
+ return selectLocation(cx, location, { keepContext: false });
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+export function jumpToMappedLocation(cx: Context, location: SourceLocation) {
+ return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+ if (!client) {
+ return;
+ }
+
+ const pairedLocation = await mapLocation(getState(), sourceMaps, location);
+
+ return dispatch(selectSpecificLocation(cx, { ...pairedLocation }));
+ };
+}
+
+export function jumpToMappedSelectedLocation(cx: Context) {
+ return async function({ dispatch, getState }: ThunkArgs) {
+ const location = getSelectedLocation(getState());
+ if (!location) {
+ return;
+ }
+
+ await dispatch(jumpToMappedLocation(cx, location));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/symbols.js b/devtools/client/debugger/src/actions/sources/symbols.js
new file mode 100644
index 0000000000..46eec46e2c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/symbols.js
@@ -0,0 +1,63 @@
+/* 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 { getSymbols } from "../../selectors";
+
+import { PROMISE } from "../utils/middleware/promise";
+import { updateTab } from "../tabs";
+import { loadSourceText } from "./loadSourceText";
+
+import {
+ memoizeableAction,
+ type MemoizedAction,
+} from "../../utils/memoizableAction";
+import { fulfilled } from "../../utils/async-value";
+
+import type { ThunkArgs } from "../../actions/types";
+import type { Source, Context } from "../../types";
+import type { Symbols } from "../../reducers/types";
+
+async function doSetSymbols(
+ cx: Context,
+ source: Source,
+ { dispatch, getState, parser }: ThunkArgs
+) {
+ const sourceId = source.id;
+
+ await dispatch(loadSourceText({ cx, source }));
+
+ await dispatch({
+ type: "SET_SYMBOLS",
+ cx,
+ sourceId,
+ [PROMISE]: parser.getSymbols(sourceId),
+ });
+
+ const symbols = getSymbols(getState(), source);
+ if (symbols && symbols.framework) {
+ dispatch(updateTab(source, symbols.framework));
+ }
+}
+
+export const setSymbols: MemoizedAction<
+ {| cx: Context, source: Source |},
+ ?Symbols
+> = memoizeableAction("setSymbols", {
+ getValue: ({ source }, { getState }) => {
+ if (source.isWasm) {
+ return fulfilled(null);
+ }
+
+ const symbols = getSymbols(getState(), source);
+ if (!symbols || symbols.loading) {
+ return null;
+ }
+
+ return fulfilled(symbols);
+ },
+ createKey: ({ source }) => source.id,
+ action: ({ cx, source }, thunkArgs) => doSetSymbols(cx, source, thunkArgs),
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/__snapshots__/newSources.spec.js.snap b/devtools/client/debugger/src/actions/sources/tests/__snapshots__/newSources.spec.js.snap
new file mode 100644
index 0000000000..e888466114
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/__snapshots__/newSources.spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sources - new sources sources - sources with querystrings should find two sources when same source with
+ querystring 1`] = `
+Array [
+ "http://localhost:8000/examples/base.js?v=1",
+ "http://localhost:8000/examples/base.js?v=2",
+]
+`;
diff --git a/devtools/client/debugger/src/actions/sources/tests/__snapshots__/prettyPrint.spec.js.snap b/devtools/client/debugger/src/actions/sources/tests/__snapshots__/prettyPrint.spec.js.snap
new file mode 100644
index 0000000000..5930bf2293
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/__snapshots__/prettyPrint.spec.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sources - pretty print returns a pretty source for a minified file 1`] = `
+Object {
+ "extensionName": null,
+ "id": "base.js/originalSource-36c718d4bde9a75edb388ff7733efe7f",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": true,
+ "isPrettyPrinted": true,
+ "isWasm": false,
+ "relativeUrl": "/examples/base.js:formatted",
+ "url": "http://localhost:8000/examples/base.js:formatted",
+}
+`;
+
+exports[`sources - pretty print returns a pretty source for a minified file 2`] = `
+Object {
+ "state": "fulfilled",
+ "value": Object {
+ "contentType": "text/javascript",
+ "type": "text",
+ "value": "function base() {
+ return base
+}
+",
+ },
+}
+`;
diff --git a/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js b/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
new file mode 100644
index 0000000000..dbe163e3dc
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
@@ -0,0 +1,39 @@
+/* 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 {
+ actions,
+ selectors,
+ createStore,
+ makeSource,
+} from "../../../utils/test-head";
+
+describe("blackbox", () => {
+ it("should blackbox a source", async () => {
+ const store = createStore({
+ blackBox: async () => true,
+ getSourceActorBreakableLines: async () => [],
+ });
+ const { dispatch, getState, cx } = store;
+
+ const foo1Source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ await dispatch(actions.toggleBlackBox(cx, foo1Source));
+
+ const fooSource = selectors.getSource(getState(), "foo1");
+
+ if (!fooSource) {
+ throw new Error("foo should exist");
+ }
+
+ const displayedSources = selectors.getDisplayedSources(getState());
+ expect(displayedSources.FakeThread[fooSource.id].isBlackBoxed).toEqual(
+ true
+ );
+ expect(fooSource.isBlackBoxed).toEqual(true);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
new file mode 100644
index 0000000000..de1a0746d3
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
@@ -0,0 +1,275 @@
+/* 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 {
+ actions,
+ selectors,
+ watchForState,
+ createStore,
+ makeOriginalSource,
+ makeSource,
+} from "../../../utils/test-head";
+import {
+ createSource,
+ mockCommandClient,
+} from "../../tests/helpers/mockCommandClient";
+import { getBreakpointsList } from "../../../selectors";
+import { isFulfilled, isRejected } from "../../../utils/async-value";
+
+describe("loadSourceText", () => {
+ it("should load source text", async () => {
+ const store = createStore(mockCommandClient);
+ const { dispatch, getState, cx } = store;
+
+ const foo1Source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source: foo1Source }));
+
+ const foo1Content = selectors.getSourceContent(getState(), foo1Source.id);
+ expect(
+ foo1Content &&
+ isFulfilled(foo1Content) &&
+ foo1Content.value.type === "text"
+ ? foo1Content.value.value.indexOf("return foo1")
+ : -1
+ ).not.toBe(-1);
+
+ const foo2Source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo2"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source: foo2Source }));
+
+ const foo2Content = selectors.getSourceContent(getState(), foo2Source.id);
+ expect(
+ foo2Content &&
+ isFulfilled(foo2Content) &&
+ foo2Content.value.type === "text"
+ ? foo2Content.value.value.indexOf("return foo2")
+ : -1
+ ).not.toBe(-1);
+ });
+
+ it("should update breakpoint text when a source loads", async () => {
+ const fooOrigContent = createSource("fooOrig", "var fooOrig = 42;");
+ const fooGenContent = createSource("fooGen", "var fooGen = 42;");
+
+ const store = createStore(
+ {
+ ...mockCommandClient,
+ sourceContents: async () => fooGenContent,
+ getSourceActorBreakpointPositions: async () => ({ "1": [0] }),
+ getSourceActorBreakableLines: async () => [],
+ },
+ {},
+ {
+ getGeneratedRangesForOriginal: async () => [
+ { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } },
+ ],
+ getOriginalLocations: async items =>
+ items.map(item => ({
+ ...item,
+ sourceId:
+ item.sourceId === fooGenSource1.id
+ ? fooOrigSource1.id
+ : fooOrigSource2.id,
+ })),
+ getOriginalSourceText: async s => ({
+ text: fooOrigContent.source,
+ contentType: fooOrigContent.contentType,
+ }),
+ }
+ );
+ const { cx, dispatch, getState } = store;
+
+ const fooGenSource1 = await dispatch(
+ actions.newGeneratedSource(makeSource("fooGen1"))
+ );
+ const fooOrigSource1 = await dispatch(
+ actions.newOriginalSource(makeOriginalSource(fooGenSource1))
+ );
+ const fooGenSource2 = await dispatch(
+ actions.newGeneratedSource(makeSource("fooGen2"))
+ );
+ const fooOrigSource2 = await dispatch(
+ actions.newOriginalSource(makeOriginalSource(fooGenSource2))
+ );
+
+ await dispatch(actions.loadSourceText({ cx, source: fooOrigSource1 }));
+
+ await dispatch(
+ actions.addBreakpoint(
+ cx,
+ {
+ sourceId: fooOrigSource1.id,
+ line: 1,
+ column: 0,
+ },
+ {}
+ )
+ );
+
+ const breakpoint1 = getBreakpointsList(getState())[0];
+ expect(breakpoint1.text).toBe("");
+ expect(breakpoint1.originalText).toBe("var fooOrig = 42;");
+
+ await dispatch(actions.loadSourceText({ cx, source: fooGenSource1 }));
+
+ const breakpoint2 = getBreakpointsList(getState())[0];
+ expect(breakpoint2.text).toBe("var fooGen = 42;");
+ expect(breakpoint2.originalText).toBe("var fooOrig = 42;");
+
+ await dispatch(actions.loadSourceText({ cx, source: fooGenSource2 }));
+
+ await dispatch(
+ actions.addBreakpoint(
+ cx,
+ {
+ sourceId: fooGenSource2.id,
+ line: 1,
+ column: 0,
+ },
+ {}
+ )
+ );
+
+ const breakpoint3 = getBreakpointsList(getState())[1];
+ expect(breakpoint3.text).toBe("var fooGen = 42;");
+ expect(breakpoint3.originalText).toBe("");
+
+ await dispatch(actions.loadSourceText({ cx, source: fooOrigSource2 }));
+
+ const breakpoint4 = getBreakpointsList(getState())[1];
+ expect(breakpoint4.text).toBe("var fooGen = 42;");
+ expect(breakpoint4.originalText).toBe("var fooOrig = 42;");
+ });
+
+ it("loads two sources w/ one request", async () => {
+ let resolve;
+ let count = 0;
+ const { dispatch, getState, cx } = createStore({
+ sourceContents: () =>
+ new Promise(r => {
+ count++;
+ resolve = r;
+ }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ });
+ const id = "foo";
+
+ await dispatch(actions.newGeneratedSource(makeSource(id)));
+
+ let source = selectors.getSourceFromId(getState(), id);
+ dispatch(actions.loadSourceText({ cx, source }));
+
+ source = selectors.getSourceFromId(getState(), id);
+ const loading = dispatch(actions.loadSourceText({ cx, source }));
+
+ if (!resolve) {
+ throw new Error("no resolve");
+ }
+ resolve({ source: "yay", contentType: "text/javascript" });
+ await loading;
+ expect(count).toEqual(1);
+
+ const content = selectors.getSourceContent(getState(), id);
+ expect(
+ content &&
+ isFulfilled(content) &&
+ content.value.type === "text" &&
+ content.value.value
+ ).toEqual("yay");
+ });
+
+ it("doesn't re-load loaded sources", async () => {
+ let resolve;
+ let count = 0;
+ const { dispatch, getState, cx } = createStore({
+ sourceContents: () =>
+ new Promise(r => {
+ count++;
+ resolve = r;
+ }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ });
+ const id = "foo";
+
+ await dispatch(actions.newGeneratedSource(makeSource(id)));
+ let source = selectors.getSourceFromId(getState(), id);
+ const loading = dispatch(actions.loadSourceText({ cx, source }));
+
+ if (!resolve) {
+ throw new Error("no resolve");
+ }
+ resolve({ source: "yay", contentType: "text/javascript" });
+ await loading;
+
+ source = selectors.getSourceFromId(getState(), id);
+ await dispatch(actions.loadSourceText({ cx, source }));
+ expect(count).toEqual(1);
+
+ const content = selectors.getSourceContent(getState(), id);
+ expect(
+ content &&
+ isFulfilled(content) &&
+ content.value.type === "text" &&
+ content.value.value
+ ).toEqual("yay");
+ });
+
+ it("should cache subsequent source text loads", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+ const prevSource = selectors.getSourceFromId(getState(), "foo1");
+
+ await dispatch(actions.loadSourceText({ cx, source: prevSource }));
+ const curSource = selectors.getSource(getState(), "foo1");
+
+ expect(prevSource === curSource).toBeTruthy();
+ });
+
+ it("should indicate a loading source", async () => {
+ const store = createStore(mockCommandClient);
+ const { dispatch, cx } = store;
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo2"))
+ );
+
+ const wasLoading = watchForState(store, state => {
+ return !selectors.getSourceContent(state, "foo2");
+ });
+
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ expect(wasLoading()).toBe(true);
+ });
+
+ it("should indicate an errored source text", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bad-id"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+ const badSource = selectors.getSource(getState(), "bad-id");
+
+ const content = badSource
+ ? selectors.getSourceContent(getState(), badSource.id)
+ : null;
+ expect(
+ content && isRejected(content) && typeof content.value === "string"
+ ? content.value.indexOf("Unknown source")
+ : -1
+ ).not.toBe(-1);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js b/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js
new file mode 100644
index 0000000000..fd534b8052
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js
@@ -0,0 +1,198 @@
+/* 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 {
+ actions,
+ selectors,
+ createStore,
+ makeSource,
+ makeSourceURL,
+ makeOriginalSource,
+ waitForState,
+} from "../../../utils/test-head";
+const {
+ getSource,
+ getSourceCount,
+ getSelectedSource,
+ getSourceByURL,
+} = selectors;
+import sourceQueue from "../../../utils/source-queue";
+import { generatedToOriginalId } from "devtools-source-map";
+
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+
+describe("sources - new sources", () => {
+ it("should add sources to state", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("jquery.js")));
+
+ expect(getSourceCount(getState())).toEqual(2);
+ const base = getSource(getState(), "base.js");
+ const jquery = getSource(getState(), "jquery.js");
+ expect(base && base.id).toEqual("base.js");
+ expect(jquery && jquery.id).toEqual("jquery.js");
+ });
+
+ it("should not add multiple identical generated sources", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ const generated = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ await dispatch(actions.newOriginalSource(makeOriginalSource(generated)));
+ await dispatch(actions.newOriginalSource(makeOriginalSource(generated)));
+
+ expect(getSourceCount(getState())).toEqual(2);
+ });
+
+ it("should not add multiple identical original sources", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+
+ expect(getSourceCount(getState())).toEqual(1);
+ });
+
+ it("should automatically select a pending source", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+ const baseSourceURL = makeSourceURL("base.js");
+ await dispatch(actions.selectSourceURL(cx, baseSourceURL));
+
+ expect(getSelectedSource(getState())).toBe(undefined);
+ const baseSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.url).toBe(baseSource.url);
+ });
+
+ it("should add original sources", async () => {
+ const { dispatch, getState } = createStore(
+ mockCommandClient,
+ {},
+ {
+ getOriginalURLs: async source => [
+ {
+ id: generatedToOriginalId(source.id, "magic.js"),
+ url: "magic.js",
+ },
+ ],
+ getOriginalLocations: async items => items,
+ }
+ );
+
+ await dispatch(
+ actions.newGeneratedSource(
+ makeSource("base.js", { sourceMapURL: "base.js.map" })
+ )
+ );
+ const magic = getSourceByURL(getState(), "magic.js");
+ expect(magic && magic.url).toEqual("magic.js");
+ });
+
+ // eslint-disable-next-line
+ it("should not attempt to fetch original sources if it's missing a source map url", async () => {
+ const getOriginalURLs = jest.fn();
+ const { dispatch } = createStore(
+ mockCommandClient,
+ {},
+ {
+ getOriginalURLs,
+ getOriginalLocations: async items => items,
+ }
+ );
+
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+ expect(getOriginalURLs).not.toHaveBeenCalled();
+ });
+
+ // eslint-disable-next-line
+ it("should process new sources immediately, without waiting for source maps to be fetched first", async () => {
+ const { dispatch, getState } = createStore(
+ mockCommandClient,
+ {},
+ {
+ getOriginalURLs: async () => new Promise(_ => {}),
+ getOriginalLocations: async items => items,
+ }
+ );
+ await dispatch(
+ actions.newGeneratedSource(
+ makeSource("base.js", { sourceMapURL: "base.js.map" })
+ )
+ );
+ expect(getSourceCount(getState())).toEqual(1);
+ const base = getSource(getState(), "base.js");
+ expect(base && base.id).toEqual("base.js");
+ });
+
+ // eslint-disable-next-line
+ it("shouldn't let one slow loading source map delay all the other source maps", async () => {
+ const dbg = createStore(
+ mockCommandClient,
+ {},
+ {
+ getOriginalURLs: async source => {
+ if (source.id == "foo.js") {
+ // simulate a hang loading foo.js.map
+ return new Promise(_ => {});
+ }
+ const url = source.id.replace(".js", ".cljs");
+ return [
+ {
+ id: generatedToOriginalId(source.id, url),
+ url,
+ },
+ ];
+ },
+ getOriginalLocations: async items => items,
+ getGeneratedLocation: location => location,
+ }
+ );
+ const { dispatch, getState } = dbg;
+ await dispatch(
+ actions.newGeneratedSources([
+ makeSource("foo.js", { sourceMapURL: "foo.js.map" }),
+ makeSource("bar.js", { sourceMapURL: "bar.js.map" }),
+ makeSource("bazz.js", { sourceMapURL: "bazz.js.map" }),
+ ])
+ );
+ await sourceQueue.flush();
+ await waitForState(dbg, state => getSourceCount(state) == 5);
+ expect(getSourceCount(getState())).toEqual(5);
+ const barCljs = getSourceByURL(getState(), "bar.cljs");
+ expect(barCljs && barCljs.url).toEqual("bar.cljs");
+ const bazzCljs = getSourceByURL(getState(), "bazz.cljs");
+ expect(bazzCljs && bazzCljs.url).toEqual("bazz.cljs");
+ });
+
+ describe("sources - sources with querystrings", () => {
+ it(`should find two sources when same source with
+ querystring`, async () => {
+ const { getSourcesUrlsInSources } = selectors;
+ const { dispatch, getState } = createStore(mockCommandClient);
+ await dispatch(actions.newGeneratedSource(makeSource("base.js?v=1")));
+ await dispatch(actions.newGeneratedSource(makeSource("base.js?v=2")));
+ await dispatch(actions.newGeneratedSource(makeSource("diff.js?v=1")));
+
+ const base1 = "http://localhost:8000/examples/base.js?v=1";
+ const diff1 = "http://localhost:8000/examples/diff.js?v=1";
+ const diff2 = "http://localhost:8000/examples/diff.js?v=1";
+
+ expect(getSourcesUrlsInSources(getState(), base1)).toHaveLength(2);
+ expect(getSourcesUrlsInSources(getState(), base1)).toMatchSnapshot();
+
+ expect(getSourcesUrlsInSources(getState(), diff1)).toHaveLength(1);
+ await dispatch(actions.newGeneratedSource(makeSource("diff.js?v=2")));
+ expect(getSourcesUrlsInSources(getState(), diff2)).toHaveLength(2);
+ expect(getSourcesUrlsInSources(getState(), diff1)).toHaveLength(2);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js b/devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js
new file mode 100644
index 0000000000..a490949cb6
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.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 {
+ actions,
+ selectors,
+ createStore,
+ makeSource,
+} from "../../../utils/test-head";
+import { createPrettySource } from "../prettyPrint";
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { isFulfilled } from "../../../utils/async-value";
+
+describe("sources - pretty print", () => {
+ it("returns a pretty source for a minified file", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const url = "base.js";
+ const source = await dispatch(actions.newGeneratedSource(makeSource(url)));
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(createPrettySource(cx, source.id));
+
+ const prettyURL = `${source.url}:formatted`;
+ const pretty = selectors.getSourceByURL(getState(), prettyURL);
+ const content = pretty
+ ? selectors.getSourceContent(getState(), pretty.id)
+ : null;
+ expect(pretty && pretty.url.includes(prettyURL)).toEqual(true);
+ expect(pretty).toMatchSnapshot();
+
+ expect(
+ content &&
+ isFulfilled(content) &&
+ content.value.type === "text" &&
+ content.value.contentType
+ ).toEqual("text/javascript");
+ expect(content).toMatchSnapshot();
+ });
+
+ it("should create a source when first toggling pretty print", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foobar.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.togglePrettyPrint(cx, source.id));
+ expect(selectors.getSourceCount(getState())).toEqual(2);
+ });
+
+ it("should not make a second source when toggling pretty print", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foobar.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.togglePrettyPrint(cx, source.id));
+ expect(selectors.getSourceCount(getState())).toEqual(2);
+ await dispatch(actions.togglePrettyPrint(cx, source.id));
+ expect(selectors.getSourceCount(getState())).toEqual(2);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/querystrings.spec.js b/devtools/client/debugger/src/actions/sources/tests/querystrings.spec.js
new file mode 100644
index 0000000000..a3d43c19c8
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/querystrings.spec.js
@@ -0,0 +1,38 @@
+/* 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 {
+ actions,
+ selectors,
+ createStore,
+ makeSource,
+} from "../../../utils/test-head";
+const { getSourcesUrlsInSources } = selectors;
+
+// eslint-disable-next-line max-len
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+
+describe("sources - sources with querystrings", () => {
+ it("should find two sources when same source with querystring", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ await dispatch(actions.newGeneratedSource(makeSource("base.js?v=1")));
+ await dispatch(actions.newGeneratedSource(makeSource("base.js?v=2")));
+ await dispatch(actions.newGeneratedSource(makeSource("diff.js?v=1")));
+
+ expect(
+ getSourcesUrlsInSources(
+ getState(),
+ "http://localhost:8000/examples/base.js?v=1"
+ )
+ ).toHaveLength(2);
+ expect(
+ getSourcesUrlsInSources(
+ getState(),
+ "http://localhost:8000/examples/diff.js?v=1"
+ )
+ ).toHaveLength(1);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/select.spec.js b/devtools/client/debugger/src/actions/sources/tests/select.spec.js
new file mode 100644
index 0000000000..43101d0019
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/select.spec.js
@@ -0,0 +1,307 @@
+/* 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 { getSymbols } from "../../../reducers/ast";
+import {
+ actions,
+ selectors,
+ createStore,
+ makeFrame,
+ makeSource,
+ makeSourceURL,
+ waitForState,
+ makeOriginalSource,
+} from "../../../utils/test-head";
+const {
+ getSource,
+ getSourceCount,
+ getSelectedSource,
+ getSourceTabs,
+ getSelectedLocation,
+} = selectors;
+
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+
+process.on("unhandledRejection", (reason, p) => {});
+
+function initialLocation(sourceId) {
+ return { sourceId, line: 1 };
+}
+
+describe("sources", () => {
+ it("should select a source", async () => {
+ // Note that we pass an empty client in because the action checks
+ // if it exists.
+ const store = createStore(mockCommandClient);
+ const { dispatch, getState } = store;
+
+ const frame = makeFrame({ id: "1", sourceId: "foo1" });
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(
+ actions.paused({
+ thread: "FakeThread",
+ why: { type: "debuggerStatement" },
+ frame,
+ frames: [frame],
+ })
+ );
+
+ const cx = selectors.getThreadContext(getState());
+ await dispatch(
+ actions.selectLocation(cx, { sourceId: "foo1", line: 1, column: 5 })
+ );
+
+ const selectedSource = getSelectedSource(getState());
+ if (!selectedSource) {
+ throw new Error("bad selectedSource");
+ }
+ expect(selectedSource.id).toEqual("foo1");
+
+ const source = getSource(getState(), selectedSource.id);
+ if (!source) {
+ throw new Error("bad source");
+ }
+ expect(source.id).toEqual("foo1");
+ });
+
+ it("should select next tab on tab closed if no previous tab", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("baz.js")));
+
+ // 3rd tab
+ await dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
+
+ // 2nd tab
+ await dispatch(actions.selectLocation(cx, initialLocation("bar.js")));
+
+ // 1st tab
+ await dispatch(actions.selectLocation(cx, initialLocation("baz.js")));
+
+ // 3rd tab is reselected
+ await dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
+
+ // closes the 1st tab, which should have no previous tab
+ await dispatch(actions.closeTab(cx, fooSource));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("bar.js");
+ expect(getSourceTabs(getState())).toHaveLength(2);
+ });
+
+ it("should open a tab for the source", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
+
+ const tabs = getSourceTabs(getState());
+ expect(tabs).toHaveLength(1);
+ expect(tabs[0].url).toEqual("http://localhost:8000/examples/foo.js");
+ });
+
+ it("should select previous tab on tab closed", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+
+ const bazSource = await dispatch(
+ actions.newGeneratedSource(makeSource("baz.js"))
+ );
+
+ await dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
+ await dispatch(actions.selectLocation(cx, initialLocation("bar.js")));
+ await dispatch(actions.selectLocation(cx, initialLocation("baz.js")));
+ await dispatch(actions.closeTab(cx, bazSource));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("bar.js");
+ expect(getSourceTabs(getState())).toHaveLength(2);
+ });
+
+ it("should keep the selected source when other tab closed", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ const bazSource = await dispatch(
+ actions.newGeneratedSource(makeSource("baz.js"))
+ );
+
+ // 3rd tab
+ await dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
+
+ // 2nd tab
+ await dispatch(actions.selectLocation(cx, initialLocation("bar.js")));
+
+ // 1st tab
+ await dispatch(actions.selectLocation(cx, initialLocation("baz.js")));
+
+ // 3rd tab is reselected
+ await dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
+ await dispatch(actions.closeTab(cx, bazSource));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("foo.js");
+ expect(getSourceTabs(getState())).toHaveLength(2);
+ });
+
+ it("should not select new sources that lack a URL", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ await dispatch(
+ actions.newGeneratedSource({
+ ...makeSource("foo"),
+ url: "",
+ })
+ );
+
+ expect(getSourceCount(getState())).toEqual(1);
+ const selectedLocation = getSelectedLocation(getState());
+ expect(selectedLocation).toEqual(undefined);
+ });
+
+ it("sets and clears selected location correctly", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("testSource"))
+ );
+ const location = ({ test: "testLocation" }: any);
+
+ // set value
+ dispatch(actions.setSelectedLocation(cx, source, location));
+ expect(getSelectedLocation(getState())).toEqual({
+ sourceId: source.id,
+ ...location,
+ });
+
+ // clear value
+ dispatch(actions.clearSelectedLocation(cx));
+ expect(getSelectedLocation(getState())).toEqual(null);
+ });
+
+ it("sets and clears pending selected location correctly", () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+ const url = "testURL";
+ const options = { line: "testLine", column: "testColumn" };
+
+ // set value
+ dispatch(actions.setPendingSelectedLocation(cx, url, options));
+ const setResult = getState().sources.pendingSelectedLocation;
+ expect(setResult).toEqual({
+ url,
+ line: options.line,
+ column: options.column,
+ });
+
+ // clear value
+ dispatch(actions.clearSelectedLocation(cx));
+ const clearResult = getState().sources.pendingSelectedLocation;
+ expect(clearResult).toEqual({ url: "" });
+ });
+
+ it("should keep the generated the viewing context", async () => {
+ const store = createStore(mockCommandClient);
+ const { dispatch, getState, cx } = store;
+ const baseSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ await dispatch(
+ actions.selectLocation(cx, { sourceId: baseSource.id, line: 1 })
+ );
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe(baseSource.id);
+ await waitForState(store, state => getSymbols(state, baseSource));
+ });
+
+ it("should keep the original the viewing context", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockCommandClient,
+ {},
+ {
+ getOriginalLocation: async location => ({ ...location, line: 12 }),
+ getOriginalLocations: async items => items,
+ getGeneratedLocation: async location => ({ ...location, line: 12 }),
+ getOriginalSourceText: async () => ({ text: "" }),
+ getGeneratedRangesForOriginal: async () => [],
+ }
+ );
+
+ const baseSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const originalBaseSource = await dispatch(
+ actions.newOriginalSource(makeOriginalSource(baseSource))
+ );
+
+ await dispatch(actions.selectSource(cx, originalBaseSource.id));
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(
+ actions.selectLocation(cx, { sourceId: fooSource.id, line: 1 })
+ );
+
+ const selected = getSelectedLocation(getState());
+ expect(selected && selected.line).toBe(12);
+ });
+
+ it("should change the original the viewing context", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockCommandClient,
+ {},
+ {
+ getOriginalLocation: async location => ({ ...location, line: 12 }),
+ getOriginalLocations: async items => items,
+ getGeneratedRangesForOriginal: async () => [],
+ getOriginalSourceText: async () => ({ text: "" }),
+ }
+ );
+
+ const baseGenSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const baseSource = await dispatch(
+ actions.newOriginalSource(makeOriginalSource(baseGenSource))
+ );
+ await dispatch(actions.selectSource(cx, baseSource.id));
+
+ await dispatch(
+ actions.selectSpecificLocation(cx, {
+ sourceId: baseSource.id,
+ line: 1,
+ })
+ );
+
+ const selected = getSelectedLocation(getState());
+ expect(selected && selected.line).toBe(1);
+ });
+
+ describe("selectSourceURL", () => {
+ it("should automatically select a pending source", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+ const baseSourceURL = makeSourceURL("base.js");
+ await dispatch(actions.selectSourceURL(cx, baseSourceURL));
+
+ expect(getSelectedSource(getState())).toBe(undefined);
+ const baseSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.url).toBe(baseSource.url);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tabs.js b/devtools/client/debugger/src/actions/tabs.js
new file mode 100644
index 0000000000..f3464a7bfb
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tabs.js
@@ -0,0 +1,109 @@
+/* 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
+
+/**
+ * Redux actions for the editor tabs
+ * @module actions/tabs
+ */
+
+import { removeDocument } from "../utils/editor";
+import { recordEvent } from "../utils/telemetry";
+import { selectSource } from "./sources";
+
+import {
+ getSourceByURL,
+ getSourceTabs,
+ getNewSelectedSourceId,
+} from "../selectors";
+
+import type { Action, ThunkArgs } from "./types";
+import type { Source, Context, SourceId, URL } from "../types";
+
+export function updateTab(source: Source, framework: string): Action {
+ const { url, id: sourceId, isOriginal } = source;
+
+ return {
+ type: "UPDATE_TAB",
+ url,
+ framework,
+ isOriginal,
+ sourceId,
+ };
+}
+
+export function addTab(source: Source): Action {
+ const { url, id: sourceId, isOriginal } = source;
+
+ return {
+ type: "ADD_TAB",
+ url,
+ isOriginal,
+ sourceId,
+ };
+}
+
+export function moveTab(url: URL, tabIndex: number): Action {
+ return {
+ type: "MOVE_TAB",
+ url,
+ tabIndex,
+ };
+}
+
+export function moveTabBySourceId(
+ sourceId: SourceId,
+ tabIndex: number
+): Action {
+ return {
+ type: "MOVE_TAB_BY_SOURCE_ID",
+ sourceId,
+ tabIndex,
+ };
+}
+
+/**
+ * @memberof actions/tabs
+ * @static
+ */
+export function closeTab(
+ cx: Context,
+ source: Source,
+ reason: string = "click"
+) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ removeDocument(source.id);
+
+ const tabs = getSourceTabs(getState());
+ dispatch(({ type: "CLOSE_TAB", source }: Action));
+
+ const sourceId = getNewSelectedSourceId(getState(), tabs);
+ dispatch(selectSource(cx, sourceId));
+
+ recordEvent("close_source_tab", {
+ reason,
+ num_tabs: tabs.length,
+ });
+ };
+}
+
+/**
+ * @memberof actions/tabs
+ * @static
+ */
+export function closeTabs(cx: Context, urls: string[]) {
+ return ({ dispatch, getState, client }: ThunkArgs) => {
+ const sources = urls
+ .map(url => getSourceByURL(getState(), url))
+ .filter(Boolean);
+
+ const tabs = getSourceTabs(getState());
+ sources.map(source => removeDocument(source.id));
+ dispatch(({ type: "CLOSE_TABS", sources }: Action));
+
+ const sourceId = getNewSelectedSourceId(getState(), tabs);
+ dispatch(selectSource(cx, sourceId));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap
new file mode 100644
index 0000000000..f9f1af276e
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap
@@ -0,0 +1,83 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ast setSymbols when the source is loaded should be able to set symbols 1`] = `
+Object {
+ "callExpressions": Array [],
+ "classes": Array [],
+ "comments": Array [],
+ "functions": Array [
+ Object {
+ "identifier": Object {
+ "end": 13,
+ "loc": Object {
+ "end": Object {
+ "column": 13,
+ "line": 1,
+ },
+ "identifierName": "base",
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ },
+ },
+ "name": "base",
+ "start": 9,
+ "type": "Identifier",
+ },
+ "index": 0,
+ "klass": null,
+ "location": Object {
+ "end": Object {
+ "column": 21,
+ "line": 1,
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ },
+ },
+ "name": "base",
+ "parameterNames": Array [
+ "boo",
+ ],
+ },
+ ],
+ "hasJsx": false,
+ "hasTypes": false,
+ "identifiers": Array [
+ Object {
+ "expression": "base",
+ "location": Object {
+ "end": Object {
+ "column": 13,
+ "line": 1,
+ },
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ },
+ },
+ "name": "base",
+ },
+ Object {
+ "expression": "boo",
+ "location": Object {
+ "end": Object {
+ "column": 17,
+ "line": 1,
+ },
+ "start": Object {
+ "column": 14,
+ "line": 1,
+ },
+ },
+ "name": "boo",
+ },
+ ],
+ "imports": Array [],
+ "literals": Array [],
+ "loading": false,
+ "memberExpressions": Array [],
+ "objectProperties": Array [],
+}
+`;
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap
new file mode 100644
index 0000000000..f27eb26f50
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`expressions should get the autocomplete matches for the input 1`] = `
+Array [
+ "toLocaleString",
+ "toSource",
+ "toString",
+ "toolbar",
+ "top",
+]
+`;
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap
new file mode 100644
index 0000000000..bb87128f1d
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap
@@ -0,0 +1,112 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`initializing when pending breakpoints exist in prefs syncs pending breakpoints 1`] = `
+Object {
+ "http://localhost:8000/examples/bar.js:5:2": Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": undefined,
+ "offset": Object {
+ "line": 5,
+ },
+ },
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 2,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/bar.js",
+ },
+ "location": Object {
+ "column": 2,
+ "line": 5,
+ "sourceId": "",
+ "sourceUrl": "http://localhost:8000/examples/bar.js",
+ },
+ "options": Object {
+ "condition": null,
+ "hidden": false,
+ },
+ },
+}
+`;
+
+exports[`when adding breakpoints a corresponding pending breakpoint should be added 1`] = `
+Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": undefined,
+ "offset": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "foo.js",
+ "sourceUrl": "http://localhost:8000/examples/foo.js",
+ },
+ },
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/foo.js",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/foo.js",
+ },
+ "options": Object {},
+}
+`;
+
+exports[`when adding breakpoints adding and deleting breakpoints add a corresponding pendingBreakpoint for each addition 1`] = `
+Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": undefined,
+ "offset": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "foo",
+ "sourceUrl": "http://localhost:8000/examples/foo",
+ },
+ },
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 0,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/foo",
+ },
+ "location": Object {
+ "column": 0,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/foo",
+ },
+ "options": Object {},
+}
+`;
+
+exports[`when adding breakpoints adding and deleting breakpoints add a corresponding pendingBreakpoint for each addition 2`] = `
+Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": undefined,
+ "offset": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "foo2",
+ "sourceUrl": "http://localhost:8000/examples/foo2",
+ },
+ },
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 0,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/foo2",
+ },
+ "location": Object {
+ "column": 0,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/foo2",
+ },
+ "options": Object {},
+}
+`;
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap
new file mode 100644
index 0000000000..026bfe4a89
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`preview should generate previews 1`] = `null`;
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap
new file mode 100644
index 0000000000..e7f0e40d64
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap
@@ -0,0 +1,180 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`project text search should clear all the search results 1`] = `
+Array [
+ Object {
+ "filepath": "http://localhost:8000/examples/foo1",
+ "matches": Array [
+ Object {
+ "column": 9,
+ "line": 1,
+ "match": "foo",
+ "matchIndex": 9,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": "function foo1() {",
+ },
+ Object {
+ "column": 8,
+ "line": 2,
+ "match": "foo",
+ "matchIndex": 8,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": " const foo = 5; return foo;",
+ },
+ Object {
+ "column": 24,
+ "line": 2,
+ "match": "foo",
+ "matchIndex": 24,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": " const foo = 5; return foo;",
+ },
+ ],
+ "sourceId": "foo1",
+ "type": "RESULT",
+ },
+]
+`;
+
+exports[`project text search should clear all the search results 2`] = `Array []`;
+
+exports[`project text search should close project search 1`] = `
+Array [
+ Object {
+ "filepath": "http://localhost:8000/examples/foo1",
+ "matches": Array [
+ Object {
+ "column": 9,
+ "line": 1,
+ "match": "foo",
+ "matchIndex": 9,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": "function foo1() {",
+ },
+ Object {
+ "column": 8,
+ "line": 2,
+ "match": "foo",
+ "matchIndex": 8,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": " const foo = 5; return foo;",
+ },
+ Object {
+ "column": 24,
+ "line": 2,
+ "match": "foo",
+ "matchIndex": 24,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": " const foo = 5; return foo;",
+ },
+ ],
+ "sourceId": "foo1",
+ "type": "RESULT",
+ },
+]
+`;
+
+exports[`project text search should close project search 2`] = `Array []`;
+
+exports[`project text search should ignore sources with minified versions 1`] = `
+Array [
+ Object {
+ "filepath": "http://localhost:8000/examples/bar:formatted",
+ "matches": Array [
+ Object {
+ "column": 9,
+ "line": 1,
+ "match": "bla",
+ "matchIndex": 9,
+ "sourceId": "bar/originalSource-79d3ab91075b948b7044296e606a28c5",
+ "type": "MATCH",
+ "value": "function bla(x, y) {",
+ },
+ ],
+ "sourceId": "bar/originalSource-79d3ab91075b948b7044296e606a28c5",
+ "type": "RESULT",
+ },
+]
+`;
+
+exports[`project text search should search a specific source 1`] = `
+Array [
+ Object {
+ "filepath": "http://localhost:8000/examples/bar",
+ "matches": Array [
+ Object {
+ "column": 9,
+ "line": 1,
+ "match": "bla",
+ "matchIndex": 9,
+ "sourceId": "bar",
+ "type": "MATCH",
+ "value": "function bla(x, y) {",
+ },
+ ],
+ "sourceId": "bar",
+ "type": "RESULT",
+ },
+]
+`;
+
+exports[`project text search should search all the loaded sources based on the query 1`] = `
+Array [
+ Object {
+ "filepath": "http://localhost:8000/examples/foo1",
+ "matches": Array [
+ Object {
+ "column": 9,
+ "line": 1,
+ "match": "foo",
+ "matchIndex": 9,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": "function foo1() {",
+ },
+ Object {
+ "column": 8,
+ "line": 2,
+ "match": "foo",
+ "matchIndex": 8,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": " const foo = 5; return foo;",
+ },
+ Object {
+ "column": 24,
+ "line": 2,
+ "match": "foo",
+ "matchIndex": 24,
+ "sourceId": "foo1",
+ "type": "MATCH",
+ "value": " const foo = 5; return foo;",
+ },
+ ],
+ "sourceId": "foo1",
+ "type": "RESULT",
+ },
+ Object {
+ "filepath": "http://localhost:8000/examples/foo2",
+ "matches": Array [
+ Object {
+ "column": 9,
+ "line": 1,
+ "match": "foo",
+ "matchIndex": 9,
+ "sourceId": "foo2",
+ "type": "MATCH",
+ "value": "function foo2(x, y) {",
+ },
+ ],
+ "sourceId": "foo2",
+ "type": "RESULT",
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/actions/tests/ast.spec.js b/devtools/client/debugger/src/actions/tests/ast.spec.js
new file mode 100644
index 0000000000..9dde191e77
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/ast.spec.js
@@ -0,0 +1,128 @@
+/* eslint max-nested-callbacks: ["error", 6] */
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ makeOriginalSource,
+ waitForState,
+} from "../../utils/test-head";
+
+import readFixture from "./helpers/readFixture";
+const { getSymbols, isSymbolsLoading, getFramework } = selectors;
+
+const mockCommandClient = {
+ sourceContents: async ({ source }) => ({
+ source: sourceTexts[source],
+ contentType: "text/javascript",
+ }),
+ getFrameScopes: async () => {},
+ evaluate: async expression => ({ result: evaluationResult[expression] }),
+ evaluateExpressions: async expressions =>
+ expressions.map(expression => ({ result: evaluationResult[expression] })),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+};
+
+const sourceMaps = {
+ getOriginalSourceText: async id => ({
+ id,
+ text: sourceTexts[id],
+ contentType: "text/javascript",
+ }),
+ getGeneratedRangesForOriginal: async () => [],
+ getOriginalLocations: async items => items,
+};
+
+const sourceTexts = {
+ "base.js": "function base(boo) {}",
+ "foo.js": "function base(boo) { return this.bazz; } outOfScope",
+ "reactComponent.js/originalSource": readFixture("reactComponent.js"),
+ "reactFuncComponent.js/originalSource": readFixture("reactFuncComponent.js"),
+};
+
+const evaluationResult = {
+ "this.bazz": { actor: "bazz", preview: {} },
+ this: { actor: "this", preview: {} },
+};
+
+describe("ast", () => {
+ describe("setSymbols", () => {
+ describe("when the source is loaded", () => {
+ it("should be able to set symbols", async () => {
+ const store = createStore(mockCommandClient);
+ const { dispatch, getState, cx } = store;
+ const base = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source: base }));
+
+ const loadedSource = selectors.getSourceFromId(getState(), base.id);
+ await dispatch(actions.setSymbols({ cx, source: loadedSource }));
+ await waitForState(store, state => !isSymbolsLoading(state, base));
+
+ const baseSymbols = getSymbols(getState(), base);
+ expect(baseSymbols).toMatchSnapshot();
+ });
+ });
+
+ describe("when the source is not loaded", () => {
+ it("should return null", async () => {
+ const { getState, dispatch } = createStore(mockCommandClient);
+ const base = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const baseSymbols = getSymbols(getState(), base);
+ expect(baseSymbols).toEqual(null);
+ });
+ });
+
+ describe("when there is no source", () => {
+ it("should return null", async () => {
+ const { getState } = createStore(mockCommandClient);
+ const baseSymbols = getSymbols(getState());
+ expect(baseSymbols).toEqual(null);
+ });
+ });
+
+ describe("frameworks", () => {
+ it("should detect react components", async () => {
+ const store = createStore(mockCommandClient, {}, sourceMaps);
+ const { cx, dispatch, getState } = store;
+
+ const genSource = await dispatch(
+ actions.newGeneratedSource(makeSource("reactComponent.js"))
+ );
+
+ const source = await dispatch(
+ actions.newOriginalSource(makeOriginalSource(genSource))
+ );
+
+ await dispatch(actions.loadSourceText({ cx, source }));
+ const loadedSource = selectors.getSourceFromId(getState(), source.id);
+ await dispatch(actions.setSymbols({ cx, source: loadedSource }));
+
+ expect(getFramework(getState(), source)).toBe("React");
+ });
+
+ it("should not give false positive on non react components", async () => {
+ const store = createStore(mockCommandClient);
+ const { cx, dispatch, getState } = store;
+ const base = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source: base }));
+ await dispatch(actions.setSymbols({ cx, source: base }));
+
+ expect(getFramework(getState(), base)).toBe(undefined);
+ });
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/expressions.spec.js b/devtools/client/debugger/src/actions/tests/expressions.spec.js
new file mode 100644
index 0000000000..0a1cffefe7
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/expressions.spec.js
@@ -0,0 +1,186 @@
+/* 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 {
+ actions,
+ selectors,
+ createStore,
+ makeSource,
+} from "../../utils/test-head";
+
+import { makeMockFrame } from "../../utils/test-mockup";
+
+const mockThreadFront = {
+ evaluateInFrame: (script, { frameId }) =>
+ new Promise((resolve, reject) => {
+ if (!frameId) {
+ resolve("bla");
+ } else {
+ resolve("boo");
+ }
+ }),
+ evaluateExpressions: (inputs, { frameId }) =>
+ Promise.all(
+ inputs.map(
+ input =>
+ new Promise((resolve, reject) => {
+ if (!frameId) {
+ resolve("bla");
+ } else {
+ resolve("boo");
+ }
+ })
+ )
+ ),
+ getFrameScopes: async () => {},
+ getFrames: async () => [],
+ sourceContents: () => ({ source: "", contentType: "text/javascript" }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ autocomplete: () => {
+ return new Promise(resolve => {
+ resolve({
+ from: "foo",
+ matches: ["toLocaleString", "toSource", "toString", "toolbar", "top"],
+ matchProp: "to",
+ });
+ });
+ },
+};
+
+describe("expressions", () => {
+ it("should add an expression", async () => {
+ const { dispatch, getState, cx } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression(cx, "foo"));
+ expect(selectors.getExpressions(getState())).toHaveLength(1);
+ });
+
+ it("should not add empty expressions", () => {
+ const { dispatch, getState, cx } = createStore(mockThreadFront);
+
+ dispatch(actions.addExpression(cx, (undefined: any)));
+ dispatch(actions.addExpression(cx, ""));
+ expect(selectors.getExpressions(getState())).toHaveLength(0);
+ });
+
+ it("should not add invalid expressions", async () => {
+ const { dispatch, getState, cx } = createStore(mockThreadFront);
+ await dispatch(actions.addExpression(cx, "foo#"));
+ const state = getState();
+ expect(selectors.getExpressions(state)).toHaveLength(0);
+ expect(selectors.getExpressionError(state)).toBe(true);
+ });
+
+ it("should update an expression", async () => {
+ const { dispatch, getState, cx } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression(cx, "foo"));
+ const expression = selectors.getExpression(getState(), "foo");
+ if (!expression) {
+ throw new Error("expression must exist");
+ }
+
+ await dispatch(actions.updateExpression(cx, "bar", expression));
+ const bar = selectors.getExpression(getState(), "bar");
+
+ expect(bar && bar.input).toBe("bar");
+ });
+
+ it("should not update an expression w/ invalid code", async () => {
+ const { dispatch, getState, cx } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression(cx, "foo"));
+ const expression = selectors.getExpression(getState(), "foo");
+ if (!expression) {
+ throw new Error("expression must exist");
+ }
+ await dispatch(actions.updateExpression(cx, "#bar", expression));
+ expect(selectors.getExpression(getState(), "bar")).toBeUndefined();
+ });
+
+ it("should delete an expression", async () => {
+ const { dispatch, getState, cx } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression(cx, "foo"));
+ await dispatch(actions.addExpression(cx, "bar"));
+ expect(selectors.getExpressions(getState())).toHaveLength(2);
+
+ const expression = selectors.getExpression(getState(), "foo");
+
+ if (!expression) {
+ throw new Error("expression must exist");
+ }
+
+ const bar = selectors.getExpression(getState(), "bar");
+ dispatch(actions.deleteExpression(expression));
+ expect(selectors.getExpressions(getState())).toHaveLength(1);
+ expect(bar && bar.input).toBe("bar");
+ });
+
+ it("should evaluate expressions global scope", async () => {
+ const { dispatch, getState, cx } = createStore(mockThreadFront);
+ await dispatch(actions.addExpression(cx, "foo"));
+ await dispatch(actions.addExpression(cx, "bar"));
+
+ let foo = selectors.getExpression(getState(), "foo");
+ let bar = selectors.getExpression(getState(), "bar");
+ expect(foo && foo.value).toBe("bla");
+ expect(bar && bar.value).toBe("bla");
+
+ await dispatch(actions.evaluateExpressions(cx));
+ foo = selectors.getExpression(getState(), "foo");
+ bar = selectors.getExpression(getState(), "bar");
+ expect(foo && foo.value).toBe("bla");
+ expect(bar && bar.value).toBe("bla");
+ });
+
+ it("should evaluate expressions in specific scope", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+ await createFrames(getState, dispatch);
+
+ const cx = selectors.getThreadContext(getState());
+ await dispatch(actions.newGeneratedSource(makeSource("source")));
+ await dispatch(actions.addExpression(cx, "foo"));
+ await dispatch(actions.addExpression(cx, "bar"));
+
+ let foo = selectors.getExpression(getState(), "foo");
+ let bar = selectors.getExpression(getState(), "bar");
+ expect(foo && foo.value).toBe("boo");
+ expect(bar && bar.value).toBe("boo");
+
+ await dispatch(actions.evaluateExpressions(cx));
+ foo = selectors.getExpression(getState(), "foo");
+ bar = selectors.getExpression(getState(), "bar");
+ expect(foo && foo.value).toBe("boo");
+ expect(bar && bar.value).toBe("boo");
+ });
+
+ it("should get the autocomplete matches for the input", async () => {
+ const { cx, dispatch, getState } = createStore(mockThreadFront);
+ await dispatch(actions.autocomplete(cx, "to", 2));
+ expect(selectors.getAutocompleteMatchset(getState())).toMatchSnapshot();
+ });
+});
+
+async function createFrames(getState, dispatch) {
+ const frame = makeMockFrame();
+ await dispatch(actions.newGeneratedSource(makeSource("example.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("source")));
+
+ await dispatch(
+ actions.paused({
+ thread: "FakeThread",
+ frame,
+ frames: [frame],
+ why: { type: "just because" },
+ })
+ );
+
+ await dispatch(
+ actions.selectFrame(selectors.getThreadContext(getState()), frame)
+ );
+}
diff --git a/devtools/client/debugger/src/actions/tests/file-search.spec.js b/devtools/client/debugger/src/actions/tests/file-search.spec.js
new file mode 100644
index 0000000000..06118edc8d
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/file-search.spec.js
@@ -0,0 +1,66 @@
+/* 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 { createStore, selectors, actions } from "../../utils/test-head";
+
+const {
+ getFileSearchQuery,
+ getFileSearchModifiers,
+ getFileSearchResults,
+} = selectors;
+
+describe("file text search", () => {
+ it("should update search results", () => {
+ const { dispatch, getState, cx } = createStore();
+ expect(getFileSearchResults(getState())).toEqual({
+ matches: [],
+ matchIndex: -1,
+ index: -1,
+ count: 0,
+ });
+
+ const matches = [
+ { line: 1, ch: 3 },
+ { line: 3, ch: 2 },
+ ];
+ dispatch(actions.updateSearchResults(cx, 2, 3, matches));
+
+ expect(getFileSearchResults(getState())).toEqual({
+ count: 2,
+ index: 2,
+ matchIndex: 1,
+ matches,
+ });
+ });
+
+ it("should update the file search query", () => {
+ const { dispatch, getState, cx } = createStore();
+ let fileSearchQueryState = getFileSearchQuery(getState());
+ expect(fileSearchQueryState).toBe("");
+ dispatch(actions.setFileSearchQuery(cx, "foobar"));
+ fileSearchQueryState = getFileSearchQuery(getState());
+ expect(fileSearchQueryState).toBe("foobar");
+ });
+
+ it("should toggle a file search modifier", () => {
+ const { dispatch, getState, cx } = createStore();
+ let fileSearchModState = getFileSearchModifiers(getState());
+ expect(fileSearchModState.caseSensitive).toBe(false);
+ dispatch(actions.toggleFileSearchModifier(cx, "caseSensitive"));
+ fileSearchModState = getFileSearchModifiers(getState());
+ expect(fileSearchModState.caseSensitive).toBe(true);
+ });
+
+ it("should toggle a file search query cleaning", () => {
+ const { dispatch, getState, cx } = createStore();
+ dispatch(actions.setFileSearchQuery(cx, "foobar"));
+ let fileSearchQueryState = getFileSearchQuery(getState());
+ expect(fileSearchQueryState).toBe("foobar");
+ dispatch(actions.setFileSearchQuery(cx, ""));
+ fileSearchQueryState = getFileSearchQuery(getState());
+ expect(fileSearchQueryState).toBe("");
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/fixtures/immutable.js b/devtools/client/debugger/src/actions/tests/fixtures/immutable.js
new file mode 100644
index 0000000000..e8ac7fb233
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/fixtures/immutable.js
@@ -0,0 +1,2 @@
+
+const m = Immutable.Map({a: 2})
diff --git a/devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js b/devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js
new file mode 100644
index 0000000000..526c852d99
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js
@@ -0,0 +1,7 @@
+import React, { Component } from "react";
+
+class FixtureComponent extends Component {
+ render() {
+ return null;
+ }
+}
diff --git a/devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js b/devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js
new file mode 100644
index 0000000000..3103161fe0
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js
@@ -0,0 +1,5 @@
+import React, { Component } from "react";
+
+export default FixtureComponent = (props) => {
+ return <div>props.a</div>;
+}
diff --git a/devtools/client/debugger/src/actions/tests/fixtures/scopes.js b/devtools/client/debugger/src/actions/tests/fixtures/scopes.js
new file mode 100644
index 0000000000..3a38097f5e
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/fixtures/scopes.js
@@ -0,0 +1,11 @@
+// Program Scope
+
+function outer() {
+ function inner() {
+ const x = 1;
+ }
+
+ const declaration = function() {
+ const x = 1;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js b/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js
new file mode 100644
index 0000000000..dd4f108d71
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/helpers/breakpoints.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
+
+export function mockPendingBreakpoint(overrides: Object = {}) {
+ const { sourceUrl, line, column, condition, disabled, hidden } = overrides;
+ return {
+ location: {
+ sourceId: "",
+ sourceUrl: sourceUrl || "http://localhost:8000/examples/bar.js",
+ line: line || 5,
+ column: column || 1,
+ },
+ generatedLocation: {
+ sourceUrl: sourceUrl || "http://localhost:8000/examples/bar.js",
+ line: line || 5,
+ column: column || 1,
+ },
+ astLocation: {
+ name: undefined,
+ offset: {
+ line: line || 5,
+ },
+ index: 0,
+ },
+ options: {
+ condition: condition || null,
+ hidden: hidden || false,
+ },
+ disabled: disabled || false,
+ };
+}
+
+export function generateBreakpoint(
+ filename: string,
+ line: number = 5,
+ column: number = 0
+) {
+ return {
+ id: "breakpoint",
+ originalText: "",
+ text: "",
+ location: {
+ sourceUrl: `http://localhost:8000/examples/${filename}`,
+ sourceId: `${filename}`,
+ line,
+ column,
+ },
+ generatedLocation: {
+ sourceUrl: `http://localhost:8000/examples/${filename}`,
+ sourceId: filename,
+ line,
+ column,
+ },
+ astLocation: undefined,
+ options: {
+ condition: "",
+ hidden: false,
+ },
+ disabled: false,
+ };
+}
diff --git a/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
new file mode 100644
index 0000000000..9980b721f9
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import type { SourceActor } from "../../../types";
+
+export function createSource(name: string, code?: string) {
+ name = name.replace(/\..*$/, "");
+ return {
+ source: code || `function ${name}() {\n return ${name} \n}`,
+ contentType: "text/javascript",
+ };
+}
+
+const sources = [
+ "a",
+ "b",
+ "foo",
+ "bar",
+ "foo1",
+ "foo2",
+ "a.js",
+ "baz.js",
+ "foobar.js",
+ "barfoo.js",
+ "foo.js",
+ "bar.js",
+ "base.js",
+ "bazz.js",
+ "jquery.js",
+];
+
+export const mockCommandClient = {
+ sourceContents: function({
+ source,
+ }: SourceActor): Promise<{| source: any, contentType: ?string |}> {
+ return new Promise((resolve, reject) => {
+ if (sources.includes(source)) {
+ resolve(createSource(source));
+ }
+
+ reject(`unknown source: ${source}`);
+ });
+ },
+ setBreakpoint: async () => {},
+ removeBreakpoint: (_id: string) => Promise.resolve(),
+ threadFront: async () => {},
+ getFrameScopes: async () => {},
+ getFrames: async () => [],
+ evaluateExpressions: async () => {},
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+};
diff --git a/devtools/client/debugger/src/actions/tests/helpers/readFixture.js b/devtools/client/debugger/src/actions/tests/helpers/readFixture.js
new file mode 100644
index 0000000000..0206514b00
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/helpers/readFixture.js
@@ -0,0 +1,16 @@
+/* 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 fs from "fs";
+import path from "path";
+
+export default function readFixture(name: string) {
+ const text = fs.readFileSync(
+ path.join(__dirname, `../fixtures/${name}`),
+ "utf8"
+ );
+ return text;
+}
diff --git a/devtools/client/debugger/src/actions/tests/navigation.spec.js b/devtools/client/debugger/src/actions/tests/navigation.spec.js
new file mode 100644
index 0000000000..cc8bd8300a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/navigation.spec.js
@@ -0,0 +1,108 @@
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+} from "../../utils/test-head";
+
+jest.mock("../../utils/editor");
+
+const {
+ getActiveSearch,
+ getTextSearchQuery,
+ getTextSearchResults,
+ getTextSearchStatus,
+ getFileSearchQuery,
+ getFileSearchResults,
+} = selectors;
+
+const threadFront = {
+ sourceContents: async () => ({
+ source: "function foo1() {\n const foo = 5; return foo;\n}",
+ contentType: "text/javascript",
+ }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+};
+
+describe("navigation", () => {
+ it("navigation closes project-search", async () => {
+ const { dispatch, getState, cx } = createStore(threadFront);
+ const mockQuery = "foo";
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.searchSources(cx, mockQuery));
+
+ let results = getTextSearchResults(getState());
+ expect(results).toHaveLength(1);
+ expect(selectors.getTextSearchQuery(getState())).toEqual("foo");
+ expect(getTextSearchStatus(getState())).toEqual("DONE");
+
+ await dispatch(actions.willNavigate("will-navigate"));
+
+ results = getTextSearchResults(getState());
+ expect(results).toHaveLength(0);
+ expect(getTextSearchQuery(getState())).toEqual("");
+ expect(getTextSearchStatus(getState())).toEqual("INITIAL");
+ });
+
+ it("navigation removes activeSearch 'project' value", async () => {
+ const { dispatch, getState } = createStore(threadFront);
+ dispatch(actions.setActiveSearch("project"));
+ expect(getActiveSearch(getState())).toBe("project");
+
+ await dispatch(actions.willNavigate("will-navigate"));
+ expect(getActiveSearch(getState())).toBe(null);
+ });
+
+ it("navigation clears the file-search query", async () => {
+ const { dispatch, getState, cx } = createStore(threadFront);
+
+ dispatch(actions.setFileSearchQuery(cx, "foobar"));
+ expect(getFileSearchQuery(getState())).toBe("foobar");
+
+ await dispatch(actions.willNavigate("will-navigate"));
+
+ expect(getFileSearchQuery(getState())).toBe("");
+ });
+
+ it("navigation clears the file-search results", async () => {
+ const { dispatch, getState, cx } = createStore(threadFront);
+
+ const searchResults = [
+ { line: 1, ch: 3 },
+ { line: 3, ch: 2 },
+ ];
+ dispatch(actions.updateSearchResults(cx, 2, 3, searchResults));
+ expect(getFileSearchResults(getState())).toEqual({
+ count: 2,
+ index: 2,
+ matchIndex: 1,
+ matches: searchResults,
+ });
+
+ await dispatch(actions.willNavigate("will-navigate"));
+
+ expect(getFileSearchResults(getState())).toEqual({
+ count: 0,
+ index: -1,
+ matchIndex: -1,
+ matches: [],
+ });
+ });
+
+ it("navigation removes activeSearch 'file' value", async () => {
+ const { dispatch, getState } = createStore(threadFront);
+ dispatch(actions.setActiveSearch("file"));
+ expect(getActiveSearch(getState())).toBe("file");
+
+ await dispatch(actions.willNavigate("will-navigate"));
+ expect(getActiveSearch(getState())).toBe(null);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
new file mode 100644
index 0000000000..f43686d199
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
@@ -0,0 +1,439 @@
+/* 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
+
+// TODO: we would like to mock this in the local tests
+import {
+ generateBreakpoint,
+ mockPendingBreakpoint,
+} from "./helpers/breakpoints.js";
+
+import { mockCommandClient } from "./helpers/mockCommandClient";
+import { asyncStore } from "../../utils/prefs";
+
+function loadInitialState(opts = {}) {
+ const mockedPendingBreakpoint = mockPendingBreakpoint({ ...opts, column: 2 });
+ const id = makePendingLocationId(mockedPendingBreakpoint.location);
+ asyncStore.pendingBreakpoints = { [id]: mockedPendingBreakpoint };
+
+ return { pendingBreakpoints: asyncStore.pendingBreakpoints };
+}
+
+jest.mock("../../utils/prefs", () => ({
+ prefs: {
+ clientSourceMapsEnabled: true,
+ expressions: [],
+ },
+ asyncStore: {
+ pendingBreakpoints: {},
+ },
+ clear: jest.fn(),
+ features: {
+ inlinePreview: true,
+ },
+}));
+
+import {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ makeSourceURL,
+ waitForState,
+} from "../../utils/test-head";
+
+import sourceMaps from "devtools-source-map";
+
+import { makePendingLocationId } from "../../utils/breakpoint";
+function mockClient(bpPos = {}) {
+ return {
+ ...mockCommandClient,
+ setSkipPausing: jest.fn(),
+ getSourceActorBreakpointPositions: async () => bpPos,
+ getSourceActorBreakableLines: async () => [],
+ };
+}
+
+function mockSourceMaps() {
+ return {
+ ...sourceMaps,
+ getOriginalSourceText: async id => ({
+ id,
+ text: "",
+ contentType: "text/javascript",
+ }),
+ getGeneratedRangesForOriginal: async () => [
+ { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } },
+ ],
+ getOriginalLocations: async items => items,
+ };
+}
+
+describe("when adding breakpoints", () => {
+ it("a corresponding pending breakpoint should be added", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [1] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ const bp = generateBreakpoint("foo.js", 5, 1);
+ const id = makePendingLocationId(bp.location);
+
+ await dispatch(actions.addBreakpoint(cx, bp.location));
+ const pendingBps = selectors.getPendingBreakpoints(getState());
+
+ expect(selectors.getPendingBreakpointList(getState())).toHaveLength(2);
+ expect(pendingBps[id]).toMatchSnapshot();
+ });
+
+ describe("adding and deleting breakpoints", () => {
+ let breakpoint1;
+ let breakpoint2;
+ let breakpointLocationId1;
+ let breakpointLocationId2;
+
+ beforeEach(() => {
+ breakpoint1 = generateBreakpoint("foo");
+ breakpoint2 = generateBreakpoint("foo2");
+ breakpointLocationId1 = makePendingLocationId(breakpoint1.location);
+ breakpointLocationId2 = makePendingLocationId(breakpoint2.location);
+ });
+
+ it("add a corresponding pendingBreakpoint for each addition", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+ await dispatch(actions.newGeneratedSource(makeSource("foo2")));
+
+ const source1 = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ const source2 = await dispatch(
+ actions.newGeneratedSource(makeSource("foo2"))
+ );
+
+ await dispatch(actions.loadSourceText({ cx, source: source1 }));
+ await dispatch(actions.loadSourceText({ cx, source: source2 }));
+
+ await dispatch(actions.addBreakpoint(cx, breakpoint1.location));
+ await dispatch(actions.addBreakpoint(cx, breakpoint2.location));
+
+ const pendingBps = selectors.getPendingBreakpoints(getState());
+
+ // NOTE the sourceId should be `foo2/originalSource`, but is `foo2`
+ // because we do not have a real source map for `getOriginalLocation`
+ // to map.
+ expect(pendingBps[breakpointLocationId1]).toMatchSnapshot();
+ expect(pendingBps[breakpointLocationId2]).toMatchSnapshot();
+ });
+
+ it("hidden breakponts do not create pending bps", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(
+ actions.addBreakpoint(cx, breakpoint1.location, { hidden: true })
+ );
+ const pendingBps = selectors.getPendingBreakpoints(getState());
+
+ expect(pendingBps[breakpointLocationId1]).toBeUndefined();
+ });
+
+ it("remove a corresponding pending breakpoint when deleting", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+ await dispatch(actions.newGeneratedSource(makeSource("foo2")));
+
+ const source1 = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ const source2 = await dispatch(
+ actions.newGeneratedSource(makeSource("foo2"))
+ );
+
+ await dispatch(actions.loadSourceText({ cx, source: source1 }));
+ await dispatch(actions.loadSourceText({ cx, source: source2 }));
+
+ await dispatch(actions.addBreakpoint(cx, breakpoint1.location));
+ await dispatch(actions.addBreakpoint(cx, breakpoint2.location));
+ await dispatch(actions.removeBreakpoint(cx, breakpoint1));
+
+ const pendingBps = selectors.getPendingBreakpoints(getState());
+ expect(pendingBps.hasOwnProperty(breakpointLocationId1)).toBe(false);
+ expect(pendingBps.hasOwnProperty(breakpointLocationId2)).toBe(true);
+ });
+ });
+});
+
+describe("when changing an existing breakpoint", () => {
+ it("updates corresponding pendingBreakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bp = generateBreakpoint("foo");
+ const id = makePendingLocationId(bp.location);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.addBreakpoint(cx, bp.location));
+ await dispatch(
+ actions.setBreakpointOptions(cx, bp.location, { condition: "2" })
+ );
+ const bps = selectors.getPendingBreakpoints(getState());
+ const breakpoint = bps[id];
+ expect(breakpoint.options.condition).toBe("2");
+ });
+
+ it("if disabled, updates corresponding pendingBreakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bp = generateBreakpoint("foo");
+ const id = makePendingLocationId(bp.location);
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.addBreakpoint(cx, bp.location));
+ await dispatch(actions.disableBreakpoint(cx, bp));
+ const bps = selectors.getPendingBreakpoints(getState());
+ const breakpoint = bps[id];
+ expect(breakpoint.disabled).toBe(true);
+ });
+
+ it("does not delete the pre-existing pendingBreakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bp = generateBreakpoint("foo.js");
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ const id = makePendingLocationId(bp.location);
+
+ await dispatch(actions.addBreakpoint(cx, bp.location));
+ await dispatch(
+ actions.setBreakpointOptions(cx, bp.location, { condition: "2" })
+ );
+ const bps = selectors.getPendingBreakpoints(getState());
+ const breakpoint = bps[id];
+ expect(breakpoint.options.condition).toBe("2");
+ });
+});
+
+describe("initializing when pending breakpoints exist in prefs", () => {
+ it("syncs pending breakpoints", async () => {
+ const { getState } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bps = selectors.getPendingBreakpoints(getState());
+ expect(bps).toMatchSnapshot();
+ });
+
+ it("re-adding breakpoints update existing pending breakpoints", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [1, 2] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bar = generateBreakpoint("bar.js", 5, 1);
+
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+ await dispatch(actions.addBreakpoint(cx, bar.location));
+
+ const bps = selectors.getPendingBreakpointList(getState());
+ expect(bps).toHaveLength(2);
+ });
+
+ it("adding bps doesn't remove existing pending breakpoints", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ "5": [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bp = generateBreakpoint("foo.js");
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await dispatch(actions.addBreakpoint(cx, bp.location));
+
+ const bps = selectors.getPendingBreakpointList(getState());
+ expect(bps).toHaveLength(2);
+ });
+});
+
+describe("initializing with disabled pending breakpoints in prefs", () => {
+ it("syncs breakpoints with pending breakpoints", async () => {
+ const store = createStore(
+ mockClient({ "5": [2] }),
+ loadInitialState({ disabled: true }),
+ mockSourceMaps()
+ );
+
+ const { getState, dispatch, cx } = store;
+
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await waitForState(store, state => {
+ const bps = selectors.getBreakpointsForSource(state, source.id);
+ return bps && Object.values(bps).length > 0;
+ });
+
+ const bp = selectors.getBreakpointForLocation(getState(), {
+ line: 5,
+ column: 2,
+ sourceUrl: source.url,
+ sourceId: source.id,
+ });
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ expect(bp.location.sourceId).toEqual(source.id);
+ expect(bp.disabled).toEqual(true);
+ });
+});
+
+describe("adding sources", () => {
+ it("corresponding breakpoints are added for a single source", async () => {
+ const store = createStore(
+ mockClient({ "5": [2] }),
+ loadInitialState({ disabled: true }),
+ mockSourceMaps()
+ );
+ const { getState, dispatch, cx } = store;
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(0);
+
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("corresponding breakpoints are added to the original source", async () => {
+ const sourceURL = makeSourceURL("bar.js");
+ const store = createStore(mockClient({ "5": [2] }), loadInitialState(), {
+ getOriginalURLs: async source => [
+ {
+ id: sourceMaps.generatedToOriginalId(source.id, sourceURL),
+ url: sourceURL,
+ },
+ ],
+ getOriginalSourceText: async () => ({ text: "" }),
+ getGeneratedLocation: async location => ({
+ line: location.line,
+ column: location.column,
+ sourceId: location.sourceId,
+ }),
+ getOriginalLocation: async location => location,
+ getGeneratedRangesForOriginal: async () => [
+ { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } },
+ ],
+ getOriginalLocations: async items =>
+ items.map(item => ({
+ ...item,
+ sourceId: sourceMaps.generatedToOriginalId(item.sourceId, sourceURL),
+ })),
+ });
+
+ const { getState, dispatch } = store;
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(0);
+
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js", { sourceMapURL: "foo" }))
+ );
+
+ await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("add corresponding breakpoints for multiple sources", async () => {
+ const store = createStore(
+ mockClient({ "5": [2] }),
+ loadInitialState({ disabled: true }),
+ mockSourceMaps()
+ );
+ const { getState, dispatch, cx } = store;
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(0);
+
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ const [source1, source2] = await dispatch(
+ actions.newGeneratedSources([makeSource("bar.js"), makeSource("foo.js")])
+ );
+ await dispatch(actions.loadSourceText({ cx, source: source1 }));
+ await dispatch(actions.loadSourceText({ cx, source: source2 }));
+
+ await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/preview.spec.js b/devtools/client/debugger/src/actions/tests/preview.spec.js
new file mode 100644
index 0000000000..4cb2542adf
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/preview.spec.js
@@ -0,0 +1,215 @@
+/* eslint max-nested-callbacks: ["error", 6] */
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ makeFrame,
+ waitForState,
+ waitATick,
+} from "../../utils/test-head";
+
+function waitForPreview(store, expression) {
+ return waitForState(store, state => {
+ const preview = selectors.getPreview(state);
+ return preview && preview.expression == expression;
+ });
+}
+
+function mockThreadFront(overrides) {
+ return {
+ evaluateInFrame: async () => ({ result: {} }),
+ getFrameScopes: async () => {},
+ getFrames: async () => [],
+ sourceContents: async () => ({
+ source: "",
+ contentType: "text/javascript",
+ }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ evaluateExpressions: async () => [],
+ loadObjectProperties: async () => ({}),
+ ...overrides,
+ };
+}
+
+function dispatchSetPreview(dispatch, context, expression, target) {
+ return dispatch(
+ actions.setPreview(
+ context,
+ expression,
+ {
+ start: { url: "foo.js", line: 1, column: 2 },
+ end: { url: "foo.js", line: 1, column: 5 },
+ },
+ { line: 2, column: 3 },
+ target.getBoundingClientRect(),
+ target
+ )
+ );
+}
+
+async function pause(store, client) {
+ const { dispatch, cx } = store;
+ const base = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ await dispatch(actions.selectSource(cx, base.id));
+ await waitForState(store, state => selectors.hasSymbols(state, base));
+
+ const { thread } = cx;
+ const frames = [makeFrame({ id: "frame1", sourceId: base.id, thread })];
+ client.getFrames = async () => frames;
+
+ await dispatch(
+ actions.paused({
+ thread,
+ frame: frames[0],
+ loadedObjects: [],
+ why: { type: "debuggerStatement" },
+ })
+ );
+}
+
+describe("preview", () => {
+ it("should generate previews", async () => {
+ const store = createStore(mockThreadFront());
+ const { dispatch, getState, cx } = store;
+ const base = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ await dispatch(actions.selectSource(cx, base.id));
+ await waitForState(store, state => selectors.hasSymbols(state, base));
+ const frames = [makeFrame({ id: "f1", sourceId: base.id })];
+
+ await dispatch(
+ actions.paused({
+ thread: store.cx.thread,
+ frame: frames[0],
+ frames,
+ loadedObjects: [],
+ why: { type: "debuggerStatement" },
+ })
+ );
+
+ const newCx = selectors.getContext(getState());
+ const firstTarget = document.createElement("div");
+
+ dispatchSetPreview(dispatch, newCx, "foo", firstTarget);
+
+ expect(selectors.getPreview(getState())).toMatchSnapshot();
+ });
+
+ // When a 2nd setPreview is called before a 1st setPreview dispatches
+ // and the 2nd setPreview has not dispatched yet,
+ // the first setPreview should not finish dispatching
+ it("queued previews (w/ the 1st finishing first)", async () => {
+ let resolveFirst, resolveSecond;
+ const promises = [
+ new Promise(resolve => {
+ resolveFirst = resolve;
+ }),
+ new Promise(resolve => {
+ resolveSecond = resolve;
+ }),
+ ];
+
+ const client = mockThreadFront({
+ loadObjectProperties: () => promises.shift(),
+ });
+ const store = createStore(client);
+
+ const { dispatch, getState } = store;
+ await pause(store, client);
+
+ const newCx = selectors.getContext(getState());
+ const firstTarget = document.createElement("div");
+ const secondTarget = document.createElement("div");
+
+ // Start the dispatch of the first setPreview. At this point, it will not
+ // finish execution until we resolve the firstSetPreview
+ dispatchSetPreview(dispatch, newCx, "firstSetPreview", firstTarget);
+
+ // Start the dispatch of the second setPreview. At this point, it will not
+ // finish execution until we resolve the secondSetPreview
+ dispatchSetPreview(dispatch, newCx, "secondSetPreview", secondTarget);
+
+ let fail = false;
+
+ /* $FlowIgnore[not-a-function] this is guarantied to be initialized because
+ `new new Promise(foo)` calls foo synchronously */
+ resolveFirst();
+ waitForPreview(store, "firstSetPreview").then(() => {
+ fail = true;
+ });
+
+ // $FlowIgnore[not-a-function] same as above
+ resolveSecond();
+ await waitForPreview(store, "secondSetPreview");
+ expect(fail).toEqual(false);
+
+ const preview = selectors.getPreview(getState());
+ expect(preview && preview.expression).toEqual("secondSetPreview");
+ });
+
+ // When a 2nd setPreview is called before a 1st setPreview dispatches
+ // and the 2nd setPreview has dispatched,
+ // the first setPreview should not finish dispatching
+ it("queued previews (w/ the 2nd finishing first)", async () => {
+ let resolveFirst, resolveSecond;
+ const promises = [
+ new Promise(resolve => {
+ resolveFirst = resolve;
+ }),
+ new Promise(resolve => {
+ resolveSecond = resolve;
+ }),
+ ];
+
+ const client = mockThreadFront({
+ loadObjectProperties: () => promises.shift(),
+ });
+ const store = createStore(client);
+
+ const { dispatch, getState } = store;
+ await pause(store, client);
+
+ const cx = selectors.getThreadContext(getState());
+ const firstTarget = document.createElement("div");
+ const secondTarget = document.createElement("div");
+
+ // Start the dispatch of the first setPreview. At this point, it will not
+ // finish execution until we resolve the firstSetPreview
+ dispatchSetPreview(dispatch, cx, "firstSetPreview", firstTarget);
+
+ // Start the dispatch of the second setPreview. At this point, it will not
+ // finish execution until we resolve the secondSetPreview
+ dispatchSetPreview(dispatch, cx, "secondSetPreview", secondTarget);
+
+ let fail = false;
+
+ /* $FlowIgnore[not-a-function] this is guarantied to be initialized because
+ `new new Promise(foo)` calls foo synchronously */
+ resolveSecond();
+ await waitForPreview(store, "secondSetPreview");
+
+ // $FlowIgnore[not-a-function] same as above
+ resolveFirst();
+ waitForPreview(store, "firstSetPreview").then(() => {
+ fail = true;
+ });
+
+ await waitATick(() => expect(fail).toEqual(false));
+
+ const preview = selectors.getPreview(getState());
+ expect(preview && preview.expression).toEqual("secondSetPreview");
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/project-text-search.spec.js b/devtools/client/debugger/src/actions/tests/project-text-search.spec.js
new file mode 100644
index 0000000000..e63714c58c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/project-text-search.spec.js
@@ -0,0 +1,161 @@
+/* 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 {
+ actions,
+ createStore,
+ selectors,
+ makeSource,
+} from "../../utils/test-head";
+
+const {
+ getSource,
+ getTextSearchQuery,
+ getTextSearchResults,
+ getTextSearchStatus,
+} = selectors;
+
+const sources = {
+ foo1: {
+ source: "function foo1() {\n const foo = 5; return foo;\n}",
+ contentType: "text/javascript",
+ },
+ foo2: {
+ source: "function foo2(x, y) {\n return x + y;\n}",
+ contentType: "text/javascript",
+ },
+ bar: {
+ source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
+ contentType: "text/javascript",
+ },
+ "bar:formatted": {
+ source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
+ contentType: "text/javascript",
+ },
+};
+
+const threadFront = {
+ sourceContents: async ({ source }) => sources[source],
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+};
+
+describe("project text search", () => {
+ it("should add a project text search query", () => {
+ const { dispatch, getState, cx } = createStore();
+ const mockQuery = "foo";
+
+ dispatch(actions.addSearchQuery(cx, mockQuery));
+
+ expect(getTextSearchQuery(getState())).toEqual(mockQuery);
+ });
+
+ it("should search all the loaded sources based on the query", async () => {
+ const { dispatch, getState, cx } = createStore(threadFront);
+ const mockQuery = "foo";
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.newGeneratedSource(makeSource("foo2")));
+
+ await dispatch(actions.searchSources(cx, mockQuery));
+
+ const results = getTextSearchResults(getState());
+ expect(results).toMatchSnapshot();
+ });
+
+ it("should ignore sources with minified versions", async () => {
+ const mockMaps = {
+ getOriginalSourceText: async () => ({
+ source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
+ contentType: "text/javascript",
+ }),
+ applySourceMap: async () => {},
+ getGeneratedRangesForOriginal: async () => [],
+ getOriginalLocations: async items => items,
+ getOriginalLocation: async loc => loc,
+ };
+
+ const { dispatch, getState, cx } = createStore(threadFront, {}, mockMaps);
+
+ const source1 = await dispatch(
+ actions.newGeneratedSource(makeSource("bar"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source: source1 }));
+
+ await dispatch(actions.togglePrettyPrint(cx, source1.id));
+
+ await dispatch(actions.searchSources(cx, "bla"));
+
+ const results = getTextSearchResults(getState());
+ expect(results).toMatchSnapshot();
+ });
+
+ it("should search a specific source", async () => {
+ const { dispatch, getState, cx } = createStore(threadFront);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bar"))
+ );
+ await dispatch(actions.loadSourceText({ cx, source }));
+
+ dispatch(actions.addSearchQuery(cx, "bla"));
+
+ const barSource = getSource(getState(), "bar");
+ if (!barSource) {
+ throw new Error("no barSource");
+ }
+ const sourceId = barSource.id;
+
+ await dispatch(actions.searchSource(cx, sourceId, "bla"), "bla");
+
+ const results = getTextSearchResults(getState());
+
+ expect(results).toMatchSnapshot();
+ expect(results).toHaveLength(1);
+ });
+
+ it("should clear all the search results", async () => {
+ const { dispatch, getState, cx } = createStore(threadFront);
+ const mockQuery = "foo";
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.searchSources(cx, mockQuery));
+
+ expect(getTextSearchResults(getState())).toMatchSnapshot();
+
+ await dispatch(actions.clearSearchResults(cx));
+
+ expect(getTextSearchResults(getState())).toMatchSnapshot();
+ });
+
+ it("should set the status properly", () => {
+ const { dispatch, getState, cx } = createStore();
+ const mockStatus = "FETCHING";
+ dispatch(actions.updateSearchStatus(cx, mockStatus));
+ expect(getTextSearchStatus(getState())).toEqual(mockStatus);
+ });
+
+ it("should close project search", async () => {
+ const { dispatch, getState, cx } = createStore(threadFront);
+ const mockQuery = "foo";
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.searchSources(cx, mockQuery));
+
+ expect(getTextSearchResults(getState())).toMatchSnapshot();
+
+ dispatch(actions.closeProjectSearch(cx));
+
+ expect(getTextSearchQuery(getState())).toEqual("");
+
+ const results = getTextSearchResults(getState());
+
+ expect(results).toMatchSnapshot();
+ expect(results).toHaveLength(0);
+ const status = getTextSearchStatus(getState());
+ expect(status).toEqual("INITIAL");
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js b/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
new file mode 100644
index 0000000000..b8d2fb2906
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
@@ -0,0 +1,95 @@
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+} from "../../utils/test-head";
+
+const {
+ getProjectDirectoryRoot,
+ getProjectDirectoryRootName,
+ getDisplayedSources,
+} = selectors;
+
+describe("setProjectDirectoryRoot", () => {
+ it("should set domain directory as root", async () => {
+ const { dispatch, getState, cx } = createStore();
+ dispatch(actions.setProjectDirectoryRoot(cx, "example.com", "foo"));
+ expect(getProjectDirectoryRoot(getState())).toBe("example.com");
+ });
+
+ it("should set a directory as root directory", async () => {
+ const { dispatch, getState, cx } = createStore();
+ dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo", "foo"));
+ expect(getProjectDirectoryRoot(getState())).toBe("/example.com/foo");
+ });
+
+ it("should add to the directory ", () => {
+ const { dispatch, getState, cx } = createStore();
+ dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo", "foo"));
+ dispatch(actions.setProjectDirectoryRoot(cx, "/foo/bar", "foo"));
+ expect(getProjectDirectoryRoot(getState())).toBe("/example.com/foo/bar");
+ });
+
+ it("should update the directory ", () => {
+ const { dispatch, getState, cx } = createStore();
+ dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo", "foo"));
+ dispatch(actions.clearProjectDirectoryRoot(cx));
+ dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/bar", "foo"));
+ expect(getProjectDirectoryRoot(getState())).toBe("/example.com/bar");
+ });
+
+ it("should filter sources", async () => {
+ const store = createStore({
+ getSourceActorBreakableLines: async () => [],
+ });
+ const { dispatch, getState, cx } = store;
+ await dispatch(actions.newGeneratedSource(makeSource("js/scopes.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("lib/vendor.js")));
+
+ dispatch(
+ actions.setProjectDirectoryRoot(cx, "localhost:8000/examples/js", "foo")
+ );
+
+ const filteredSourcesByThread = getDisplayedSources(getState());
+ const filteredSources = (Object.values(
+ filteredSourcesByThread.FakeThread
+ ): any)[0];
+
+ expect(filteredSources.url).toEqual(
+ "http://localhost:8000/examples/js/scopes.js"
+ );
+
+ expect(filteredSources.relativeUrl).toEqual("scopes.js");
+ });
+
+ it("should update the child directory ", () => {
+ const { dispatch, getState, cx } = createStore({
+ getSourceActorBreakableLines: async () => [],
+ });
+ dispatch(actions.setProjectDirectoryRoot(cx, "example.com", "foo"));
+ dispatch(actions.setProjectDirectoryRoot(cx, "example.com/foo/bar", "foo"));
+ expect(getProjectDirectoryRoot(getState())).toBe("example.com/foo/bar");
+ });
+
+ it("should update the child directory when domain name is Webpack://", () => {
+ const { dispatch, getState, cx } = createStore({
+ getSourceActorBreakableLines: async () => [],
+ });
+ dispatch(actions.setProjectDirectoryRoot(cx, "webpack://", "foo"));
+ dispatch(actions.setProjectDirectoryRoot(cx, "webpack:///app", "foo"));
+ expect(getProjectDirectoryRoot(getState())).toBe("webpack:///app");
+ });
+
+ it("should set the name of the root directory", () => {
+ const { dispatch, getState, cx } = createStore();
+ dispatch(actions.setProjectDirectoryRoot(cx, "foo", "example.com"));
+ expect(getProjectDirectoryRootName(getState())).toBe("example.com");
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/source-tree.spec.js b/devtools/client/debugger/src/actions/tests/source-tree.spec.js
new file mode 100644
index 0000000000..fcbd56da33
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/source-tree.spec.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { actions, selectors, createStore } from "../../utils/test-head";
+const { getExpandedState } = selectors;
+
+describe("source tree", () => {
+ it("should set the expanded state", () => {
+ const { dispatch, getState } = createStore();
+ const expandedState = new Set(["foo", "bar"]);
+
+ expect(getExpandedState(getState())).toEqual(new Set([]));
+ dispatch(actions.setExpandedState(expandedState));
+ expect(getExpandedState(getState())).toEqual(expandedState);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/tabs.spec.js b/devtools/client/debugger/src/actions/tests/tabs.spec.js
new file mode 100644
index 0000000000..210cc0373a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/tabs.spec.js
@@ -0,0 +1,138 @@
+/* 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 {
+ actions,
+ selectors,
+ createStore,
+ makeSource,
+} from "../../utils/test-head";
+const { getSelectedSource, getSourceTabs } = selectors;
+
+import { mockCommandClient } from "./helpers/mockCommandClient";
+
+describe("closing tabs", () => {
+ it("closing a tab", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
+ dispatch(actions.closeTab(cx, fooSource));
+
+ expect(getSelectedSource(getState())).toBe(undefined);
+ expect(getSourceTabs(getState())).toHaveLength(0);
+ });
+
+ it("closing the inactive tab", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
+ await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
+ dispatch(actions.closeTab(cx, fooSource));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("bar.js");
+ expect(getSourceTabs(getState())).toHaveLength(1);
+ });
+
+ it("closing the only tab", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
+ dispatch(actions.closeTab(cx, fooSource));
+
+ expect(getSelectedSource(getState())).toBe(undefined);
+ expect(getSourceTabs(getState())).toHaveLength(0);
+ });
+
+ it("closing the active tab", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ const barSource = await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js"))
+ );
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
+ await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
+ await dispatch(actions.closeTab(cx, barSource));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("foo.js");
+ expect(getSourceTabs(getState())).toHaveLength(1);
+ });
+
+ it("closing many inactive tabs", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bazz.js")));
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
+ await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
+ await dispatch(
+ actions.selectLocation(cx, { sourceId: "bazz.js", line: 1 })
+ );
+
+ const tabs = [
+ "http://localhost:8000/examples/foo.js",
+ "http://localhost:8000/examples/bar.js",
+ ];
+ dispatch(actions.closeTabs(cx, tabs));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("bazz.js");
+ expect(getSourceTabs(getState())).toHaveLength(1);
+ });
+
+ it("closing many tabs including the active tab", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bazz.js")));
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
+ await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
+ await dispatch(
+ actions.selectLocation(cx, { sourceId: "bazz.js", line: 1 })
+ );
+ const tabs = [
+ "http://localhost:8000/examples/bar.js",
+ "http://localhost:8000/examples/bazz.js",
+ ];
+ await dispatch(actions.closeTabs(cx, tabs));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("foo.js");
+ expect(getSourceTabs(getState())).toHaveLength(1);
+ });
+
+ it("closing all the tabs", async () => {
+ const { dispatch, getState, cx } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
+ await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
+ await dispatch(
+ actions.closeTabs(cx, [
+ "http://localhost:8000/examples/foo.js",
+ "http://localhost:8000/examples/bar.js",
+ ])
+ );
+
+ expect(getSelectedSource(getState())).toBe(undefined);
+ expect(getSourceTabs(getState())).toHaveLength(0);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/ui.spec.js b/devtools/client/debugger/src/actions/tests/ui.spec.js
new file mode 100644
index 0000000000..e0cce0225a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/ui.spec.js
@@ -0,0 +1,75 @@
+/* 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 { createStore, selectors, actions } from "../../utils/test-head";
+
+const {
+ getActiveSearch,
+ getFrameworkGroupingState,
+ getPaneCollapse,
+ getHighlightedLineRange,
+} = selectors;
+
+describe("ui", () => {
+ it("should toggle the visible state of project search", () => {
+ const { dispatch, getState } = createStore();
+ expect(getActiveSearch(getState())).toBe(null);
+ dispatch(actions.setActiveSearch("project"));
+ expect(getActiveSearch(getState())).toBe("project");
+ });
+
+ it("should close project search", () => {
+ const { dispatch, getState } = createStore();
+ expect(getActiveSearch(getState())).toBe(null);
+ dispatch(actions.setActiveSearch("project"));
+ dispatch(actions.closeActiveSearch());
+ expect(getActiveSearch(getState())).toBe(null);
+ });
+
+ it("should toggle the visible state of file search", () => {
+ const { dispatch, getState } = createStore();
+ expect(getActiveSearch(getState())).toBe(null);
+ dispatch(actions.setActiveSearch("file"));
+ expect(getActiveSearch(getState())).toBe("file");
+ });
+
+ it("should close file search", () => {
+ const { dispatch, getState } = createStore();
+ expect(getActiveSearch(getState())).toBe(null);
+ dispatch(actions.setActiveSearch("file"));
+ dispatch(actions.closeActiveSearch());
+ expect(getActiveSearch(getState())).toBe(null);
+ });
+
+ it("should toggle the collapse state of a pane", () => {
+ const { dispatch, getState } = createStore();
+ expect(getPaneCollapse(getState(), "start")).toBe(false);
+ dispatch(actions.togglePaneCollapse("start", true));
+ expect(getPaneCollapse(getState(), "start")).toBe(true);
+ });
+
+ it("should toggle the collapsed state of frameworks in the callstack", () => {
+ const { dispatch, getState } = createStore();
+ const currentState = getFrameworkGroupingState(getState());
+ dispatch(actions.toggleFrameworkGrouping(!currentState));
+ expect(getFrameworkGroupingState(getState())).toBe(!currentState);
+ });
+
+ it("should highlight lines", () => {
+ const { dispatch, getState } = createStore();
+ const range = { start: 3, end: 5, sourceId: "2" };
+ dispatch(actions.highlightLineRange(range));
+ expect(getHighlightedLineRange(getState())).toEqual(range);
+ });
+
+ it("should clear highlight lines", () => {
+ const { dispatch, getState } = createStore();
+ const range = { start: 3, end: 5, sourceId: "2" };
+ dispatch(actions.highlightLineRange(range));
+ dispatch(actions.clearHighlightLineRange());
+ expect(getHighlightedLineRange(getState())).toEqual({});
+ });
+});
diff --git a/devtools/client/debugger/src/actions/threads.js b/devtools/client/debugger/src/actions/threads.js
new file mode 100644
index 0000000000..0fdcd16eca
--- /dev/null
+++ b/devtools/client/debugger/src/actions/threads.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import type { Target } from "../client/firefox/types";
+import type { Action, ThunkArgs } from "./types";
+import { removeSourceActors } from "./source-actors";
+import { validateContext } from "../utils/context";
+
+import { getContext, getThread, getSourceActorsForThread } from "../selectors";
+
+export function addTarget(targetFront: Target) {
+ return async function(args: ThunkArgs) {
+ const { client, getState, dispatch } = args;
+ const cx = getContext(getState());
+ const thread = await client.addThread(targetFront);
+ validateContext(getState(), cx);
+
+ dispatch(({ type: "INSERT_THREAD", cx, newThread: thread }: Action));
+ };
+}
+
+export function removeTarget(targetFront: Target) {
+ return async function(args: ThunkArgs) {
+ const { getState, client, dispatch } = args;
+ const cx = getContext(getState());
+ const thread = getThread(getState(), targetFront.targetForm.threadActor);
+
+ if (!thread) {
+ return;
+ }
+
+ client.removeThread(thread);
+ const sourceActors = getSourceActorsForThread(getState(), thread.actor);
+ dispatch(removeSourceActors(sourceActors));
+ dispatch(
+ ({
+ type: "REMOVE_THREAD",
+ cx,
+ oldThread: thread,
+ }: Action)
+ );
+ };
+}
+
+export function toggleJavaScriptEnabled(enabled: Boolean) {
+ return async ({ panel, dispatch, client }: ThunkArgs) => {
+ await client.toggleJavaScriptEnabled(enabled);
+ dispatch({
+ type: "TOGGLE_JAVASCRIPT_ENABLED",
+ value: enabled,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/toolbox.js b/devtools/client/debugger/src/actions/toolbox.js
new file mode 100644
index 0000000000..5d59344721
--- /dev/null
+++ b/devtools/client/debugger/src/actions/toolbox.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import type { ThunkArgs } from "./types";
+import type { Grip, URL } from "../types";
+
+/**
+ * @memberof actions/toolbox
+ * @static
+ */
+export function openLink(url: URL) {
+ return async function({ panel }: ThunkArgs) {
+ return panel.openLink(url);
+ };
+}
+
+export function evaluateInConsole(inputString: string) {
+ return async ({ panel }: ThunkArgs) => {
+ return panel.openConsoleAndEvaluate(inputString);
+ };
+}
+
+export function openElementInInspectorCommand(grip: Grip) {
+ return async ({ panel }: ThunkArgs) => {
+ return panel.openElementInInspector(grip);
+ };
+}
+
+export function openInspector(grip: Grip) {
+ return async ({ panel }: ThunkArgs) => {
+ return panel.openInspector();
+ };
+}
+
+export function highlightDomElement(grip: Grip) {
+ return async ({ panel }: ThunkArgs) => {
+ return panel.highlightDomElement(grip);
+ };
+}
+
+export function unHighlightDomElement(grip: Grip) {
+ return async ({ panel }: ThunkArgs) => {
+ return panel.unHighlightDomElement(grip);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/types/ASTAction.js b/devtools/client/debugger/src/actions/types/ASTAction.js
new file mode 100644
index 0000000000..4edb39ef55
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/ASTAction.js
@@ -0,0 +1,25 @@
+/* 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 { SymbolDeclarations } from "../../workers/parser";
+import type { PromiseAction } from "../utils/middleware/promise";
+import type { Context, SourceLocation, SourceId } from "../../types";
+
+export type ASTAction =
+ | PromiseAction<
+ {|
+ +type: "SET_SYMBOLS",
+ +cx: Context,
+ +sourceId: SourceId,
+ |},
+ SymbolDeclarations
+ >
+ | {|
+ +type: "IN_SCOPE_LINES",
+ +cx: Context,
+ location: SourceLocation,
+ lines: Array<number>,
+ |};
diff --git a/devtools/client/debugger/src/actions/types/BreakpointAction.js b/devtools/client/debugger/src/actions/types/BreakpointAction.js
new file mode 100644
index 0000000000..18cf1c33a3
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/BreakpointAction.js
@@ -0,0 +1,65 @@
+/* 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 {
+ Breakpoint,
+ SourceLocation,
+ XHRBreakpoint,
+ Source,
+ BreakpointPositions,
+ PendingLocation,
+ Context,
+} from "../../types";
+
+import type { PromiseAction } from "../utils/middleware/promise";
+
+export type BreakpointAction =
+ | PromiseAction<{|
+ +type: "SET_XHR_BREAKPOINT",
+ +breakpoint: XHRBreakpoint,
+ |}>
+ | PromiseAction<{|
+ +type: "ENABLE_XHR_BREAKPOINT",
+ +breakpoint: XHRBreakpoint,
+ +index: number,
+ |}>
+ | PromiseAction<{|
+ +type: "UPDATE_XHR_BREAKPOINT",
+ +breakpoint: XHRBreakpoint,
+ +index: number,
+ |}>
+ | PromiseAction<{|
+ +type: "DISABLE_XHR_BREAKPOINT",
+ +breakpoint: XHRBreakpoint,
+ +index: number,
+ |}>
+ | PromiseAction<{|
+ +type: "REMOVE_XHR_BREAKPOINT",
+ +index: number,
+ +breakpoint: XHRBreakpoint,
+ |}>
+ | PromiseAction<{|
+ +type: "SET_BREAKPOINT",
+ +cx: Context,
+ +breakpoint: Breakpoint,
+ |}>
+ | PromiseAction<{|
+ +type: "REMOVE_BREAKPOINT",
+ +cx: Context,
+ +location: SourceLocation,
+ |}>
+ | PromiseAction<{| +type: "REMOVE_BREAKPOINTS" |}>
+ | {|
+ +type: "REMOVE_PENDING_BREAKPOINT",
+ +cx: Context,
+ +location: PendingLocation,
+ |}
+ | {|
+ type: "ADD_BREAKPOINT_POSITIONS",
+ +cx: Context,
+ positions: BreakpointPositions,
+ source: Source,
+ |};
diff --git a/devtools/client/debugger/src/actions/types/PauseAction.js b/devtools/client/debugger/src/actions/types/PauseAction.js
new file mode 100644
index 0000000000..f4c0c96a21
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/PauseAction.js
@@ -0,0 +1,181 @@
+/* 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 { Command } from "../../reducers/types";
+import type {
+ Expression,
+ Frame,
+ Scope,
+ Why,
+ ThreadContext,
+ Previews,
+ HighlightedCalls,
+} from "../../types";
+
+import type { PromiseAction } from "../utils/middleware/promise";
+
+export type PauseAction =
+ | {|
+ +type: "BREAK_ON_NEXT",
+ +cx: ThreadContext,
+ +thread: string,
+ +value: boolean,
+ |}
+ | {|
+ // Note: Do not include cx, as this action is triggered by the server.
+ +type: "RESUME",
+ +thread: string,
+ +value: void,
+ +wasStepping: boolean,
+ |}
+ | {|
+ // Note: Do not include cx, as this action is triggered by the server.
+ +type: "PAUSED",
+ +thread: string,
+ +why: Why,
+ +scopes: Scope,
+ +frame: Frame,
+ |}
+ | {|
+ type: "FETCHED_FRAMES",
+ frames: Frame[],
+ cx: ThreadContext,
+ thread: string,
+ |}
+ | {|
+ +type: "PAUSE_ON_EXCEPTIONS",
+ +shouldPauseOnExceptions: boolean,
+ +shouldPauseOnCaughtExceptions: boolean,
+ |}
+ | PromiseAction<{|
+ +type: "COMMAND",
+ +cx: ThreadContext,
+ +thread: string,
+ +command: Command,
+ |}>
+ | {|
+ +type: "SELECT_FRAME",
+ +cx: ThreadContext,
+ +thread: string,
+ +frame: Frame,
+ |}
+ | {|
+ +type: "SELECT_COMPONENT",
+ +thread: string,
+ +componentIndex: number,
+ |}
+ | {|
+ +type: "ADD_EXPRESSION",
+ +cx: ThreadContext,
+ +thread: string,
+ +id: number,
+ +input: string,
+ +value: string,
+ +expressionError: ?string,
+ |}
+ | PromiseAction<
+ {|
+ +type: "EVALUATE_EXPRESSION",
+ +cx: ThreadContext,
+ +thread: string,
+ +input: string,
+ |},
+ Object
+ >
+ | PromiseAction<{|
+ +type: "EVALUATE_EXPRESSIONS",
+ +cx: ThreadContext,
+ +results: Expression[],
+ +inputs: string[],
+ |}>
+ | {|
+ +type: "UPDATE_EXPRESSION",
+ +cx: ThreadContext,
+ +expression: Expression,
+ +input: string,
+ +expressionError: ?string,
+ |}
+ | {|
+ +type: "DELETE_EXPRESSION",
+ +input: string,
+ |}
+ | {|
+ +type: "CLEAR_AUTOCOMPLETE",
+ |}
+ | {|
+ +type: "CLEAR_EXPRESSION_ERROR",
+ |}
+ | {|
+ +type: "AUTOCOMPLETE",
+ +cx: ThreadContext,
+ +input: string,
+ +result: Object,
+ |}
+ | PromiseAction<
+ {|
+ +type: "MAP_SCOPES",
+ +cx: ThreadContext,
+ +thread: string,
+ +frame: Frame,
+ |},
+ {
+ scope: Scope,
+ mappings: {
+ [string]: string | null,
+ },
+ }
+ >
+ | {|
+ +type: "MAP_FRAMES",
+ +cx: ThreadContext,
+ +thread: string,
+ +frames: Frame[],
+ +selectedFrameId: string,
+ |}
+ | {|
+ +type: "MAP_FRAME_DISPLAY_NAMES",
+ +cx: ThreadContext,
+ +thread: string,
+ +frames: Frame[],
+ |}
+ | PromiseAction<
+ {|
+ +type: "ADD_SCOPES",
+ +cx: ThreadContext,
+ +thread: string,
+ +frame: Frame,
+ |},
+ Scope
+ >
+ | {|
+ +type: "TOGGLE_SKIP_PAUSING",
+ +thread: string,
+ skipPausing: boolean,
+ |}
+ | {|
+ +type: "TOGGLE_MAP_SCOPES",
+ +mapScopes: boolean,
+ |}
+ | {|
+ +type: "SET_EXPANDED_SCOPE",
+ +cx: ThreadContext,
+ +thread: string,
+ +path: string,
+ +expanded: boolean,
+ |}
+ | {|
+ +type: "ADD_INLINE_PREVIEW",
+ +thread: string,
+ +frame: Frame,
+ +previews: Previews,
+ |}
+ | {|
+ +type: "HIGHLIGHT_CALLS",
+ +highlightedCalls: HighlightedCalls,
+ |}
+ | {|
+ +type: "UNHIGHLIGHT_CALLS",
+ |};
diff --git a/devtools/client/debugger/src/actions/types/PreviewAction.js b/devtools/client/debugger/src/actions/types/PreviewAction.js
new file mode 100644
index 0000000000..7b59897a15
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/PreviewAction.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import type { Context } from "../../types";
+import type { Preview } from "../../reducers/types";
+
+export type PreviewAction =
+ | {|
+ +type: "SET_PREVIEW",
+ +cx: Context,
+ value: Preview,
+ |}
+ | {|
+ +type: "CLEAR_PREVIEW",
+ +cx: Context,
+ |}
+ | {|
+ +type: "START_PREVIEW",
+ |};
diff --git a/devtools/client/debugger/src/actions/types/SourceAction.js b/devtools/client/debugger/src/actions/types/SourceAction.js
new file mode 100644
index 0000000000..0cf19e7c32
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/SourceAction.js
@@ -0,0 +1,103 @@
+/* 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 {
+ Source,
+ SourceId,
+ SourceLocation,
+ Context,
+ URL,
+ Exception,
+} from "../../types";
+import type { PromiseAction } from "../utils/middleware/promise";
+import type { SourceBase } from "../../reducers/sources";
+
+export type LoadSourceAction = PromiseAction<
+ {|
+ +type: "LOAD_SOURCE_TEXT",
+ +cx: Context,
+ +sourceId: SourceId,
+ +epoch: number,
+ |},
+ {
+ text: string | {| binary: Object |},
+ contentType: string | void,
+ }
+>;
+export type SourceAction =
+ | LoadSourceAction
+ | {|
+ +type: "ADD_SOURCE",
+ +cx: Context,
+ +source: SourceBase,
+ |}
+ | {|
+ +type: "ADD_SOURCES",
+ +cx: Context,
+ +sources: Array<SourceBase>,
+ |}
+ | {|
+ +type: "SET_SELECTED_LOCATION",
+ +cx: Context,
+ +source: Source,
+ +location?: SourceLocation,
+ |}
+ | {|
+ +type: "SET_PENDING_SELECTED_LOCATION",
+ +cx: Context,
+ +url: URL,
+ +line?: number,
+ +column?: number,
+ |}
+ | {| type: "CLEAR_SELECTED_LOCATION", +cx: Context |}
+ | PromiseAction<
+ {|
+ +type: "BLACKBOX",
+ +cx: Context,
+ +source: Source,
+ |},
+ {|
+ +isBlackBoxed: boolean,
+ |}
+ >
+ | PromiseAction<
+ {|
+ +type: "BLACKBOX_SOURCES",
+ +cx: Context,
+ +shouldBlackBox: boolean,
+ |},
+ {|
+ +sources: Source[],
+ |}
+ >
+ | {|
+ +type: "MOVE_TAB",
+ +url: URL,
+ +tabIndex: number,
+ |}
+ | {|
+ +type: "MOVE_TAB_BY_SOURCE_ID",
+ +sourceId: SourceId,
+ +tabIndex: number,
+ |}
+ | {|
+ +type: "CLOSE_TAB",
+ +source: Source,
+ |}
+ | {|
+ +type: "CLOSE_TABS",
+ +sources: Array<Source>,
+ |}
+ | {|
+ type: "SET_ORIGINAL_BREAKABLE_LINES",
+ +cx: Context,
+ breakableLines: number[],
+ sourceId: SourceId,
+ |}
+ | {|
+ type: "ADD_EXCEPTION",
+ exception: Exception,
+ |};
diff --git a/devtools/client/debugger/src/actions/types/SourceActorAction.js b/devtools/client/debugger/src/actions/types/SourceActorAction.js
new file mode 100644
index 0000000000..a848a7972a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/SourceActorAction.js
@@ -0,0 +1,50 @@
+/* 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 { Context } from "../../types";
+import { type PromiseAction } from "../utils/middleware/promise";
+import type {
+ SourceActorId,
+ SourceActor,
+} from "../../reducers/source-actors.js";
+
+export type SourceActorsInsertAction = {|
+ type: "INSERT_SOURCE_ACTORS",
+ items: Array<SourceActor>,
+|};
+export type SourceActorsRemoveAction = {|
+ type: "REMOVE_SOURCE_ACTORS",
+ items: Array<SourceActor>,
+|};
+export type SourceActorClearMapAction = {|
+ +type: "CLEAR_SOURCE_ACTOR_MAP_URL",
+ +cx: Context,
+ +id: SourceActorId,
+|};
+
+export type SourceActorBreakpointColumnsAction = PromiseAction<
+ {|
+ type: "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS",
+ sourceId: SourceActorId,
+ line: number,
+ |},
+ Array<number>
+>;
+
+export type SourceActorBreakableLinesAction = PromiseAction<
+ {|
+ type: "SET_SOURCE_ACTOR_BREAKABLE_LINES",
+ sourceId: SourceActorId,
+ |},
+ Array<number>
+>;
+
+export type SourceActorAction =
+ | SourceActorsInsertAction
+ | SourceActorsRemoveAction
+ | SourceActorBreakpointColumnsAction
+ | SourceActorBreakableLinesAction
+ | SourceActorClearMapAction;
diff --git a/devtools/client/debugger/src/actions/types/UIAction.js b/devtools/client/debugger/src/actions/types/UIAction.js
new file mode 100644
index 0000000000..23165e07c5
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/UIAction.js
@@ -0,0 +1,101 @@
+/* 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 { Source, Range, SourceLocation, Context, URL } from "../../types";
+
+import type {
+ ActiveSearchType,
+ OrientationType,
+ SelectedPrimaryPaneTabType,
+} from "../../reducers/ui";
+
+export type panelPositionType = "start" | "end";
+
+export type UIAction =
+ | {|
+ +type: "TOGGLE_ACTIVE_SEARCH",
+ +value: ?ActiveSearchType,
+ |}
+ | {|
+ +type: "OPEN_QUICK_OPEN",
+ +query?: string,
+ |}
+ | {|
+ +type: "CLOSE_QUICK_OPEN",
+ |}
+ | {|
+ +type: "TOGGLE_FRAMEWORK_GROUPING",
+ +value: boolean,
+ |}
+ | {|
+ +type: "TOGGLE_INLINE_PREVIEW",
+ +value: boolean,
+ |}
+ | {|
+ +type: "TOGGLE_EDITOR_WRAPPING",
+ +value: boolean,
+ |}
+ | {|
+ +type: "TOGGLE_SOURCE_MAPS_ENABLED",
+ +value: boolean,
+ |}
+ | {|
+ +type: "TOGGLE_JAVASCRIPT_ENABLED",
+ +value: boolean,
+ |}
+ | {|
+ +type: "SHOW_SOURCE",
+ +source: Source,
+ |}
+ | {|
+ +type: "TOGGLE_PANE",
+ +position: panelPositionType,
+ +paneCollapsed: boolean,
+ |}
+ | {|
+ +type: "SET_ORIENTATION",
+ +orientation: OrientationType,
+ |}
+ | {|
+ +type: "HIGHLIGHT_LINES",
+ +location: {
+ start: number,
+ end: number,
+ sourceId: number,
+ },
+ |}
+ | {|
+ +type: "CLEAR_HIGHLIGHT_LINES",
+ |}
+ | {|
+ +type: "OPEN_CONDITIONAL_PANEL",
+ +location: SourceLocation,
+ +log: boolean,
+ |}
+ | {|
+ +type: "CLOSE_CONDITIONAL_PANEL",
+ |}
+ | {|
+ +type: "SET_PROJECT_DIRECTORY_ROOT",
+ +cx: Context,
+ +url: URL,
+ +name: string,
+ |}
+ | {|
+ +type: "SET_PRIMARY_PANE_TAB",
+ +tabName: SelectedPrimaryPaneTabType,
+ |}
+ | {|
+ +type: "CLOSE_PROJECT_SEARCH",
+ |}
+ | {|
+ +type: "SET_VIEWPORT",
+ +viewport: Range,
+ |}
+ | {|
+ +type: "SET_CURSOR_POSITION",
+ +cursorPosition: SourceLocation,
+ |};
diff --git a/devtools/client/debugger/src/actions/types/index.js b/devtools/client/debugger/src/actions/types/index.js
new file mode 100644
index 0000000000..1e2d927f9f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/types/index.js
@@ -0,0 +1,238 @@
+/* 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 typeof SourceMaps from "devtools-source-map";
+import type {
+ Thread,
+ Context,
+ ThreadId,
+ SourceId,
+ SourceLocation,
+ URL,
+} from "../../types";
+import type { State } from "../../reducers/types";
+import type { MatchedLocations } from "../../reducers/file-search";
+import type { TreeNode } from "../../utils/sources-tree/types";
+import type {
+ SearchOperation,
+ StatusType,
+} from "../../reducers/project-text-search";
+
+import type { BreakpointAction } from "./BreakpointAction";
+import type { SourceAction } from "./SourceAction";
+import type { SourceActorAction } from "./SourceActorAction";
+import type { UIAction } from "./UIAction";
+import type { PauseAction } from "./PauseAction";
+import type { PreviewAction } from "./PreviewAction";
+import type { ASTAction } from "./ASTAction";
+import { clientCommands } from "../../client/firefox";
+import type { Panel } from "../../client/firefox/types";
+import type { ParserDispatcher } from "../../workers/parser";
+
+/**
+ * Flow types
+ * @module actions/types
+ */
+
+/**
+ * Argument parameters via Thunk middleware for {@link https://github.com/gaearon/redux-thunk|Redux Thunk}
+ *
+ * @memberof actions/breakpoints
+ * @static
+ * @typedef {Object} ThunkArgs
+ */
+export type ThunkArgs = {
+ dispatch: (action: any) => Promise<any>,
+ forkedDispatch: (action: any) => Promise<any>,
+ getState: () => State,
+ client: typeof clientCommands,
+ sourceMaps: SourceMaps,
+ parser: ParserDispatcher,
+ evaluationsParser: ParserDispatcher,
+ panel: Panel,
+};
+
+export type Thunk = ThunkArgs => any;
+
+export type ActionType = Object | Function;
+
+type ProjectTextSearchResult = {
+ sourceId: SourceId,
+ filepath: string,
+ matches: MatchedLocations[],
+};
+
+type AddTabAction = {|
+ +type: "ADD_TAB",
+ +url: URL,
+ +framework?: string,
+ +isOriginal?: boolean,
+ +sourceId: SourceId,
+|};
+
+type UpdateTabAction = {|
+ +type: "UPDATE_TAB",
+ +url: URL,
+ +framework?: string,
+ +isOriginal?: boolean,
+ +sourceId: SourceId,
+|};
+
+type NavigateAction =
+ | {|
+ +type: "CONNECT",
+ +traits: Object,
+ +isWebExtension: boolean,
+ +mainThreadActorID: ThreadId,
+ |}
+ | {| +type: "NAVIGATE", +mainThread: Thread |};
+
+export type FocusItem = TreeNode;
+
+export type SourceTreeAction =
+ | {| +type: "SET_EXPANDED_STATE", +thread: string, +expanded: any |}
+ | {| +type: "SET_FOCUSED_SOURCE_ITEM", +cx: Context, item: FocusItem |};
+
+export type ProjectTextSearchAction =
+ | {| +type: "ADD_QUERY", +cx: Context, +query: string |}
+ | {|
+ +type: "ADD_SEARCH_RESULT",
+ +cx: Context,
+ +result: ProjectTextSearchResult,
+ |}
+ | {| +type: "UPDATE_STATUS", +cx: Context, +status: StatusType |}
+ | {| +type: "CLEAR_SEARCH_RESULTS", +cx: Context |}
+ | {|
+ +type: "ADD_ONGOING_SEARCH",
+ +cx: Context,
+ +ongoingSearch: SearchOperation,
+ |}
+ | {| +type: "CLEAR_SEARCH", +cx: Context |};
+
+export type FileTextSearchModifier =
+ | "caseSensitive"
+ | "wholeWord"
+ | "regexMatch";
+
+export type FileTextSearchAction =
+ | {|
+ +type: "TOGGLE_FILE_SEARCH_MODIFIER",
+ +cx: Context,
+ +modifier: FileTextSearchModifier,
+ |}
+ | {|
+ +type: "UPDATE_FILE_SEARCH_QUERY",
+ +cx: Context,
+ +query: string,
+ |}
+ | {|
+ +type: "UPDATE_SEARCH_RESULTS",
+ +cx: Context,
+ +results: {
+ matches: MatchedLocations[],
+ matchIndex: number,
+ count: number,
+ index: number,
+ },
+ |};
+
+export type QuickOpenAction =
+ | {| +type: "SET_QUICK_OPEN_QUERY", +query: string |}
+ | {| +type: "OPEN_QUICK_OPEN", +query?: string |}
+ | {| +type: "CLOSE_QUICK_OPEN" |};
+
+export type ThreadsAction =
+ | {|
+ +type: "INSERT_THREAD",
+ +cx: Context,
+ +newThread: Thread,
+ |}
+ | {|
+ +type: "REMOVE_THREAD",
+ +cx: Context,
+ +oldThread: Thread,
+ |}
+ | {|
+ +type: "UPDATE_SERVICE_WORKER_STATUS",
+ +cx: Context,
+ +thread: string,
+ +status: string,
+ |}
+ | {|
+ +type: "SELECT_THREAD",
+ +cx: Context,
+ +thread: ThreadId,
+ |}
+ | {|
+ +type: "PREVIEW_PAUSED_LOCATION",
+ +location: SourceLocation,
+ |}
+ | {|
+ +type: "CLEAR_PREVIEW_PAUSED_LOCATION",
+ |};
+
+export type {
+ StartPromiseAction,
+ DonePromiseAction,
+ ErrorPromiseAction,
+} from "../utils/middleware/promise";
+
+export type { panelPositionType } from "./UIAction";
+
+export type { ASTAction } from "./ASTAction";
+
+type ActiveEventListener = string;
+export type EventListenerEvent = { name: string, id: ActiveEventListener };
+export type EventListenerCategory = {
+ name: string,
+ events: EventListenerEvent[],
+};
+
+export type EventListenerActiveList = ActiveEventListener[];
+export type EventListenerCategoryList = EventListenerCategory[];
+export type EventListenerExpandedList = string[];
+
+export type EventListenerAction =
+ | {|
+ +type: "UPDATE_EVENT_LISTENERS",
+ +active: EventListenerActiveList,
+ |}
+ | {|
+ +type: "RECEIVE_EVENT_LISTENER_TYPES",
+ +categories: EventListenerCategoryList,
+ |}
+ | {|
+ +type: "UPDATE_EVENT_LISTENER_EXPANDED",
+ +expanded: EventListenerExpandedList,
+ |}
+ | {|
+ +type: "TOGGLE_EVENT_LISTENERS",
+ +logEventBreakpoints: boolean,
+ |};
+
+/**
+ * Actions: Source, Breakpoint, and Navigation
+ *
+ * @memberof actions/types
+ * @static
+ */
+export type Action =
+ | AddTabAction
+ | UpdateTabAction
+ | SourceActorAction
+ | SourceAction
+ | BreakpointAction
+ | PauseAction
+ | NavigateAction
+ | UIAction
+ | ASTAction
+ | PreviewAction
+ | QuickOpenAction
+ | FileTextSearchAction
+ | ProjectTextSearchAction
+ | ThreadsAction
+ | SourceTreeAction
+ | EventListenerAction;
diff --git a/devtools/client/debugger/src/actions/ui.js b/devtools/client/debugger/src/actions/ui.js
new file mode 100644
index 0000000000..acf63446ae
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ui.js
@@ -0,0 +1,282 @@
+/* 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 {
+ getActiveSearch,
+ getPaneCollapse,
+ getQuickOpenEnabled,
+ getSource,
+ getSourceContent,
+ startsWithThreadActor,
+ getFileSearchQuery,
+ getProjectDirectoryRoot,
+} from "../selectors";
+import { selectSource } from "../actions/sources/select";
+import type { ThunkArgs, panelPositionType } from "./types";
+import {
+ getEditor,
+ getLocationsInViewport,
+ updateDocuments,
+} from "../utils/editor";
+import { searchContents } from "./file-search";
+import { copyToTheClipboard } from "../utils/clipboard";
+import { isFulfilled } from "../utils/async-value";
+
+import type { SourceLocation, Context, Source, SourceId } from "../types";
+import type {
+ ActiveSearchType,
+ OrientationType,
+ SelectedPrimaryPaneTabType,
+} from "../reducers/ui";
+import type { UIAction } from "./types/UIAction";
+
+export function setPrimaryPaneTab(
+ tabName: SelectedPrimaryPaneTabType
+): UIAction {
+ return { type: "SET_PRIMARY_PANE_TAB", tabName };
+}
+
+export function closeActiveSearch(): UIAction {
+ return {
+ type: "TOGGLE_ACTIVE_SEARCH",
+ value: null,
+ };
+}
+
+export function setActiveSearch(activeSearch?: ActiveSearchType) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const activeSearchState = getActiveSearch(getState());
+ if (activeSearchState === activeSearch) {
+ return;
+ }
+
+ if (getQuickOpenEnabled(getState())) {
+ dispatch({ type: "CLOSE_QUICK_OPEN" });
+ }
+
+ dispatch({
+ type: "TOGGLE_ACTIVE_SEARCH",
+ value: activeSearch,
+ });
+ };
+}
+
+export function updateActiveFileSearch(cx: Context) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const isFileSearchOpen = getActiveSearch(getState()) === "file";
+ const fileSearchQuery = getFileSearchQuery(getState());
+ if (isFileSearchOpen && fileSearchQuery) {
+ const editor = getEditor();
+ dispatch(searchContents(cx, fileSearchQuery, editor, false));
+ }
+ };
+}
+
+export function toggleFrameworkGrouping(toggleValue: boolean) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ dispatch({
+ type: "TOGGLE_FRAMEWORK_GROUPING",
+ value: toggleValue,
+ });
+ };
+}
+
+export function toggleInlinePreview(toggleValue: boolean) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ dispatch({
+ type: "TOGGLE_INLINE_PREVIEW",
+ value: toggleValue,
+ });
+ };
+}
+
+export function toggleEditorWrapping(toggleValue: boolean) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ updateDocuments(doc => doc.cm.setOption("lineWrapping", toggleValue));
+
+ dispatch({
+ type: "TOGGLE_EDITOR_WRAPPING",
+ value: toggleValue,
+ });
+ };
+}
+
+export function toggleSourceMapsEnabled(toggleValue: boolean) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ dispatch({
+ type: "TOGGLE_SOURCE_MAPS_ENABLED",
+ value: toggleValue,
+ });
+ };
+}
+
+export function showSource(cx: Context, sourceId: SourceId) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const source = getSource(getState(), sourceId);
+ if (!source) {
+ return;
+ }
+
+ if (getPaneCollapse(getState(), "start")) {
+ dispatch({
+ type: "TOGGLE_PANE",
+ position: "start",
+ paneCollapsed: false,
+ });
+ }
+
+ dispatch(setPrimaryPaneTab("sources"));
+
+ dispatch({ type: "SHOW_SOURCE", source: null });
+ dispatch(selectSource(cx, source.id));
+ dispatch({ type: "SHOW_SOURCE", source });
+ };
+}
+
+export function togglePaneCollapse(
+ position: panelPositionType,
+ paneCollapsed: boolean
+) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const prevPaneCollapse = getPaneCollapse(getState(), position);
+ if (prevPaneCollapse === paneCollapsed) {
+ return;
+ }
+
+ dispatch({
+ type: "TOGGLE_PANE",
+ position,
+ paneCollapsed,
+ });
+ };
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+export function highlightLineRange(location: {
+ start: number,
+ end: number,
+ sourceId: SourceId,
+}) {
+ return {
+ type: "HIGHLIGHT_LINES",
+ location,
+ };
+}
+
+export function flashLineRange(location: {
+ start: number,
+ end: number,
+ sourceId: SourceId,
+}) {
+ return ({ dispatch }: ThunkArgs) => {
+ dispatch(highlightLineRange(location));
+ setTimeout(() => dispatch(clearHighlightLineRange()), 200);
+ };
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+export function clearHighlightLineRange(): UIAction {
+ return {
+ type: "CLEAR_HIGHLIGHT_LINES",
+ };
+}
+
+export function openConditionalPanel(
+ location: ?SourceLocation,
+ log: boolean = false
+): ?UIAction {
+ if (!location) {
+ return;
+ }
+
+ return {
+ type: "OPEN_CONDITIONAL_PANEL",
+ location,
+ log,
+ };
+}
+
+export function closeConditionalPanel(): UIAction {
+ return {
+ type: "CLOSE_CONDITIONAL_PANEL",
+ };
+}
+
+export function clearProjectDirectoryRoot(cx: Context): UIAction {
+ return {
+ type: "SET_PROJECT_DIRECTORY_ROOT",
+ cx,
+ url: "",
+ name: "",
+ };
+}
+
+export function setProjectDirectoryRoot(
+ cx: Context,
+ newRoot: string,
+ newName: string
+) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const threadActor = startsWithThreadActor(getState(), newRoot);
+
+ let curRoot = getProjectDirectoryRoot(getState());
+
+ // Remove the thread actor ID from the root path
+ if (threadActor) {
+ newRoot = newRoot.slice(threadActor.length + 1);
+ curRoot = curRoot.slice(threadActor.length + 1);
+ }
+
+ if (newRoot && curRoot) {
+ const newRootArr = newRoot.replace(/\/+/g, "/").split("/");
+ const curRootArr = curRoot
+ .replace(/^\//, "")
+ .replace(/\/+/g, "/")
+ .split("/");
+ if (newRootArr[0] !== curRootArr[0]) {
+ newRootArr.splice(0, 2);
+ newRoot = `${curRoot}/${newRootArr.join("/")}`;
+ }
+ }
+
+ dispatch({
+ type: "SET_PROJECT_DIRECTORY_ROOT",
+ cx,
+ url: newRoot,
+ name: newName,
+ });
+ };
+}
+
+export function updateViewport(): UIAction {
+ return {
+ type: "SET_VIEWPORT",
+ viewport: getLocationsInViewport(getEditor()),
+ };
+}
+
+export function updateCursorPosition(cursorPosition: SourceLocation): UIAction {
+ return { type: "SET_CURSOR_POSITION", cursorPosition };
+}
+
+export function setOrientation(orientation: OrientationType): UIAction {
+ return { type: "SET_ORIENTATION", orientation };
+}
+
+export function copyToClipboard(source: Source) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const content = getSourceContent(getState(), source.id);
+ if (content && isFulfilled(content) && content.value.type === "text") {
+ copyToTheClipboard(content.value.value);
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/create-store.js b/devtools/client/debugger/src/actions/utils/create-store.js
new file mode 100644
index 0000000000..fbfe7b9d3c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/create-store.js
@@ -0,0 +1,83 @@
+/* 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
+
+/* global window */
+
+/**
+ * Redux store utils
+ * @module utils/create-store
+ */
+
+import { createStore, applyMiddleware, type StoreCreator } from "redux";
+import { waitUntilService } from "./middleware/wait-service";
+import { log } from "./middleware/log";
+import { promise } from "./middleware/promise";
+import { thunk } from "./middleware/thunk";
+import { timing } from "./middleware/timing";
+import { context } from "./middleware/context";
+
+/**
+ * @memberof utils/create-store
+ * @static
+ */
+type ReduxStoreOptions = {
+ makeThunkArgs?: Function,
+ history?: Array<Object>,
+ middleware?: Function[],
+ log?: boolean,
+ timing?: boolean,
+};
+
+/**
+ * This creates a dispatcher with all the standard middleware in place
+ * that all code requires. It can also be optionally configured in
+ * various ways, such as logging and recording.
+ *
+ * @param {object} opts:
+ * - log: log all dispatched actions to console
+ * - history: an array to store every action in. Should only be
+ * used in tests.
+ * - middleware: array of middleware to be included in the redux store
+ * @memberof utils/create-store
+ * @static
+ */
+const configureStore = (
+ opts: ReduxStoreOptions = {}
+): StoreCreator<any, any, any> => {
+ const middleware: any = [
+ thunk(opts.makeThunkArgs),
+ context,
+ promise,
+
+ // Order is important: services must go last as they always
+ // operate on "already transformed" actions. Actions going through
+ // them shouldn't have any special fields like promises, they
+ // should just be normal JSON objects.
+ waitUntilService,
+ ];
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ if (opts.timing) {
+ middleware.push(timing);
+ }
+
+ // Hook in the redux devtools browser extension if it exists
+ const devtoolsExt =
+ typeof window === "object" && window.devToolsExtension
+ ? window.devToolsExtension()
+ : f => f;
+
+ return applyMiddleware(...middleware)(devtoolsExt(createStore));
+};
+
+export default configureStore;
diff --git a/devtools/client/debugger/src/actions/utils/middleware/context.js b/devtools/client/debugger/src/actions/utils/middleware/context.js
new file mode 100644
index 0000000000..4a9ee09024
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/context.js
@@ -0,0 +1,37 @@
+/* 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 {
+ validateNavigateContext,
+ validateContext,
+} from "../../../utils/context";
+
+import type { ThunkArgs } from "../../types";
+
+function validateActionContext(getState, action): void {
+ if (action.type == "COMMAND" && action.status == "done") {
+ // The thread will have resumed execution since the action was initiated,
+ // so just make sure we haven't navigated.
+ validateNavigateContext(getState(), action.cx);
+ return;
+ }
+
+ // Validate using all available information in the context.
+ validateContext(getState(), action.cx);
+}
+
+// Middleware which looks for actions that have a cx property and ignores
+// them if the context is no longer valid.
+function context({ dispatch, getState }: ThunkArgs) {
+ return (next: Function) => (action: Object) => {
+ if ("cx" in action) {
+ validateActionContext(getState, action);
+ }
+ return next(action);
+ };
+}
+
+export { context };
diff --git a/devtools/client/debugger/src/actions/utils/middleware/log.js b/devtools/client/debugger/src/actions/utils/middleware/log.js
new file mode 100644
index 0000000000..3b688c090f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/log.js
@@ -0,0 +1,116 @@
+/* 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
+
+// $FlowIgnore
+import flags from "devtools/shared/flags";
+import type { ThunkArgs } from "../../types";
+import { prefs } from "../../../utils/prefs";
+
+const ignoreList = [
+ "ADD_BREAKPOINT_POSITIONS",
+ "SET_SYMBOLS",
+ "OUT_OF_SCOPE_LOCATIONS",
+ "MAP_SCOPES",
+ "MAP_FRAMES",
+ "ADD_SCOPES",
+ "IN_SCOPE_LINES",
+ "REMOVE_BREAKPOINT",
+ "NODE_PROPERTIES_LOADED",
+ "SET_FOCUSED_SOURCE_ITEM",
+ "NODE_EXPAND",
+ "IN_SCOPE_LINES",
+ "SET_PREVIEW",
+];
+
+function cloneAction(action: any) {
+ action = action || {};
+ action = { ...action };
+
+ // ADD_TAB, ...
+ if (action.source?.text) {
+ const source = { ...action.source, text: "" };
+ action.source = source;
+ }
+
+ if (action.sources) {
+ const sources = action.sources.slice(0, 20).map(source => {
+ const url = !source.url || source.url.includes("data:") ? "" : source.url;
+ return { ...source, url };
+ });
+ action.sources = sources;
+ }
+
+ // LOAD_SOURCE_TEXT
+ if (action.text) {
+ action.text = "";
+ }
+
+ if (action.value?.text) {
+ const value = { ...action.value, text: "" };
+ action.value = value;
+ }
+
+ return action;
+}
+
+function formatPause(pause) {
+ return {
+ ...pause,
+ pauseInfo: { why: pause.why },
+ scopes: [],
+ loadedObjects: [],
+ };
+}
+
+function serializeAction(action) {
+ try {
+ action = cloneAction(action);
+ if (ignoreList.includes(action.type)) {
+ action = {};
+ }
+
+ if (action.type === "PAUSED") {
+ action = formatPause(action);
+ }
+
+ const serializer = function(key, value) {
+ // Serialize Object/LongString fronts
+ if (value?.getGrip) {
+ return value.getGrip();
+ }
+ return value;
+ };
+
+ // dump(`> ${action.type}...\n ${JSON.stringify(action, serializer)}\n`);
+ return JSON.stringify(action, serializer);
+ } catch (e) {
+ console.error(e);
+ return "";
+ }
+}
+
+/**
+ * A middleware that logs all actions coming through the system
+ * to the console.
+ */
+export function log({ dispatch, getState }: ThunkArgs) {
+ return (next: any) => (action: any) => {
+ const asyncMsg = !action.status ? "" : `[${action.status}]`;
+
+ if (prefs.logActions) {
+ if (flags.testing) {
+ // $FlowIgnore
+ dump(
+ `[ACTION] ${action.type} ${asyncMsg} - ${serializeAction(action)}\n`
+ );
+ } else {
+ console.log(action, asyncMsg);
+ }
+ }
+
+ next(action);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/middleware/moz.build b/devtools/client/debugger/src/actions/utils/middleware/moz.build
new file mode 100644
index 0000000000..f46a0bb725
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/moz.build
@@ -0,0 +1,15 @@
+# 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(
+ "context.js",
+ "log.js",
+ "promise.js",
+ "thunk.js",
+ "timing.js",
+ "wait-service.js",
+)
diff --git a/devtools/client/debugger/src/actions/utils/middleware/promise.js b/devtools/client/debugger/src/actions/utils/middleware/promise.js
new file mode 100644
index 0000000000..46c73c7325
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/promise.js
@@ -0,0 +1,122 @@
+/* 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 { fromPairs, toPairs } from "lodash";
+import { executeSoon } from "../../../utils/DevToolsUtils";
+import type { ThunkArgs } from "../../types";
+
+type BasePromiseAction = {|
+ +"@@dispatch/promise": Promise<mixed>,
+|};
+
+export type StartPromiseAction = {|
+ ...BasePromiseAction,
+ +status: "start",
+|};
+
+export type DonePromiseAction = {|
+ ...BasePromiseAction,
+ +status: "done",
+ +value: any,
+|};
+
+export type ErrorPromiseAction = {|
+ ...BasePromiseAction,
+ +status: "error",
+ +error: any,
+|};
+
+import {
+ pending,
+ rejected,
+ fulfilled,
+ type AsyncValue,
+} from "../../../utils/async-value";
+export function asyncActionAsValue<T>(
+ action: PromiseAction<mixed, T>
+): AsyncValue<T> {
+ if (action.status === "start") {
+ return pending();
+ }
+ if (action.status === "error") {
+ return rejected(action.error);
+ }
+ return fulfilled(action.value);
+}
+
+export type PromiseAction<+Action, Value = any> =
+ // | {| ...Action, "@@dispatch/promise": Promise<Object> |}
+ | {|
+ ...BasePromiseAction,
+ ...Action,
+ +status: "start",
+ value: void,
+ |}
+ | {|
+ ...BasePromiseAction,
+ ...Action,
+ +status: "done",
+ +value: Value,
+ |}
+ | {|
+ ...BasePromiseAction,
+ ...Action,
+ +status: "error",
+ +error?: any,
+ value: void,
+ |};
+
+let seqIdVal = 1;
+
+function seqIdGen() {
+ return seqIdVal++;
+}
+
+function filterAction(action: Object): Object {
+ return fromPairs(toPairs(action).filter(pair => pair[0] !== PROMISE));
+}
+
+function promiseMiddleware({
+ dispatch,
+ getState,
+}: ThunkArgs): Function | Promise<mixed> {
+ return (next: Function) => (action: Object) => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+
+ const promiseInst = action[PROMISE];
+ const seqId = seqIdGen().toString();
+
+ // Create a new action that doesn't have the promise field and has
+ // the `seqId` field that represents the sequence id
+ action = { ...filterAction(action), seqId };
+
+ dispatch({ ...action, status: "start" });
+
+ // Return the promise so action creators can still compose if they
+ // want to.
+ return Promise.resolve(promiseInst)
+ .finally(() => new Promise(resolve => executeSoon(resolve)))
+ .then(
+ value => {
+ dispatch({ ...action, status: "done", value: value });
+ return value;
+ },
+ error => {
+ dispatch({
+ ...action,
+ status: "error",
+ error: error.message || error,
+ });
+ return Promise.reject(error);
+ }
+ );
+ };
+}
+
+export const PROMISE = "@@dispatch/promise";
+export { promiseMiddleware as promise };
diff --git a/devtools/client/debugger/src/actions/utils/middleware/thunk.js b/devtools/client/debugger/src/actions/utils/middleware/thunk.js
new file mode 100644
index 0000000000..a4d63f80e7
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/thunk.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import type { ThunkArgs, ActionType } from "../../types";
+
+/**
+ * A middleware that allows thunks (functions) to be dispatched. If
+ * it's a thunk, it is called with an argument that contains
+ * `dispatch`, `getState`, and any additional args passed in via the
+ * middleware constructure. This allows the action to create multiple
+ * actions (most likely asynchronously).
+ */
+export function thunk(makeArgs: any) {
+ return ({ dispatch, getState }: ThunkArgs) => {
+ const args = { dispatch, getState };
+
+ return (next: Function) => (action: ActionType) => {
+ return typeof action === "function"
+ ? action(makeArgs ? makeArgs(args, getState()) : args)
+ : next(action);
+ };
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/middleware/timing.js b/devtools/client/debugger/src/actions/utils/middleware/timing.js
new file mode 100644
index 0000000000..84661efb57
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/timing.js
@@ -0,0 +1,28 @@
+/* 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
+
+/**
+ * Redux middleware that sets performance markers for all actions such that they
+ * will appear in performance tooling under the User Timing API
+ */
+
+const mark = window.performance?.mark
+ ? window.performance.mark.bind(window.performance)
+ : a => {};
+
+const measure = window.performance?.measure
+ ? window.performance.measure.bind(window.performance)
+ : (a, b, c) => {};
+
+export function timing(store: any) {
+ return (next: any) => (action: any) => {
+ mark(`${action.type}_start`);
+ const result = next(action);
+ mark(`${action.type}_end`);
+ measure(`${action.type}`, `${action.type}_start`, `${action.type}_end`);
+ return result;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/middleware/wait-service.js b/devtools/client/debugger/src/actions/utils/middleware/wait-service.js
new file mode 100644
index 0000000000..0d83eee91c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/wait-service.js
@@ -0,0 +1,65 @@
+/* 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
+
+/**
+ * A middleware which acts like a service, because it is stateful
+ * and "long-running" in the background. It provides the ability
+ * for actions to install a function to be run once when a specific
+ * condition is met by an action coming through the system. Think of
+ * it as a thunk that blocks until the condition is met. Example:
+ *
+ * ```js
+ * const services = { WAIT_UNTIL: require('wait-service').NAME };
+ *
+ * { type: services.WAIT_UNTIL,
+ * predicate: action => action.type === "ADD_ITEM",
+ * run: (dispatch, getState, action) => {
+ * // Do anything here. You only need to accept the arguments
+ * // if you need them. `action` is the action that satisfied
+ * // the predicate.
+ * }
+ * }
+ * ```
+ */
+export const NAME = "@@service/waitUntil";
+import type { ThunkArgs } from "../../types";
+
+export function waitUntilService({ dispatch, getState }: ThunkArgs) {
+ let pending = [];
+
+ function checkPending(action) {
+ const readyRequests = [];
+ const stillPending = [];
+
+ // Find the pending requests whose predicates are satisfied with
+ // this action. Wait to run the requests until after we update the
+ // pending queue because the request handler may synchronously
+ // dispatch again and run this service (that use case is
+ // completely valid).
+ for (const request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (const request of readyRequests) {
+ request.run(dispatch, getState, action);
+ }
+ }
+
+ return (next: Function) => (action: Object) => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ const result = next(action);
+ checkPending(action);
+ return result;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/moz.build b/devtools/client/debugger/src/actions/utils/moz.build
new file mode 100644
index 0000000000..08a43a218c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/moz.build
@@ -0,0 +1,12 @@
+# 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 += [
+ "middleware",
+]
+
+CompiledModules(
+ "create-store.js",
+)