forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D50583 --HG-- extra : moz-landing-system : lando
312 lines
8.7 KiB
JavaScript
312 lines
8.7 KiB
JavaScript
/* 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("devtools/client/shared/vendor/react");
|
|
const dom = require("devtools/client/shared/vendor/react-dom-factories");
|
|
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
|
const {
|
|
getUnicodeUrl,
|
|
getUnicodeUrlPath,
|
|
getUnicodeHostname,
|
|
} = require("devtools/client/shared/unicode-url");
|
|
const {
|
|
getSourceNames,
|
|
parseURL,
|
|
getSourceMappedFile,
|
|
} = require("devtools/client/shared/source-utils");
|
|
const { LocalizationHelper } = require("devtools/shared/l10n");
|
|
const { MESSAGE_SOURCE } = require("devtools/client/webconsole/constants");
|
|
|
|
const l10n = new LocalizationHelper(
|
|
"devtools/client/locales/components.properties"
|
|
);
|
|
const webl10n = new LocalizationHelper(
|
|
"devtools/client/locales/webconsole.properties"
|
|
);
|
|
|
|
class Frame extends Component {
|
|
static get propTypes() {
|
|
return {
|
|
// SavedFrame, or an object containing all the required properties.
|
|
frame: PropTypes.shape({
|
|
functionDisplayName: PropTypes.string,
|
|
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.
|
|
sourceMapService: 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._locationChanged = this._locationChanged.bind(this);
|
|
this.getSourceForClick = this.getSourceForClick.bind(this);
|
|
}
|
|
|
|
componentWillMount() {
|
|
if (this.props.sourceMapService) {
|
|
const { source, line, column } = this.props.frame;
|
|
this.unsubscribeSourceMapService = this.props.sourceMapService.subscribe(
|
|
source,
|
|
line,
|
|
column,
|
|
this._locationChanged
|
|
);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (typeof this.unsubscribeSourceMapService === "function") {
|
|
this.unsubscribeSourceMapService();
|
|
}
|
|
}
|
|
|
|
_locationChanged(isSourceMapped, url, line, column) {
|
|
const newState = {
|
|
isSourceMapped,
|
|
};
|
|
if (isSourceMapped) {
|
|
newState.frame = {
|
|
source: url,
|
|
line,
|
|
column,
|
|
functionDisplayName: this.props.frame.functionDisplayName,
|
|
};
|
|
}
|
|
|
|
this.setState(newState);
|
|
}
|
|
|
|
/**
|
|
* Utility method to convert the Frame object model to the
|
|
* object model required by the onClick callback.
|
|
* @param Frame frame
|
|
* @returns {{url: *, line: *, column: *, functionDisplayName: *}}
|
|
*/
|
|
getSourceForClick(frame) {
|
|
const { source, line, column, sourceId } = frame;
|
|
return {
|
|
url: source,
|
|
line,
|
|
column,
|
|
functionDisplayName: this.props.frame.functionDisplayName,
|
|
sourceId,
|
|
};
|
|
}
|
|
|
|
/* eslint-disable complexity */
|
|
render() {
|
|
let frame, isSourceMapped;
|
|
const {
|
|
onClick,
|
|
showFunctionName,
|
|
showAnonymousFunctionName,
|
|
showHost,
|
|
showEmptyPathAsHost,
|
|
showFullSourceUrl,
|
|
messageSource,
|
|
} = this.props;
|
|
|
|
if (this.state && this.state.isSourceMapped && this.state.frame) {
|
|
frame = this.state.frame;
|
|
isSourceMapped = this.state.isSourceMapped;
|
|
} else {
|
|
frame = this.props.frame;
|
|
}
|
|
|
|
const source = frame.source || "";
|
|
const sourceId = frame.sourceId;
|
|
const line = frame.line != void 0 ? Number(frame.line) : null;
|
|
const column = frame.column != void 0 ? Number(frame.column) : null;
|
|
|
|
const { short, long, host } = getSourceNames(source);
|
|
const unicodeShort = getUnicodeUrlPath(short);
|
|
const unicodeLong = getUnicodeUrl(long);
|
|
const unicodeHost = host ? getUnicodeHostname(host) : "";
|
|
|
|
// 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.
|
|
const isLinkable = !!parseURL(source) || isSourceMapped || sourceId;
|
|
const elements = [];
|
|
const sourceElements = [];
|
|
let sourceEl;
|
|
let tooltip = unicodeLong;
|
|
|
|
// Exclude all falsy values, including `0`, as line numbers start with 1.
|
|
if (line) {
|
|
tooltip += `:${line}`;
|
|
// Intentionally exclude 0
|
|
if (column) {
|
|
tooltip += `:${column}`;
|
|
}
|
|
}
|
|
|
|
const attributes = {
|
|
"data-url": long,
|
|
className: "frame-link",
|
|
};
|
|
|
|
if (showFunctionName) {
|
|
let functionDisplayName = frame.functionDisplayName;
|
|
if (!functionDisplayName && showAnonymousFunctionName) {
|
|
functionDisplayName = webl10n.getStr("stacktrace.anonymousFunction");
|
|
}
|
|
|
|
if (functionDisplayName) {
|
|
elements.push(
|
|
dom.span(
|
|
{
|
|
key: "function-display-name",
|
|
className: "frame-link-function-display-name",
|
|
},
|
|
functionDisplayName
|
|
),
|
|
" "
|
|
);
|
|
}
|
|
}
|
|
|
|
let displaySource = showFullSourceUrl ? unicodeLong : unicodeShort;
|
|
if (isSourceMapped) {
|
|
displaySource = getSourceMappedFile(displaySource);
|
|
} else if (
|
|
showEmptyPathAsHost &&
|
|
(displaySource === "" || displaySource === "/")
|
|
) {
|
|
displaySource = host;
|
|
}
|
|
|
|
sourceElements.push(
|
|
dom.span(
|
|
{
|
|
key: "filename",
|
|
className: "frame-link-filename",
|
|
},
|
|
displaySource
|
|
)
|
|
);
|
|
|
|
// If we have a line number > 0.
|
|
if (line) {
|
|
let lineInfo = `:${line}`;
|
|
// Add `data-line` attribute for testing
|
|
attributes["data-line"] = line;
|
|
|
|
// Intentionally exclude 0
|
|
if (column) {
|
|
lineInfo += `:${column}`;
|
|
// Add `data-column` attribute for testing
|
|
attributes["data-column"] = column;
|
|
}
|
|
|
|
sourceElements.push(
|
|
dom.span(
|
|
{
|
|
key: "line",
|
|
className: "frame-link-line",
|
|
},
|
|
lineInfo
|
|
)
|
|
);
|
|
}
|
|
|
|
// 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.
|
|
let tooltipMessage;
|
|
if (messageSource && messageSource === MESSAGE_SOURCE.CSS) {
|
|
tooltipMessage = l10n.getFormatStr(
|
|
"frame.viewsourceinstyleeditor",
|
|
tooltip
|
|
);
|
|
} else {
|
|
tooltipMessage = l10n.getFormatStr("frame.viewsourceindebugger", tooltip);
|
|
}
|
|
|
|
const sourceInnerEl = dom.span(
|
|
{
|
|
key: "source-inner",
|
|
className: "frame-link-source-inner",
|
|
title: isLinkable ? tooltipMessage : tooltip,
|
|
},
|
|
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.
|
|
if (isLinkable) {
|
|
sourceEl = dom.a(
|
|
{
|
|
onClick: e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onClick(this.getSourceForClick({ ...frame, source, sourceId }));
|
|
},
|
|
href: source,
|
|
className: "frame-link-source",
|
|
draggable: false,
|
|
},
|
|
sourceInnerEl
|
|
);
|
|
} else {
|
|
sourceEl = dom.span(
|
|
{
|
|
key: "source",
|
|
className: "frame-link-source",
|
|
},
|
|
sourceInnerEl
|
|
);
|
|
}
|
|
elements.push(sourceEl);
|
|
|
|
if (showHost && unicodeHost) {
|
|
elements.push(" ");
|
|
elements.push(
|
|
dom.span(
|
|
{
|
|
key: "host",
|
|
className: "frame-link-host",
|
|
},
|
|
unicodeHost
|
|
)
|
|
);
|
|
}
|
|
|
|
return dom.span(attributes, ...elements);
|
|
}
|
|
/* eslint-enable complexity */
|
|
}
|
|
|
|
module.exports = Frame;
|