diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 099471e03..68482ca30 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -118,7 +118,7 @@ When using the syntax-highlighting SQL editors, the following shortcuts are avai +--------------------------+----------------------+-------------------------------------+ | Shift + Tab | Shift + Tab | Un-indent selected text | +--------------------------+----------------------+-------------------------------------+ - | Alt + g | Option + g | Jump (to line:column) | + | Ctrl + l | Cmd + l | Go to line, column | +--------------------------+----------------------+-------------------------------------+ | Ctrl + Space | Ctrl + Space | Auto-complete | +--------------------------+----------------------+-------------------------------------+ diff --git a/docs/en_US/query_tool_toolbar.rst b/docs/en_US/query_tool_toolbar.rst index bb04f5f4f..1a2003c19 100644 --- a/docs/en_US/query_tool_toolbar.rst +++ b/docs/en_US/query_tool_toolbar.rst @@ -87,7 +87,7 @@ Query Editing Options | | Select *Replace* to locate and replace (with prompting) individual occurrences of the target. | Option+Cmd+F (MAC) | | | | Ctrl+Shift+F (Others) | | +---------------------------------------------------------------------------------------------------+-----------------------+ - | | Select *Jump* to navigate to the next occurrence of the search target. | Alt+G | + | | Select *Go to Line/Column* to go to specified line number and column position | Cmd+L or Ctrl+L | | +---------------------------------------------------------------------------------------------------+-----------------------+ | | Select *Indent Selection* to indent the currently selected text. | Tab | | +---------------------------------------------------------------------------------------------------+-----------------------+ diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js index 47adda2a6..c11eb4bb5 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js +++ b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js @@ -3,7 +3,7 @@ import { } from '@codemirror/view'; import { StateEffect, EditorState } from '@codemirror/state'; import { autocompletion } from '@codemirror/autocomplete'; -import {undo} from '@codemirror/commands'; +import {undo, indentMore, indentLess, toggleComment} from '@codemirror/commands'; import { errorMarkerEffect } from './extensions/errorMarker'; import { activeLineEffect, activeLineField } from './extensions/activeLineMarker'; import { clearBreakpoints, hasBreakpoint, toggleBreakpoint } from './extensions/breakpointGutter'; @@ -59,8 +59,19 @@ export default class CustomEditorView extends EditorView { } setCursor(lineNo, ch) { - const n = this.state.doc.line(lineNo).from + ch; - this.dispatch({ selection: { anchor: n, head: n } }); + // line is 1-based; + // ch is 0-based; + let pos = 0; + if(lineNo > this.state.doc.lines) { + pos = this.state.doc.length; + } else { + const line = this.state.doc.line(lineNo); + pos = line.from + ch; + if(pos > line.to) { + pos = line.to; + } + } + this.dispatch({ selection: { anchor: pos, head: pos } }); } getCurrentLineNo() { @@ -106,13 +117,24 @@ export default class CustomEditorView extends EditorView { return !this._cleanDoc.eq(this.state.doc); } - undo() { - return undo(this); - } - fireDOMEvent(event) { this.contentDOM.dispatchEvent(event); } + + execCommand(cmd) { + switch (cmd) { + case 'undo': undo(this); + break; + case 'indentMore': indentMore(this); + break; + case 'indentLess': indentLess(this); + break; + case 'toggleComment': toggleComment(this); + break; + default: + break; + } + } registerAutocomplete(completionFunc) { this.dispatch({ diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/Editor.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx similarity index 90% rename from web/pgadmin/static/js/components/ReactCodeMirror/Editor.jsx rename to web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx index c5dad0dab..2a9387e56 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/Editor.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx @@ -12,11 +12,11 @@ import ReactDOMServer from 'react-dom/server'; import PropTypes from 'prop-types'; import gettext from 'sources/gettext'; import { makeStyles } from '@material-ui/core'; -import { PgIconButton } from '../Buttons'; -import { checkTrojanSource } from '../../utils'; -import { copyToClipboard } from '../../clipboard'; -import { useDelayedCaller } from '../../custom_hooks'; -import usePreferences from '../../../../preferences/static/js/store'; +import { PgIconButton } from '../../Buttons'; +import { checkTrojanSource } from '../../../utils'; +import { copyToClipboard } from '../../../clipboard'; +import { useDelayedCaller } from '../../../custom_hooks'; +import usePreferences from '../../../../../preferences/static/js/store'; import FileCopyRoundedIcon from '@material-ui/icons/FileCopyRounded'; import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; import KeyboardArrowRightRoundedIcon from '@material-ui/icons/KeyboardArrowRightRounded'; @@ -35,7 +35,7 @@ import { import { EditorState, Compartment } from '@codemirror/state'; import { history, defaultKeymap, historyKeymap, indentLess, insertTab } from '@codemirror/commands'; import { highlightSelectionMatches } from '@codemirror/search'; -import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'; +import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap, acceptCompletion } from '@codemirror/autocomplete'; import { foldGutter, indentOnInput, @@ -45,13 +45,14 @@ import { } from '@codemirror/language'; import FindDialog from './FindDialog'; -import syntaxHighlighting from './extensions/highlighting'; -import PgSQL from './extensions/dialect'; +import syntaxHighlighting from '../extensions/highlighting'; +import PgSQL from '../extensions/dialect'; import { sql } from '@codemirror/lang-sql'; -import errorMarkerExtn from './extensions/errorMarker'; -import CustomEditorView from './CustomEditorView'; -import breakpointGutter, { breakpointEffect } from './extensions/breakpointGutter'; -import activeLineExtn from './extensions/activeLineMarker'; +import errorMarkerExtn from '../extensions/errorMarker'; +import CustomEditorView from '../CustomEditorView'; +import breakpointGutter, { breakpointEffect } from '../extensions/breakpointGutter'; +import activeLineExtn from '../extensions/activeLineMarker'; +import GotoDialog from './GotoDialog'; const arrowRightHtml = ReactDOMServer.renderToString(); const arrowDownHtml = ReactDOMServer.renderToString(); @@ -151,6 +152,9 @@ const defaultExtensions = [ key: 'Shift-Tab', preventDefault: true, run: indentLess, + },{ + key: 'Tab', + run: acceptCompletion, }]), sql({ dialect: PgSQL, @@ -170,6 +174,7 @@ export default function Editor({ breakpoint = false, onBreakPointChange, showActiveLine=false, showCopyBtn = false, keepHistory = true, cid, helpid, labelledBy}) { const [[showFind, isReplace], setShowFind] = useState([false, false]); + const [showGoto, setShowGoto] = useState(false); const [showCopy, setShowCopy] = useState(false); const editorContainerRef = useRef(); @@ -184,7 +189,7 @@ export default function Editor({ const configurables = useRef(new Compartment()); const editableConfig = useRef(new Compartment()); - const findDialogKeyMap = [{ + const editMenuKeyMap = [{ key: 'Mod-f', run: (view, e) => { e.preventDefault(); e.stopPropagation(); @@ -198,6 +203,12 @@ export default function Editor({ setShowFind([false, false]); setShowFind([true, true]); }, + }, { + key: 'Mod-l', run: (view, e) => { + e.preventDefault(); + e.stopPropagation(); + setShowGoto(true); + }, }]; useEffect(() => { @@ -226,7 +237,7 @@ export default function Editor({ extensions: [ ...finalExtns, configurables.current.of([]), - keymap.of(findDialogKeyMap), + keymap.of(editMenuKeyMap), editableConfig.current.of([ EditorView.editable.of(!disabled), EditorState.readOnly.of(readonly), @@ -397,6 +408,11 @@ export default function Editor({ editor.current?.focus(); }; + const closeGoto = () => { + setShowGoto(false); + editor.current?.focus(); + }; + const onMouseEnter = useCallback(()=>{showCopyBtn && setShowCopy(true);}); const onMouseLeave = useCallback(()=>{showCopyBtn && setShowCopy(false);}); @@ -404,9 +420,8 @@ export default function Editor({
{showCopy && } - {showFind && - - } + +
); } diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/FindDialog.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/components/FindDialog.jsx similarity index 95% rename from web/pgadmin/static/js/components/ReactCodeMirror/FindDialog.jsx rename to web/pgadmin/static/js/components/ReactCodeMirror/components/FindDialog.jsx index d1b0daddf..073f44184 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/FindDialog.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/components/FindDialog.jsx @@ -11,14 +11,14 @@ import React, { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import gettext from 'sources/gettext'; import { Box, InputAdornment, makeStyles } from '@material-ui/core'; -import { InputText } from '../FormComponents'; -import { PgIconButton } from '../Buttons'; +import { InputText } from '../../FormComponents'; +import { PgIconButton } from '../../Buttons'; import CloseIcon from '@material-ui/icons/CloseRounded'; import ArrowDownwardRoundedIcon from '@material-ui/icons/ArrowDownwardRounded'; import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded'; import SwapHorizRoundedIcon from '@material-ui/icons/SwapHorizRounded'; import SwapCallsRoundedIcon from '@material-ui/icons/SwapCallsRounded'; -import { RegexIcon, FormatCaseIcon } from '../ExternalIcon'; +import { RegexIcon, FormatCaseIcon } from '../../ExternalIcon'; import { openSearchPanel, @@ -89,8 +89,8 @@ export default function FindDialog({editor, show, replace, onClose}) { }, [findVal, replaceVal, useRegex, matchCase]); const clearAndClose = ()=>{ - onClose(); closeSearchPanel(editor); + onClose(); }; const toggle = (name)=>{ @@ -147,7 +147,7 @@ export default function FindDialog({editor, show, replace, onClose}) { } return ( - + {findInputRef.current = ele;}} onChange={(value)=>setFindVal(value)} diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/components/GotoDialog.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/components/GotoDialog.jsx new file mode 100644 index 000000000..13838b1a7 --- /dev/null +++ b/web/pgadmin/static/js/components/ReactCodeMirror/components/GotoDialog.jsx @@ -0,0 +1,95 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import gettext from 'sources/gettext'; +import { Box, FormControl, makeStyles } from '@material-ui/core'; +import { InputText } from '../../FormComponents'; +import { PgIconButton } from '../../Buttons'; +import CloseIcon from '@material-ui/icons/CloseRounded'; + +const useStyles = makeStyles((theme)=>({ + root: { + position: 'absolute', + zIndex: 99, + right: '4px', + top: '0px', + ...theme.mixins.panelBorder.all, + borderTop: 'none', + padding: '2px 4px', + width: '250px', + backgroundColor: theme.palette.background.default, + display: 'flex', + alignItems: 'center', + gap: '4px', + }, +})); + +export default function GotoDialog({editor, show, onClose}) { + const [gotoVal, setGotoVal] = useState(''); + const inputRef = useRef(); + const classes = useStyles(); + + useEffect(()=>{ + if(show) { + setGotoVal(''); + inputRef.current?.focus(); + } + }, [show]); + + const onKeyPress = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if(!/^[ ]*[1-9][0-9]*[ ]*(,[ ]*[1-9][0-9]*[ ]*){0,1}$/.test(gotoVal)) { + return; + } + const v = gotoVal.split(',').map(Number); + if(v.length == 1) { + v.push(1); + } + editor.setCursor(v[0], v[1]-1); + onClose(); + } + }; + + const onEscape = (e)=>{ + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }; + + if(!editor) { + return <>; + } + + return ( + +
Ln [,Col]
+ + {inputRef.current = ele;}} + onChange={(value)=>setGotoVal(value)} + onKeyPress={onKeyPress} + /> + + } size="xs" noBorder onClick={onClose}/> +
+ ); +} + +GotoDialog.propTypes = { + editor: PropTypes.object, + show: PropTypes.bool, + replace: PropTypes.bool, + onClose: PropTypes.func, + selFindVal: PropTypes.string, +}; diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx index 20f3bd419..b02aa8dc2 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx @@ -10,7 +10,7 @@ import React from 'react'; import { makeStyles } from '@material-ui/core'; import clsx from 'clsx'; -import Editor from './Editor'; +import Editor from './components/Editor'; import CustomPropTypes from '../../custom_prop_types'; const useStyles = makeStyles(() => ({ diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx index dbe00e611..53db03b34 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx @@ -70,13 +70,14 @@ const FIXED_PREF = { 'char': 'F', }, }, - jump: { - 'control': false, + gotolinecol: { + 'control': true, + ctrl_is_meta: true, 'shift': false, - 'alt': true, + 'alt': false, 'key': { - 'key_code': 71, - 'char': 'G', + 'key_code': 76, + 'char': 'L', }, }, indent: { @@ -599,8 +600,8 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) { onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')} {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')} - {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'jumpToLine');}}>{gettext('Jump')} + {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'gotoLineCol');}}>{gettext('Go to Line/Column')} {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentMore');}}>{gettext('Indent Selection')} diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx index 0edf6dd3a..505e30682 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -211,7 +211,21 @@ export default function Query() { }); eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, (cmd='')=>{ - editor.current?.execCommand(cmd); + if(cmd == 'gotoLineCol') { + editor.current?.focus(); + let key = { + keyCode: 76, metaKey: false, ctrlKey: true, shiftKey: false, altKey: false, + }; + if(isMac()) { + key.metaKey = true; + key.ctrlKey = false; + key.shiftKey = false; + key.altKey = false; + } + editor.current?.fireDOMEvent(new KeyboardEvent('keydown', key)); + } else { + editor.current?.execCommand(cmd); + } }); eventBus.registerListener(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, (text)=>{ editor.current?.setValue(text); @@ -327,9 +341,7 @@ export default function Query() { }, [queryToolCtx.preferences]); useEffect(()=>{ - registerAutocomplete(editor.current, queryToolCtx.api, queryToolCtx.params.trans_id, queryToolCtx.preferences.sqleditor, - (err)=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);} - ); + registerAutocomplete(editor.current, queryToolCtx.api, queryToolCtx.params.trans_id); }, [queryToolCtx.params.trans_id]); const cursorActivity = useCallback(_.debounce((cursor)=>{ @@ -351,7 +363,7 @@ export default function Query() { const closePromotionWarning = (closeModal)=>{ if(editor.current.isDirty()) { - editor.current.undo(); + editor.current.execCommand('undo'); closeModal?.(); } }; @@ -400,6 +412,5 @@ export default function Query() { onCursorActivity={cursorActivity} onChange={change} autocomplete={true} - keepHistory={queryToolCtx.params.is_query_tool} />; } diff --git a/web/regression/javascript/components/CodeMirror.spec.js b/web/regression/javascript/components/CodeMirror.spec.js index 1f261c139..57688316a 100644 --- a/web/regression/javascript/components/CodeMirror.spec.js +++ b/web/regression/javascript/components/CodeMirror.spec.js @@ -13,7 +13,7 @@ import { withTheme } from '../fake_theme'; import pgWindow from 'sources/window'; import CodeMirror from 'sources/components/ReactCodeMirror'; -import FindDialog from 'sources/components/ReactCodeMirror/FindDialog'; +import FindDialog from 'sources/components/ReactCodeMirror/components/FindDialog'; import CustomEditorView from 'sources/components/ReactCodeMirror/CustomEditorView'; import fakePgAdmin from '../fake_pgadmin'; import { render, screen, fireEvent } from '@testing-library/react';