diff --git a/devtools/client/sourceeditor/editor.js b/devtools/client/sourceeditor/editor.js index 109dc208ddf4..a9e5fad2834f 100644 --- a/devtools/client/sourceeditor/editor.js +++ b/devtools/client/sourceeditor/editor.js @@ -234,7 +234,7 @@ Editor.prototype = { /** * Appends the current Editor instance to the element specified by - * 'el'. You can also provide your won iframe to host the editor as + * 'el'. You can also provide your own iframe to host the editor as * an optional second parameter. This method actually creates and * loads CodeMirror and all its dependencies. * diff --git a/devtools/client/themes/webconsole.css b/devtools/client/themes/webconsole.css index 4c16cd76c992..a556deee9ae0 100644 --- a/devtools/client/themes/webconsole.css +++ b/devtools/client/themes/webconsole.css @@ -348,6 +348,40 @@ textarea.jsterm-input-node:focus { border-bottom-right-radius: 0; } +/* CodeMirror-powered JsTerm */ +.jsterm-cm .jsterm-input-container { + /* Always allow scrolling on input - it auto expands in js by setting height, + but don't want it to get bigger than the window. 24px = toolbar height. */ + max-height: calc(90vh - 24px); +} + +.jsterm-cm .jsterm-input-container > .CodeMirror { + border: 1px solid transparent; + font-size: inherit; + line-height: 16px; + padding-inline-start: 20px; + /* input icon */ + background-image: var(--theme-command-line-image); + background-repeat: no-repeat; + background-size: 16px 16px; + background-position: 4px 4px; +} + +.jsterm-cm .jsterm-input-container > .CodeMirror-focused { + background-image: var(--theme-command-line-image-focus); + border: 1px solid var(--blue-50); + transition: border-color 0.2s ease-in-out; +} + +:root[platform="mac"] .jsterm-cm .jsterm-input-container > .CodeMirror { + border-radius: 0 0 4px 4px; +} + +/* Unset the bottom right radius on the jsterm inputs when the sidebar is visible */ +:root[platform="mac"] .jsterm-cm .sidebar ~ .jsterm-input-container > .CodeMirror { + border-bottom-right-radius: 0; +} + /* Security styles */ .message.security > .indent { @@ -630,7 +664,6 @@ a.learn-more-link.webconsole-learn-more-link { } .webconsole-output { - flex: 1; overflow: auto; } @@ -955,6 +988,7 @@ body #output-container { grid-template-columns: minmax(200px, 1fr) auto; grid-template-rows: auto 1fr auto auto; height: 100%; + max-height: 100%; width: 100vw; } @@ -1035,6 +1069,13 @@ html[dir="rtl"] .webconsole-output-wrapper img.collapse-button.arrow:not(.expand grid-row: 1 / -1; grid-column: -1 / -2; background-color: var(--theme-sidebar-background); + border-inline-start: 1px solid var(--theme-splitter-color); +} + +.sidebar .splitter { + /* Let the parent component handle the border. This is needed otherwise there is a visual + glitch between the input and the sidebar borders */ + background-color: transparent; } .split-box.vert.sidebar { diff --git a/devtools/client/webconsole/components/App.js b/devtools/client/webconsole/components/App.js index 9c8d5397a2e9..f9a428dd5e59 100644 --- a/devtools/client/webconsole/components/App.js +++ b/devtools/client/webconsole/components/App.js @@ -43,6 +43,7 @@ class App extends Component { onFirstMeaningfulPaint: PropTypes.func.isRequired, serviceContainer: PropTypes.object.isRequired, closeSplitConsole: PropTypes.func.isRequired, + jstermCodeMirror: PropTypes.bool, }; } @@ -122,8 +123,14 @@ class App extends Component { onFirstMeaningfulPaint, serviceContainer, closeSplitConsole, + jstermCodeMirror, } = this.props; + const classNames = ["webconsole-output-wrapper"]; + if (jstermCodeMirror) { + classNames.push("jsterm-cm"); + } + // Render the entire Console panel. The panel consists // from the following parts: // * FilterBar - Buttons & free text for content filtering @@ -133,7 +140,7 @@ class App extends Component { // * JSTerm - Input command line. return ( div({ - className: "webconsole-output-wrapper", + className: classNames.join(" "), ref: node => { this.node = node; }}, @@ -158,6 +165,7 @@ class App extends Component { JSTerm({ hud, onPaste: this.onPaste, + codeMirrorEnabled: jstermCodeMirror, }), ) ); diff --git a/devtools/client/webconsole/components/JSTerm.js b/devtools/client/webconsole/components/JSTerm.js index 6af175b09347..aee6a8a54cf1 100644 --- a/devtools/client/webconsole/components/JSTerm.js +++ b/devtools/client/webconsole/components/JSTerm.js @@ -18,6 +18,7 @@ loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage"); loader.lazyRequireGetter(this, "PropTypes", "devtools/client/shared/vendor/react-prop-types"); loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true); +loader.lazyRequireGetter(this, "Editor", "devtools/client/sourceeditor/editor"); const l10n = require("devtools/client/webconsole/webconsole-l10n"); @@ -53,6 +54,7 @@ class JSTerm extends Component { hud: PropTypes.object.isRequired, // Handler for clipboard 'paste' event (also used for 'drop' event). onPaste: PropTypes.func, + codeMirrorEnabled: PropTypes.bool, }; } @@ -146,10 +148,6 @@ class JSTerm extends Component { } componentDidMount() { - if (!this.inputNode) { - return; - } - let autocompleteOptions = { onSelect: this.onAutocompleteSelect.bind(this), onClick: this.acceptProposedCompletion.bind(this), @@ -166,21 +164,54 @@ class JSTerm extends Component { // such as the browser console which doesn't have a toolbox. this.autocompletePopup = new AutocompletePopup(tooltipDoc, autocompleteOptions); - this.inputBorderSize = this.inputNode.getBoundingClientRect().height - - this.inputNode.clientHeight; + this.inputBorderSize = this.inputNode + ? this.inputNode.getBoundingClientRect().height - this.inputNode.clientHeight + : 0; // Update the character width and height needed for the popup offset // calculations. this._updateCharSize(); - this.inputNode.addEventListener("keypress", this._keyPress); - this.inputNode.addEventListener("input", this._inputEventHandler); - this.inputNode.addEventListener("keyup", this._inputEventHandler); - this.inputNode.addEventListener("focus", this._focusEventHandler); + if (this.props.codeMirrorEnabled) { + if (this.node) { + this.editor = new Editor({ + autofocus: true, + enableCodeFolding: false, + gutters: [], + lineWrapping: true, + mode: Editor.modes.js, + styleActiveLine: false, + tabIndex: "0", + viewportMargin: Infinity, + extraKeys: { + "Enter": (e, cm) => { + let autoMultiline = Services.prefs.getBoolPref(PREF_AUTO_MULTILINE); + if (e.shiftKey + || ( + !Debugger.isCompilableUnit(this.getInputValue()) + && autoMultiline + ) + ) { + // shift return or incomplete statement + return "CodeMirror.Pass"; + } + this.execute(); + return null; + }, + }, + }); + this.editor.appendToLocalElement(this.node); + } + } else if (this.inputNode) { + this.inputNode.addEventListener("keypress", this._keyPress); + this.inputNode.addEventListener("input", this._inputEventHandler); + this.inputNode.addEventListener("keyup", this._inputEventHandler); + this.inputNode.addEventListener("focus", this._focusEventHandler); + this.focus(); + } + this.hud.window.addEventListener("blur", this._blurEventHandler); this.lastInputValue && this.setInputValue(this.lastInputValue); - - this.focus(); } shouldComponentUpdate() { @@ -256,7 +287,9 @@ class JSTerm extends Component { } focus() { - if (this.inputNode && !this.inputNode.getAttribute("focused")) { + if (this.editor) { + this.editor.focus(); + } else if (this.inputNode && !this.inputNode.getAttribute("focused")) { this.inputNode.focus(); } } @@ -531,6 +564,10 @@ class JSTerm extends Component { * @returns void */ resizeInput() { + if (this.props.codeMirrorEnabled) { + return; + } + if (!this.inputNode) { return; } @@ -542,10 +579,7 @@ class JSTerm extends Component { inputNode.style.height = "auto"; // Now resize the input field to fit its contents. - // TODO: remove `inputNode.inputField.scrollHeight` when the old - // console UI is removed. See bug 1381834 - let scrollHeight = inputNode.inputField ? - inputNode.inputField.scrollHeight : inputNode.scrollHeight; + let scrollHeight = inputNode.scrollHeight; if (scrollHeight > 0) { inputNode.style.height = (scrollHeight + this.inputBorderSize) + "px"; @@ -562,13 +596,20 @@ class JSTerm extends Component { * @returns void */ setInputValue(newValue) { - if (!this.inputNode) { - return; + if (this.props.codeMirrorEnabled) { + if (this.editor) { + this.editor.setText(newValue); + } + } else { + if (!this.inputNode) { + return; + } + + this.inputNode.value = newValue; + this.completeNode.value = ""; } - this.inputNode.value = newValue; this.lastInputValue = newValue; - this.completeNode.value = ""; this.resizeInput(); this._inputChanged = true; this.emit("set-input-value"); @@ -579,6 +620,10 @@ class JSTerm extends Component { * @returns string */ getInputValue() { + if (this.props.codeMirrorEnabled) { + return this.editor.getText() || ""; + } + return this.inputNode ? this.inputNode.value || "" : ""; } @@ -1256,6 +1301,10 @@ class JSTerm extends Component { * @private */ _updateCharSize() { + if (this.props.codeMirrorEnabled || !this.inputNode) { + return; + } + let doc = this.hud.document; let tempLabel = doc.createElement("span"); let style = tempLabel.style; @@ -1307,6 +1356,18 @@ class JSTerm extends Component { return null; } + if (this.props.codeMirrorEnabled) { + return dom.div({ + className: "jsterm-input-container devtools-monospace", + key: "jsterm-container", + style: {direction: "ltr"}, + "aria-live": "off", + ref: node => { + this.node = node; + }, + }); + } + let { onPaste } = this.props; diff --git a/devtools/client/webconsole/constants.js b/devtools/client/webconsole/constants.js index ce0415877fc5..ba8c90707335 100644 --- a/devtools/client/webconsole/constants.js +++ b/devtools/client/webconsole/constants.js @@ -52,8 +52,11 @@ const prefs = { FILTER_BAR: "ui.filterbar", // Persist is only used by the webconsole. PERSIST: "devtools.webconsole.persistlog", + }, + FEATURES: { // We use the same pref to enable the sidebar on webconsole and browser console. SIDEBAR_TOGGLE: "devtools.webconsole.sidebarToggle", + JSTERM_CODE_MIRROR: "devtools.webconsole.jsterm.codeMirror", } } }; diff --git a/devtools/client/webconsole/reducers/prefs.js b/devtools/client/webconsole/reducers/prefs.js index 6331f1f5d1b3..9ad1fcdbcd34 100644 --- a/devtools/client/webconsole/reducers/prefs.js +++ b/devtools/client/webconsole/reducers/prefs.js @@ -7,7 +7,8 @@ const PrefState = (overrides) => Object.freeze(Object.assign({ logLimit: 1000, - sidebarToggle: false + sidebarToggle: false, + jstermCodeMirror: false, }, overrides)); function prefs(state = PrefState(), action) { diff --git a/devtools/client/webconsole/store.js b/devtools/client/webconsole/store.js index 5115d73cee35..c63b9b0997a2 100644 --- a/devtools/client/webconsole/store.js +++ b/devtools/client/webconsole/store.js @@ -49,10 +49,15 @@ function configureStore(hud, options = {}) { const logLimit = options.logLimit || Math.max(getIntPref("devtools.hud.loglimit"), 1); - const sidebarToggle = getBoolPref(PREFS.UI.SIDEBAR_TOGGLE); + const sidebarToggle = getBoolPref(PREFS.FEATURES.SIDEBAR_TOGGLE); + const jstermCodeMirror = getBoolPref(PREFS.FEATURES.JSTERM_CODE_MIRROR); const initialState = { - prefs: PrefState({ logLimit, sidebarToggle }), + prefs: PrefState({ + logLimit, + sidebarToggle, + jstermCodeMirror, + }), filters: FilterState({ error: getBoolPref(PREFS.FILTER.ERROR), warn: getBoolPref(PREFS.FILTER.WARN), diff --git a/devtools/client/webconsole/webconsole-output-wrapper.js b/devtools/client/webconsole/webconsole-output-wrapper.js index 0687304f86de..b3b904c3a9aa 100644 --- a/devtools/client/webconsole/webconsole-output-wrapper.js +++ b/devtools/client/webconsole/webconsole-output-wrapper.js @@ -214,6 +214,7 @@ WebConsoleOutputWrapper.prototype = { hud, onFirstMeaningfulPaint: resolve, closeSplitConsole: this.closeSplitConsole.bind(this), + jstermCodeMirror: store.getState().prefs.jstermCodeMirror, }); // Render the root Application component.