diff --git a/devtools/client/locales/en-US/webconsole.properties b/devtools/client/locales/en-US/webconsole.properties index 675ac57b29dc..8690706f7f30 100644 --- a/devtools/client/locales/en-US/webconsole.properties +++ b/devtools/client/locales/en-US/webconsole.properties @@ -285,3 +285,35 @@ webconsole.closeSplitConsoleButton.tooltip=Close Split Console (Esc) # LOCALIZATION NOTE (webconsole.closeSidebarButton.tooltip): This is the tooltip for # the close button of the sidebar. webconsole.closeSidebarButton.tooltip=Close Sidebar + +# LOCALIZATION NOTE (webconsole.reverseSearch.input.placeHolder): +# This string is displayed in the placeholder of the reverse search input in the console. +webconsole.reverseSearch.input.placeHolder=Search history + +# LOCALIZATION NOTE (webconsole.reverseSearch.result.closeButton.tooltip): +# This string is displayed in the tooltip of the close button in the reverse search toolbar. +# A keyboard shortcut will be shown inside the latter pair of brackets. +webconsole.reverseSearch.closeButton.tooltip=Close (%S) + +# LOCALIZATION NOTE (webconsole.reverseSearch.results): +# This string is displayed in the reverse search UI when there are at least one result +# to the search. +# This is a semi-colon list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 index of current search result displayed. +# #2 total number of search results. +webconsole.reverseSearch.results=1 result;#1 of #2 results + +# LOCALIZATION NOTE (webconsole.reverseSearch.noResult): +# This string is displayed in the reverse search UI when there is no results to the search. +webconsole.reverseSearch.noResult=No results + +# LOCALIZATION NOTE (webconsole.reverseSearch.result.previousButton.tooltip): +# This string is displayed in the tooltip of the "previous result" button in the reverse search toolbar. +# A keyboard shortcut will be shown inside the latter pair of brackets. +webconsole.reverseSearch.result.previousButton.tooltip=Previous result (%S) + +# LOCALIZATION NOTE (webconsole.reverseSearch.result.nextButton.tooltip): +# This string is displayed in the tooltip of the "next result" button in the reverse search toolbar. +# A keyboard shortcut will be shown inside the latter pair of brackets. +webconsole.reverseSearch.result.nextButton.tooltip=Next result (%S) diff --git a/devtools/client/themes/webconsole.css b/devtools/client/themes/webconsole.css index 0362df45f1ce..f474aff3c6e6 100644 --- a/devtools/client/themes/webconsole.css +++ b/devtools/client/themes/webconsole.css @@ -882,6 +882,7 @@ body { min-height: 28px; overflow-y: auto; overflow-x: hidden; + flex-grow: 1; } .jsterm-cm .jsterm-input-container { @@ -892,11 +893,6 @@ body { border-top: none; } -/* Last item in the flex wrapper should take the whole remaining height */ -.webconsole-flex-wrapper > :last-child { - flex-grow: 1; -} - /* Object Inspector */ .webconsole-output-wrapper .object-inspector.tree { display: inline-block; diff --git a/devtools/client/webconsole/actions/history.js b/devtools/client/webconsole/actions/history.js index b89b760d19b2..7fa023e7a64a 100644 --- a/devtools/client/webconsole/actions/history.js +++ b/devtools/client/webconsole/actions/history.js @@ -11,6 +11,9 @@ const { CLEAR_HISTORY, HISTORY_LOADED, UPDATE_HISTORY_POSITION, + REVERSE_SEARCH_INPUT_CHANGE, + REVERSE_SEARCH_BACK, + REVERSE_SEARCH_NEXT, } = require("devtools/client/webconsole/constants"); /** @@ -57,9 +60,31 @@ function updateHistoryPosition(direction, expression) { }; } +function reverseSearchInputChange(value) { + return { + type: REVERSE_SEARCH_INPUT_CHANGE, + value, + }; +} + +function showReverseSearchNext() { + return { + type: REVERSE_SEARCH_NEXT, + }; +} + +function showReverseSearchBack() { + return { + type: REVERSE_SEARCH_BACK + }; +} + module.exports = { appendToHistory, clearHistory, historyLoaded, updateHistoryPosition, + reverseSearchInputChange, + showReverseSearchNext, + showReverseSearchBack, }; diff --git a/devtools/client/webconsole/actions/ui.js b/devtools/client/webconsole/actions/ui.js index 18a38631e7af..0d0f6f8b743b 100644 --- a/devtools/client/webconsole/actions/ui.js +++ b/devtools/client/webconsole/actions/ui.js @@ -14,14 +14,15 @@ const { INITIALIZE, PERSIST_TOGGLE, PREFS, + REVERSE_SEARCH_INPUT_TOGGLE, SELECT_NETWORK_MESSAGE_TAB, - SIDEBAR_CLOSE, SHOW_OBJECT_IN_SIDEBAR, - TIMESTAMPS_TOGGLE, + SIDEBAR_CLOSE, SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE, + TIMESTAMPS_TOGGLE, } = require("devtools/client/webconsole/constants"); -function filterBarToggle(show) { +function filterBarToggle() { return (dispatch, getState, {prefsService}) => { dispatch({ type: FILTER_BAR_TOGGLE, @@ -31,7 +32,7 @@ function filterBarToggle(show) { }; } -function persistToggle(show) { +function persistToggle() { return (dispatch, getState, {prefsService}) => { dispatch({ type: PERSIST_TOGGLE, @@ -102,14 +103,21 @@ function showObjectInSidebar(grip) { }; } +function reverseSearchInputToggle() { + return { + type: REVERSE_SEARCH_INPUT_TOGGLE + }; +} + module.exports = { filterBarToggle, initialize, persistToggle, + reverseSearchInputToggle, selectNetworkMessageTab, - sidebarClose, showMessageObjectInSidebar, showObjectInSidebar, - timestampsToggle, + sidebarClose, splitConsoleCloseButtonToggle, + timestampsToggle, }; diff --git a/devtools/client/webconsole/components/App.js b/devtools/client/webconsole/components/App.js index 04a27bba5562..989365523d1f 100644 --- a/devtools/client/webconsole/components/App.js +++ b/devtools/client/webconsole/components/App.js @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; +const Services = require("Services"); const { Component, createFactory } = require("devtools/client/shared/vendor/react"); const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); const dom = require("devtools/client/shared/vendor/react-dom-factories"); @@ -12,6 +13,7 @@ const actions = require("devtools/client/webconsole/actions/index"); const ConsoleOutput = createFactory(require("devtools/client/webconsole/components/ConsoleOutput")); const FilterBar = createFactory(require("devtools/client/webconsole/components/FilterBar")); const SideBar = createFactory(require("devtools/client/webconsole/components/SideBar")); +const ReverseSearchInput = createFactory(require("devtools/client/webconsole/components/ReverseSearchInput")); const JSTerm = createFactory(require("devtools/client/webconsole/components/JSTerm")); const NotificationBox = createFactory(require("devtools/client/shared/components/NotificationBox").NotificationBox); @@ -27,8 +29,8 @@ const { } = require("devtools/client/shared/components/NotificationBox"); const { getAllNotifications } = require("devtools/client/webconsole/selectors/notifications"); - const { div } = dom; +const isMacOS = Services.appinfo.OS === "Darwin"; /** * Console root Application component. @@ -44,6 +46,9 @@ class App extends Component { serviceContainer: PropTypes.object.isRequired, closeSplitConsole: PropTypes.func.isRequired, jstermCodeMirror: PropTypes.bool, + jstermReverseSearch: PropTypes.bool, + currentReverseSearchEntry: PropTypes.string, + reverseSearchInputVisible: PropTypes.bool, }; } @@ -52,14 +57,41 @@ class App extends Component { this.onClick = this.onClick.bind(this); this.onPaste = this.onPaste.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + } + + onKeyDown(event) { + const { + dispatch, + jstermReverseSearch, + } = this.props; + + if ( + jstermReverseSearch && ( + (!isMacOS && event.key === "F9") || + (isMacOS && event.key === "r" && event.ctrlKey === true) + ) + ) { + dispatch(actions.reverseSearchInputToggle()); + event.stopPropagation(); + } } onClick(event) { const target = event.originalTarget || event.target; const { + reverseSearchInputVisible, + dispatch, hud, } = this.props; + if (reverseSearchInputVisible === true && !target.closest(".reverse-search")) { + event.preventDefault(); + event.stopPropagation(); + dispatch(actions.reverseSearchInputToggle()); + return; + } + // Do not focus on middle/right-click or 2+ clicks. if (event.detail !== 1 || event.button !== 0) { return; @@ -74,6 +106,12 @@ class App extends Component { if (target.closest("input")) { return; } + + // Do not focus if the click happened in the reverse search toolbar. + if (target.closest(".reverse-search")) { + return; + } + // Do not focus if something other than the output region was clicked // (including e.g. the clear messages button in toolbar) if (!target.closest(".webconsole-output-wrapper")) { @@ -161,6 +199,7 @@ class App extends Component { serviceContainer, closeSplitConsole, jstermCodeMirror, + jstermReverseSearch, } = this.props; const classNames = ["webconsole-output-wrapper"]; @@ -172,12 +211,14 @@ class App extends Component { // from the following parts: // * FilterBar - Buttons & free text for content filtering // * Content - List of logs & messages - // * SideBar - Object inspector // * NotificationBox - Notifications for JSTerm (self-xss warning at the moment) // * JSTerm - Input command line. + // * ReverseSearchInput - Reverse search input. + // * SideBar - Object inspector return ( div({ className: classNames.join(" "), + onKeyDown: this.onKeyDown, onClick: this.onClick, ref: node => { this.node = node; @@ -204,6 +245,11 @@ class App extends Component { onPaste: this.onPaste, codeMirrorEnabled: jstermCodeMirror, }), + jstermReverseSearch + ? ReverseSearchInput({ + hud, + }) + : null ), SideBar({ serviceContainer, @@ -215,6 +261,7 @@ class App extends Component { const mapStateToProps = state => ({ notifications: getAllNotifications(state), + reverseSearchInputVisible: state.ui.reverseSearchInputVisible, }); const mapDispatchToProps = dispatch => ({ diff --git a/devtools/client/webconsole/components/JSTerm.js b/devtools/client/webconsole/components/JSTerm.js index ef636523ea3d..357fd9a581f4 100644 --- a/devtools/client/webconsole/components/JSTerm.js +++ b/devtools/client/webconsole/components/JSTerm.js @@ -682,7 +682,10 @@ class JSTerm extends Component { * The new value to set. * @returns void */ - setInputValue(newValue = "") { + setInputValue(newValue) { + newValue = newValue || ""; + this.lastInputValue = newValue; + if (this.props.codeMirrorEnabled) { if (this.editor) { // In order to get the autocomplete popup to work properly, we need to set the @@ -710,9 +713,7 @@ class JSTerm extends Component { this.completeNode.value = ""; } - this.lastInputValue = newValue; this.resizeInput(); - this.emit("set-input-value"); } diff --git a/devtools/client/webconsole/components/ReverseSearchInput.css b/devtools/client/webconsole/components/ReverseSearchInput.css new file mode 100644 index 000000000000..630dfa84b135 --- /dev/null +++ b/devtools/client/webconsole/components/ReverseSearchInput.css @@ -0,0 +1,84 @@ +/* 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/. */ + +.reverse-search { + display: flex; + font-size: inherit; + min-height: 26px; + color: var(--theme-body-color); + padding-block-start: 2px; + align-items: baseline; + border: 1px solid transparent; + transition: border-color 0.2s ease-in-out; +} + +.reverse-search:focus-within { + border-color: var(--blue-50); +} + +.reverse-search { + flex-shrink: 0; +} + +.reverse-search input { + border: none; + flex-grow: 1; + padding-inline-start: var(--console-inline-start-gutter); + background: transparent; + color: currentColor; + background-image: var(--magnifying-glass-image); + background-repeat: no-repeat; + background-size: 12px 12px; + background-position: 10px 2px; + -moz-context-properties: fill; +} + +.reverse-search input:focus { + border: none; + outline: none; +} + +.reverse-search:not(.no-result) input:focus { + fill: var(--console-input-icon-focused); +} + +.reverse-search-info { + flex-shrink: 0; + padding: 0 8px; + color: var(--comment-node-color); +} + +.search-result-button-prev, +.search-result-button-next, +.reverse-search-close-button { + padding: 4px 0; + margin: 0; + border-radius: 0; +} + +.search-result-button-prev::before { + background-image: url("chrome://devtools/skin/images/arrowhead-up.svg"); + background-size: 16px; + fill: var(--comment-node-color); +} + +.search-result-button-next::before { + background-image: url("chrome://devtools/skin/images/arrowhead-down.svg"); + background-size: 16px; + fill: var(--comment-node-color); +} + +.reverse-search-close-button::before { + fill: var(--comment-node-color); + background-image: var(--close-button-image); +} + +.reverse-search.no-result input { + fill: var(--error-color); +} + +.reverse-search.no-result, +.reverse-search.no-result input { + color: var(--error-color); +} diff --git a/devtools/client/webconsole/components/ReverseSearchInput.js b/devtools/client/webconsole/components/ReverseSearchInput.js new file mode 100644 index 000000000000..1d6c198868ed --- /dev/null +++ b/devtools/client/webconsole/components/ReverseSearchInput.js @@ -0,0 +1,225 @@ +/* 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"; + +// React & Redux +const { Component } = require("devtools/client/shared/vendor/react"); +const dom = require("devtools/client/shared/vendor/react-dom-factories"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); + +const { l10n } = require("devtools/client/webconsole/utils/messages"); +const { PluralForm } = require("devtools/shared/plural-form"); +const { KeyCodes } = require("devtools/client/shared/keycodes"); + +const actions = require("devtools/client/webconsole/actions/index"); +const { + getReverseSearchTotalResults, + getReverseSearchResultPosition, + getReverseSearchResult, +} = require("devtools/client/webconsole/selectors/history"); + +const Services = require("Services"); +const isMacOS = Services.appinfo.OS === "Darwin"; + +class ReverseSearchInput extends Component { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + hud: PropTypes.object.isRequired, + reverseSearchResult: PropTypes.string, + reverseSearchTotalResults: PropTypes.Array, + reverseSearchResultPosition: PropTypes.int, + visible: PropTypes.bool, + }; + } + + constructor(props) { + super(props); + + this.onInputKeyDown = this.onInputKeyDown.bind(this); + } + + componentDidUpdate(prevProps) { + const {jsterm} = this.props.hud; + if ( + prevProps.reverseSearchResult !== this.props.reverseSearchResult + && this.props.visible + && this.props.reverseSearchTotalResults > 0 + ) { + jsterm.setInputValue(this.props.reverseSearchResult); + } + + if (prevProps.visible === true && this.props.visible === false) { + jsterm.focus(); + } + } + + onInputKeyDown(event) { + const { + keyCode, + key, + ctrlKey, + shiftKey, + } = event; + + const { + dispatch, + hud, + reverseSearchTotalResults, + } = this.props; + + // On Enter, we trigger an execute. + if (keyCode === KeyCodes.DOM_VK_RETURN) { + event.stopPropagation(); + dispatch(actions.reverseSearchInputToggle()); + hud.jsterm.execute(); + return; + } + + // On Escape (and Ctrl + c on OSX), we close the reverse search input. + if ( + keyCode === KeyCodes.DOM_VK_ESCAPE || ( + isMacOS && ctrlKey === true && key.toLowerCase() === "c" + ) + ) { + event.stopPropagation(); + dispatch(actions.reverseSearchInputToggle()); + return; + } + + const canNavigate = Number.isInteger(reverseSearchTotalResults) + && reverseSearchTotalResults > 1; + + if ( + (!isMacOS && key === "F9" && shiftKey === false) || + (isMacOS && ctrlKey === true && key.toLowerCase() === "r") + ) { + event.stopPropagation(); + event.preventDefault(); + if (canNavigate) { + dispatch(actions.showReverseSearchBack()); + } + return; + } + + if ( + (!isMacOS && key === "F9" && shiftKey === true) || + (isMacOS && ctrlKey === true && key.toLowerCase() === "s") + ) { + event.stopPropagation(); + event.preventDefault(); + if (canNavigate) { + dispatch(actions.showReverseSearchNext()); + } + } + } + + renderSearchInformation() { + const { + reverseSearchTotalResults, + reverseSearchResultPosition, + } = this.props; + + if (!Number.isInteger(reverseSearchTotalResults)) { + return null; + } + + let text; + if (reverseSearchTotalResults === 0) { + text = l10n.getStr("webconsole.reverseSearch.noResult"); + } else { + const resultsString = l10n.getStr("webconsole.reverseSearch.results"); + text = PluralForm.get(reverseSearchTotalResults, resultsString) + .replace("#1", reverseSearchResultPosition) + .replace("#2", reverseSearchTotalResults); + } + + return dom.div({className: "reverse-search-info"}, text); + } + + renderNavigationButtons() { + const { + dispatch, + reverseSearchTotalResults, + } = this.props; + + if (!Number.isInteger(reverseSearchTotalResults) || reverseSearchTotalResults <= 1) { + return null; + } + + return [ + dom.button({ + className: "devtools-button search-result-button-prev", + title: l10n.getFormatStr("webconsole.reverseSearch.result.previousButton.tooltip", + [isMacOS ? "Ctrl + R" : "F9"]), + onClick: () => { + dispatch(actions.showReverseSearchBack()); + this.inputNode.focus(); + } + }), + dom.button({ + className: "devtools-button search-result-button-next", + title: l10n.getFormatStr("webconsole.reverseSearch.result.nextButton.tooltip", + [isMacOS ? "Ctrl + S" : "Shift + F9"]), + onClick: () => { + dispatch(actions.showReverseSearchNext()); + this.inputNode.focus(); + } + }) + ]; + } + + render() { + const { + dispatch, + visible, + reverseSearchTotalResults, + } = this.props; + + if (!visible) { + return null; + } + + const classNames = ["reverse-search"]; + if (reverseSearchTotalResults === 0) { + classNames.push("no-result"); + } + + return dom.div({className: classNames.join(" ")}, + dom.input({ + ref: node => { + this.inputNode = node; + }, + autoFocus: true, + placeHolder: l10n.getStr("webconsole.reverseSearch.input.placeHolder"), + className: "reverse-search-input devtools-monospace", + onKeyDown: this.onInputKeyDown, + onInput: ({target}) => dispatch(actions.reverseSearchInputChange(target.value)) + }), + this.renderSearchInformation(), + this.renderNavigationButtons(), + dom.button({ + className: "devtools-button reverse-search-close-button", + title: l10n.getFormatStr("webconsole.reverseSearch.closeButton.tooltip", + ["Esc" + (isMacOS ? " | Ctrl + C" : "")]), + onClick: () => { + dispatch(actions.reverseSearchInputToggle()); + } + }) + ); + } +} + +const mapStateToProps = state => ({ + visible: state.ui.reverseSearchInputVisible, + reverseSearchTotalResults: getReverseSearchTotalResults(state), + reverseSearchResultPosition: getReverseSearchResultPosition(state), + reverseSearchResult: getReverseSearchResult(state), +}); + +const mapDispatchToProps = dispatch => ({dispatch}); + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ReverseSearchInput); diff --git a/devtools/client/webconsole/components/moz.build b/devtools/client/webconsole/components/moz.build index 31b12c04c895..06794fe469e9 100644 --- a/devtools/client/webconsole/components/moz.build +++ b/devtools/client/webconsole/components/moz.build @@ -22,5 +22,7 @@ DevToolsModules( 'MessageIcon.js', 'MessageIndent.js', 'MessageRepeat.js', + 'ReverseSearchInput.css', + 'ReverseSearchInput.js', 'SideBar.js' ) diff --git a/devtools/client/webconsole/constants.js b/devtools/client/webconsole/constants.js index bbd678da661c..909fde07aa49 100644 --- a/devtools/client/webconsole/constants.js +++ b/devtools/client/webconsole/constants.js @@ -6,12 +6,16 @@ "use strict"; const actionTypes = { + APPEND_NOTIFICATION: "APPEND_NOTIFICATION", + APPEND_TO_HISTORY: "APPEND_TO_HISTORY", BATCH_ACTIONS: "BATCH_ACTIONS", + CLEAR_HISTORY: "CLEAR_HISTORY", DEFAULT_FILTERS_RESET: "DEFAULT_FILTERS_RESET", FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE", FILTER_TEXT_SET: "FILTER_TEXT_SET", FILTER_TOGGLE: "FILTER_TOGGLE", FILTERS_CLEAR: "FILTERS_CLEAR", + HISTORY_LOADED: "HISTORY_LOADED", INITIALIZE: "INITIALIZE", MESSAGE_CLOSE: "MESSAGE_CLOSE", MESSAGE_OPEN: "MESSAGE_OPEN", @@ -22,18 +26,18 @@ const actionTypes = { NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST", PERSIST_TOGGLE: "PERSIST_TOGGLE", PRIVATE_MESSAGES_CLEAR: "PRIVATE_MESSAGES_CLEAR", - REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR", - SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB", - SIDEBAR_CLOSE: "SIDEBAR_CLOSE", - SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR", - TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE", - APPEND_NOTIFICATION: "APPEND_NOTIFICATION", REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION", + REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR", + REVERSE_SEARCH_INPUT_TOGGLE: "REVERSE_SEARCH_INPUT_TOGGLE", + SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB", + SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR", + SIDEBAR_CLOSE: "SIDEBAR_CLOSE", SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE: "SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE", - APPEND_TO_HISTORY: "APPEND_TO_HISTORY", - CLEAR_HISTORY: "CLEAR_HISTORY", - HISTORY_LOADED: "HISTORY_LOADED", + TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE", UPDATE_HISTORY_POSITION: "UPDATE_HISTORY_POSITION", + REVERSE_SEARCH_INPUT_CHANGE: "REVERSE_SEARCH_INPUT_CHANGE", + REVERSE_SEARCH_NEXT: "REVERSE_SEARCH_NEXT", + REVERSE_SEARCH_BACK: "REVERSE_SEARCH_BACK", }; const prefs = { @@ -63,6 +67,7 @@ const prefs = { // 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", + JSTERM_REVERSE_SEARCH: "devtools.webconsole.jsterm.reverse-search", } } }; diff --git a/devtools/client/webconsole/index.html b/devtools/client/webconsole/index.html index dfa1af4bc246..ddc783d2aaea 100644 --- a/devtools/client/webconsole/index.html +++ b/devtools/client/webconsole/index.html @@ -19,6 +19,7 @@ +