From 972770731c2495f2b344fd712e93ead276bca15d Mon Sep 17 00:00:00 2001 From: Eiman Date: Wed, 8 Sep 2021 23:16:03 +0430 Subject: [PATCH] Add a keyboard shortcut for beautify request body (#2733) --- packages/insomnia-app/app/common/hotkeys.ts | 5 ++ .../ui/components/codemirror/code-editor.tsx | 60 +++++++++------- .../ui/components/codemirror/modes/curl.ts | 70 ++++++++++++------- .../app/ui/components/keydown-binder.ts | 4 +- 4 files changed, 86 insertions(+), 53 deletions(-) diff --git a/packages/insomnia-app/app/common/hotkeys.ts b/packages/insomnia-app/app/common/hotkeys.ts index a69ed9c523..3cf6d19831 100644 --- a/packages/insomnia-app/app/common/hotkeys.ts +++ b/packages/insomnia-app/app/common/hotkeys.ts @@ -129,6 +129,7 @@ export const hotKeyRefs: Record = { CLOSE_DROPDOWN: defineHotKey('closeDropdown', 'Close Dropdown'), CLOSE_MODAL: defineHotKey('closeModal', 'Close Modal'), ENVIRONMENT_UNCOVER_VARIABLES: defineHotKey('environment_uncoverVariables', 'Uncover Variables'), + BEAUTIFY_REQUEST_BODY: defineHotKey('beautifyRequestBody', 'Beautify Active Code Editors'), GRAPHQL_EXPLORER_FOCUS_FILTER: defineHotKey('graphql_explorer_focus_filter', 'Focus GraphQL Explorer Filter'), // Designer-specific SHOW_SPEC_EDITOR: defineHotKey('activity_specEditor', 'Show Spec Activity'), @@ -278,6 +279,10 @@ const defaultRegistry: HotKeyRegistry = { keyComb(false, false, true, true, keyboardKeys.f.keyCode), keyComb(true, false, true, false, keyboardKeys.f.keyCode), ), + [hotKeyRefs.BEAUTIFY_REQUEST_BODY.id]: keyBinds( + keyComb(false, false, true, true, keyboardKeys.i.keyCode), + keyComb(true, false, true, false, keyboardKeys.i.keyCode), + ), [hotKeyRefs.SHOW_SPEC_EDITOR.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.s.keyCode), keyComb(true, false, true, false, keyboardKeys.s.keyCode), diff --git a/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx b/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx index da8630d7f0..93ebb43206 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx +++ b/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx @@ -11,6 +11,7 @@ import { json as jsonPrettify } from 'insomnia-prettify'; import { query as queryXPath } from 'insomnia-xpath'; import jq from 'jsonpath'; import React, { Component, CSSProperties, ReactNode } from 'react'; +import { unreachable } from 'ts-assert-unreachable'; import vkBeautify from 'vkbeautify'; import zprint from 'zprint-clj'; @@ -20,6 +21,8 @@ import { EDITOR_KEY_MAP_VIM, isMac, } from '../../../common/constants'; +import { hotKeyRefs } from '../../../common/hotkeys'; +import { executeHotKey } from '../../../common/hotkeys-listener'; import { keyboardKeys as keyCodes } from '../../../common/keyboard-keys'; import * as misc from '../../../common/misc'; import { HandleGetRenderContext, HandleRender } from '../../../common/render'; @@ -28,6 +31,7 @@ import { NunjucksParsedTag } from '../../../templating/utils'; import Dropdown from '../base/dropdown/dropdown'; import DropdownButton from '../base/dropdown/dropdown-button'; import DropdownItem from '../base/dropdown/dropdown-item'; +import KeydownBinder from '../keydown-binder'; import FilterHelpModal from '../modals/filter-help-modal'; import { showModal } from '../modals/index'; import { normalizeIrregularWhitespace } from './normalizeIrregularWhitespace'; @@ -162,7 +166,6 @@ class CodeEditor extends Component { codeMirror?: CodeMirror.EditorFromTextArea; private _filterInput: HTMLInputElement; private _autocompleteDebounce: NodeJS.Timeout | null = null; - private _ignoreNextChange: boolean; private _filterTimeout: NodeJS.Timeout | null = null; constructor(props: Props) { @@ -641,12 +644,14 @@ class CodeEditor extends Component { : new Array((this.codeMirror?.getOption?.('indentUnit') || 0) + 1).join(' '); } - _handleBeautify() { - this._prettify(this.codeMirror?.getValue()); - } + _prettify() { + const canPrettify = this._canPrettify(); + if (!canPrettify) { + return; + } - _prettify(code?: string) { - this._codemirrorSetValue(code, true); + const code = this.codeMirror?.getValue(); + this._codemirrorSetValue(code, canPrettify); } _prettifyJSON(code: string) { @@ -698,6 +703,10 @@ class CodeEditor extends Component { } } + async _handleKeyDown(event: KeyboardEvent) { + executeHotKey(event, hotKeyRefs.BEAUTIFY_REQUEST_BODY, this._prettify); + } + /** * Sets options on the CodeMirror editor while also sanitizing them */ @@ -1047,9 +1056,7 @@ class CodeEditor extends Component { * Wrapper function to add extra behaviour to our onChange event */ _codemirrorValueChanged() { - // Don't trigger change event if we're ignoring changes - if (this._ignoreNextChange || !this.props.onChange) { - this._ignoreNextChange = false; + if (!this.props.onChange) { return; } @@ -1074,33 +1081,34 @@ class CodeEditor extends Component { * @param code the code to set in the editor * @param forcePrettify */ - _codemirrorSetValue(code = '', forcePrettify = false) { + _codemirrorSetValue(code?: string, forcePrettify?: boolean) { if (typeof code !== 'string') { console.warn('Code editor was passed non-string value', code); return; } - + const { autoPrettify, mode } = this.props; this._originalCode = code; - - // If we're setting initial value, don't trigger onChange because the - // user hasn't done anything yet - if (!forcePrettify) { - this._ignoreNextChange = true; - } - - const shouldPrettify = forcePrettify || this.props.autoPrettify; + const shouldPrettify = forcePrettify || autoPrettify; if (shouldPrettify && this._canPrettify()) { - if (CodeEditor._isXML(this.props.mode)) { + if (CodeEditor._isXML(mode)) { code = this._prettifyXML(code); - } else if (CodeEditor._isEDN(this.props.mode)) { + } else if (CodeEditor._isEDN(mode)) { code = CodeEditor._prettifyEDN(code); - } else { + } else if (CodeEditor._isJSON(mode)) { code = this._prettifyJSON(code); + } else { + unreachable('attempted to prettify in a mode that should not support prettifying'); } } - this.codeMirror?.setValue(code); + // this prevents codeMirror from needlessly setting the same thing repeatedly (which has the effect of moving the user's cursor and resetting the viewport scroll: a bad user experience) + const currentCode = this.codeMirror?.getValue(); + if (currentCode === code) { + return; + } + + this.codeMirror?.setValue(code || ''); } _handleFilterHistorySelect(filter = '') { @@ -1219,7 +1227,7 @@ class CodeEditor extends Component { key="prettify" className="btn btn--compact" title="Auto-format request body whitespace" - onClick={this._handleBeautify} + onClick={this._prettify} > Beautify {contentTypeName} , @@ -1244,6 +1252,7 @@ class CodeEditor extends Component { return (
+
{ }} readOnly={readOnly} autoComplete="off" - // NOTE: When setting this to empty string, it breaks the _ignoreNextChange logic on initial component mount - defaultValue=" " + defaultValue="" />
{toolbar} diff --git a/packages/insomnia-app/app/ui/components/codemirror/modes/curl.ts b/packages/insomnia-app/app/ui/components/codemirror/modes/curl.ts index 43c251e27f..b5cdad4cff 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/modes/curl.ts +++ b/packages/insomnia-app/app/ui/components/codemirror/modes/curl.ts @@ -1,33 +1,53 @@ import 'codemirror/addon/mode/simple'; import CodeMirror from 'codemirror'; + +/** regular key-value header tokens */ +const keyValueHeaders = [ + { + regex: /^(> )([^:]*:)(.*)$/, + token: ['curl-prefix curl-out', 'curl-out', 'curl-out curl-value'], + }, + { + regex: /^(< )([^:]*:)(.*)$/, + token: ['curl-prefix curl-in', 'curl-in', 'curl-in curl-value'], + }, +]; + +/** + * @example POST /foo/bar HTTP/1.1 + */ +const headerFields = [ + { + regex: /^(> )([^:]+ .*)$/, + token: ['curl-prefix curl-out curl-header', 'curl-out curl-header'], + }, + { + regex: /^(< )([^:]+ .*)$/, + token: ['curl-prefix curl-in curl-header', 'curl-in curl-header'], + }, +]; + +const data = [ + { + regex: /^(\| )(.*)$/, + token: ['curl-prefix curl-data', 'curl-data'], + }, +]; + +const informationalText = [ + { + regex: /^(\* )(.*)$/, + token: ['curl-prefix curl-comment', 'curl-comment'], + }, +]; + CodeMirror.defineSimpleMode('curl', { start: [ - // Regular key-value header tokens - { - regex: /^(> )([^:]*:)(.*)$/, - token: ['curl-prefix curl-out', 'curl-out', 'curl-out curl-value'], - }, - { - regex: /^(< )([^:]*:)(.*)$/, - token: ['curl-prefix curl-in', 'curl-in', 'curl-in curl-value'], - }, // Header fields ("POST /foo/bar HTTP/1.1") - { - regex: /^(> )([^:]+ .*)$/, - token: ['curl-prefix curl-out curl-header', 'curl-out curl-header'], - }, - { - regex: /^(< )([^:]+ .*)$/, - token: ['curl-prefix curl-in curl-header', 'curl-in curl-header'], - }, // Data - { - regex: /^(\| )(.*)$/, - token: ['curl-prefix curl-data', 'curl-data'], - }, // Informational text - { - regex: /^(\* )(.*)$/, - token: ['curl-prefix curl-comment', 'curl-comment'], - }, + ...keyValueHeaders, + ...headerFields, + ...data, + ...informationalText, ], comment: [], meta: {}, diff --git a/packages/insomnia-app/app/ui/components/keydown-binder.ts b/packages/insomnia-app/app/ui/components/keydown-binder.ts index b6d0344e60..d8b8dc4fd3 100644 --- a/packages/insomnia-app/app/ui/components/keydown-binder.ts +++ b/packages/insomnia-app/app/ui/components/keydown-binder.ts @@ -5,7 +5,7 @@ import ReactDOM from 'react-dom'; import { AUTOBIND_CFG, isMac } from '../../common/constants'; interface Props { - children: ReactNode; + children?: ReactNode; onKeydown?: (...args: any[]) => any; onKeyup?: (...args: any[]) => any; disabled?: boolean; @@ -74,7 +74,7 @@ class KeydownBinder extends PureComponent { } render() { - return this.props.children; + return this.props.children ?? null; } }