Bug 1577900: Add a prettyPrint button to the multiline editor of devtools. r=nchevobbe
authorPing Chen <remotenonsense@gmail.com>
Tue, 17 Nov 2020 07:38:54 +0000
changeset 557493 1766ea267c5e6ccad0961d2fde92a670dc476ad3
parent 557490 2a10175a100221e9de8412a7292d616199cdd53d
child 557494 28811d85b1b13479546d6a1c21c8e1d034b9cdc3
push id37959
push userbtara@mozilla.com
push dateTue, 17 Nov 2020 21:55:29 +0000
treeherdermozilla-central@9dd0b13d77b9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1577900
milestone85.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 1577900: Add a prettyPrint button to the multiline editor of devtools. r=nchevobbe Differential Revision: https://phabricator.services.mozilla.com/D95457
devtools/client/locales/en-US/webconsole.properties
devtools/client/webconsole/actions/input.js
devtools/client/webconsole/components/App.css
devtools/client/webconsole/components/Input/EditorToolbar.js
devtools/client/webconsole/components/Input/JSTerm.js
devtools/client/webconsole/constants.js
devtools/client/webconsole/reducers/ui.js
devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js
devtools/client/webconsole/test/node/mocha-test-setup.js
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -474,16 +474,21 @@ webconsole.editor.toolbar.reverseSearchB
 webconsole.editor.toolbar.reverseSearchButton.closeReverseSearch.tooltip=Close History Reverse Search (%S)
 
 # LOCALIZATION NOTE (webconsole.editor.toolbar.executeButton.tooltip)
 # Label used for the tooltip on the execute button, in the editor toolbar, which is
 # displayed when the editor mode is enabled (devtools.webconsole.input.editor=true).
 # Parameters: %S is the keyboard shortcut.
 webconsole.editor.toolbar.executeButton.tooltip=Run expression (%S). This won’t clear the input.
 
+# LOCALIZATION NOTE (webconsole.editor.toolbar.prettyPrintButton.tooltip)
+# Label used for the tooltip on the prettyPrint button, in the editor toolbar, which is
+# displayed when the editor mode is enabled (devtools.webconsole.input.editor=true).
+webconsole.editor.toolbar.prettyPrintButton.tooltip=Pretty print expression
+
 # LOCALIZATION NOTE (webconsole.editor.toolbar.executeButton.tooltip)
 # Label used for the tooltip on the history previous expression, in the editor toolbar,
 # which is displayed when the editor mode is enabled (devtools.webconsole.input.editor=true).
 webconsole.editor.toolbar.history.prevExpressionButton.tooltip=Previous Expression
 
 
 # LOCALIZATION NOTE (webconsole.editor.toolbar.executeButton.tooltip)
 # Label used for the tooltip on the history next expression, in the editor toolbar,
