diff options
Diffstat (limited to 'devtools/client/debugger/src/actions/context-menus')
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)); + }; +} |