diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/shared/components/Frame.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/components/Frame.js')
-rw-r--r-- | devtools/client/shared/components/Frame.js | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/devtools/client/shared/components/Frame.js b/devtools/client/shared/components/Frame.js new file mode 100644 index 0000000000..4efc7d3bd6 --- /dev/null +++ b/devtools/client/shared/components/Frame.js @@ -0,0 +1,401 @@ +/* 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/. */ + +"use strict"; + +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getUnicodeUrl, + getUnicodeUrlPath, + getUnicodeHostname, +} = require("resource://devtools/client/shared/unicode-url.js"); +const { + getSourceNames, + parseURL, + getSourceMappedFile, +} = require("resource://devtools/client/shared/source-utils.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const { + MESSAGE_SOURCE, +} = require("resource://devtools/client/webconsole/constants.js"); + +const l10n = new LocalizationHelper( + "devtools/client/locales/components.properties" +); +const webl10n = new LocalizationHelper( + "devtools/client/locales/webconsole.properties" +); + +function savedFrameToLocation(frame) { + const { source: url, line, column, sourceId } = frame; + return { + url, + line, + column, + // The sourceId will be a string if it's a source actor ID, otherwise + // it is either a Spidermonkey-internal ID from a SavedFrame or missing, + // and in either case we can't use the ID for anything useful. + id: typeof sourceId === "string" ? sourceId : null, + }; +} + +/** + * Get the tooltip message. + * @param {string|undefined} messageSource + * @param {string} url + * @returns {string} + */ +function getTooltipMessage(messageSource, url) { + if (messageSource && messageSource === MESSAGE_SOURCE.CSS) { + return l10n.getFormatStr("frame.viewsourceinstyleeditor", url); + } + return l10n.getFormatStr("frame.viewsourceindebugger", url); +} + +class Frame extends Component { + static get propTypes() { + return { + // Optional className that will be put into the element. + className: PropTypes.string, + // SavedFrame, or an object containing all the required properties. + frame: PropTypes.shape({ + functionDisplayName: PropTypes.string, + // This could be a SavedFrame with a numeric sourceId, or it could + // be a SavedFrame-like client-side object, in which case the + // "sourceId" will be a source actor ID. + sourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + source: PropTypes.string.isRequired, + line: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + column: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }).isRequired, + // Clicking on the frame link -- probably should link to the debugger. + onClick: PropTypes.func, + // Option to display a function name before the source link. + showFunctionName: PropTypes.bool, + // Option to display a function name even if it's anonymous. + showAnonymousFunctionName: PropTypes.bool, + // Option to display a host name after the source link. + showHost: PropTypes.bool, + // Option to display a host name if the filename is empty or just '/' + showEmptyPathAsHost: PropTypes.bool, + // Option to display a full source instead of just the filename. + showFullSourceUrl: PropTypes.bool, + // Service to enable the source map feature for console. + sourceMapURLService: PropTypes.object, + // The source of the message + messageSource: PropTypes.string, + }; + } + + static get defaultProps() { + return { + showFunctionName: false, + showAnonymousFunctionName: false, + showHost: false, + showEmptyPathAsHost: false, + showFullSourceUrl: false, + }; + } + + constructor(props) { + super(props); + this.state = { + originalLocation: null, + }; + this._locationChanged = this._locationChanged.bind(this); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillMount() { + if (this.props.sourceMapURLService) { + const location = savedFrameToLocation(this.props.frame); + // Many things that make use of this component either: + // a) Pass in no sourceId because they have no way to know. + // b) Pass in no sourceId because the actor wasn't created when the + // server sent its response. + // + // and due to that, we need to use subscribeByLocation in order to + // handle both cases with an without an ID. + this.unsubscribeSourceMapURLService = + this.props.sourceMapURLService.subscribeByLocation( + location, + this._locationChanged + ); + } + } + + componentWillUnmount() { + if (this.unsubscribeSourceMapURLService) { + this.unsubscribeSourceMapURLService(); + } + } + + _locationChanged(originalLocation) { + this.setState({ originalLocation }); + } + + /** + * Get current location's source, line, and column. + * @returns {{source: string, line: number|null, column: number|null}} + */ + #getCurrentLocationInfo = () => { + const { frame } = this.props; + const { originalLocation } = this.state; + + const generatedLocation = savedFrameToLocation(frame); + const currentLocation = originalLocation || generatedLocation; + + const source = currentLocation.url || ""; + const line = + currentLocation.line != void 0 ? Number(currentLocation.line) : null; + const column = + currentLocation.column != void 0 ? Number(currentLocation.column) : null; + return { + source, + line, + column, + }; + }; + + /** + * Get unicode hostname of the source link. + * @returns {string} + */ + #getCurrentLocationUnicodeHostName = () => { + const { source } = this.#getCurrentLocationInfo(); + + const { host } = getSourceNames(source); + return host ? getUnicodeHostname(host) : ""; + }; + + /** + * Check if the current location is linkable. + * @returns {boolean} + */ + #isCurrentLocationLinkable = () => { + const { frame } = this.props; + const { originalLocation } = this.state; + + const generatedLocation = savedFrameToLocation(frame); + + // Reparse the URL to determine if we should link this; `getSourceNames` + // has already cached this indirectly. We don't want to attempt to + // link to "self-hosted" and "(unknown)". + // Source mapped sources might not necessary linkable, but they + // are still valid in the debugger. + // If we have a source ID then we can show the source in the debugger. + return !!( + originalLocation || + generatedLocation.id || + !!parseURL(generatedLocation.url) + ); + }; + + /** + * Get the props of the top element. + */ + #getTopElementProps = () => { + const { className } = this.props; + + const { source, line, column } = this.#getCurrentLocationInfo(); + const { long } = getSourceNames(source); + const props = { + "data-url": long, + className: "frame-link" + (className ? ` ${className}` : ""), + }; + + // If we have a line number > 0. + if (line) { + // Add `data-line` attribute for testing + props["data-line"] = line; + + // Intentionally exclude 0 + if (column) { + // Add `data-column` attribute for testing + props["data-column"] = column; + } + } + return props; + }; + + /** + * Get the props of the source element. + */ + #getSourceElementsProps = () => { + const { frame, onClick, messageSource } = this.props; + + const generatedLocation = savedFrameToLocation(frame); + const { source, line, column } = this.#getCurrentLocationInfo(); + const { long } = getSourceNames(source); + let url = getUnicodeUrl(long); + + // Exclude all falsy values, including `0`, as line numbers start with 1. + if (line) { + url += `:${line}`; + // Intentionally exclude 0 + if (column) { + url += `:${column}`; + } + } + + const isLinkable = this.#isCurrentLocationLinkable(); + + // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL + // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056. + const tooltipMessage = getTooltipMessage(messageSource, url); + + const sourceElConfig = { + key: "source", + className: "frame-link-source", + title: isLinkable ? tooltipMessage : url, + }; + + if (isLinkable) { + return { + ...sourceElConfig, + onClick: e => { + e.preventDefault(); + e.stopPropagation(); + + onClick(generatedLocation); + }, + href: source, + draggable: false, + }; + } + + return sourceElConfig; + }; + + /** + * Render the source elements. + * @returns {React.ReactNode} + */ + #renderSourceElements = () => { + const { line, column } = this.#getCurrentLocationInfo(); + + const sourceElements = [this.#renderDisplaySource()]; + + if (line) { + let lineInfo = `:${line}`; + + // Intentionally exclude 0 + if (column) { + lineInfo += `:${column}`; + } + + sourceElements.push( + dom.span( + { + key: "line", + className: "frame-link-line", + }, + lineInfo + ) + ); + } + + if (this.#isCurrentLocationLinkable()) { + return dom.a(this.#getSourceElementsProps(), sourceElements); + } + // If source is not a URL (self-hosted, eval, etc.), don't make + // it an anchor link, as we can't link to it. + return dom.span(this.#getSourceElementsProps(), sourceElements); + }; + + /** + * Render the display source. + * @returns {React.ReactNode} + */ + #renderDisplaySource = () => { + const { showEmptyPathAsHost, showFullSourceUrl } = this.props; + const { originalLocation } = this.state; + + const { source } = this.#getCurrentLocationInfo(); + const { short, long, host } = getSourceNames(source); + const unicodeShort = getUnicodeUrlPath(short); + const unicodeLong = getUnicodeUrl(long); + let displaySource = showFullSourceUrl ? unicodeLong : unicodeShort; + if (originalLocation) { + displaySource = getSourceMappedFile(displaySource); + + // In case of pretty-printed HTML file, we would only get the formatted suffix; replace + // it with the full URL instead + if (showEmptyPathAsHost && displaySource == ":formatted") { + displaySource = host + displaySource; + } + } else if ( + showEmptyPathAsHost && + (displaySource === "" || displaySource === "/") + ) { + displaySource = host; + } + + return dom.span( + { + key: "filename", + className: "frame-link-filename", + }, + displaySource + ); + }; + + /** + * Render the function display name. + * @returns {React.ReactNode} + */ + #renderFunctionDisplayName = () => { + const { frame, showFunctionName, showAnonymousFunctionName } = this.props; + if (!showFunctionName) { + return null; + } + const functionDisplayName = frame.functionDisplayName; + if (functionDisplayName || showAnonymousFunctionName) { + return [ + dom.span( + { + key: "function-display-name", + className: "frame-link-function-display-name", + }, + functionDisplayName || webl10n.getStr("stacktrace.anonymousFunction") + ), + " ", + ]; + } + return null; + }; + + render() { + const { showHost } = this.props; + + const elements = [ + this.#renderFunctionDisplayName(), + this.#renderSourceElements(), + ]; + + const unicodeHost = showHost + ? this.#getCurrentLocationUnicodeHostName() + : null; + if (unicodeHost) { + elements.push(" "); + elements.push( + dom.span( + { + key: "host", + className: "frame-link-host", + }, + unicodeHost + ) + ); + } + + return dom.span(this.#getTopElementProps(), ...elements); + } +} + +module.exports = Frame; |