--- a/devtools/client/webconsole/actions/input.js
+++ b/devtools/client/webconsole/actions/input.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
 const {
   EVALUATE_EXPRESSION,
   SET_TERMINAL_INPUT,
   SET_TERMINAL_EAGER_RESULT,
+  EDITOR_PRETTY_PRINT,
 } = require("devtools/client/webconsole/constants");
 const { getAllPrefs } = require("devtools/client/webconsole/selectors/prefs");
 const {
   ResourceWatcher,
 } = require("devtools/shared/resources/resource-watcher");
 const l10n = require("devtools/client/webconsole/utils/l10n");
 
 loader.lazyServiceGetter(
@@ -359,15 +360,22 @@ function getEagerEvaluationResult(respon
   // Don't show syntax errors results to the user.
   if (result?.isSyntaxError || (result && result.type == "undefined")) {
     return null;
   }
 
   return result;
 }
 
+function prettyPrintEditor() {
+  return {
+    type: EDITOR_PRETTY_PRINT,
+  };
+}
+
 module.exports = {
   evaluateExpression,
   focusInput,
   setInputValue,
   terminalInputChanged,
   updateInstantEvaluationResultForCurrentExpression,
+  prettyPrintEditor,
 };
--- a/devtools/client/webconsole/components/App.css
+++ b/devtools/client/webconsole/components/App.css
@@ -299,26 +299,28 @@ body {
   grid-area: editor-toolbar;
   border-inline-end: 1px solid var(--theme-splitter-color);
   display: grid;
   align-items: center;
   /*
      * The following elements are going to be present in the toolbar:
      *   - The run button
      *   - The evaluation selector button
+     *   - The pretty print button
+     *   - A separator
      *   - The history nav
      *   - A separator
      *   - The close button
      *
      * +-------------------------------------------+
-     * | ▶︎ Run  Top↕                    ˄ ˅ 🔍 | ✕ |
+     * | ▶︎ Run  Top↕               {} | ˄ ˅ 🔍 | ✕ |
      * +-------------------------------------------+
      *
      */
-  grid-template-columns: auto auto 1fr auto auto auto auto auto;
+  grid-template-columns: auto auto 1fr auto auto auto auto auto auto auto;
   height: unset;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-executeButton {
   padding-inline: 4px 8px;
   height: 20px;
   margin-inline-start: 5px;
   display: flex;
@@ -330,16 +332,24 @@ body {
   content: url("chrome://devtools/skin/images/webconsole/run.svg");
   height: 16px;
   width: 16px;
   -moz-context-properties: fill;
   fill: currentColor;
   margin-inline-end: 2px;
 }
 
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-prettyPrintButton {
+  grid-column: -7 / -8;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-prettyPrintSeparator {
+  grid-column: -6 / -7;
+}
+
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-prevExpressionButton {
   grid-column: -5 / -6;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-nextExpressionButton {
   grid-column: -4 / -5;
 }
 
@@ -350,16 +360,22 @@ body {
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-historyNavSeparator {
   grid-column: -2 / -3;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-closeButton {
   grid-column: -1 / -2;
 }
 
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-prettyPrintButton::before {
+  mask-image: url("chrome://devtools/content/debugger/images/prettyPrint.svg");
+  background-size: 16px;
+  background-color: var(--theme-icon-color);
+}
+
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-prevExpressionButton::before {
   background-image: url("chrome://devtools/skin/images/arrowhead-up.svg");
   background-size: 16px;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-nextExpressionButton::before {
   background-image: url("chrome://devtools/skin/images/arrowhead-down.svg");
   background-size: 16px;
--- a/devtools/client/webconsole/components/Input/EditorToolbar.js
+++ b/devtools/client/webconsole/components/Input/EditorToolbar.js
@@ -100,16 +100,28 @@ class EditorToolbar extends Component {
           ),
           onClick: () => dispatch(actions.evaluateExpression()),
         },
         l10n.getStr("webconsole.editor.toolbar.executeButton.label")
       ),
       this.renderEvaluationContextSelector(),
       dom.button({
         className:
+          "devtools-button webconsole-editor-toolbar-prettyPrintButton",
+        title: l10n.getStr(
+          "webconsole.editor.toolbar.prettyPrintButton.tooltip"
+        ),
+        onClick: () => dispatch(actions.prettyPrintEditor()),
+      }),
+      dom.div({
+        className:
+          "devtools-separator webconsole-editor-toolbar-prettyPrintSeparator",
+      }),
+      dom.button({
+        className:
           "devtools-button webconsole-editor-toolbar-history-prevExpressionButton",
         title: l10n.getStr(
           "webconsole.editor.toolbar.history.prevExpressionButton.tooltip"
         ),
         onClick: () => {
           webConsoleUI.jsterm.historyPeruse(HISTORY_BACK);
         },
       }),
--- a/devtools/client/webconsole/components/Input/JSTerm.js
+++ b/devtools/client/webconsole/components/Input/JSTerm.js
@@ -40,16 +40,21 @@ loader.lazyRequireGetter(
 );
 loader.lazyRequireGetter(
   this,
   "l10n",
   "devtools/client/webconsole/utils/messages",
   true
 );
 loader.lazyRequireGetter(this, "saveAs", "devtools/shared/DevToolsUtils", true);
+loader.lazyRequireGetter(
+  this,
+  "beautify",
+  "devtools/shared/jsbeautify/beautify"
+);
 
 // React & Redux
 const {
   Component,
   createFactory,
 } = 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");
@@ -108,16 +113,17 @@ class JSTerm extends Component {
       editorToggle: PropTypes.func.isRequired,
       // Dismiss the editor onboarding UI.
       editorOnboardingDismiss: PropTypes.func.isRequired,
       // Set the last JS input value.
       terminalInputChanged: PropTypes.func.isRequired,
       // Is the input in editor mode.
       editorMode: PropTypes.bool,
       editorWidth: PropTypes.number,
+      editorPrettifiedAt: PropTypes.number,
       showEditorOnboarding: PropTypes.bool,
       autocomplete: PropTypes.bool,
       showEvaluationContextSelector: PropTypes.bool,
       autocompletePopupPosition: PropTypes.string,
       inputEnabled: PropTypes.bool,
     };
   }
 
@@ -591,16 +597,33 @@ class JSTerm extends Component {
 
     if (
       nextProps.autocompletePopupPosition !==
         this.props.autocompletePopupPosition &&
       this.autocompletePopup
     ) {
       this.autocompletePopup.position = nextProps.autocompletePopupPosition;
     }
+
+    if (
+      nextProps.editorPrettifiedAt &&
+      nextProps.editorPrettifiedAt !== this.props.editorPrettifiedAt
+    ) {
+      this._setValue(
+        beautify.js(this._getValue(), {
+          // Read directly from prefs because this.editor.config.indentUnit and
+          // this.editor.getOption('indentUnit') are not really synced with
+          // prefs.
+          indent_size: Services.prefs.getIntPref("devtools.editor.tabsize"),
+          indent_with_tabs: !Services.prefs.getBoolPref(
+            "devtools.editor.expandtab"
+          ),
+        })
+      );
+    }
   }
 
   /**
    *
    * @param {Number|null} editorWidth: The width to set the node to. If null, removes any
    *                                   `width` property on node style.
    */
   setEditorWidth(editorWidth) {
@@ -1543,16 +1566,17 @@ class JSTerm extends Component {
 function mapStateToProps(state) {
   return {
     history: getHistory(state),
     getValueFromHistory: direction => getHistoryValue(state, direction),
     autocompleteData: getAutocompleteState(state),
     showEditorOnboarding: state.ui.showEditorOnboarding,
     showEvaluationContextSelector: state.ui.showEvaluationContextSelector,
     autocompletePopupPosition: state.prefs.eagerEvaluation ? "top" : "bottom",
+    editorPrettifiedAt: state.ui.editorPrettifiedAt,
   };
 }
 
 function mapDispatchToProps(dispatch) {
   return {
     updateHistoryPosition: (direction, expression) =>
       dispatch(actions.updateHistoryPosition(direction, expression)),
     autocompleteUpdate: (force, getterPath, expressionVars) =>
--- a/devtools/client/webconsole/constants.js
+++ b/devtools/client/webconsole/constants.js
@@ -10,16 +10,17 @@ const actionTypes = {
   AUTOCOMPLETE_DATA_RECEIVE: "AUTOCOMPLETE_DATA_RECEIVE",
   AUTOCOMPLETE_PENDING_REQUEST: "AUTOCOMPLETE_PENDING_REQUEST",
   AUTOCOMPLETE_RETRIEVE_FROM_CACHE: "AUTOCOMPLETE_RETRIEVE_FROM_CACHE",
   AUTOCOMPLETE_TOGGLE: "AUTOCOMPLETE_TOGGLE",
   BATCH_ACTIONS: "BATCH_ACTIONS",
   CLEAR_HISTORY: "CLEAR_HISTORY",
   EDITOR_TOGGLE: "EDITOR_TOGGLE",
   EDITOR_ONBOARDING_DISMISS: "EDITOR_ONBOARDING_DISMISS",
+  EDITOR_PRETTY_PRINT: "EDITOR_PRETTY_PRINT",
   EVALUATE_EXPRESSION: "EVALUATE_EXPRESSION",
   SET_TERMINAL_INPUT: "SET_TERMINAL_INPUT",
   SET_TERMINAL_EAGER_RESULT: "SET_TERMINAL_EAGER_RESULT",
   FILTER_TEXT_SET: "FILTER_TEXT_SET",
   FILTER_TOGGLE: "FILTER_TOGGLE",
   FILTERS_CLEAR: "FILTERS_CLEAR",
   FILTERBAR_DISPLAY_MODE_SET: "FILTERBAR_DISPLAY_MODE_SET",
   HISTORY_LOADED: "HISTORY_LOADED",
--- a/devtools/client/webconsole/reducers/ui.js
+++ b/devtools/client/webconsole/reducers/ui.js
@@ -13,16 +13,17 @@ const {
   SHOW_OBJECT_IN_SIDEBAR,
   SIDEBAR_CLOSE,
   SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
   TIMESTAMPS_TOGGLE,
   FILTERBAR_DISPLAY_MODE_SET,
   FILTERBAR_DISPLAY_MODES,
   EDITOR_ONBOARDING_DISMISS,
   EDITOR_TOGGLE,
+  EDITOR_PRETTY_PRINT,
   EDITOR_SET_WIDTH,
 } = require("devtools/client/webconsole/constants");
 
 const { PANELS } = require("devtools/client/netmonitor/src/constants");
 
 const UiState = overrides =>
   Object.freeze(
     Object.assign(
@@ -34,16 +35,17 @@ const UiState = overrides =>
         sidebarVisible: false,
         timestampsVisible: true,
         frontInSidebar: null,
         closeButtonVisible: false,
         reverseSearchInputVisible: false,
         reverseSearchInitialValue: "",
         editor: false,
         editorWidth: null,
+        editorPrettifiedAt: null,
         showEditorOnboarding: false,
         filterBarDisplayMode: FILTERBAR_DISPLAY_MODES.WIDE,
       },
       overrides
     )
   );
 
 function ui(state = UiState(), action) {
@@ -94,16 +96,21 @@ function ui(state = UiState(), action) {
         ...state,
         showEditorOnboarding: false,
       };
     case EDITOR_SET_WIDTH:
       return {
         ...state,
         editorWidth: action.width,
       };
+    case EDITOR_PRETTY_PRINT:
+      return {
+        ...state,
+        editorPrettifiedAt: Date.now(),
+      };
   }
 
   return state;
 }
 
 module.exports = {
   UiState,
   ui,
--- a/devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_editor_toolbar.js
@@ -93,16 +93,71 @@ add_task(async function() {
   is(
     getInputValue(hud),
     nextHistoryJsStatement,
     `The JS Terminal Editor has the correct next expresion ${nextHistoryJsStatement}`
   );
   nextHistoryButton.click();
   is(getInputValue(hud), "");
 
+  info("Test that clicking the pretty print button works as expected");
+  const expressionToPrettyPrint = [
+    // [raw, prettified, prettifiedWithTab, prettifiedWith4Spaces]
+    ["fn=n=>n*n", "fn = n => n * n", "fn = n => n * n", "fn = n => n * n"],
+    [
+      "{x:1, y:2}",
+      "{\n  x: 1,\n  y: 2\n}",
+      "{\n\tx: 1,\n\ty: 2\n}",
+      "{\n    x: 1,\n    y: 2\n}",
+    ],
+  ];
+
+  const prettyPrintButton = toolbar.querySelector(
+    ".webconsole-editor-toolbar-prettyPrintButton"
+  );
+  ok(prettyPrintButton, "The pretty print button is displayed in editor mode");
+  for (const [
+    input,
+    output,
+    outputWithTab,
+    outputWith4Spaces,
+  ] of expressionToPrettyPrint) {
+    // Setting the input value.
+    setInputValue(hud, input);
+    await pushPref("devtools.editor.tabsize", 2);
+    prettyPrintButton.click();
+    is(
+      getInputValue(hud),
+      output,
+      `Pretty print works for expression ${input}`
+    );
+    // Turn on indent with tab.
+    await pushPref("devtools.editor.expandtab", false);
+    prettyPrintButton.click();
+    is(
+      getInputValue(hud),
+      outputWithTab,
+      `Pretty print works for expression ${input} when expandtab is false`
+    );
+    await pushPref("devtools.editor.expandtab", true);
+    // Set indent size to 4.
+    await pushPref("devtools.editor.tabsize", 4);
+    prettyPrintButton.click();
+    is(
+      getInputValue(hud),
+      outputWith4Spaces,
+      `Pretty print works for expression ${input} when tabsize is 4`
+    );
+    await pushPref("devtools.editor.tabsize", 2);
+    ok(
+      isInputFocused(hud),
+      "input is still focused after clicking the pretty print button"
+    );
+  }
+
   info("Test that clicking the close button works as expected");
   const closeButton = toolbar.querySelector(
     ".webconsole-editor-toolbar-closeButton"
   );
   const closeKeyShortcut =
     (Services.appinfo.OS === "Darwin" ? "Cmd" : "Ctrl") + " + B";
   is(
     closeButton.title,
--- a/devtools/client/webconsole/test/node/mocha-test-setup.js
+++ b/devtools/client/webconsole/test/node/mocha-test-setup.js
@@ -102,16 +102,18 @@ global.define = function() {};
 global.document.nodePrincipal = {
   isSystemPrincipal: false,
 };
 
 // Point to vendored-in files and mocks when needed.
 const requireHacker = require("require-hacker");
 requireHacker.global_hook("default", (path, module) => {
   const paths = {
+    "acorn/acorn": () => getModule("devtools/shared/acorn/acorn"),
+
     // For Enzyme
     "react-dom": () => getModule("devtools/client/shared/vendor/react-dom"),
     "react-dom/server": () =>
       getModule("devtools/client/shared/vendor/react-dom-server"),
     "react-dom/test-utils": () =>
       getModule("devtools/client/shared/vendor/react-dom-test-utils-dev"),
     "react-redux": () => getModule("devtools/client/shared/vendor/react-redux"),
     // Use react-dev. This would be handled by browserLoader in Firefox.