summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/context-menus
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/actions/context-menus')
-rw-r--r--devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js78
-rw-r--r--devtools/client/debugger/src/actions/context-menus/breakpoint.js396
-rw-r--r--devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js273
-rw-r--r--devtools/client/debugger/src/actions/context-menus/editor.js436
-rw-r--r--devtools/client/debugger/src/actions/context-menus/frame.js97
-rw-r--r--devtools/client/debugger/src/actions/context-menus/index.js12
-rw-r--r--devtools/client/debugger/src/actions/context-menus/moz.build16
-rw-r--r--devtools/client/debugger/src/actions/context-menus/outline.js54
-rw-r--r--devtools/client/debugger/src/actions/context-menus/source-tree-item.js281
-rw-r--r--devtools/client/debugger/src/actions/context-menus/tab.js128
10 files changed, 1771 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js b/devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js
new file mode 100644
index 0000000000..bded531cfe
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js
@@ -0,0 +1,78 @@
+/* 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 { buildMenu, showMenu } from "../../context-menu/menu";
+
+import { getBreakpointsForSource } from "../../selectors/index";
+
+import {
+ disableBreakpointsInSource,
+ enableBreakpointsInSource,
+ removeBreakpointsInSource,
+} from "../../actions/breakpoints/index";
+
+export function showBreakpointHeadingContextMenu(event, source) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const breakpointsForSource = getBreakpointsForSource(state, source);
+
+ const enableInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.enableInSource.label"
+ );
+ const disableInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.disableInSource.label"
+ );
+ const removeInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.removeInSource.label"
+ );
+ const enableInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.enableInSource.accesskey"
+ );
+ const disableInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.disableInSource.accesskey"
+ );
+ const removeInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.removeInSource.accesskey"
+ );
+
+ const disableInSourceItem = {
+ id: "node-menu-disable-in-source",
+ label: disableInSourceLabel,
+ accesskey: disableInSourceKey,
+ disabled: false,
+ click: () => dispatch(disableBreakpointsInSource(source)),
+ };
+
+ const enableInSourceItem = {
+ id: "node-menu-enable-in-source",
+ label: enableInSourceLabel,
+ accesskey: enableInSourceKey,
+ disabled: false,
+ click: () => dispatch(enableBreakpointsInSource(source)),
+ };
+
+ const removeInSourceItem = {
+ id: "node-menu-enable-in-source",
+ label: removeInSourceLabel,
+ accesskey: removeInSourceKey,
+ disabled: false,
+ click: () => dispatch(removeBreakpointsInSource(source)),
+ };
+
+ const hideDisableInSourceItem = breakpointsForSource.every(
+ breakpoint => breakpoint.disabled
+ );
+ const hideEnableInSourceItem = breakpointsForSource.every(
+ breakpoint => !breakpoint.disabled
+ );
+
+ const items = [
+ { item: disableInSourceItem, hidden: () => hideDisableInSourceItem },
+ { item: enableInSourceItem, hidden: () => hideEnableInSourceItem },
+ { item: removeInSourceItem, hidden: () => false },
+ ];
+
+ showMenu(event, buildMenu(items));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/breakpoint.js b/devtools/client/debugger/src/actions/context-menus/breakpoint.js
new file mode 100644
index 0000000000..d70254130c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/breakpoint.js
@@ -0,0 +1,396 @@
+/* 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 { buildMenu, showMenu } from "../../context-menu/menu";
+import { getSelectedLocation } from "../../utils/selected-location";
+import { isLineBlackboxed } from "../../utils/source";
+import { features } from "../../utils/prefs";
+import { formatKeyShortcut } from "../../utils/text";
+
+import {
+ getBreakpointsList,
+ getSelectedSource,
+ getBlackBoxRanges,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors/index";
+
+import {
+ removeBreakpoint,
+ setBreakpointOptions,
+} from "../../actions/breakpoints/modify";
+import {
+ removeBreakpoints,
+ removeAllBreakpoints,
+ toggleBreakpoints,
+ toggleAllBreakpoints,
+ toggleDisabledBreakpoint,
+} from "../../actions/breakpoints/index";
+import { selectSpecificLocation } from "../../actions/sources/select";
+import { openConditionalPanel } from "../../actions/ui";
+
+export function showBreakpointContextMenu(event, breakpoint, source) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const breakpoints = getBreakpointsList(state);
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const blackboxedRangesForSource = blackboxedRanges[source.url];
+ const checkSourceOnIgnoreList = _source =>
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, _source);
+ const selectedSource = getSelectedSource(state);
+
+ const deleteSelfLabel = L10N.getStr("breakpointMenuItem.deleteSelf2.label");
+ const deleteAllLabel = L10N.getStr("breakpointMenuItem.deleteAll2.label");
+ const deleteOthersLabel = L10N.getStr(
+ "breakpointMenuItem.deleteOthers2.label"
+ );
+ const enableSelfLabel = L10N.getStr("breakpointMenuItem.enableSelf2.label");
+ const enableAllLabel = L10N.getStr("breakpointMenuItem.enableAll2.label");
+ const enableOthersLabel = L10N.getStr(
+ "breakpointMenuItem.enableOthers2.label"
+ );
+ const disableSelfLabel = L10N.getStr(
+ "breakpointMenuItem.disableSelf2.label"
+ );
+ const disableAllLabel = L10N.getStr("breakpointMenuItem.disableAll2.label");
+ const disableOthersLabel = L10N.getStr(
+ "breakpointMenuItem.disableOthers2.label"
+ );
+ const enableDbgStatementLabel = L10N.getStr(
+ "breakpointMenuItem.enabledbg.label"
+ );
+ const disableDbgStatementLabel = L10N.getStr(
+ "breakpointMenuItem.disabledbg.label"
+ );
+ const removeConditionLabel = L10N.getStr(
+ "breakpointMenuItem.removeCondition2.label"
+ );
+ const addConditionLabel = L10N.getStr(
+ "breakpointMenuItem.addCondition2.label"
+ );
+ const editConditionLabel = L10N.getStr(
+ "breakpointMenuItem.editCondition2.label"
+ );
+
+ const deleteSelfKey = L10N.getStr(
+ "breakpointMenuItem.deleteSelf2.accesskey"
+ );
+ const deleteAllKey = L10N.getStr("breakpointMenuItem.deleteAll2.accesskey");
+ const deleteOthersKey = L10N.getStr(
+ "breakpointMenuItem.deleteOthers2.accesskey"
+ );
+ const enableSelfKey = L10N.getStr(
+ "breakpointMenuItem.enableSelf2.accesskey"
+ );
+ const enableAllKey = L10N.getStr("breakpointMenuItem.enableAll2.accesskey");
+ const enableOthersKey = L10N.getStr(
+ "breakpointMenuItem.enableOthers2.accesskey"
+ );
+ const disableSelfKey = L10N.getStr(
+ "breakpointMenuItem.disableSelf2.accesskey"
+ );
+ const disableAllKey = L10N.getStr(
+ "breakpointMenuItem.disableAll2.accesskey"
+ );
+ const disableOthersKey = L10N.getStr(
+ "breakpointMenuItem.disableOthers2.accesskey"
+ );
+ const removeConditionKey = L10N.getStr(
+ "breakpointMenuItem.removeCondition2.accesskey"
+ );
+ const editConditionKey = L10N.getStr(
+ "breakpointMenuItem.editCondition2.accesskey"
+ );
+ const addConditionKey = L10N.getStr(
+ "breakpointMenuItem.addCondition2.accesskey"
+ );
+
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ const otherBreakpoints = breakpoints.filter(b => b.id !== breakpoint.id);
+ const enabledBreakpoints = breakpoints.filter(b => !b.disabled);
+ const disabledBreakpoints = breakpoints.filter(b => b.disabled);
+ const otherEnabledBreakpoints = breakpoints.filter(
+ b => !b.disabled && b.id !== breakpoint.id
+ );
+ const otherDisabledBreakpoints = breakpoints.filter(
+ b => b.disabled && b.id !== breakpoint.id
+ );
+
+ const deleteSelfItem = {
+ id: "node-menu-delete-self",
+ label: deleteSelfLabel,
+ accesskey: deleteSelfKey,
+ disabled: false,
+ click: () => {
+ dispatch(removeBreakpoint(breakpoint));
+ },
+ };
+
+ const deleteAllItem = {
+ id: "node-menu-delete-all",
+ label: deleteAllLabel,
+ accesskey: deleteAllKey,
+ disabled: false,
+ click: () => dispatch(removeAllBreakpoints()),
+ };
+
+ const deleteOthersItem = {
+ id: "node-menu-delete-other",
+ label: deleteOthersLabel,
+ accesskey: deleteOthersKey,
+ disabled: false,
+ click: () => dispatch(removeBreakpoints(otherBreakpoints)),
+ };
+
+ const enableSelfItem = {
+ id: "node-menu-enable-self",
+ label: enableSelfLabel,
+ accesskey: enableSelfKey,
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSource,
+ breakpoint.location.line,
+ checkSourceOnIgnoreList(breakpoint.location.source)
+ ),
+ click: () => {
+ dispatch(toggleDisabledBreakpoint(breakpoint));
+ },
+ };
+
+ const enableAllItem = {
+ id: "node-menu-enable-all",
+ label: enableAllLabel,
+ accesskey: enableAllKey,
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSource,
+ breakpoint.location.line,
+ checkSourceOnIgnoreList(breakpoint.location.source)
+ ),
+ click: () => dispatch(toggleAllBreakpoints(false)),
+ };
+
+ const enableOthersItem = {
+ id: "node-menu-enable-others",
+ label: enableOthersLabel,
+ accesskey: enableOthersKey,
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSource,
+ breakpoint.location.line,
+ checkSourceOnIgnoreList(breakpoint.location.source)
+ ),
+ click: () => dispatch(toggleBreakpoints(false, otherDisabledBreakpoints)),
+ };
+
+ const disableSelfItem = {
+ id: "node-menu-disable-self",
+ label: disableSelfLabel,
+ accesskey: disableSelfKey,
+ disabled: false,
+ click: () => {
+ dispatch(toggleDisabledBreakpoint(breakpoint));
+ },
+ };
+
+ const disableAllItem = {
+ id: "node-menu-disable-all",
+ label: disableAllLabel,
+ accesskey: disableAllKey,
+ disabled: false,
+ click: () => dispatch(toggleAllBreakpoints(true)),
+ };
+
+ const disableOthersItem = {
+ id: "node-menu-disable-others",
+ label: disableOthersLabel,
+ accesskey: disableOthersKey,
+ click: () => dispatch(toggleBreakpoints(true, otherEnabledBreakpoints)),
+ };
+
+ const enableDbgStatementItem = {
+ id: "node-menu-enable-dbgStatement",
+ label: enableDbgStatementLabel,
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ condition: null,
+ })
+ ),
+ };
+
+ const disableDbgStatementItem = {
+ id: "node-menu-disable-dbgStatement",
+ label: disableDbgStatementLabel,
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ condition: "false",
+ })
+ ),
+ };
+
+ const removeConditionItem = {
+ id: "node-menu-remove-condition",
+ label: removeConditionLabel,
+ accesskey: removeConditionKey,
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ condition: null,
+ })
+ ),
+ };
+
+ const addConditionItem = {
+ id: "node-menu-add-condition",
+ label: addConditionLabel,
+ accesskey: addConditionKey,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.breakpoint.key")
+ ),
+ };
+
+ const editConditionItem = {
+ id: "node-menu-edit-condition",
+ label: editConditionLabel,
+ accesskey: editConditionKey,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.breakpoint.key")
+ ),
+ };
+
+ const addLogPointItem = {
+ id: "node-menu-add-log-point",
+ label: L10N.getStr("editor.addLogPoint"),
+ accesskey: L10N.getStr("editor.addLogPoint.accesskey"),
+ disabled: false,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation, true));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.logPoint.key")
+ ),
+ };
+
+ const editLogPointItem = {
+ id: "node-menu-edit-log-point",
+ label: L10N.getStr("editor.editLogPoint"),
+ accesskey: L10N.getStr("editor.editLogPoint.accesskey"),
+ disabled: false,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation, true));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.logPoint.key")
+ ),
+ };
+
+ const removeLogPointItem = {
+ id: "node-menu-remove-log",
+ label: L10N.getStr("editor.removeLogPoint.label"),
+ accesskey: L10N.getStr("editor.removeLogPoint.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ logValue: null,
+ })
+ ),
+ };
+
+ const logPointItem = breakpoint.options.logValue
+ ? editLogPointItem
+ : addLogPointItem;
+
+ const hideEnableSelfItem = !breakpoint.disabled;
+ const hideEnableAllItem = disabledBreakpoints.length === 0;
+ const hideEnableOthersItem = otherDisabledBreakpoints.length === 0;
+ const hideDisableAllItem = enabledBreakpoints.length === 0;
+ const hideDisableOthersItem = otherEnabledBreakpoints.length === 0;
+ const hideDisableSelfItem = breakpoint.disabled;
+ const hideEnableDbgStatementItem =
+ !breakpoint.originalText.startsWith("debugger") ||
+ (breakpoint.originalText.startsWith("debugger") &&
+ breakpoint.options.condition !== "false");
+ const hideDisableDbgStatementItem =
+ !breakpoint.originalText.startsWith("debugger") ||
+ (breakpoint.originalText.startsWith("debugger") &&
+ breakpoint.options.condition === "false");
+ const items = [
+ { item: enableSelfItem, hidden: () => hideEnableSelfItem },
+ { item: enableAllItem, hidden: () => hideEnableAllItem },
+ { item: enableOthersItem, hidden: () => hideEnableOthersItem },
+ {
+ item: { type: "separator" },
+ hidden: () =>
+ hideEnableSelfItem && hideEnableAllItem && hideEnableOthersItem,
+ },
+ { item: deleteSelfItem },
+ { item: deleteAllItem },
+ { item: deleteOthersItem, hidden: () => breakpoints.length === 1 },
+ {
+ item: { type: "separator" },
+ hidden: () =>
+ hideDisableSelfItem && hideDisableAllItem && hideDisableOthersItem,
+ },
+
+ { item: disableSelfItem, hidden: () => hideDisableSelfItem },
+ { item: disableAllItem, hidden: () => hideDisableAllItem },
+ { item: disableOthersItem, hidden: () => hideDisableOthersItem },
+ {
+ item: { type: "separator" },
+ },
+ {
+ item: enableDbgStatementItem,
+ hidden: () => hideEnableDbgStatementItem,
+ },
+ {
+ item: disableDbgStatementItem,
+ hidden: () => hideDisableDbgStatementItem,
+ },
+ {
+ item: { type: "separator" },
+ hidden: () => hideDisableDbgStatementItem && hideEnableDbgStatementItem,
+ },
+ {
+ item: addConditionItem,
+ hidden: () => breakpoint.options.condition,
+ },
+ {
+ item: editConditionItem,
+ hidden: () => !breakpoint.options.condition,
+ },
+ {
+ item: removeConditionItem,
+ hidden: () => !breakpoint.options.condition,
+ },
+ {
+ item: logPointItem,
+ hidden: () => !features.logPoints,
+ },
+ {
+ item: removeLogPointItem,
+ hidden: () => !features.logPoints || !breakpoint.options.logValue,
+ },
+ ];
+
+ showMenu(event, buildMenu(items));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js b/devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js
new file mode 100644
index 0000000000..39ec2f1589
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js
@@ -0,0 +1,273 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+import { getSelectedLocation } from "../../utils/selected-location";
+import { features } from "../../utils/prefs";
+import { formatKeyShortcut } from "../../utils/text";
+import { isLineBlackboxed } from "../../utils/source";
+
+import {
+ getSelectedSource,
+ getBlackBoxRanges,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors/index";
+import {
+ addBreakpoint,
+ removeBreakpoint,
+ setBreakpointOptions,
+} from "../../actions/breakpoints/modify";
+import {
+ enableBreakpointsAtLine,
+ disableBreakpointsAtLine,
+ toggleDisabledBreakpoint,
+ removeBreakpointsAtLine,
+} from "../../actions/breakpoints/index";
+import { openConditionalPanel } from "../../actions/ui";
+
+export function showEditorEditBreakpointContextMenu(event, breakpoint) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const selectedSource = getSelectedSource(state);
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const blackboxedRangesForSelectedSource =
+ blackboxedRanges[selectedSource.url];
+ const isSelectedSourceOnIgnoreList =
+ selectedSource &&
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, selectedSource);
+
+ const items = [
+ removeBreakpointItem(breakpoint, dispatch),
+ toggleDisabledBreakpointItem(
+ breakpoint,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+ ),
+ ];
+
+ if (breakpoint.originalText.startsWith("debugger")) {
+ items.push(
+ { type: "separator" },
+ toggleDbgStatementItem(selectedLocation, breakpoint, dispatch)
+ );
+ }
+
+ items.push(
+ { type: "separator" },
+ removeBreakpointsOnLineItem(selectedLocation, dispatch),
+ breakpoint.disabled
+ ? enableBreakpointsOnLineItem(
+ selectedLocation,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+ )
+ : disableBreakpointsOnLineItem(selectedLocation, dispatch),
+ { type: "separator" }
+ );
+
+ items.push(
+ conditionalBreakpointItem(breakpoint, selectedLocation, dispatch)
+ );
+ items.push(logPointItem(breakpoint, selectedLocation, dispatch));
+
+ showMenu(event, items);
+ };
+}
+
+export function showEditorCreateBreakpointContextMenu(
+ event,
+ location,
+ lineText
+) {
+ return async ({ dispatch, getState }) => {
+ const items = createBreakpointItems(location, lineText, dispatch);
+
+ showMenu(event, items);
+ };
+}
+
+export function createBreakpointItems(location, lineText, dispatch) {
+ const items = [
+ addBreakpointItem(location, dispatch),
+ addConditionalBreakpointItem(location, dispatch),
+ ];
+
+ if (features.logPoints) {
+ items.push(addLogPointItem(location, dispatch));
+ }
+
+ if (lineText && lineText.startsWith("debugger")) {
+ items.push(toggleDbgStatementItem(location, null, dispatch));
+ }
+ return items;
+}
+
+const addBreakpointItem = (location, dispatch) => ({
+ id: "node-menu-add-breakpoint",
+ label: L10N.getStr("editor.addBreakpoint"),
+ accesskey: L10N.getStr("shortcuts.toggleBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(addBreakpoint(location)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleBreakpoint.key")),
+});
+
+const removeBreakpointItem = (breakpoint, dispatch) => ({
+ id: "node-menu-remove-breakpoint",
+ label: L10N.getStr("editor.removeBreakpoint"),
+ accesskey: L10N.getStr("shortcuts.toggleBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(removeBreakpoint(breakpoint)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleBreakpoint.key")),
+});
+
+const addConditionalBreakpointItem = (location, dispatch) => ({
+ id: "node-menu-add-conditional-breakpoint",
+ label: L10N.getStr("editor.addConditionBreakpoint"),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.breakpoint.key")),
+ accesskey: L10N.getStr("editor.addConditionBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location)),
+});
+
+const editConditionalBreakpointItem = (location, dispatch) => ({
+ id: "node-menu-edit-conditional-breakpoint",
+ label: L10N.getStr("editor.editConditionBreakpoint"),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.breakpoint.key")),
+ accesskey: L10N.getStr("editor.addConditionBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location)),
+});
+
+const conditionalBreakpointItem = (breakpoint, location, dispatch) => {
+ const {
+ options: { condition },
+ } = breakpoint;
+ return condition
+ ? editConditionalBreakpointItem(location, dispatch)
+ : addConditionalBreakpointItem(location, dispatch);
+};
+
+const addLogPointItem = (location, dispatch) => ({
+ id: "node-menu-add-log-point",
+ label: L10N.getStr("editor.addLogPoint"),
+ accesskey: L10N.getStr("editor.addLogPoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location, true)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key")),
+});
+
+const editLogPointItem = (location, dispatch) => ({
+ id: "node-menu-edit-log-point",
+ label: L10N.getStr("editor.editLogPoint"),
+ accesskey: L10N.getStr("editor.editLogPoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location, true)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key")),
+});
+
+const logPointItem = (breakpoint, location, dispatch) => {
+ const {
+ options: { logValue },
+ } = breakpoint;
+ return logValue
+ ? editLogPointItem(location, dispatch)
+ : addLogPointItem(location, dispatch);
+};
+
+const toggleDisabledBreakpointItem = (
+ breakpoint,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+) => {
+ return {
+ accesskey: L10N.getStr("editor.disableBreakpoint.accesskey"),
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSelectedSource,
+ breakpoint.location.line,
+ isSelectedSourceOnIgnoreList
+ ),
+ click: () => dispatch(toggleDisabledBreakpoint(breakpoint)),
+ ...(breakpoint.disabled
+ ? {
+ id: "node-menu-enable-breakpoint",
+ label: L10N.getStr("editor.enableBreakpoint"),
+ }
+ : {
+ id: "node-menu-disable-breakpoint",
+ label: L10N.getStr("editor.disableBreakpoint"),
+ }),
+ };
+};
+
+const toggleDbgStatementItem = (location, breakpoint, dispatch) => {
+ if (breakpoint && breakpoint.options.condition === "false") {
+ return {
+ disabled: false,
+ id: "node-menu-enable-dbgStatement",
+ label: L10N.getStr("breakpointMenuItem.enabledbg.label"),
+ click: () =>
+ dispatch(
+ setBreakpointOptions(location, {
+ ...breakpoint.options,
+ condition: null,
+ })
+ ),
+ };
+ }
+
+ return {
+ disabled: false,
+ id: "node-menu-disable-dbgStatement",
+ label: L10N.getStr("breakpointMenuItem.disabledbg.label"),
+ click: () =>
+ dispatch(
+ setBreakpointOptions(location, {
+ condition: "false",
+ })
+ ),
+ };
+};
+
+// ToDo: Only enable if there are more than one breakpoints on a line?
+const removeBreakpointsOnLineItem = (location, dispatch) => ({
+ id: "node-menu-remove-breakpoints-on-line",
+ label: L10N.getStr("breakpointMenuItem.removeAllAtLine.label"),
+ accesskey: L10N.getStr("breakpointMenuItem.removeAllAtLine.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(removeBreakpointsAtLine(location.source, location.line)),
+});
+
+const enableBreakpointsOnLineItem = (
+ location,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+) => ({
+ id: "node-menu-remove-breakpoints-on-line",
+ label: L10N.getStr("breakpointMenuItem.enableAllAtLine.label"),
+ accesskey: L10N.getStr("breakpointMenuItem.enableAllAtLine.accesskey"),
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSelectedSource,
+ location.line,
+ isSelectedSourceOnIgnoreList
+ ),
+ click: () =>
+ dispatch(enableBreakpointsAtLine(location.source, location.line)),
+});
+
+const disableBreakpointsOnLineItem = (location, dispatch) => ({
+ id: "node-menu-remove-breakpoints-on-line",
+ label: L10N.getStr("breakpointMenuItem.disableAllAtLine.label"),
+ accesskey: L10N.getStr("breakpointMenuItem.disableAllAtLine.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(disableBreakpointsAtLine(location.source, location.line)),
+});
diff --git a/devtools/client/debugger/src/actions/context-menus/editor.js b/devtools/client/debugger/src/actions/context-menus/editor.js
new file mode 100644
index 0000000000..1125790a9b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/editor.js
@@ -0,0 +1,436 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+
+import { copyToTheClipboard } from "../../utils/clipboard";
+import {
+ isPretty,
+ getRawSourceURL,
+ getFilename,
+ shouldBlackbox,
+ findBlackBoxRange,
+} from "../../utils/source";
+import { toSourceLine } from "../../utils/editor/index";
+import { downloadFile } from "../../utils/utils";
+import { features } from "../../utils/prefs";
+import { isFulfilled } from "../../utils/async-value";
+
+import { createBreakpointItems } from "./editor-breakpoint";
+
+import {
+ getPrettySource,
+ getIsCurrentThreadPaused,
+ isSourceWithMap,
+ getBlackBoxRanges,
+ isSourceOnSourceMapIgnoreList,
+ isSourceMapIgnoreListEnabled,
+ getEditorWrapping,
+} from "../../selectors/index";
+
+import { continueToHere } from "../../actions/pause/continueToHere";
+import { jumpToMappedLocation } from "../../actions/sources/select";
+import {
+ showSource,
+ toggleInlinePreview,
+ toggleEditorWrapping,
+} from "../../actions/ui";
+import { toggleBlackBox } from "../../actions/sources/blackbox";
+import { addExpression } from "../../actions/expressions";
+import { evaluateInConsole } from "../../actions/toolbox";
+
+export function showEditorContextMenu(event, editor, location) {
+ return async ({ dispatch, getState }) => {
+ const { source } = location;
+ const state = getState();
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const hasMappedLocation =
+ (source.isOriginal ||
+ isSourceWithMap(state, source.id) ||
+ isPretty(source)) &&
+ !getPrettySource(state, source.id);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, source);
+ const editorWrappingEnabled = getEditorWrapping(state);
+
+ showMenu(
+ event,
+ editorMenuItems({
+ blackboxedRanges,
+ hasMappedLocation,
+ location,
+ isPaused,
+ editorWrappingEnabled,
+ selectionText: editor.codeMirror.getSelection().trim(),
+ isTextSelected: editor.codeMirror.somethingSelected(),
+ editor,
+ isSourceOnIgnoreList,
+ dispatch,
+ })
+ );
+ };
+}
+
+export function showEditorGutterContextMenu(event, editor, location, lineText) {
+ return async ({ dispatch, getState }) => {
+ const { source } = location;
+ const state = getState();
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, source);
+
+ showMenu(event, [
+ ...createBreakpointItems(location, lineText, dispatch),
+ { type: "separator" },
+ continueToHereItem(location, isPaused, dispatch),
+ { type: "separator" },
+ blackBoxLineMenuItem(
+ source,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ location.line,
+ dispatch
+ ),
+ ]);
+ };
+}
+
+// Menu Items
+const continueToHereItem = (location, isPaused, dispatch) => ({
+ accesskey: L10N.getStr("editor.continueToHere.accesskey"),
+ disabled: !isPaused,
+ click: () => dispatch(continueToHere(location)),
+ id: "node-menu-continue-to-here",
+ label: L10N.getStr("editor.continueToHere.label"),
+});
+
+const copyToClipboardItem = selectionText => ({
+ id: "node-menu-copy-to-clipboard",
+ label: L10N.getStr("copyToClipboard.label"),
+ accesskey: L10N.getStr("copyToClipboard.accesskey"),
+ disabled: selectionText.length === 0,
+ click: () => copyToTheClipboard(selectionText),
+});
+
+const copySourceItem = selectedContent => ({
+ id: "node-menu-copy-source",
+ label: L10N.getStr("copySource.label"),
+ accesskey: L10N.getStr("copySource.accesskey"),
+ disabled: false,
+ click: () =>
+ selectedContent.type === "text" &&
+ copyToTheClipboard(selectedContent.value),
+});
+
+const copySourceUri2Item = selectedSource => ({
+ id: "node-menu-copy-source-url",
+ label: L10N.getStr("copySourceUri2"),
+ accesskey: L10N.getStr("copySourceUri2.accesskey"),
+ disabled: !selectedSource.url,
+ click: () => copyToTheClipboard(getRawSourceURL(selectedSource.url)),
+});
+
+const jumpToMappedLocationItem = (location, hasMappedLocation, dispatch) => ({
+ id: "node-menu-jump",
+ label: L10N.getFormatStr(
+ "editor.jumpToMappedLocation1",
+ location.source.isOriginal
+ ? L10N.getStr("generated")
+ : L10N.getStr("original")
+ ),
+ accesskey: L10N.getStr("editor.jumpToMappedLocation1.accesskey"),
+ disabled: !hasMappedLocation,
+ click: () => dispatch(jumpToMappedLocation(location)),
+});
+
+const showSourceMenuItem = (selectedSource, dispatch) => ({
+ id: "node-menu-show-source",
+ label: L10N.getStr("sourceTabs.revealInTree"),
+ accesskey: L10N.getStr("sourceTabs.revealInTree.accesskey"),
+ disabled: !selectedSource.url,
+ click: () => dispatch(showSource(selectedSource.id)),
+});
+
+const blackBoxMenuItem = (
+ selectedSource,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ dispatch
+) => {
+ const isBlackBoxed = !!blackboxedRanges[selectedSource.url];
+ return {
+ id: "node-menu-blackbox",
+ label: isBlackBoxed
+ ? L10N.getStr("ignoreContextItem.unignore")
+ : L10N.getStr("ignoreContextItem.ignore"),
+ accesskey: isBlackBoxed
+ ? L10N.getStr("ignoreContextItem.unignore.accesskey")
+ : L10N.getStr("ignoreContextItem.ignore.accesskey"),
+ disabled: isSourceOnIgnoreList || !shouldBlackbox(selectedSource),
+ click: () => dispatch(toggleBlackBox(selectedSource)),
+ };
+};
+
+const blackBoxLineMenuItem = (
+ selectedSource,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ // the clickedLine is passed when the context menu
+ // is opened from the gutter, it is not available when the
+ // the context menu is opened from the editor.
+ clickedLine = null,
+ dispatch
+) => {
+ const { codeMirror } = editor;
+ const from = codeMirror.getCursor("from");
+ const to = codeMirror.getCursor("to");
+
+ const startLine = clickedLine ?? toSourceLine(selectedSource.id, from.line);
+ const endLine = clickedLine ?? toSourceLine(selectedSource.id, to.line);
+
+ const blackboxRange = findBlackBoxRange(selectedSource, blackboxedRanges, {
+ start: startLine,
+ end: endLine,
+ });
+
+ const selectedLineIsBlackBoxed = !!blackboxRange;
+
+ const isSingleLine = selectedLineIsBlackBoxed
+ ? blackboxRange.start.line == blackboxRange.end.line
+ : startLine == endLine;
+
+ const isSourceFullyBlackboxed =
+ blackboxedRanges[selectedSource.url] &&
+ !blackboxedRanges[selectedSource.url].length;
+
+ // The ignore/unignore line context menu item should be disabled when
+ // 1) The source is on the sourcemap ignore list
+ // 2) The whole source is blackboxed or
+ // 3) Multiple lines are blackboxed or
+ // 4) Multiple lines are selected in the editor
+ const shouldDisable =
+ isSourceOnIgnoreList || isSourceFullyBlackboxed || !isSingleLine;
+
+ return {
+ id: "node-menu-blackbox-line",
+ label: !selectedLineIsBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLine")
+ : L10N.getStr("ignoreContextItem.unignoreLine"),
+ accesskey: !selectedLineIsBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLine.accesskey")
+ : L10N.getStr("ignoreContextItem.unignoreLine.accesskey"),
+ disabled: shouldDisable,
+ click: () => {
+ const selectionRange = {
+ start: {
+ line: startLine,
+ column: clickedLine == null ? from.ch : 0,
+ },
+ end: {
+ line: endLine,
+ column: clickedLine == null ? to.ch : 0,
+ },
+ };
+
+ dispatch(
+ toggleBlackBox(
+ selectedSource,
+ !selectedLineIsBlackBoxed,
+ selectedLineIsBlackBoxed ? [blackboxRange] : [selectionRange]
+ )
+ );
+ },
+ };
+};
+
+const blackBoxLinesMenuItem = (
+ selectedSource,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ clickedLine = null,
+ dispatch
+) => {
+ const { codeMirror } = editor;
+ const from = codeMirror.getCursor("from");
+ const to = codeMirror.getCursor("to");
+
+ const startLine = toSourceLine(selectedSource.id, from.line);
+ const endLine = toSourceLine(selectedSource.id, to.line);
+
+ const blackboxRange = findBlackBoxRange(selectedSource, blackboxedRanges, {
+ start: startLine,
+ end: endLine,
+ });
+
+ const selectedLinesAreBlackBoxed = !!blackboxRange;
+
+ return {
+ id: "node-menu-blackbox-lines",
+ label: !selectedLinesAreBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLines")
+ : L10N.getStr("ignoreContextItem.unignoreLines"),
+ accesskey: !selectedLinesAreBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLines.accesskey")
+ : L10N.getStr("ignoreContextItem.unignoreLines.accesskey"),
+ disabled: isSourceOnIgnoreList,
+ click: () => {
+ const selectionRange = {
+ start: {
+ line: startLine,
+ column: from.ch,
+ },
+ end: {
+ line: endLine,
+ column: to.ch,
+ },
+ };
+
+ dispatch(
+ toggleBlackBox(
+ selectedSource,
+ !selectedLinesAreBlackBoxed,
+ selectedLinesAreBlackBoxed ? [blackboxRange] : [selectionRange]
+ )
+ );
+ },
+ };
+};
+
+const watchExpressionItem = (selectedSource, selectionText, dispatch) => ({
+ id: "node-menu-add-watch-expression",
+ label: L10N.getStr("expressions.label"),
+ accesskey: L10N.getStr("expressions.accesskey"),
+ click: () => dispatch(addExpression(selectionText)),
+});
+
+const evaluateInConsoleItem = (selectedSource, selectionText, dispatch) => ({
+ id: "node-menu-evaluate-in-console",
+ label: L10N.getStr("evaluateInConsole.label"),
+ click: () => dispatch(evaluateInConsole(selectionText)),
+});
+
+const downloadFileItem = (selectedSource, selectedContent) => ({
+ id: "node-menu-download-file",
+ label: L10N.getStr("downloadFile.label"),
+ accesskey: L10N.getStr("downloadFile.accesskey"),
+ click: () => downloadFile(selectedContent, getFilename(selectedSource)),
+});
+
+const inlinePreviewItem = dispatch => ({
+ id: "node-menu-inline-preview",
+ label: features.inlinePreview
+ ? L10N.getStr("inlinePreview.hide.label")
+ : L10N.getStr("inlinePreview.show.label"),
+ click: () => dispatch(toggleInlinePreview(!features.inlinePreview)),
+});
+
+const editorWrappingItem = (editorWrappingEnabled, dispatch) => ({
+ id: "node-menu-editor-wrapping",
+ label: editorWrappingEnabled
+ ? L10N.getStr("editorWrapping.hide.label")
+ : L10N.getStr("editorWrapping.show.label"),
+ click: () => dispatch(toggleEditorWrapping(!editorWrappingEnabled)),
+});
+
+function editorMenuItems({
+ blackboxedRanges,
+ location,
+ selectionText,
+ hasMappedLocation,
+ isTextSelected,
+ isPaused,
+ editorWrappingEnabled,
+ editor,
+ isSourceOnIgnoreList,
+ dispatch,
+}) {
+ const items = [];
+
+ const { source } = location;
+
+ const content =
+ source.content && isFulfilled(source.content) ? source.content.value : null;
+
+ items.push(
+ jumpToMappedLocationItem(location, hasMappedLocation, dispatch),
+ continueToHereItem(location, isPaused, dispatch),
+ { type: "separator" },
+ copyToClipboardItem(selectionText),
+ ...(!source.isWasm
+ ? [
+ ...(content ? [copySourceItem(content)] : []),
+ copySourceUri2Item(source),
+ ]
+ : []),
+ ...(content ? [downloadFileItem(source, content)] : []),
+ { type: "separator" },
+ showSourceMenuItem(source, dispatch),
+ { type: "separator" },
+ blackBoxMenuItem(source, blackboxedRanges, isSourceOnIgnoreList, dispatch)
+ );
+
+ const startLine = toSourceLine(
+ source.id,
+ editor.codeMirror.getCursor("from").line
+ );
+ const endLine = toSourceLine(
+ source.id,
+ editor.codeMirror.getCursor("to").line
+ );
+
+ // Find any blackbox ranges that exist for the selected lines
+ const blackboxRange = findBlackBoxRange(source, blackboxedRanges, {
+ start: startLine,
+ end: endLine,
+ });
+
+ const isMultiLineSelection = blackboxRange
+ ? blackboxRange.start.line !== blackboxRange.end.line
+ : startLine !== endLine;
+
+ // When the range is defined and is an empty array,
+ // the whole source is blackboxed
+ const theWholeSourceIsBlackBoxed =
+ blackboxedRanges[source.url] && !blackboxedRanges[source.url].length;
+
+ if (!theWholeSourceIsBlackBoxed) {
+ const blackBoxSourceLinesMenuItem = isMultiLineSelection
+ ? blackBoxLinesMenuItem
+ : blackBoxLineMenuItem;
+
+ items.push(
+ blackBoxSourceLinesMenuItem(
+ source,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ null,
+ dispatch
+ )
+ );
+ }
+
+ if (isTextSelected) {
+ items.push(
+ { type: "separator" },
+ watchExpressionItem(source, selectionText, dispatch),
+ evaluateInConsoleItem(source, selectionText, dispatch)
+ );
+ }
+
+ items.push(
+ { type: "separator" },
+ inlinePreviewItem(dispatch),
+ editorWrappingItem(editorWrappingEnabled, dispatch)
+ );
+
+ return items;
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/frame.js b/devtools/client/debugger/src/actions/context-menus/frame.js
new file mode 100644
index 0000000000..1d287b1028
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/frame.js
@@ -0,0 +1,97 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import {
+ getShouldSelectOriginalLocation,
+ getCurrentThreadFrames,
+ getFrameworkGroupingState,
+} from "../../selectors/index";
+import { toggleFrameworkGrouping } from "../../actions/ui";
+import { restart, toggleBlackBox } from "../../actions/pause/index";
+import { formatCopyName } from "../../utils/pause/frames/index";
+
+function formatMenuElement(labelString, click, disabled = false) {
+ const label = L10N.getStr(labelString);
+ const accesskey = L10N.getStr(`${labelString}.accesskey`);
+ const id = `node-menu-${labelString}`;
+ return {
+ id,
+ label,
+ accesskey,
+ disabled,
+ click,
+ };
+}
+
+function isValidRestartFrame(frame, callbacks) {
+ // Any frame state than 'on-stack' is either dismissed by the server
+ // or can potentially cause unexpected errors.
+ // Global frame has frame.callee equal to null and can't be restarted.
+ return frame.type === "call" && frame.state === "on-stack";
+}
+
+function copyStackTrace() {
+ return async ({ dispatch, getState }) => {
+ const frames = getCurrentThreadFrames(getState());
+ const shouldDisplayOriginalLocation = getShouldSelectOriginalLocation(
+ getState()
+ );
+
+ const framesToCopy = frames
+ .map(frame => formatCopyName(frame, L10N, shouldDisplayOriginalLocation))
+ .join("\n");
+ copyToTheClipboard(framesToCopy);
+ };
+}
+
+export function showFrameContextMenu(event, frame, hideRestart = false) {
+ return async ({ dispatch, getState }) => {
+ const items = [];
+
+ // Hides 'Restart Frame' item for call stack groups context menu,
+ // otherwise can be misleading for the user which frame gets restarted.
+ if (!hideRestart && isValidRestartFrame(frame)) {
+ items.push(
+ formatMenuElement("restartFrame", () => dispatch(restart(frame)))
+ );
+ }
+
+ const toggleFrameWorkL10nLabel = getFrameworkGroupingState(getState())
+ ? "framework.disableGrouping"
+ : "framework.enableGrouping";
+ items.push(
+ formatMenuElement(toggleFrameWorkL10nLabel, () =>
+ dispatch(
+ toggleFrameworkGrouping(!getFrameworkGroupingState(getState()))
+ )
+ )
+ );
+
+ const { source } = frame;
+ if (frame.source) {
+ items.push(
+ formatMenuElement("copySourceUri2", () =>
+ copyToTheClipboard(source.url)
+ )
+ );
+
+ const toggleBlackBoxL10nLabel = source.isBlackBoxed
+ ? "ignoreContextItem.unignore"
+ : "ignoreContextItem.ignore";
+ items.push(
+ formatMenuElement(toggleBlackBoxL10nLabel, () =>
+ dispatch(toggleBlackBox(source))
+ )
+ );
+ }
+
+ items.push(
+ formatMenuElement("copyStackTrace", () => dispatch(copyStackTrace()))
+ );
+
+ showMenu(event, items);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/index.js b/devtools/client/debugger/src/actions/context-menus/index.js
new file mode 100644
index 0000000000..c988d94ccc
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/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/>. */
+
+export * from "./breakpoint";
+export * from "./breakpoint-heading";
+export * from "./frame";
+export * from "./editor";
+export * from "./editor-breakpoint";
+export * from "./outline";
+export * from "./source-tree-item";
+export * from "./tab";
diff --git a/devtools/client/debugger/src/actions/context-menus/moz.build b/devtools/client/debugger/src/actions/context-menus/moz.build
new file mode 100644
index 0000000000..776cb436f9
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/moz.build
@@ -0,0 +1,16 @@
+# 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/.
+
+CompiledModules(
+ "breakpoint.js",
+ "breakpoint-heading.js",
+ "frame.js",
+ "editor.js",
+ "editor-breakpoint.js",
+ "index.js",
+ "outline.js",
+ "source-tree-item.js",
+ "tab.js",
+)
diff --git a/devtools/client/debugger/src/actions/context-menus/outline.js b/devtools/client/debugger/src/actions/context-menus/outline.js
new file mode 100644
index 0000000000..4ba0fe8f6f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/outline.js
@@ -0,0 +1,54 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import { findFunctionText } from "../../utils/function";
+
+import { flashLineRange } from "../../actions/ui";
+
+import {
+ getSelectedSource,
+ getSelectedSourceTextContent,
+} from "../../selectors/index";
+
+export function showOutlineContextMenu(event, func, symbols) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ return;
+ }
+ const selectedSourceTextContent = getSelectedSourceTextContent(state);
+
+ const sourceLine = func.location.start.line;
+ const functionText = findFunctionText(
+ sourceLine,
+ selectedSource,
+ selectedSourceTextContent,
+ symbols
+ );
+
+ const copyFunctionItem = {
+ id: "node-menu-copy-function",
+ label: L10N.getStr("copyFunction.label"),
+ accesskey: L10N.getStr("copyFunction.accesskey"),
+ disabled: !functionText,
+ click: () => {
+ dispatch(
+ flashLineRange({
+ start: sourceLine,
+ end: func.location.end.line,
+ sourceId: selectedSource.id,
+ })
+ );
+ return copyToTheClipboard(functionText);
+ },
+ };
+
+ const items = [copyFunctionItem];
+ showMenu(event, items);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/source-tree-item.js b/devtools/client/debugger/src/actions/context-menus/source-tree-item.js
new file mode 100644
index 0000000000..1b7bc37dc3
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/source-tree-item.js
@@ -0,0 +1,281 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+
+import {
+ isSourceOverridden,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+ getProjectDirectoryRoot,
+ getSourcesTreeSources,
+ getBlackBoxRanges,
+} from "../../selectors/index";
+
+import { setOverrideSource, removeOverrideSource } from "../sources/index";
+import { loadSourceText } from "../sources/loadSourceText";
+import { toggleBlackBox, blackBoxSources } from "../sources/blackbox";
+import {
+ setProjectDirectoryRoot,
+ clearProjectDirectoryRoot,
+} from "../sources-tree";
+
+import { shouldBlackbox } from "../../utils/source";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import { saveAsLocalFile } from "../../utils/utils";
+
+/**
+ * Show the context menu of SourceTreeItem.
+ *
+ * @param {object} event
+ * The context-menu DOM event.
+ * @param {object} item
+ * Source Tree Item object.
+ */
+export function showSourceTreeItemContextMenu(
+ event,
+ item,
+ depth,
+ setExpanded,
+ itemName
+) {
+ return async ({ dispatch, getState }) => {
+ const copySourceUri2Label = L10N.getStr("copySourceUri2");
+ const copySourceUri2Key = L10N.getStr("copySourceUri2.accesskey");
+ const setDirectoryRootLabel = L10N.getStr("setDirectoryRoot.label");
+ const setDirectoryRootKey = L10N.getStr("setDirectoryRoot.accesskey");
+ const removeDirectoryRootLabel = L10N.getStr("removeDirectoryRoot.label");
+
+ const menuOptions = [];
+
+ const state = getState();
+ const isOverridden = isSourceOverridden(state, item.source);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, item.source);
+ const projectRoot = getProjectDirectoryRoot(state);
+
+ if (item.type == "source") {
+ const { source } = item;
+ const copySourceUri2 = {
+ id: "node-menu-copy-source",
+ label: copySourceUri2Label,
+ accesskey: copySourceUri2Key,
+ disabled: false,
+ click: () => copyToTheClipboard(source.url),
+ };
+
+ const ignoreStr = item.isBlackBoxed ? "unignore" : "ignore";
+ const blackBoxMenuItem = {
+ id: "node-menu-blackbox",
+ label: L10N.getStr(`ignoreContextItem.${ignoreStr}`),
+ accesskey: L10N.getStr(`ignoreContextItem.${ignoreStr}.accesskey`),
+ disabled: isSourceOnIgnoreList || !shouldBlackbox(source),
+ click: () => dispatch(toggleBlackBox(source)),
+ };
+ const downloadFileItem = {
+ id: "node-menu-download-file",
+ label: L10N.getStr("downloadFile.label"),
+ accesskey: L10N.getStr("downloadFile.accesskey"),
+ disabled: false,
+ click: () => saveLocalFile(dispatch, source),
+ };
+
+ const overrideStr = !isOverridden ? "override" : "removeOverride";
+ const overridesItem = {
+ id: "node-menu-overrides",
+ label: L10N.getStr(`overridesContextItem.${overrideStr}`),
+ accesskey: L10N.getStr(`overridesContextItem.${overrideStr}.accesskey`),
+ disabled: !!source.isHTML,
+ click: () => handleLocalOverride(dispatch, source, isOverridden),
+ };
+
+ menuOptions.push(
+ copySourceUri2,
+ blackBoxMenuItem,
+ downloadFileItem,
+ overridesItem
+ );
+ }
+
+ // All other types other than source are folder-like
+ if (item.type != "source") {
+ addCollapseExpandAllOptions(menuOptions, item, setExpanded);
+
+ if (projectRoot == item.uniquePath) {
+ menuOptions.push({
+ id: "node-remove-directory-root",
+ label: removeDirectoryRootLabel,
+ disabled: false,
+ click: () => dispatch(clearProjectDirectoryRoot()),
+ });
+ } else {
+ menuOptions.push({
+ id: "node-set-directory-root",
+ label: setDirectoryRootLabel,
+ accesskey: setDirectoryRootKey,
+ disabled: false,
+ click: () =>
+ dispatch(setProjectDirectoryRoot(item.uniquePath, itemName)),
+ });
+ }
+
+ addBlackboxAllOption(dispatch, state, menuOptions, item, depth);
+ }
+
+ showMenu(event, menuOptions);
+ };
+}
+
+async function saveLocalFile(dispatch, source) {
+ if (!source) {
+ return null;
+ }
+
+ const data = await dispatch(loadSourceText(source));
+ if (!data) {
+ return null;
+ }
+ return saveAsLocalFile(data.value, source.displayURL.filename);
+}
+
+async function handleLocalOverride(dispatch, source, isOverridden) {
+ if (!isOverridden) {
+ const localPath = await saveLocalFile(dispatch, source);
+ if (localPath) {
+ dispatch(setOverrideSource(source, localPath));
+ }
+ } else {
+ dispatch(removeOverrideSource(source));
+ }
+}
+
+function addBlackboxAllOption(dispatch, state, menuOptions, item, depth) {
+ const {
+ sourcesInside,
+ sourcesOutside,
+ allInsideBlackBoxed,
+ allOutsideBlackBoxed,
+ } = getBlackBoxSourcesGroups(state, item);
+ const projectRoot = getProjectDirectoryRoot(state);
+
+ let blackBoxInsideMenuItemLabel;
+ let blackBoxOutsideMenuItemLabel;
+ if (depth === 0 || (depth === 1 && projectRoot === "")) {
+ blackBoxInsideMenuItemLabel = allInsideBlackBoxed
+ ? L10N.getStr("unignoreAllInGroup.label")
+ : L10N.getStr("ignoreAllInGroup.label");
+ if (sourcesOutside.length) {
+ blackBoxOutsideMenuItemLabel = allOutsideBlackBoxed
+ ? L10N.getStr("unignoreAllOutsideGroup.label")
+ : L10N.getStr("ignoreAllOutsideGroup.label");
+ }
+ } else {
+ blackBoxInsideMenuItemLabel = allInsideBlackBoxed
+ ? L10N.getStr("unignoreAllInDir.label")
+ : L10N.getStr("ignoreAllInDir.label");
+ if (sourcesOutside.length) {
+ blackBoxOutsideMenuItemLabel = allOutsideBlackBoxed
+ ? L10N.getStr("unignoreAllOutsideDir.label")
+ : L10N.getStr("ignoreAllOutsideDir.label");
+ }
+ }
+
+ const blackBoxInsideMenuItem = {
+ id: allInsideBlackBoxed
+ ? "node-unblackbox-all-inside"
+ : "node-blackbox-all-inside",
+ label: blackBoxInsideMenuItemLabel,
+ disabled: false,
+ click: () => dispatch(blackBoxSources(sourcesInside, !allInsideBlackBoxed)),
+ };
+
+ if (sourcesOutside.length) {
+ menuOptions.push({
+ id: "node-blackbox-all",
+ label: L10N.getStr("ignoreAll.label"),
+ submenu: [
+ blackBoxInsideMenuItem,
+ {
+ id: allOutsideBlackBoxed
+ ? "node-unblackbox-all-outside"
+ : "node-blackbox-all-outside",
+ label: blackBoxOutsideMenuItemLabel,
+ disabled: false,
+ click: () =>
+ dispatch(blackBoxSources(sourcesOutside, !allOutsideBlackBoxed)),
+ },
+ ],
+ });
+ } else {
+ menuOptions.push(blackBoxInsideMenuItem);
+ }
+}
+
+function addCollapseExpandAllOptions(menuOptions, item, setExpanded) {
+ menuOptions.push({
+ id: "node-menu-collapse-all",
+ label: L10N.getStr("collapseAll.label"),
+ disabled: false,
+ click: () => setExpanded(item, false, true),
+ });
+
+ menuOptions.push({
+ id: "node-menu-expand-all",
+ label: L10N.getStr("expandAll.label"),
+ disabled: false,
+ click: () => setExpanded(item, true, true),
+ });
+}
+
+/**
+ * Computes 4 lists:
+ * - `sourcesInside`: the list of all Source Items that are
+ * children of the current item (can be thread/group/directory).
+ * This include any nested level of children.
+ * - `sourcesOutside`: all other Source Items.
+ * i.e. all sources that are in any other folder of any group/thread.
+ * - `allInsideBlackBoxed`, all sources of `sourcesInside` which are currently
+ * blackboxed.
+ * - `allOutsideBlackBoxed`, all sources of `sourcesOutside` which are currently
+ * blackboxed.
+ */
+function getBlackBoxSourcesGroups(state, item) {
+ const allSources = [];
+ function collectAllSources(list, _item) {
+ if (_item.children) {
+ _item.children.forEach(i => collectAllSources(list, i));
+ }
+ if (_item.type == "source") {
+ list.push(_item.source);
+ }
+ }
+
+ const rootItems = getSourcesTreeSources(state);
+ const blackBoxRanges = getBlackBoxRanges(state);
+
+ for (const rootItem of rootItems) {
+ collectAllSources(allSources, rootItem);
+ }
+
+ const sourcesInside = [];
+ collectAllSources(sourcesInside, item);
+
+ const sourcesOutside = allSources.filter(
+ source => !sourcesInside.includes(source)
+ );
+ const allInsideBlackBoxed = sourcesInside.every(
+ source => blackBoxRanges[source.url]
+ );
+ const allOutsideBlackBoxed = sourcesOutside.every(
+ source => blackBoxRanges[source.url]
+ );
+
+ return {
+ sourcesInside,
+ sourcesOutside,
+ allInsideBlackBoxed,
+ allOutsideBlackBoxed,
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/tab.js b/devtools/client/debugger/src/actions/context-menus/tab.js
new file mode 100644
index 0000000000..193396a746
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/tab.js
@@ -0,0 +1,128 @@
+/* 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 { showMenu, buildMenu } from "../../context-menu/menu";
+import { getTabMenuItems } from "../../utils/tabs";
+
+import {
+ getSelectedLocation,
+ getSourcesForTabs,
+ isSourceBlackBoxed,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors/index";
+
+import { toggleBlackBox } from "../sources/blackbox";
+import { prettyPrintAndSelectSource } from "../sources/prettyPrint";
+import { copyToClipboard, showSource } from "../ui";
+import { closeTab, closeTabs } from "../tabs";
+
+import { getRawSourceURL, isPretty, shouldBlackbox } from "../../utils/source";
+import { copyToTheClipboard } from "../../utils/clipboard";
+
+/**
+ * Show the context menu of Tab.
+ *
+ * @param {object} event
+ * The context-menu DOM event.
+ * @param {object} source
+ * Source object of the related Tab.
+ */
+export function showTabContextMenu(event, source) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const selectedLocation = getSelectedLocation(state);
+
+ const isBlackBoxed = isSourceBlackBoxed(state, source);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, source);
+ const tabsSources = getSourcesForTabs(state);
+
+ const otherTabsSources = tabsSources.filter(s => s !== source);
+ const tabIndex = tabsSources.findIndex(s => s === source);
+ const followingTabsSources = tabsSources.slice(tabIndex + 1);
+
+ const tabMenuItems = getTabMenuItems();
+ const items = [
+ {
+ item: {
+ ...tabMenuItems.closeTab,
+ click: () => dispatch(closeTab(source)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.closeOtherTabs,
+ disabled: otherTabsSources.length === 0,
+ click: () => dispatch(closeTabs(otherTabsSources)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.closeTabsToEnd,
+ disabled: followingTabsSources.length === 0,
+ click: () => {
+ dispatch(closeTabs(followingTabsSources));
+ },
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.closeAllTabs,
+ click: () => dispatch(closeTabs(tabsSources)),
+ },
+ },
+ { item: { type: "separator" } },
+ {
+ item: {
+ ...tabMenuItems.copySource,
+ // Only enable when this is the selected source as this requires the source to be loaded,
+ // which may not be the case if the tab wasn't ever selected.
+ //
+ // Note that when opening the debugger, you may have tabs opened from a previous session,
+ // but no selected location.
+ disabled: selectedLocation?.source.id !== source.id,
+ click: () => {
+ dispatch(copyToClipboard(selectedLocation));
+ },
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.copySourceUri2,
+ disabled: !source.url,
+ click: () => copyToTheClipboard(getRawSourceURL(source.url)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.showSource,
+ // Source Tree only shows sources with URL
+ disabled: !source.url,
+ click: () => dispatch(showSource(source.id)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.toggleBlackBox,
+ label: isBlackBoxed
+ ? L10N.getStr("ignoreContextItem.unignore")
+ : L10N.getStr("ignoreContextItem.ignore"),
+ disabled: isSourceOnIgnoreList || !shouldBlackbox(source),
+ click: () => dispatch(toggleBlackBox(source)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.prettyPrint,
+ disabled: isPretty(source),
+ click: () => dispatch(prettyPrintAndSelectSource(source)),
+ },
+ },
+ ];
+
+ showMenu(event, buildMenu(items));
+ };
+}