summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/Editor/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/Editor/index.js')
-rw-r--r--devtools/client/debugger/src/components/Editor/index.js770
1 files changed, 770 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/Editor/index.js b/devtools/client/debugger/src/components/Editor/index.js
new file mode 100644
index 0000000000..5b139376f5
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/index.js
@@ -0,0 +1,770 @@
+/* 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 PropTypes from "prop-types";
+import React, { PureComponent } from "react";
+import { bindActionCreators } from "redux";
+import ReactDOM from "react-dom";
+import { connect } from "../../utils/connect";
+import classnames from "classnames";
+import { debounce } from "lodash";
+
+import { getLineText } from "./../../utils/source";
+import { features } from "../../utils/prefs";
+import { getIndentation } from "../../utils/indentation";
+
+import { showMenu } from "../../context-menu/menu";
+import {
+ createBreakpointItems,
+ breakpointItemActions,
+} from "./menus/breakpoints";
+
+import { continueToHereItem, editorItemActions } from "./menus/editor";
+
+import type { BreakpointItemActions } from "./menus/breakpoints";
+import type { EditorItemActions } from "./menus/editor";
+
+import {
+ getActiveSearch,
+ getSelectedLocation,
+ getSelectedSourceWithContent,
+ getConditionalPanelLocation,
+ getSymbols,
+ getIsPaused,
+ getCurrentThread,
+ getThreadContext,
+ getSkipPausing,
+ getInlinePreview,
+ getEditorWrapping,
+ getHighlightedCalls,
+} from "../../selectors";
+
+// Redux actions
+import actions from "../../actions";
+
+import SearchBar from "./SearchBar";
+import HighlightLines from "./HighlightLines";
+import Preview from "./Preview";
+import Breakpoints from "./Breakpoints";
+import ColumnBreakpoints from "./ColumnBreakpoints";
+import DebugLine from "./DebugLine";
+import HighlightLine from "./HighlightLine";
+import EmptyLines from "./EmptyLines";
+import EditorMenu from "./EditorMenu";
+import ConditionalPanel from "./ConditionalPanel";
+import InlinePreviews from "./InlinePreviews";
+import HighlightCalls from "./HighlightCalls";
+import Exceptions from "./Exceptions";
+
+import {
+ showSourceText,
+ showLoading,
+ showErrorMessage,
+ getEditor,
+ clearEditor,
+ getCursorLine,
+ getCursorColumn,
+ lineAtHeight,
+ toSourceLine,
+ getDocument,
+ scrollToColumn,
+ toEditorPosition,
+ getSourceLocationFromMouseEvent,
+ hasDocument,
+ onMouseOver,
+ startOperation,
+ endOperation,
+} from "../../utils/editor";
+
+import { resizeToggleButton, resizeBreakpointGutter } from "../../utils/ui";
+import Services from "devtools-services";
+const { appinfo } = Services;
+
+const isMacOS = appinfo.OS === "Darwin";
+
+function isSecondary(ev) {
+ return isMacOS && ev.ctrlKey && ev.button === 0;
+}
+
+function isCmd(ev) {
+ return isMacOS ? ev.metaKey : ev.ctrlKey;
+}
+
+import "./Editor.css";
+import "./Breakpoints.css";
+import "./InlinePreview.css";
+
+import type SourceEditor from "../../utils/editor/source-editor";
+import type { SymbolDeclarations } from "../../workers/parser";
+import type {
+ SourceLocation,
+ SourceWithContent,
+ ThreadContext,
+ HighlightedCalls as highlightedCallsType,
+} from "../../types";
+
+const cssVars = {
+ searchbarHeight: "var(--editor-searchbar-height)",
+};
+
+type OwnProps = {|
+ startPanelSize: number,
+ endPanelSize: number,
+|};
+export type Props = {
+ cx: ThreadContext,
+ selectedLocation: ?SourceLocation,
+ selectedSource: ?SourceWithContent,
+ searchOn: boolean,
+ startPanelSize: number,
+ endPanelSize: number,
+ conditionalPanelLocation: SourceLocation,
+ symbols: SymbolDeclarations,
+ isPaused: boolean,
+ skipPausing: boolean,
+ inlinePreviewEnabled: boolean,
+ editorWrappingEnabled: boolean,
+ highlightedCalls: ?highlightedCallsType,
+
+ // Actions
+ openConditionalPanel: typeof actions.openConditionalPanel,
+ closeConditionalPanel: typeof actions.closeConditionalPanel,
+ continueToHere: typeof actions.continueToHere,
+ addBreakpointAtLine: typeof actions.addBreakpointAtLine,
+ jumpToMappedLocation: typeof actions.jumpToMappedLocation,
+ toggleBreakpointAtLine: typeof actions.toggleBreakpointAtLine,
+ traverseResults: typeof actions.traverseResults,
+ updateViewport: typeof actions.updateViewport,
+ updateCursorPosition: typeof actions.updateCursorPosition,
+ closeTab: typeof actions.closeTab,
+ breakpointActions: BreakpointItemActions,
+ editorActions: EditorItemActions,
+ toggleBlackBox: typeof actions.toggleBlackBox,
+ highlightCalls: typeof actions.highlightCalls,
+ unhighlightCalls: typeof actions.unhighlightCalls,
+};
+
+type State = {
+ editor: SourceEditor,
+ contextMenu: ?MouseEvent,
+};
+
+class Editor extends PureComponent<Props, State> {
+ $editorWrapper: ?HTMLDivElement;
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ highlightedLineRange: null,
+ editor: (null: any),
+ contextMenu: null,
+ };
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ let { editor } = this.state;
+
+ if (!editor && nextProps.selectedSource) {
+ editor = this.setupEditor();
+ }
+
+ startOperation();
+ this.setText(nextProps, editor);
+ this.setSize(nextProps, editor);
+ this.scrollToLocation(nextProps, editor);
+ endOperation();
+
+ if (this.props.selectedSource != nextProps.selectedSource) {
+ this.props.updateViewport();
+ resizeBreakpointGutter(editor.codeMirror);
+ resizeToggleButton(editor.codeMirror);
+ }
+ }
+
+ setupEditor() {
+ const editor = getEditor();
+
+ // disables the default search shortcuts
+ // $FlowIgnore
+ editor._initShortcuts = () => {};
+
+ const node = ReactDOM.findDOMNode(this);
+ if (node instanceof HTMLElement) {
+ editor.appendToLocalElement(node.querySelector(".editor-mount"));
+ }
+
+ const { codeMirror } = editor;
+ const codeMirrorWrapper = codeMirror.getWrapperElement();
+
+ codeMirror.on("gutterClick", this.onGutterClick);
+
+ if (features.commandClick) {
+ document.addEventListener("keydown", this.commandKeyDown);
+ document.addEventListener("keyup", this.commandKeyUp);
+ }
+
+ // Set code editor wrapper to be focusable
+ codeMirrorWrapper.tabIndex = 0;
+ codeMirrorWrapper.addEventListener("keydown", e => this.onKeyDown(e));
+ codeMirrorWrapper.addEventListener("click", e => this.onClick(e));
+ codeMirrorWrapper.addEventListener("mouseover", onMouseOver(codeMirror));
+
+ const toggleFoldMarkerVisibility = e => {
+ if (node instanceof HTMLElement) {
+ node
+ .querySelectorAll(".CodeMirror-guttermarker-subtle")
+ .forEach(elem => {
+ elem.classList.toggle("visible");
+ });
+ }
+ };
+
+ const codeMirrorGutter = codeMirror.getGutterElement();
+ codeMirrorGutter.addEventListener("mouseleave", toggleFoldMarkerVisibility);
+ codeMirrorGutter.addEventListener("mouseenter", toggleFoldMarkerVisibility);
+ codeMirrorWrapper.addEventListener("contextmenu", event =>
+ this.openMenu(event)
+ );
+
+ codeMirror.on("scroll", this.onEditorScroll);
+ this.onEditorScroll();
+ this.setState({ editor });
+ return editor;
+ }
+
+ componentDidMount() {
+ const { shortcuts } = this.context;
+
+ shortcuts.on(L10N.getStr("toggleBreakpoint.key"), this.onToggleBreakpoint);
+ shortcuts.on(
+ L10N.getStr("toggleCondPanel.breakpoint.key"),
+ this.onToggleConditionalPanel
+ );
+ shortcuts.on(
+ L10N.getStr("toggleCondPanel.logPoint.key"),
+ this.onToggleConditionalPanel
+ );
+ shortcuts.on(
+ L10N.getStr("sourceTabs.closeTab.key"),
+ this.onCloseShortcutPress
+ );
+ shortcuts.on("Esc", this.onEscape);
+ }
+
+ onCloseShortcutPress = (e: KeyboardEvent) => {
+ const { cx, selectedSource } = this.props;
+ if (selectedSource) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.closeTab(cx, selectedSource, "shortcut");
+ }
+ };
+
+ componentWillUnmount() {
+ const { editor } = this.state;
+ if (editor) {
+ editor.destroy();
+ editor.codeMirror.off("scroll", this.onEditorScroll);
+ this.setState({ editor: (null: any) });
+ }
+
+ const { shortcuts } = this.context;
+ shortcuts.off(L10N.getStr("sourceTabs.closeTab.key"));
+ shortcuts.off(L10N.getStr("toggleBreakpoint.key"));
+ shortcuts.off(L10N.getStr("toggleCondPanel.breakpoint.key"));
+ shortcuts.off(L10N.getStr("toggleCondPanel.logPoint.key"));
+ }
+
+ getCurrentLine() {
+ const { codeMirror } = this.state.editor;
+ const { selectedSource } = this.props;
+ if (!selectedSource) {
+ return;
+ }
+
+ const line = getCursorLine(codeMirror);
+ return toSourceLine(selectedSource.id, line);
+ }
+
+ onToggleBreakpoint = (e: KeyboardEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const line = this.getCurrentLine();
+ if (typeof line !== "number") {
+ return;
+ }
+
+ this.props.toggleBreakpointAtLine(this.props.cx, line);
+ };
+
+ onToggleConditionalPanel = (e: KeyboardEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const {
+ conditionalPanelLocation,
+ closeConditionalPanel,
+ openConditionalPanel,
+ selectedSource,
+ } = this.props;
+
+ const line = this.getCurrentLine();
+
+ const { codeMirror } = this.state.editor;
+ // add one to column for correct position in editor.
+ const column = getCursorColumn(codeMirror) + 1;
+
+ if (conditionalPanelLocation) {
+ return closeConditionalPanel();
+ }
+
+ if (!selectedSource) {
+ return;
+ }
+
+ if (typeof line !== "number") {
+ return;
+ }
+
+ return openConditionalPanel(
+ {
+ line,
+ column,
+ sourceId: selectedSource.id,
+ },
+ false
+ );
+ };
+
+ onEditorScroll = debounce(this.props.updateViewport, 75);
+
+ commandKeyDown = (e: KeyboardEvent) => {
+ const { key } = e;
+ if (this.props.isPaused && key === "Meta") {
+ const { cx, highlightCalls } = this.props;
+ highlightCalls(cx);
+ }
+ };
+
+ commandKeyUp = (e: KeyboardEvent) => {
+ const { key } = e;
+ if (key === "Meta") {
+ const { cx, unhighlightCalls } = this.props;
+ unhighlightCalls(cx);
+ }
+ };
+
+ onKeyDown(e: KeyboardEvent) {
+ const { codeMirror } = this.state.editor;
+ const { key, target } = e;
+ const codeWrapper = codeMirror.getWrapperElement();
+ const textArea = codeWrapper.querySelector("textArea");
+
+ if (key === "Escape" && target == textArea) {
+ e.stopPropagation();
+ e.preventDefault();
+ codeWrapper.focus();
+ } else if (key === "Enter" && target == codeWrapper) {
+ e.preventDefault();
+ // Focus into editor's text area
+ textArea.focus();
+ }
+ }
+
+ /*
+ * The default Esc command is overridden in the CodeMirror keymap to allow
+ * the Esc keypress event to be catched by the toolbox and trigger the
+ * split console. Restore it here, but preventDefault if and only if there
+ * is a multiselection.
+ */
+ onEscape = (e: KeyboardEvent) => {
+ if (!this.state.editor) {
+ return;
+ }
+
+ const { codeMirror } = this.state.editor;
+ if (codeMirror.listSelections().length > 1) {
+ codeMirror.execCommand("singleSelection");
+ e.preventDefault();
+ }
+ };
+
+ openMenu(event: MouseEvent) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const {
+ cx,
+ selectedSource,
+ breakpointActions,
+ editorActions,
+ isPaused,
+ conditionalPanelLocation,
+ closeConditionalPanel,
+ } = this.props;
+ const { editor } = this.state;
+ if (!selectedSource || !editor) {
+ return;
+ }
+
+ // only allow one conditionalPanel location.
+ if (conditionalPanelLocation) {
+ closeConditionalPanel();
+ }
+
+ const target: Element = (event.target: any);
+ const { id: sourceId } = selectedSource;
+ const line = lineAtHeight(editor, sourceId, event);
+
+ if (typeof line != "number") {
+ return;
+ }
+
+ const location = { line, column: undefined, sourceId };
+
+ if (target.classList.contains("CodeMirror-linenumber")) {
+ const lineText = getLineText(
+ sourceId,
+ selectedSource.content,
+ line
+ ).trim();
+
+ return showMenu(event, [
+ ...createBreakpointItems(cx, location, breakpointActions, lineText),
+ { type: "separator" },
+ continueToHereItem(cx, location, isPaused, editorActions),
+ ]);
+ }
+
+ if (target.getAttribute("id") === "columnmarker") {
+ return;
+ }
+
+ this.setState({ contextMenu: event });
+ }
+
+ clearContextMenu = () => {
+ this.setState({ contextMenu: null });
+ };
+
+ onGutterClick = (
+ cm: Object,
+ line: number,
+ gutter: string,
+ ev: MouseEvent
+ ) => {
+ const {
+ cx,
+ selectedSource,
+ conditionalPanelLocation,
+ closeConditionalPanel,
+ addBreakpointAtLine,
+ continueToHere,
+ toggleBlackBox,
+ } = this.props;
+
+ // ignore right clicks in the gutter
+ if (isSecondary(ev) || ev.button === 2 || !selectedSource) {
+ return;
+ }
+
+ // if user clicks gutter to set breakpoint on blackboxed source, un-blackbox the source.
+ if (selectedSource?.isBlackBoxed) {
+ toggleBlackBox(cx, selectedSource);
+ }
+
+ if (conditionalPanelLocation) {
+ return closeConditionalPanel();
+ }
+
+ if (gutter === "CodeMirror-foldgutter") {
+ return;
+ }
+
+ const sourceLine = toSourceLine(selectedSource.id, line);
+ if (typeof sourceLine !== "number") {
+ return;
+ }
+
+ if (isCmd(ev)) {
+ return continueToHere(cx, {
+ line: sourceLine,
+ column: undefined,
+ sourceId: selectedSource.id,
+ });
+ }
+
+ return addBreakpointAtLine(cx, sourceLine, ev.altKey, ev.shiftKey);
+ };
+
+ onGutterContextMenu = (event: MouseEvent) => {
+ return this.openMenu(event);
+ };
+
+ onClick(e: MouseEvent) {
+ const {
+ cx,
+ selectedSource,
+ updateCursorPosition,
+ jumpToMappedLocation,
+ } = this.props;
+
+ if (selectedSource) {
+ const sourceLocation = getSourceLocationFromMouseEvent(
+ this.state.editor,
+ selectedSource,
+ e
+ );
+
+ if (e.metaKey && e.altKey) {
+ jumpToMappedLocation(cx, sourceLocation);
+ }
+
+ updateCursorPosition(sourceLocation);
+ }
+ }
+
+ shouldScrollToLocation(nextProps: Props, editor: SourceEditor) {
+ const { selectedLocation, selectedSource } = this.props;
+ if (
+ !editor ||
+ !nextProps.selectedSource ||
+ !nextProps.selectedLocation ||
+ !nextProps.selectedLocation.line ||
+ !nextProps.selectedSource.content
+ ) {
+ return false;
+ }
+
+ const isFirstLoad =
+ (!selectedSource || !selectedSource.content) &&
+ nextProps.selectedSource.content;
+ const locationChanged = selectedLocation !== nextProps.selectedLocation;
+ const symbolsChanged = nextProps.symbols != this.props.symbols;
+
+ return isFirstLoad || locationChanged || symbolsChanged;
+ }
+
+ scrollToLocation(nextProps: Props, editor: SourceEditor) {
+ const { selectedLocation, selectedSource } = nextProps;
+
+ if (selectedLocation && this.shouldScrollToLocation(nextProps, editor)) {
+ let { line, column } = toEditorPosition(selectedLocation);
+
+ if (selectedSource && hasDocument(selectedSource.id)) {
+ const doc = getDocument(selectedSource.id);
+ const lineText: ?string = doc.getLine(line);
+ column = Math.max(column, getIndentation(lineText));
+ }
+
+ scrollToColumn(editor.codeMirror, line, column);
+ }
+ }
+
+ setSize(nextProps: Props, editor: SourceEditor) {
+ if (!editor) {
+ return;
+ }
+
+ if (
+ nextProps.startPanelSize !== this.props.startPanelSize ||
+ nextProps.endPanelSize !== this.props.endPanelSize
+ ) {
+ editor.codeMirror.setSize();
+ }
+ }
+
+ setText(props: Props, editor: ?SourceEditor) {
+ const { selectedSource, symbols } = props;
+
+ if (!editor) {
+ return;
+ }
+
+ // check if we previously had a selected source
+ if (!selectedSource) {
+ return this.clearEditor();
+ }
+
+ if (!selectedSource.content) {
+ return showLoading(editor);
+ }
+
+ if (selectedSource.content.state === "rejected") {
+ let { value } = selectedSource.content;
+ if (typeof value !== "string") {
+ value = "Unexpected source error";
+ }
+
+ return this.showErrorMessage(value);
+ }
+
+ return showSourceText(
+ editor,
+ selectedSource,
+ selectedSource.content.value,
+ symbols
+ );
+ }
+
+ clearEditor() {
+ const { editor } = this.state;
+ if (!editor) {
+ return;
+ }
+
+ clearEditor(editor);
+ }
+
+ showErrorMessage(msg: string) {
+ const { editor } = this.state;
+ if (!editor) {
+ return;
+ }
+
+ showErrorMessage(editor, msg);
+ }
+
+ getInlineEditorStyles() {
+ const { searchOn } = this.props;
+
+ if (searchOn) {
+ return {
+ height: `calc(100% - ${cssVars.searchbarHeight})`,
+ };
+ }
+
+ return {
+ height: "100%",
+ };
+ }
+
+ renderItems() {
+ const {
+ cx,
+ selectedSource,
+ conditionalPanelLocation,
+ isPaused,
+ inlinePreviewEnabled,
+ editorWrappingEnabled,
+ } = this.props;
+ const { editor, contextMenu } = this.state;
+
+ if (!selectedSource || !editor || !getDocument(selectedSource.id)) {
+ return null;
+ }
+
+ return (
+ <div>
+ <HighlightCalls editor={editor} selectedSource={selectedSource} />
+ <DebugLine />
+ <HighlightLine />
+ <EmptyLines editor={editor} />
+ <Breakpoints editor={editor} cx={cx} />
+ <Preview editor={editor} editorRef={this.$editorWrapper} />
+ <HighlightLines editor={editor} />
+ <Exceptions />
+ {
+ <EditorMenu
+ editor={editor}
+ contextMenu={contextMenu}
+ clearContextMenu={this.clearContextMenu}
+ selectedSource={selectedSource}
+ editorWrappingEnabled={editorWrappingEnabled}
+ />
+ }
+ {conditionalPanelLocation ? <ConditionalPanel editor={editor} /> : null}
+ {features.columnBreakpoints ? (
+ <ColumnBreakpoints editor={editor} />
+ ) : null}
+ {isPaused && inlinePreviewEnabled ? (
+ <InlinePreviews editor={editor} selectedSource={selectedSource} />
+ ) : null}
+ </div>
+ );
+ }
+
+ renderSearchBar() {
+ const { editor } = this.state;
+
+ if (!this.props.selectedSource) {
+ return null;
+ }
+
+ return <SearchBar editor={editor} />;
+ }
+
+ render() {
+ const { selectedSource, skipPausing } = this.props;
+ return (
+ <div
+ className={classnames("editor-wrapper", {
+ blackboxed: selectedSource?.isBlackBoxed,
+ "skip-pausing": skipPausing,
+ })}
+ ref={c => (this.$editorWrapper = c)}
+ >
+ <div
+ className="editor-mount devtools-monospace"
+ style={this.getInlineEditorStyles()}
+ />
+ {this.renderSearchBar()}
+ {this.renderItems()}
+ </div>
+ );
+ }
+}
+
+Editor.contextTypes = {
+ shortcuts: PropTypes.object,
+};
+
+const mapStateToProps = state => {
+ const selectedSource = getSelectedSourceWithContent(state);
+
+ return {
+ cx: getThreadContext(state),
+ selectedLocation: getSelectedLocation(state),
+ selectedSource,
+ searchOn: getActiveSearch(state) === "file",
+ conditionalPanelLocation: getConditionalPanelLocation(state),
+ symbols: getSymbols(state, selectedSource),
+ isPaused: getIsPaused(state, getCurrentThread(state)),
+ skipPausing: getSkipPausing(state),
+ inlinePreviewEnabled: getInlinePreview(state),
+ editorWrappingEnabled: getEditorWrapping(state),
+ highlightedCalls: getHighlightedCalls(state, getCurrentThread(state)),
+ };
+};
+
+const mapDispatchToProps = dispatch => ({
+ ...bindActionCreators(
+ {
+ openConditionalPanel: actions.openConditionalPanel,
+ closeConditionalPanel: actions.closeConditionalPanel,
+ continueToHere: actions.continueToHere,
+ toggleBreakpointAtLine: actions.toggleBreakpointAtLine,
+ addBreakpointAtLine: actions.addBreakpointAtLine,
+ jumpToMappedLocation: actions.jumpToMappedLocation,
+ traverseResults: actions.traverseResults,
+ updateViewport: actions.updateViewport,
+ updateCursorPosition: actions.updateCursorPosition,
+ closeTab: actions.closeTab,
+ toggleBlackBox: actions.toggleBlackBox,
+ highlightCalls: actions.highlightCalls,
+ unhighlightCalls: actions.unhighlightCalls,
+ },
+ dispatch
+ ),
+ breakpointActions: breakpointItemActions(dispatch),
+ editorActions: editorItemActions(dispatch),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(
+ mapStateToProps,
+ mapDispatchToProps
+)(Editor);