зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1643633 - Add a stack trace to the exception tooltip r=davidwalsh
Adds a stacktrace to the exception popup of the inline exceptions in the debugger. Matches the styles of the console error messages. The popup works in two modes: - when the stacktrace is closed the exception popup gets closed when the mouse leaves the popup. - when the stacktrace is opened the exception popup gets closed only by clicking outside the popup. Differential Revision: https://phabricator.services.mozilla.com/D82690
This commit is contained in:
Родитель
a1c623c01c
Коммит
5676d0c1ff
|
@ -15,6 +15,7 @@ export function addException({ resource }: Object) {
|
|||
return;
|
||||
}
|
||||
const { columnNumber, lineNumber, sourceId, errorMessage } = pageError;
|
||||
const stacktrace = pageError.stacktrace || [];
|
||||
|
||||
if (!hasException(getState(), lineNumber, columnNumber)) {
|
||||
dispatch({
|
||||
|
@ -24,6 +25,7 @@ export function addException({ resource }: Object) {
|
|||
lineNumber,
|
||||
sourceActorId: sourceId,
|
||||
errorMessage,
|
||||
stacktrace,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
/* 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/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "../../../utils/connect";
|
||||
import classnames from "classnames";
|
||||
|
||||
import Reps from "devtools-reps";
|
||||
const {
|
||||
REPS: { StringRep },
|
||||
} = Reps;
|
||||
|
||||
import actions from "../../../actions";
|
||||
|
||||
import { getThreadContext } from "../../../selectors";
|
||||
|
||||
import AccessibleImage from "../../shared/AccessibleImage";
|
||||
import { DevToolsUtils } from "devtools-modules";
|
||||
|
||||
import type { ThreadContext, StacktraceFrame, Exception } from "../../../types";
|
||||
|
||||
type Props = {
|
||||
cx: ThreadContext,
|
||||
clearPreview: typeof actions.clearPreview,
|
||||
selectSourceURL: typeof actions.selectSourceURL,
|
||||
exception: Exception,
|
||||
mouseout: Function,
|
||||
};
|
||||
|
||||
type OwnProps = {|
|
||||
exception: Exception,
|
||||
mouseout: Function,
|
||||
|};
|
||||
|
||||
type State = {
|
||||
isStacktraceExpanded: boolean,
|
||||
};
|
||||
|
||||
const POPUP_SELECTOR = ".preview-popup.exception-popup";
|
||||
const ANONYMOUS_FN_NAME = "<anonymous>";
|
||||
|
||||
// The exception popup works in two modes:
|
||||
// a. when the stacktrace is closed the exception popup
|
||||
// gets closed when the mouse leaves the popup.
|
||||
// b. when the stacktrace is opened the exception popup
|
||||
// gets closed only by clicking outside the popup.
|
||||
class ExceptionPopup extends Component<Props, State> {
|
||||
topWindow: Object;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isStacktraceExpanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
updateTopWindow() {
|
||||
// The ChromeWindow is used when the stacktrace is expanded to capture all clicks
|
||||
// outside the popup so the popup can be closed only by clicking outside of it.
|
||||
if (this.topWindow) {
|
||||
this.topWindow.removeEventListener(
|
||||
"mousedown",
|
||||
this.onTopWindowClick,
|
||||
true
|
||||
);
|
||||
this.topWindow = null;
|
||||
}
|
||||
this.topWindow = DevToolsUtils.getTopWindow(window.parent);
|
||||
this.topWindow.addEventListener("mousedown", this.onTopWindowClick, true);
|
||||
}
|
||||
|
||||
onTopWindowClick = (e: Object) => {
|
||||
const { cx, clearPreview } = this.props;
|
||||
|
||||
// When the stactrace is expaned the exception popup gets closed
|
||||
// only by clicking ouside the popup.
|
||||
if (!e.target.closest(POPUP_SELECTOR)) {
|
||||
clearPreview(cx);
|
||||
}
|
||||
};
|
||||
|
||||
onExceptionMessageClick() {
|
||||
const isStacktraceExpanded = this.state.isStacktraceExpanded;
|
||||
|
||||
this.updateTopWindow();
|
||||
this.setState({ isStacktraceExpanded: !isStacktraceExpanded });
|
||||
}
|
||||
|
||||
buildStackFrame(frame: StacktraceFrame) {
|
||||
const { cx, selectSourceURL } = this.props;
|
||||
const { filename, lineNumber } = frame;
|
||||
const functionName = frame.functionName || ANONYMOUS_FN_NAME;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="frame"
|
||||
onClick={() => selectSourceURL(cx, filename, { line: lineNumber })}
|
||||
>
|
||||
<span className="title">{functionName}</span>
|
||||
<span className="location">
|
||||
<span className="filename">{filename}</span>:
|
||||
<span className="line">{lineNumber}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStacktrace(stacktrace: StacktraceFrame[]) {
|
||||
const isStacktraceExpanded = this.state.isStacktraceExpanded;
|
||||
|
||||
if (stacktrace.length && isStacktraceExpanded) {
|
||||
return (
|
||||
<div className="exception-stacktrace">
|
||||
{stacktrace.map(frame => this.buildStackFrame(frame))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderArrowIcon(stacktrace: StacktraceFrame[]) {
|
||||
if (stacktrace.length) {
|
||||
return (
|
||||
<AccessibleImage
|
||||
className={classnames("arrow", {
|
||||
expanded: this.state.isStacktraceExpanded,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
exception: { stacktrace, errorMessage },
|
||||
mouseout,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="preview-popup exception-popup"
|
||||
dir="ltr"
|
||||
onMouseLeave={() => mouseout(true, this.state.isStacktraceExpanded)}
|
||||
>
|
||||
<div
|
||||
className="exception-message"
|
||||
onClick={() => this.onExceptionMessageClick()}
|
||||
>
|
||||
{this.renderArrowIcon(stacktrace)}
|
||||
{StringRep.rep({
|
||||
object: errorMessage,
|
||||
useQuotes: false,
|
||||
className: "exception-text",
|
||||
})}
|
||||
</div>
|
||||
{this.renderStacktrace(stacktrace)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
cx: getThreadContext(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
selectSourceURL: actions.selectSourceURL,
|
||||
clearPreview: actions.clearPreview,
|
||||
};
|
||||
|
||||
export default connect<Props, OwnProps, _, _, _, _>(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ExceptionPopup);
|
|
@ -147,3 +147,58 @@
|
|||
.theme-dark .exception-popup .exception-text {
|
||||
color: var(--red-20);
|
||||
}
|
||||
|
||||
.exception-popup .exception-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.exception-message .arrow {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
.exception-popup .exception-stacktrace {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-column-gap: 8px;
|
||||
padding-inline: 2px 3px;
|
||||
line-height: var(--theme-code-line-height);
|
||||
}
|
||||
|
||||
.exception-stacktrace .frame {
|
||||
display: contents;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.exception-stacktrace .title {
|
||||
grid-column: 1/2;
|
||||
color: var(--grey-90);
|
||||
}
|
||||
|
||||
.theme-dark .exception-stacktrace .title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.exception-stacktrace .location {
|
||||
grid-column: -1/-2;
|
||||
color: var(--theme-highlight-purple);
|
||||
direction: rtl;
|
||||
text-align: end;
|
||||
white-space: nowrap;
|
||||
/* Force the location to be on one line and crop at start if wider then max-width */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.theme-dark .exception-stacktrace .location {
|
||||
color: var(--blue-40);
|
||||
}
|
||||
|
||||
.exception-stacktrace .line {
|
||||
color: var(--theme-highlight-blue);
|
||||
}
|
||||
|
||||
.theme-dark .exception-stacktrace .line {
|
||||
color: hsl(210, 40%, 60%);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { connect } from "../../../utils/connect";
|
|||
|
||||
import Reps from "devtools-reps";
|
||||
const {
|
||||
REPS: { Rep, StringRep },
|
||||
REPS: { Rep },
|
||||
MODE,
|
||||
objectInspector,
|
||||
} = Reps;
|
||||
|
@ -20,6 +20,8 @@ const {
|
|||
node: { nodeIsPrimitive, nodeIsFunction, nodeIsObject },
|
||||
} = utils;
|
||||
|
||||
import ExceptionPopup from "./ExceptionPopup";
|
||||
|
||||
import actions from "../../../actions";
|
||||
import { getThreadContext } from "../../../selectors";
|
||||
import Popover from "../../shared/Popover";
|
||||
|
@ -53,6 +55,7 @@ export class Popup extends Component<Props> {
|
|||
marker: any;
|
||||
pos: any;
|
||||
popover: ?React$ElementRef<typeof Popover>;
|
||||
isExceptionStactraceOpen: ?boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
@ -179,16 +182,11 @@ export class Popup extends Component<Props> {
|
|||
}
|
||||
|
||||
renderExceptionPreview(exception: Exception) {
|
||||
const errorMessage = exception.errorMessage;
|
||||
|
||||
return (
|
||||
<div className="preview-popup exception-popup">
|
||||
{StringRep.rep({
|
||||
object: errorMessage,
|
||||
useQuotes: false,
|
||||
className: "exception-text",
|
||||
})}
|
||||
</div>
|
||||
<ExceptionPopup
|
||||
exception={exception}
|
||||
mouseout={this.onMouseOutException}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -239,6 +237,27 @@ export class Popup extends Component<Props> {
|
|||
clearPreview(cx);
|
||||
};
|
||||
|
||||
onMouseOutException = (
|
||||
shouldClearOnMouseout: ?boolean,
|
||||
isExceptionStactraceOpen: ?boolean
|
||||
) => {
|
||||
// onMouseOutException can be called:
|
||||
// a. when the mouse leaves Popover element
|
||||
// b. when the mouse leaves ExceptionPopup element
|
||||
// We want to prevent closing the popup when the stacktrace
|
||||
// is expanded and the mouse leaves either the Popover element
|
||||
// or the ExceptionPopup element.
|
||||
const { clearPreview, cx } = this.props;
|
||||
|
||||
if (shouldClearOnMouseout) {
|
||||
this.isExceptionStactraceOpen = isExceptionStactraceOpen;
|
||||
}
|
||||
|
||||
if (!this.isExceptionStactraceOpen) {
|
||||
clearPreview(cx);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
preview: { cursorPos, resultGrip, exception },
|
||||
|
@ -259,7 +278,7 @@ export class Popup extends Component<Props> {
|
|||
type={type}
|
||||
editorRef={editorRef}
|
||||
target={this.props.preview.target}
|
||||
mouseout={this.onMouseOut}
|
||||
mouseout={exception ? this.onMouseOutException : this.onMouseOut}
|
||||
>
|
||||
{this.renderPreview()}
|
||||
</Popover>
|
||||
|
|
|
@ -8,6 +8,7 @@ DIRS += [
|
|||
]
|
||||
|
||||
CompiledModules(
|
||||
'ExceptionPopup.js',
|
||||
'index.js',
|
||||
'Popup.js',
|
||||
)
|
||||
|
|
|
@ -529,4 +529,13 @@ export type Exception = {
|
|||
fileName: URL,
|
||||
lineNumber: number,
|
||||
sourceActorId: SourceActorId,
|
||||
stacktrace: Array<StacktraceFrame>,
|
||||
};
|
||||
|
||||
export type StacktraceFrame = {
|
||||
columnNumber: number,
|
||||
filename: URL,
|
||||
functionName: string,
|
||||
lineNumber: number,
|
||||
sourceId: SourceActorId,
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче