Bug 1489491 - Populate reverse search with input text selection; r=bgrins.
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Fri, 18 Jan 2019 13:20:44 +0000
changeset 511542 529a105310435244ef67c078a810ad92b22d9327
parent 511541 83d6478d261167ec129ffc5286a60bfbb39d0c31
child 511543 7d151a27de64cce8b0ed96443c1888656341e420
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1489491
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1489491 - Populate reverse search with input text selection; r=bgrins. If the user selected some text in the console input and opens the reverse search UI, we populate the reverse search input with the selected text and do a reverse search with this text. A test is added to ensure this works as expected. Differential Revision: https://phabricator.services.mozilla.com/D16843
devtools/client/webconsole/actions/ui.js
devtools/client/webconsole/components/App.js
devtools/client/webconsole/components/JSTerm.js
devtools/client/webconsole/components/ReverseSearchInput.js
devtools/client/webconsole/reducers/history.js
devtools/client/webconsole/reducers/ui.js
devtools/client/webconsole/test/mochitest/browser.ini
devtools/client/webconsole/test/mochitest/browser_webconsole_reverse_search_initial_value.js
--- a/devtools/client/webconsole/actions/ui.js
+++ b/devtools/client/webconsole/actions/ui.js
@@ -98,19 +98,20 @@ function showMessageObjectInSidebar(acto
 
 function showObjectInSidebar(grip) {
   return {
     type: SHOW_OBJECT_IN_SIDEBAR,
     grip,
   };
 }
 
-function reverseSearchInputToggle() {
+function reverseSearchInputToggle({initialValue} = {}) {
   return {
     type: REVERSE_SEARCH_INPUT_TOGGLE,
+    initialValue,
   };
 }
 
 module.exports = {
   filterBarToggle,
   initialize,
   persistToggle,
   reverseSearchInputToggle,
--- a/devtools/client/webconsole/components/App.js
+++ b/devtools/client/webconsole/components/App.js
@@ -44,37 +44,40 @@ class App extends Component {
       hud: PropTypes.object.isRequired,
       notifications: PropTypes.object,
       onFirstMeaningfulPaint: PropTypes.func.isRequired,
       serviceContainer: PropTypes.object.isRequired,
       closeSplitConsole: PropTypes.func.isRequired,
       jstermCodeMirror: PropTypes.bool,
       currentReverseSearchEntry: PropTypes.string,
       reverseSearchInputVisible: PropTypes.bool,
+      reverseSearchInitialValue: PropTypes.string,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.onClick = this.onClick.bind(this);
     this.onPaste = this.onPaste.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
 
   onKeyDown(event) {
     const {
       dispatch,
+      hud,
     } = this.props;
 
     if (
       (!isMacOS && event.key === "F9") ||
       (isMacOS && event.key === "r" && event.ctrlKey === true)
     ) {
-      dispatch(actions.reverseSearchInputToggle());
+      const initialValue = hud.jsterm && hud.jsterm.getSelectedText();
+      dispatch(actions.reverseSearchInputToggle({initialValue}));
       event.stopPropagation();
     }
   }
 
   onClick(event) {
     const target = event.originalTarget || event.target;
     const {
       reverseSearchInputVisible,
@@ -191,16 +194,17 @@ class App extends Component {
     const {
       attachRefToHud,
       hud,
       notifications,
       onFirstMeaningfulPaint,
       serviceContainer,
       closeSplitConsole,
       jstermCodeMirror,
+      reverseSearchInitialValue,
     } = this.props;
 
     const classNames = ["webconsole-output-wrapper"];
     if (jstermCodeMirror) {
       classNames.push("jsterm-cm");
     }
 
     // Render the entire Console panel. The panel consists
@@ -238,16 +242,17 @@ class App extends Component {
           JSTerm({
             hud,
             serviceContainer,
             onPaste: this.onPaste,
             codeMirrorEnabled: jstermCodeMirror,
           }),
           ReverseSearchInput({
             hud,
+            initialValue: reverseSearchInitialValue,
           })
         ),
         SideBar({
           serviceContainer,
         }),
         ConfirmDialog({
           hud,
           serviceContainer,
@@ -256,15 +261,16 @@ class App extends Component {
       )
     );
   }
 }
 
 const mapStateToProps = state => ({
   notifications: getAllNotifications(state),
   reverseSearchInputVisible: state.ui.reverseSearchInputVisible,
+  reverseSearchInitialValue: state.ui.reverseSearchInitialValue,
 });
 
 const mapDispatchToProps = dispatch => ({
   dispatch,
 });
 
 module.exports = connect(mapStateToProps, mapDispatchToProps)(App);
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -729,16 +729,24 @@ class JSTerm extends Component {
   getSelectionStart() {
     if (this.props.codeMirrorEnabled) {
       return this.getInputValueBeforeCursor().length;
     }
 
     return this.inputNode ? this.inputNode.selectionStart : null;
   }
 
+  getSelectedText() {
+    if (this.inputNode) {
+      return this.inputNode.value.substring(
+        this.inputNode.selectionStart, this.inputNode.selectionEnd);
+    }
+    return this.editor.getSelection();
+  }
+
   /**
    * Even handler for the "beforeChange" event fired by codeMirror. This event is fired
    * when codeMirror is about to make a change to its DOM representation.
    */
   _onBeforeChange() {
     // clear the completionText before the change is done to prevent a visual glitch.
     // See Bug 1491776.
     this.setAutoCompletionText("");
--- a/devtools/client/webconsole/components/ReverseSearchInput.js
+++ b/devtools/client/webconsole/components/ReverseSearchInput.js
@@ -28,16 +28,17 @@ class ReverseSearchInput extends Compone
   static get propTypes() {
     return {
       dispatch: PropTypes.func.isRequired,
       hud: PropTypes.object.isRequired,
       reverseSearchResult: PropTypes.string,
       reverseSearchTotalResults: PropTypes.number,
       reverseSearchResultPosition: PropTypes.number,
       visible: PropTypes.bool,
+      initialValue: PropTypes.string,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.onInputKeyDown = this.onInputKeyDown.bind(this);
   }
@@ -50,16 +51,24 @@ class ReverseSearchInput extends Compone
       && this.props.reverseSearchTotalResults > 0
     ) {
       jsterm.setInputValue(this.props.reverseSearchResult);
     }
 
     if (prevProps.visible === true && this.props.visible === false) {
       jsterm.focus();
     }
+
+    if (
+      prevProps.visible === false &&
+      this.props.visible === true &&
+      this.props.initialValue
+    ) {
+      this.inputNode.value = this.props.initialValue;
+    }
   }
 
   onInputKeyDown(event) {
     const {
       keyCode,
       key,
       ctrlKey,
       shiftKey,
--- a/devtools/client/webconsole/reducers/history.js
+++ b/devtools/client/webconsole/reducers/history.js
@@ -49,17 +49,17 @@ function history(state = getInitialState
       return appendToHistory(state, prefsState, action.expression);
     case CLEAR_HISTORY:
       return clearHistory(state);
     case HISTORY_LOADED:
       return historyLoaded(state, action.entries);
     case UPDATE_HISTORY_POSITION:
       return updateHistoryPosition(state, action.direction, action.expression);
     case REVERSE_SEARCH_INPUT_TOGGLE:
-      return reverseSearchInputToggle(state);
+      return reverseSearchInputToggle(state, action);
     case REVERSE_SEARCH_INPUT_CHANGE:
       return reverseSearchInputChange(state, action.value);
     case REVERSE_SEARCH_BACK:
       return reverseSearchBack(state);
     case REVERSE_SEARCH_NEXT:
       return reverseSearchNext(state);
   }
   return state;
@@ -138,24 +138,33 @@ function updateHistoryPosition(state, di
       ...state,
       position: state.position + 1,
     };
   }
 
   return state;
 }
 
-function reverseSearchInputToggle(state) {
-  return {
-    ...state,
-    reverseSearchEnabled: !state.reverseSearchEnabled,
-    position: state.reverseSearchEnabled === true ? state.entries.length : undefined,
-    currentReverseSearchResults: null,
-    currentReverseSearchResultsPosition: null,
-  };
+function reverseSearchInputToggle(state, action) {
+  const { initialValue = "" } = action;
+
+  // We're going to close the reverse search, let's clean the state
+  if (state.reverseSearchEnabled) {
+    return {
+      ...state,
+      reverseSearchEnabled: false,
+      position: undefined,
+      currentReverseSearchResults: null,
+      currentReverseSearchResultsPosition: null,
+    };
+  }
+
+  // If we're enabling the reverse search, we treat it as a reverse search input change,
+  // since we can have an initial value.
+  return reverseSearchInputChange(state, initialValue);
 }
 
 function reverseSearchInputChange(state, searchString) {
   if (searchString === "") {
     return {
       ...state,
       position: undefined,
       currentReverseSearchResults: null,
--- a/devtools/client/webconsole/reducers/ui.js
+++ b/devtools/client/webconsole/reducers/ui.js
@@ -27,16 +27,17 @@ const UiState = (overrides) => Object.fr
   initialized: false,
   networkMessageActiveTabId: PANELS.HEADERS,
   persistLogs: false,
   sidebarVisible: false,
   timestampsVisible: true,
   gripInSidebar: null,
   closeButtonVisible: false,
   reverseSearchInputVisible: false,
+  reverseSearchInitialValue: "",
 }, overrides));
 
 function ui(state = UiState(), action) {
   switch (action.type) {
     case FILTER_BAR_TOGGLE:
       return Object.assign({}, state, {filterBarVisible: !state.filterBarVisible});
     case PERSIST_TOGGLE:
       return Object.assign({}, state, {persistLogs: !state.persistLogs});
@@ -56,17 +57,21 @@ function ui(state = UiState(), action) {
     case SHOW_OBJECT_IN_SIDEBAR:
       if (action.grip === state.gripInSidebar) {
         return state;
       }
       return Object.assign({}, state, {sidebarVisible: true, gripInSidebar: action.grip});
     case SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE:
       return Object.assign({}, state, {closeButtonVisible: action.shouldDisplayButton});
     case REVERSE_SEARCH_INPUT_TOGGLE:
-      return {...state, reverseSearchInputVisible: !state.reverseSearchInputVisible};
+      return {
+        ...state,
+        reverseSearchInputVisible: !state.reverseSearchInputVisible,
+        reverseSearchInitialValue: action.initialValue || "",
+      };
   }
 
   return state;
 }
 
 module.exports = {
   UiState,
   ui,
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -369,16 +369,17 @@ skip-if = true  # Bug 1438979
 subsuite = clipboard
 [browser_webconsole_output_copy_newlines.js]
 subsuite = clipboard
 [browser_webconsole_output_order.js]
 [browser_webconsole_persist.js]
 [browser_webconsole_reopen_closed_tab.js]
 [browser_webconsole_repeat_different_objects.js]
 [browser_webconsole_reverse_search.js]
+[browser_webconsole_reverse_search_initial_value.js]
 [browser_webconsole_reverse_search_keyboard_navigation.js]
 [browser_webconsole_reverse_search_mouse_navigation.js]
 [browser_webconsole_reverse_search_toggle.js]
 [browser_webconsole_sandbox_update_after_navigation.js]
 [browser_webconsole_script_errordoc_urls.js]
 [browser_webconsole_scroll.js]
 [browser_webconsole_select_all.js]
 [browser_webconsole_show_subresource_security_errors.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_reverse_search_initial_value.js
@@ -0,0 +1,87 @@
+/* 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/. */
+
+// Tests reverse search features.
+
+"use strict";
+
+const TEST_URI = `data:text/html,<meta charset=utf8>Test reverse search initial value`;
+
+add_task(async function() {
+  // Run test with legacy JsTerm
+  await pushPref("devtools.webconsole.jsterm.codeMirror", false);
+  await performTests();
+  // And then run it with the CodeMirror-powered one.
+  await pushPref("devtools.webconsole.jsterm.codeMirror", true);
+  await performTests();
+});
+
+async function performTests() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const { jsterm } = hud;
+
+  const jstermHistory = [
+    `Dog = "Snoopy"`,
+    `document
+       .querySelectorAll("*")
+       .forEach(console.log)`,
+    `document`,
+    `"😎"`,
+  ];
+
+  const onLastMessage = waitForMessage(hud, `"😎"`);
+  for (const input of jstermHistory) {
+    await jsterm.execute(input);
+  }
+  await onLastMessage;
+
+  const jstermInitialValue = "ado";
+  jsterm.setInputValue(jstermInitialValue);
+
+  info(`Select 2 chars ("do") from the input`);
+  if (jsterm.inputNode) {
+    jsterm.inputNode.selectionStart = 1;
+    jsterm.inputNode.selectionEnd = 3;
+  } else {
+    jsterm.editor.setSelection({line: 0, ch: 1}, {line: 0, ch: 3});
+  }
+
+  info("Check that the reverse search toolbar as the expected initial state");
+  let reverseSearchElement = await openReverseSearch(hud);
+  is(reverseSearchElement.querySelector("input").value, "do",
+    `Reverse search input has expected "do" value`);
+  is(isReverseSearchInputFocused(hud), true, "reverse search input is focused");
+  ok(reverseSearchElement, "Reverse search is displayed with a keyboard shortcut");
+  const infoElement = getReverseSearchInfoElement(hud);
+  is(infoElement.textContent, "3 of 3 results", "The reverse info has the expected text");
+
+  const previousButton = reverseSearchElement.querySelector(".search-result-button-prev");
+  const nextButton = reverseSearchElement.querySelector(".search-result-button-next");
+  ok(previousButton, "Previous navigation button is displayed");
+  ok(nextButton, "Next navigation button is displayed");
+
+  is(jsterm.getInputValue(), "document", "JsTerm has the expected input");
+  is(jsterm.autocompletePopup.isOpen, false,
+    "Setting the input value did not trigger the autocompletion");
+
+  const onJsTermValueChanged = jsterm.once("set-input-value");
+  EventUtils.sendString("g");
+  await onJsTermValueChanged;
+  is(jsterm.getInputValue(), `Dog = "Snoopy"`, "JsTerm input was updated");
+  is(infoElement.textContent, "1 result", "The reverse info has the expected text");
+  ok(
+    !reverseSearchElement.querySelector(".search-result-button-prev") &&
+    !reverseSearchElement.querySelector(".search-result-button-next"),
+    "The results navigation buttons are not displayed when there's only one result"
+  );
+
+  info("Check that there's no initial value when no text is selected");
+  EventUtils.synthesizeKey("KEY_Escape");
+  await waitFor(() => !getReverseSearchElement(hud));
+
+  info("Check that opening the reverse search input is empty after opening it again");
+  reverseSearchElement = await openReverseSearch(hud);
+  is(reverseSearchElement.querySelector("input").value, "",
+    "Reverse search input is empty");
+}