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 | | 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 | | 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) | | | Select *Replace* to locate and replace (with prompting) individual occurrences of the target. | Option+Cmd+F (MAC) |
| | | Ctrl+Shift+F (Others) | | | | 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 | | | Select *Indent Selection* to indent the currently selected text. | Tab |
| +---------------------------------------------------------------------------------------------------+-----------------------+ | +---------------------------------------------------------------------------------------------------+-----------------------+

View File

@ -3,7 +3,7 @@ import {
} from '@codemirror/view'; } from '@codemirror/view';
import { StateEffect, EditorState } from '@codemirror/state'; import { StateEffect, EditorState } from '@codemirror/state';
import { autocompletion } from '@codemirror/autocomplete'; import { autocompletion } from '@codemirror/autocomplete';
import {undo} from '@codemirror/commands'; import {undo, indentMore, indentLess, toggleComment} from '@codemirror/commands';
import { errorMarkerEffect } from './extensions/errorMarker'; import { errorMarkerEffect } from './extensions/errorMarker';
import { activeLineEffect, activeLineField } from './extensions/activeLineMarker'; import { activeLineEffect, activeLineField } from './extensions/activeLineMarker';
import { clearBreakpoints, hasBreakpoint, toggleBreakpoint } from './extensions/breakpointGutter'; import { clearBreakpoints, hasBreakpoint, toggleBreakpoint } from './extensions/breakpointGutter';
@ -59,8 +59,19 @@ export default class CustomEditorView extends EditorView {
} }
setCursor(lineNo, ch) { setCursor(lineNo, ch) {
const n = this.state.doc.line(lineNo).from + ch; // line is 1-based;
this.dispatch({ selection: { anchor: n, head: n } }); // 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() { getCurrentLineNo() {
@ -106,14 +117,25 @@ export default class CustomEditorView extends EditorView {
return !this._cleanDoc.eq(this.state.doc); return !this._cleanDoc.eq(this.state.doc);
} }
undo() {
return undo(this);
}
fireDOMEvent(event) { fireDOMEvent(event) {
this.contentDOM.dispatchEvent(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) { registerAutocomplete(completionFunc) {
this.dispatch({ this.dispatch({
effects: StateEffect.appendConfig.of( effects: StateEffect.appendConfig.of(

View File

@ -12,11 +12,11 @@ import ReactDOMServer from 'react-dom/server';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { makeStyles } from '@material-ui/core'; import { makeStyles } from '@material-ui/core';
import { PgIconButton } from '../Buttons'; import { PgIconButton } from '../../Buttons';
import { checkTrojanSource } from '../../utils'; import { checkTrojanSource } from '../../../utils';
import { copyToClipboard } from '../../clipboard'; import { copyToClipboard } from '../../../clipboard';
import { useDelayedCaller } from '../../custom_hooks'; import { useDelayedCaller } from '../../../custom_hooks';
import usePreferences from '../../../../preferences/static/js/store'; import usePreferences from '../../../../../preferences/static/js/store';
import FileCopyRoundedIcon from '@material-ui/icons/FileCopyRounded'; import FileCopyRoundedIcon from '@material-ui/icons/FileCopyRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import KeyboardArrowRightRoundedIcon from '@material-ui/icons/KeyboardArrowRightRounded'; import KeyboardArrowRightRoundedIcon from '@material-ui/icons/KeyboardArrowRightRounded';
@ -35,7 +35,7 @@ import {
import { EditorState, Compartment } from '@codemirror/state'; import { EditorState, Compartment } from '@codemirror/state';
import { history, defaultKeymap, historyKeymap, indentLess, insertTab } from '@codemirror/commands'; import { history, defaultKeymap, historyKeymap, indentLess, insertTab } from '@codemirror/commands';
import { highlightSelectionMatches } from '@codemirror/search'; import { highlightSelectionMatches } from '@codemirror/search';
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'; import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap, acceptCompletion } from '@codemirror/autocomplete';
import { import {
foldGutter, foldGutter,
indentOnInput, indentOnInput,
@ -45,13 +45,14 @@ import {
} from '@codemirror/language'; } from '@codemirror/language';
import FindDialog from './FindDialog'; import FindDialog from './FindDialog';
import syntaxHighlighting from './extensions/highlighting'; import syntaxHighlighting from '../extensions/highlighting';
import PgSQL from './extensions/dialect'; import PgSQL from '../extensions/dialect';
import { sql } from '@codemirror/lang-sql'; import { sql } from '@codemirror/lang-sql';
import errorMarkerExtn from './extensions/errorMarker'; import errorMarkerExtn from '../extensions/errorMarker';
import CustomEditorView from './CustomEditorView'; import CustomEditorView from '../CustomEditorView';
import breakpointGutter, { breakpointEffect } from './extensions/breakpointGutter'; import breakpointGutter, { breakpointEffect } from '../extensions/breakpointGutter';
import activeLineExtn from './extensions/activeLineMarker'; import activeLineExtn from '../extensions/activeLineMarker';
import GotoDialog from './GotoDialog';
const arrowRightHtml = ReactDOMServer.renderToString(<KeyboardArrowRightRoundedIcon style={{fontSize: '1.2em'}} />); const arrowRightHtml = ReactDOMServer.renderToString(<KeyboardArrowRightRoundedIcon style={{fontSize: '1.2em'}} />);
const arrowDownHtml = ReactDOMServer.renderToString(<ExpandMoreRoundedIcon style={{fontSize: '1.2em'}} />); const arrowDownHtml = ReactDOMServer.renderToString(<ExpandMoreRoundedIcon style={{fontSize: '1.2em'}} />);
@ -151,6 +152,9 @@ const defaultExtensions = [
key: 'Shift-Tab', key: 'Shift-Tab',
preventDefault: true, preventDefault: true,
run: indentLess, run: indentLess,
},{
key: 'Tab',
run: acceptCompletion,
}]), }]),
sql({ sql({
dialect: PgSQL, dialect: PgSQL,
@ -170,6 +174,7 @@ export default function Editor({
breakpoint = false, onBreakPointChange, showActiveLine=false, showCopyBtn = false, breakpoint = false, onBreakPointChange, showActiveLine=false, showCopyBtn = false,
keepHistory = true, cid, helpid, labelledBy}) { keepHistory = true, cid, helpid, labelledBy}) {
const [[showFind, isReplace], setShowFind] = useState([false, false]); const [[showFind, isReplace], setShowFind] = useState([false, false]);
const [showGoto, setShowGoto] = useState(false);
const [showCopy, setShowCopy] = useState(false); const [showCopy, setShowCopy] = useState(false);
const editorContainerRef = useRef(); const editorContainerRef = useRef();
@ -184,7 +189,7 @@ export default function Editor({
const configurables = useRef(new Compartment()); const configurables = useRef(new Compartment());
const editableConfig = useRef(new Compartment()); const editableConfig = useRef(new Compartment());
const findDialogKeyMap = [{ const editMenuKeyMap = [{
key: 'Mod-f', run: (view, e) => { key: 'Mod-f', run: (view, e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -198,6 +203,12 @@ export default function Editor({
setShowFind([false, false]); setShowFind([false, false]);
setShowFind([true, true]); setShowFind([true, true]);
}, },
}, {
key: 'Mod-l', run: (view, e) => {
e.preventDefault();
e.stopPropagation();
setShowGoto(true);
},
}]; }];
useEffect(() => { useEffect(() => {
@ -226,7 +237,7 @@ export default function Editor({
extensions: [ extensions: [
...finalExtns, ...finalExtns,
configurables.current.of([]), configurables.current.of([]),
keymap.of(findDialogKeyMap), keymap.of(editMenuKeyMap),
editableConfig.current.of([ editableConfig.current.of([
EditorView.editable.of(!disabled), EditorView.editable.of(!disabled),
EditorState.readOnly.of(readonly), EditorState.readOnly.of(readonly),
@ -397,6 +408,11 @@ export default function Editor({
editor.current?.focus(); editor.current?.focus();
}; };
const closeGoto = () => {
setShowGoto(false);
editor.current?.focus();
};
const onMouseEnter = useCallback(()=>{showCopyBtn && setShowCopy(true);}); const onMouseEnter = useCallback(()=>{showCopyBtn && setShowCopy(true);});
const onMouseLeave = useCallback(()=>{showCopyBtn && setShowCopy(false);}); const onMouseLeave = useCallback(()=>{showCopyBtn && setShowCopy(false);});
@ -404,9 +420,8 @@ export default function Editor({
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} style={{height: '100%'}}> <div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} style={{height: '100%'}}>
<div style={{ height: '100%' }} ref={editorContainerRef} name={name}></div> <div style={{ height: '100%' }} ref={editorContainerRef} name={name}></div>
{showCopy && <CopyButton editor={editor.current} />} {showCopy && <CopyButton editor={editor.current} />}
{showFind &&
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} /> <FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} />
} <GotoDialog editor={editor.current} show={showGoto} onClose={closeGoto} />
</div> </div>
); );
} }

View File

@ -11,14 +11,14 @@ import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { Box, InputAdornment, makeStyles } from '@material-ui/core'; import { Box, InputAdornment, makeStyles } from '@material-ui/core';
import { InputText } from '../FormComponents'; import { InputText } from '../../FormComponents';
import { PgIconButton } from '../Buttons'; import { PgIconButton } from '../../Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded'; import CloseIcon from '@material-ui/icons/CloseRounded';
import ArrowDownwardRoundedIcon from '@material-ui/icons/ArrowDownwardRounded'; import ArrowDownwardRoundedIcon from '@material-ui/icons/ArrowDownwardRounded';
import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded'; import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded';
import SwapHorizRoundedIcon from '@material-ui/icons/SwapHorizRounded'; import SwapHorizRoundedIcon from '@material-ui/icons/SwapHorizRounded';
import SwapCallsRoundedIcon from '@material-ui/icons/SwapCallsRounded'; import SwapCallsRoundedIcon from '@material-ui/icons/SwapCallsRounded';
import { RegexIcon, FormatCaseIcon } from '../ExternalIcon'; import { RegexIcon, FormatCaseIcon } from '../../ExternalIcon';
import { import {
openSearchPanel, openSearchPanel,
@ -89,8 +89,8 @@ export default function FindDialog({editor, show, replace, onClose}) {
}, [findVal, replaceVal, useRegex, matchCase]); }, [findVal, replaceVal, useRegex, matchCase]);
const clearAndClose = ()=>{ const clearAndClose = ()=>{
onClose();
closeSearchPanel(editor); closeSearchPanel(editor);
onClose();
}; };
const toggle = (name)=>{ const toggle = (name)=>{
@ -147,7 +147,7 @@ export default function FindDialog({editor, show, replace, onClose}) {
} }
return ( 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} <InputText value={findVal}
inputRef={(ele)=>{findInputRef.current = ele;}} inputRef={(ele)=>{findInputRef.current = ele;}}
onChange={(value)=>setFindVal(value)} 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 React from 'react';
import { makeStyles } from '@material-ui/core'; import { makeStyles } from '@material-ui/core';
import clsx from 'clsx'; import clsx from 'clsx';
import Editor from './Editor'; import Editor from './components/Editor';
import CustomPropTypes from '../../custom_prop_types'; import CustomPropTypes from '../../custom_prop_types';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({

View File

@ -70,13 +70,14 @@ const FIXED_PREF = {
'char': 'F', 'char': 'F',
}, },
}, },
jump: { gotolinecol: {
'control': false, 'control': true,
ctrl_is_meta: true,
'shift': false, 'shift': false,
'alt': true, 'alt': false,
'key': { 'key': {
'key_code': 71, 'key_code': 76,
'char': 'G', 'char': 'L',
}, },
}, },
indent: { indent: {
@ -599,8 +600,8 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')}</PgMenuItem> onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.replace} <PgMenuItem shortcut={FIXED_PREF.replace}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')}</PgMenuItem> onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.jump} <PgMenuItem shortcut={FIXED_PREF.gotolinecol}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'jumpToLine');}}>{gettext('Jump')}</PgMenuItem> onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'gotoLineCol');}}>{gettext('Go to Line/Column')}</PgMenuItem>
<PgMenuDivider /> <PgMenuDivider />
<PgMenuItem shortcut={FIXED_PREF.indent} <PgMenuItem shortcut={FIXED_PREF.indent}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentMore');}}>{gettext('Indent Selection')}</PgMenuItem> 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='')=>{ 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); editor.current?.execCommand(cmd);
}
}); });
eventBus.registerListener(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, (text)=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, (text)=>{
editor.current?.setValue(text); editor.current?.setValue(text);
@ -327,9 +341,7 @@ export default function Query() {
}, [queryToolCtx.preferences]); }, [queryToolCtx.preferences]);
useEffect(()=>{ useEffect(()=>{
registerAutocomplete(editor.current, queryToolCtx.api, queryToolCtx.params.trans_id, queryToolCtx.preferences.sqleditor, registerAutocomplete(editor.current, queryToolCtx.api, queryToolCtx.params.trans_id);
(err)=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);}
);
}, [queryToolCtx.params.trans_id]); }, [queryToolCtx.params.trans_id]);
const cursorActivity = useCallback(_.debounce((cursor)=>{ const cursorActivity = useCallback(_.debounce((cursor)=>{
@ -351,7 +363,7 @@ export default function Query() {
const closePromotionWarning = (closeModal)=>{ const closePromotionWarning = (closeModal)=>{
if(editor.current.isDirty()) { if(editor.current.isDirty()) {
editor.current.undo(); editor.current.execCommand('undo');
closeModal?.(); closeModal?.();
} }
}; };
@ -400,6 +412,5 @@ export default function Query() {
onCursorActivity={cursorActivity} onCursorActivity={cursorActivity}
onChange={change} onChange={change}
autocomplete={true} 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 pgWindow from 'sources/window';
import CodeMirror from 'sources/components/ReactCodeMirror'; 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 CustomEditorView from 'sources/components/ReactCodeMirror/CustomEditorView';
import fakePgAdmin from '../fake_pgadmin'; import fakePgAdmin from '../fake_pgadmin';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';