/* 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 . */ import React, { PureComponent } from "devtools/client/shared/vendor/react"; import { div, button, span, hr, } from "devtools/client/shared/vendor/react-dom-factories"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; import { connect } from "devtools/client/shared/vendor/react-redux"; import actions from "../../actions/index"; import { getSelectedSource, getSelectedLocation, getSelectedSourceTextContent, getPrettySource, getPaneCollapse, isSourceBlackBoxed, canPrettyPrintSource, getPrettyPrintMessage, isSourceOnSourceMapIgnoreList, isSourceMapIgnoreListEnabled, getSelectedMappedSource, getSourceMapErrorForSourceActor, areSourceMapsEnabled, getShouldSelectOriginalLocation, isSourceActorWithSourceMap, getSourceMapResolvedURL, isSelectedMappedSourceLoading, } from "../../selectors/index"; import { shouldBlackbox } from "../../utils/source"; import { PaneToggleButton } from "../shared/Button/index"; import DebuggerImage from "../shared/DebuggerImage"; const classnames = require("resource://devtools/client/shared/classnames.js"); const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js"); const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js"); const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js"); class SourceFooter extends PureComponent { static get propTypes() { return { canPrettyPrint: PropTypes.bool.isRequired, prettyPrintMessage: PropTypes.string, endPanelCollapsed: PropTypes.bool.isRequired, horizontal: PropTypes.bool.isRequired, jumpToMappedLocation: PropTypes.func.isRequired, mappedSource: PropTypes.object, selectedSource: PropTypes.object, selectedLocation: PropTypes.object, isSelectedSourceBlackBoxed: PropTypes.bool, sourceLoaded: PropTypes.bool.isRequired, toggleBlackBox: PropTypes.func.isRequired, togglePaneCollapse: PropTypes.func.isRequired, prettyPrintAndSelectSource: PropTypes.func.isRequired, isSourceOnIgnoreList: PropTypes.bool.isRequired, }; } prettyPrintButton() { const { selectedSource, canPrettyPrint, prettyPrintMessage, prettyPrintAndSelectSource, removePrettyPrintedSource, sourceLoaded, } = this.props; if (!selectedSource) { return null; } if (!sourceLoaded && selectedSource.isPrettyPrinted) { return div( { className: "action", key: "pretty-loader", }, React.createElement(DebuggerImage, { name: "loader", className: "spin", }) ); } const type = "prettyPrint"; return button( { onClick: () => { if (selectedSource.isPrettyPrinted) { removePrettyPrintedSource(selectedSource); return; } if (!canPrettyPrint) { return; } prettyPrintAndSelectSource(selectedSource); }, className: classnames("action", type, { pretty: selectedSource.isPrettyPrinted, }), key: type, title: prettyPrintMessage, "aria-label": prettyPrintMessage, disabled: !canPrettyPrint && !selectedSource.isPrettyPrinted, }, React.createElement(DebuggerImage, { name: type, }) ); } blackBoxButton() { const { selectedSource, isSelectedSourceBlackBoxed, toggleBlackBox, sourceLoaded, isSourceOnIgnoreList, } = this.props; if (!selectedSource || !shouldBlackbox(selectedSource)) { return null; } let tooltip = isSelectedSourceBlackBoxed ? L10N.getStr("sourceFooter.unignore") : L10N.getStr("sourceFooter.ignore"); if (isSourceOnIgnoreList) { tooltip = L10N.getStr("sourceFooter.ignoreList"); } const type = "black-box"; return button( { onClick: () => toggleBlackBox(selectedSource), className: classnames("action", type, { active: sourceLoaded, blackboxed: isSelectedSourceBlackBoxed || isSourceOnIgnoreList, }), key: type, title: tooltip, "aria-label": tooltip, disabled: isSourceOnIgnoreList, }, React.createElement(DebuggerImage, { name: "blackBox", }) ); } renderToggleButton() { if (this.props.horizontal) { return null; } return React.createElement(PaneToggleButton, { key: "toggle", collapsed: this.props.endPanelCollapsed, horizontal: this.props.horizontal, handleClick: this.props.togglePaneCollapse, position: "end", }); } renderCommands() { const commands = [ this.blackBoxButton(), this.prettyPrintButton(), this.renderSourceMapButton(), ].filter(Boolean); return commands.length ? div( { className: "commands", }, commands ) : null; } renderMappedSource() { const { mappedSource, jumpToMappedLocation, selectedLocation } = this.props; if (!mappedSource) { return null; } const tooltip = L10N.getFormatStr( mappedSource.isOriginal ? "sourceFooter.mappedGeneratedSource.tooltip" : "sourceFooter.mappedOriginalSource.tooltip", mappedSource.url ); const label = L10N.getFormatStr( mappedSource.isOriginal ? "sourceFooter.mappedOriginalSource.title" : "sourceFooter.mappedGeneratedSource.title", mappedSource.shortName ); return button( { className: "mapped-source", onClick: () => jumpToMappedLocation(selectedLocation), title: tooltip, }, span(null, label) ); } renderCursorPosition() { // When we open a new source, there is no particular location selected and the line will be set to zero or falsy if (!this.props.selectedLocation || !this.props.selectedLocation.line) { return null; } // Note that line is 1-based while column is 0-based. const { line, column } = this.props.selectedLocation; const text = L10N.getFormatStr( "sourceFooter.currentCursorPosition", line, column + 1 ); const title = L10N.getFormatStr( "sourceFooter.currentCursorPosition.tooltip", line, column + 1 ); return div( { className: "cursor-position", title, }, text ); } getSourceMapLabel() { if (!this.props.selectedLocation) { return undefined; } if (!this.props.areSourceMapsEnabled) { return L10N.getStr("sourceFooter.sourceMapButton.disabled"); } if (this.props.sourceMapError) { return undefined; } if (!this.props.isSourceActorWithSourceMap) { return L10N.getStr("sourceFooter.sourceMapButton.sourceNotMapped"); } if ( this.props.selectedLocation.source.isOriginal && !this.props.selectedLocation.source.isPrettyPrinted ) { return L10N.getStr("sourceFooter.sourceMapButton.isOriginalSource"); } return L10N.getStr("sourceFooter.sourceMapButton.isBundleSource"); } getSourceMapTitle() { if (this.props.sourceMapError) { return L10N.getFormatStr( "sourceFooter.sourceMapButton.errorTitle", this.props.sourceMapError ); } if (this.props.isSourceMapLoading) { return L10N.getStr("sourceFooter.sourceMapButton.loadingTitle"); } return L10N.getStr("sourceFooter.sourceMapButton.title"); } renderSourceMapButton() { const { toolboxDoc } = this.context; const selectedSource = this.props.selectedLocation?.source; return React.createElement( MenuButton, { menuId: "debugger-source-map-button", key: "debugger-source-map-button", toolboxDoc, className: classnames("devtools-button", "debugger-source-map-button", { error: !!this.props.sourceMapError, loading: this.props.isSourceMapLoading, disabled: !this.props.areSourceMapsEnabled, "not-mapped": (!selectedSource?.isOriginal || selectedSource?.isPrettyPrinted) && !this.props.isSourceActorWithSourceMap, original: selectedSource?.isOriginal && !selectedSource.isPrettyPrinted, }), title: this.getSourceMapTitle(), label: this.getSourceMapLabel(), icon: true, }, () => this.renderSourceMapMenuItems() ); } renderSourceMapMenuItems() { const items = [ React.createElement(MenuItem, { className: "menu-item debugger-source-map-enabled", checked: this.props.areSourceMapsEnabled, label: L10N.getStr("sourceFooter.sourceMapButton.enable"), onClick: this.toggleSourceMaps, }), hr(), React.createElement(MenuItem, { className: "menu-item debugger-source-map-open-original", checked: this.props.shouldSelectOriginalLocation, label: L10N.getStr( "sourceFooter.sourceMapButton.showOriginalSourceByDefault" ), onClick: this.toggleSelectOriginalByDefault, }), ]; if (this.props.mappedSource) { items.push( React.createElement(MenuItem, { className: "menu-item debugger-jump-mapped-source", label: this.props.mappedSource.isOriginal ? L10N.getStr("sourceFooter.sourceMapButton.jumpToOriginalSource") : L10N.getStr("sourceFooter.sourceMapButton.jumpToGeneratedSource"), tooltip: this.props.mappedSource.url, onClick: () => this.props.jumpToMappedLocation(this.props.selectedLocation), }) ); } if (this.props.resolvedSourceMapURL) { items.push( React.createElement(MenuItem, { className: "menu-item debugger-source-map-link", label: L10N.getStr( "sourceFooter.sourceMapButton.openSourceMapInNewTab" ), onClick: this.openSourceMap, }) ); } return React.createElement( MenuList, { id: "debugger-source-map-list", }, items ); } openSourceMap = () => { let line, column; if ( this.props.sourceMapError && this.props.sourceMapError.includes("JSON.parse") ) { const match = this.props.sourceMapError.match( /at line (\d+) column (\d+)/ ); if (match) { line = match[1]; column = match[2]; } } this.props.openSourceMap( this.props.resolvedSourceMapURL || this.props.selectedLocation.source.url, line, column ); }; toggleSourceMaps = () => { this.props.toggleSourceMapsEnabled(!this.props.areSourceMapsEnabled); }; toggleSelectOriginalByDefault = () => { this.props.setDefaultSelectedLocation( !this.props.shouldSelectOriginalLocation ); this.props.jumpToMappedSelectedLocation(); }; render() { return div( { className: "source-footer", }, div( { className: "source-footer-start", }, this.renderCommands() ), div( { className: "source-footer-end", }, this.renderMappedSource(), this.renderCursorPosition(), this.renderToggleButton() ) ); } } SourceFooter.contextTypes = { toolboxDoc: PropTypes.object, }; const mapStateToProps = state => { const selectedSource = getSelectedSource(state); const selectedLocation = getSelectedLocation(state); const sourceTextContent = getSelectedSourceTextContent(state); const areSourceMapsEnabledProp = areSourceMapsEnabled(state); const isSourceActorWithSourceMapProp = selectedLocation?.sourceActor ? isSourceActorWithSourceMap(state, selectedLocation?.sourceActor.id) : false; const sourceMapError = selectedLocation?.sourceActor ? getSourceMapErrorForSourceActor(state, selectedLocation.sourceActor.id) : null; const mappedSource = getSelectedMappedSource(state); const isSourceMapLoading = areSourceMapsEnabledProp && isSourceActorWithSourceMapProp && // `mappedSource` will be null while loading, we need another way to know when it is done computing !mappedSource && isSelectedMappedSourceLoading(state) && !sourceMapError && !selectedSource?.isPrettyPrinted; return { selectedSource, selectedLocation, isSelectedSourceBlackBoxed: selectedSource ? isSourceBlackBoxed(state, selectedSource) : null, isSourceOnIgnoreList: isSourceMapIgnoreListEnabled(state) && isSourceOnSourceMapIgnoreList(state, selectedSource), sourceLoaded: !!sourceTextContent, mappedSource, isSourceMapLoading, prettySource: getPrettySource( state, selectedSource ? selectedSource.id : null ), endPanelCollapsed: getPaneCollapse(state, "end"), canPrettyPrint: selectedLocation ? canPrettyPrintSource( state, selectedSource, selectedLocation.sourceActor ) : false, prettyPrintMessage: selectedLocation ? getPrettyPrintMessage(state, selectedLocation) : null, sourceMapError, resolvedSourceMapURL: selectedLocation?.sourceActor ? getSourceMapResolvedURL(state, selectedLocation.sourceActor.id) : null, isSourceActorWithSourceMap: isSourceActorWithSourceMapProp, areSourceMapsEnabled: areSourceMapsEnabledProp, shouldSelectOriginalLocation: getShouldSelectOriginalLocation(state), }; }; export default connect(mapStateToProps, { removePrettyPrintedSource: actions.removePrettyPrintedSource, prettyPrintAndSelectSource: actions.prettyPrintAndSelectSource, toggleBlackBox: actions.toggleBlackBox, jumpToMappedLocation: actions.jumpToMappedLocation, togglePaneCollapse: actions.togglePaneCollapse, toggleSourceMapsEnabled: actions.toggleSourceMapsEnabled, setDefaultSelectedLocation: actions.setDefaultSelectedLocation, jumpToMappedSelectedLocation: actions.jumpToMappedSelectedLocation, openSourceMap: actions.openSourceMap, })(SourceFooter);