/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const { connect, } = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const Editor = require("resource://devtools/client/shared/sourceeditor/editor.js"); const { setTargetSearchResult, } = require("resource://devtools/client/netmonitor/src/actions/search.js"); const { div } = dom; /** * CodeMirror editor as a React component */ class SourcePreview extends Component { static get propTypes() { return { // Source editor syntax highlight mode, which is a mime type defined in CodeMirror mode: PropTypes.string, // Source editor content text: PropTypes.string, // Search result text to select targetSearchResult: PropTypes.object, // Reset target search result that has been used for navigation in this panel. // This is done to avoid second navigation the next time. resetTargetSearchResult: PropTypes.func, }; } componentDidMount() { const { mode, text } = this.props; this.loadEditor(mode, text); } shouldComponentUpdate(nextProps) { return ( nextProps.mode !== this.props.mode || nextProps.text !== this.props.text || nextProps.targetSearchResult !== this.props.targetSearchResult ); } componentDidUpdate(prevProps) { const { mode, targetSearchResult, text } = this.props; if (prevProps.text !== text) { // When updating from editor to editor this.updateEditor(mode, text); } else if (prevProps.targetSearchResult !== targetSearchResult) { this.findSearchResult(); } } componentWillUnmount() { this.unloadEditor(); } loadEditor(mode, text) { this.editor = new Editor({ lineNumbers: true, lineWrapping: false, mode: null, // Disable auto syntax detection, but then we set mode asynchronously readOnly: true, theme: "mozilla", value: text, }); // Delay to CodeMirror initialization content to prevent UI freezing this.editorTimeout = setTimeout(() => { this.editorTimeout = null; this.editor.appendToLocalElement(this.refs.editorElement); // CodeMirror's setMode() (syntax highlight) is the performance bottleneck when // processing large content, so we enable it asynchronously within the setTimeout // to avoid UI blocking. (rendering source code -> drawing syntax highlight) this.editorSetModeTimeout = setTimeout(() => { this.editorSetModeTimeout = null; this.editor.setMode(mode); this.findSearchResult(); }); }); } updateEditor(mode, text) { // Reset the existed 'mode' attribute in order to make setText() process faster // to prevent drawing unnecessary syntax highlight. if (this?.editor?.hasCodeMirror) { this.editor.setMode(null); this.editor.setText(text); } if (this.editorSetModeTimeout) { clearTimeout(this.editorSetModeTimeout); } // CodeMirror's setMode() (syntax highlight) is the performance bottleneck when // processing large content, so we enable it asynchronously within the setTimeout // to avoid UI blocking. (rendering source code -> drawing syntax highlight) this.editorSetModeTimeout = setTimeout(() => { this.editorSetModeTimeout = null; this.editor.setMode(mode); this.findSearchResult(); }); } unloadEditor() { clearTimeout(this.editorTimeout); clearTimeout(this.editorSetModeTimeout); if (this.editor) { this.editor.destroy(); this.editor = null; } } findSearchResult() { const { targetSearchResult, resetTargetSearchResult } = this.props; if (targetSearchResult?.line) { const { line } = targetSearchResult; // scroll the editor to center the line // with the target search result if (this.editor) { this.editor.setCursor({ line: line - 1 }, "center"); } } resetTargetSearchResult(); } // Scroll to specified line if the user clicks on search results. scrollToLine(element) { const { targetSearchResult, resetTargetSearchResult } = this.props; // The following code is responsible for scrolling given line // to visible view-port. // It gets the
child element representing the target // line (by index) and uses `scrollIntoView` API to make sure // it's visible to the user. if (element && targetSearchResult && targetSearchResult.line) { const child = element.children[targetSearchResult.line - 1]; if (child) { const range = document.createRange(); range.selectNode(child); document.getSelection().addRange(range); child.scrollIntoView({ block: "center" }); } resetTargetSearchResult(); } } renderEditor() { return div( { className: "editor-row-container" }, div({ ref: "editorElement", className: "source-editor-mount devtools-monospace", }) ); } render() { return div( { key: "EDITOR_CONFIG", className: "editor-row-container" }, this.renderEditor() ); } } module.exports = connect(null, dispatch => ({ resetTargetSearchResult: () => dispatch(setTargetSearchResult(null)), }))(SourcePreview);