diff options
Diffstat (limited to 'devtools/client/debugger/src/actions/context-menus/editor.js')
-rw-r--r-- | devtools/client/debugger/src/actions/context-menus/editor.js | 436 |
1 files changed, 436 insertions, 0 deletions
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; +} |