Fix edit menu related issues of query tool codemirror

This commit is contained in:
Aditya Toshniwal 2024-02-21 15:47:58 +05:30
parent 41812c9fde
commit b5bd236387
10 changed files with 190 additions and 46 deletions

View File

@ -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 |
+--------------------------+----------------------+-------------------------------------+

View File

@ -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 |
| +---------------------------------------------------------------------------------------------------+-----------------------+

View File

@ -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(

View File

@ -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>
);
}

View File

@ -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)}

View File

@ -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,
};

View File

@ -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(() => ({

View File

@ -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>

View File

@ -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}
/>;
}

View File

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