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';