mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-22 00:37:36 -06:00
Fix edit menu related issues of query tool codemirror
This commit is contained in:
parent
41812c9fde
commit
b5bd236387
@ -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 |
|
||||
+--------------------------+----------------------+-------------------------------------+
|
||||
|
@ -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 |
|
||||
| +---------------------------------------------------------------------------------------------------+-----------------------+
|
||||
|
@ -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,14 +117,25 @@ 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({
|
||||
effects: StateEffect.appendConfig.of(
|
||||
|
@ -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(<KeyboardArrowRightRoundedIcon style={{fontSize: '1.2em'}} />);
|
||||
const arrowDownHtml = ReactDOMServer.renderToString(<ExpandMoreRoundedIcon style={{fontSize: '1.2em'}} />);
|
||||
@ -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({
|
||||
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} style={{height: '100%'}}>
|
||||
<div style={{ height: '100%' }} ref={editorContainerRef} name={name}></div>
|
||||
{showCopy && <CopyButton editor={editor.current} />}
|
||||
{showFind &&
|
||||
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} />
|
||||
}
|
||||
<GotoDialog editor={editor.current} show={showGoto} onClose={closeGoto} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<Box className={classes.root} visibility={show ? 'visible' : 'hidden'} tabIndex="0" onKeyDown={onEscape}>
|
||||
<Box className={classes.root} style={{visibility: show ? 'visible' : 'hidden'}} tabIndex="0" onKeyDown={onEscape}>
|
||||
<InputText value={findVal}
|
||||
inputRef={(ele)=>{findInputRef.current = ele;}}
|
||||
onChange={(value)=>setFindVal(value)}
|
@ -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 (
|
||||
<Box className={classes.root} style={{visibility: show ? 'visible' : 'hidden'}} tabIndex="0" onKeyDown={onEscape}>
|
||||
<div style={{whiteSpace: 'nowrap'}}>Ln [,Col]</div>
|
||||
<FormControl>
|
||||
<InputText
|
||||
value={gotoVal}
|
||||
inputRef={(ele)=>{inputRef.current = ele;}}
|
||||
onChange={(value)=>setGotoVal(value)}
|
||||
onKeyPress={onKeyPress}
|
||||
/>
|
||||
</FormControl>
|
||||
<PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={onClose}/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
GotoDialog.propTypes = {
|
||||
editor: PropTypes.object,
|
||||
show: PropTypes.bool,
|
||||
replace: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
selFindVal: PropTypes.string,
|
||||
};
|
@ -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(() => ({
|
||||
|
@ -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')}</PgMenuItem>
|
||||
<PgMenuItem shortcut={FIXED_PREF.replace}
|
||||
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')}</PgMenuItem>
|
||||
<PgMenuItem shortcut={FIXED_PREF.jump}
|
||||
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'jumpToLine');}}>{gettext('Jump')}</PgMenuItem>
|
||||
<PgMenuItem shortcut={FIXED_PREF.gotolinecol}
|
||||
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'gotoLineCol');}}>{gettext('Go to Line/Column')}</PgMenuItem>
|
||||
<PgMenuDivider />
|
||||
<PgMenuItem shortcut={FIXED_PREF.indent}
|
||||
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentMore');}}>{gettext('Indent Selection')}</PgMenuItem>
|
||||
|
@ -211,7 +211,21 @@ export default function Query() {
|
||||
});
|
||||
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, (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}
|
||||
/>;
|
||||
}
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user