Fix an issue in query tool where toggle case of selected text loses selection. #7277

Also make changes to give pgAdmin shortcuts higher priority over CodeMirror default shortcuts.
This commit is contained in:
Aditya Toshniwal 2024-03-14 18:13:13 +05:30
parent 1a02d13a28
commit f351b10ed0
8 changed files with 206 additions and 141 deletions

View File

@ -29,7 +29,10 @@ Housekeeping
Bug fixes
*********
| `Issue #7116 <https://github.com/pgadmin-org/pgadmin4/issues/7116>`_ - Bug fixes and improvements in pgAdmin CLI.
| `Issue #7165 <https://github.com/pgadmin-org/pgadmin4/issues/7165>`_ - Fixed schema diff wrong query generation for table, foreign table and sequence.
| `Issue #7229 <https://github.com/pgadmin-org/pgadmin4/issues/7229>`_ - Fix an issue in table dialog where changing column name was not syncing table constraints appropriately.
| `Issue #7262 <https://github.com/pgadmin-org/pgadmin4/issues/7262>`_ - Fix an issue in editor where replace option in query tool edit menu is not working on non-Mac OS.
| `Issue #7268 <https://github.com/pgadmin-org/pgadmin4/issues/7268>`_ - Fix an issue in editor where Format SQL shortcut and multiline selection are not working.
| `Issue #7269 <https://github.com/pgadmin-org/pgadmin4/issues/7269>`_ - Fix an issue in editor where "Use Spaces?" Preference of Editor is not working.
| `Issue #7277 <https://github.com/pgadmin-org/pgadmin4/issues/7277>`_ - Fix an issue in query tool where toggle case of selected text loses selection.

View File

@ -1,7 +1,7 @@
import {
EditorView
} from '@codemirror/view';
import { StateEffect, EditorState } from '@codemirror/state';
import { StateEffect, EditorState, EditorSelection } from '@codemirror/state';
import { autocompletion } from '@codemirror/autocomplete';
import {undo, indentMore, indentLess, toggleComment} from '@codemirror/commands';
import { errorMarkerEffect } from './extensions/errorMarker';
@ -49,7 +49,10 @@ export default class CustomEditorView extends EditorView {
}
replaceSelection(newValue) {
this.dispatch(this.state.replaceSelection(newValue));
this.dispatch(this.state.changeByRange(range => ({
changes: { from: range.from, to: range.to, insert: newValue },
range: EditorSelection.range(range.from, range.to)
})));
}
getCursor() {

View File

@ -110,7 +110,6 @@ const defaultExtensions = [
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting,
keymap.of([defaultKeymap, closeBracketsKeymap, historyKeymap, foldKeymap, completionKeymap].flat()),
keymap.of([{
key: 'Tab',
preventDefault: true,
@ -147,6 +146,8 @@ export default function Editor({
const preferencesStore = usePreferences();
const editable = !disabled;
const shortcuts = useRef(new Compartment());
const configurables = useRef(new Compartment());
const editableConfig = useRef(new Compartment());
@ -168,13 +169,14 @@ export default function Editor({
icon.innerHTML = arrowRightHtml;
}
return icon;
}
},
}));
}
if (editorContainerRef.current) {
const state = EditorState.create({
extensions: [
...finalExtns,
shortcuts.current.of([]),
configurables.current.of([]),
editableConfig.current.of([
EditorView.editable.of(!disabled),
@ -209,7 +211,6 @@ export default function Editor({
}),
breakpoint ? breakpointGutter : [],
showActiveLine ? highlightActiveLine() : activeLineExtn(),
keymap.of(customKeyMap??[]),
],
});
@ -243,6 +244,13 @@ export default function Editor({
}
}, [value]);
useEffect(()=>{
const keys = keymap.of([customKeyMap??[], defaultKeymap, closeBracketsKeymap, historyKeymap, foldKeymap, completionKeymap].flat());
editor.current?.dispatch({
effects: shortcuts.current.reconfigure(keys)
});
}, [customKeyMap]);
useEffect(() => {
let pref = preferencesStore.getPreferencesForModule('sqleditor');
let newConfigExtn = [];

View File

@ -90,7 +90,7 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu
setShowGoto(true);
},
},
...customKeyMap], []);
...customKeyMap], [customKeyMap]);
const closeFind = () => {
setShowFind([false, false]);

View File

@ -14,6 +14,7 @@ import convert from 'convert-units';
import getApiInstance from './api_instance';
import usePreferences from '../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
import { isMac } from './keyboard_shortcuts';
export function parseShortcutValue(obj) {
let shortcut = '';
@ -27,6 +28,37 @@ export function parseShortcutValue(obj) {
return shortcut;
}
export function isShortcutValue(obj) {
if(!obj) return false;
if([obj.alt, obj.control, obj?.key, obj?.key?.char].every((k)=>!_.isUndefined(k))){
return true;
}
return false;
}
// Convert shortcut obj to codemirror key format
export function toCodeMirrorKey(obj) {
let shortcut = '';
if (!obj){
return shortcut;
}
if (obj.alt) { shortcut += 'Alt-'; }
if (obj.shift) { shortcut += 'Shift-'; }
if (obj.control) {
if(isMac() && obj.ctrl_is_meta) {
shortcut += 'Mod-';
} else {
shortcut += 'Ctrl-';
}
}
if(obj?.key.char?.length == 1) {
shortcut += obj?.key.char?.toLowerCase();
} else {
shortcut += obj?.key.char;
}
return shortcut;
}
export function getEpoch(inp_date) {
let date_obj = inp_date ? inp_date : new Date();
return parseInt(date_obj.getTime()/1000);

View File

@ -79,13 +79,85 @@ function onBeforeUnload(e) {
e.preventDefault();
e.returnValue = 'prevent';
}
const FIXED_PREF = {
find: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 70,
'char': 'F',
},
},
replace: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': true,
'key': {
'key_code': 70,
'char': 'F',
},
},
gotolinecol: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 76,
'char': 'L',
},
},
indent: {
'control': false,
'shift': false,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
unindent: {
'control': false,
'shift': true,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
comment: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 191,
'char': '/',
},
},
format_sql: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 75,
'char': 'k',
},
},
};
export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedNodeInfo, qtPanelDocker, qtPanelId, eventBusObj}) {
const containerRef = React.useRef(null);
const preferencesStore = usePreferences();
const [qtState, _setQtState] = useState({
preferences: {
browser: preferencesStore.getPreferencesForModule('browser'),
sqleditor: preferencesStore.getPreferencesForModule('sqleditor'),
sqleditor: {...preferencesStore.getPreferencesForModule('sqleditor'), ...FIXED_PREF},
graphs: preferencesStore.getPreferencesForModule('graphs'),
misc: preferencesStore.getPreferencesForModule('misc'),
},
@ -362,7 +434,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
state => {
setQtState({preferences: {
browser: state.getPreferencesForModule('browser'),
sqleditor: state.getPreferencesForModule('sqleditor'),
sqleditor: {...state.getPreferencesForModule('sqleditor'), ...FIXED_PREF},
graphs: state.getPreferencesForModule('graphs'),
misc: state.getPreferencesForModule('misc'),
}});

View File

@ -48,87 +48,6 @@ const useStyles = makeStyles((theme)=>({
},
}));
const FIXED_PREF = {
find: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 70,
'char': 'F',
},
},
replace: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': true,
'key': {
'key_code': 70,
'char': 'F',
},
},
gotolinecol: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 76,
'char': 'L',
},
},
indent: {
'control': false,
'shift': false,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
unindent: {
'control': false,
'shift': true,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
comment: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 191,
'char': '/',
},
},
uncomment: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 190,
'char': '.',
},
},
format_sql: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 75,
'char': 'k',
},
},
};
function autoCommitRollback(type, api, transId, value) {
let url = url_for(`sqleditor.${type}`, {
'trans_id': transId,
@ -451,7 +370,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
}
},
{
shortcut: FIXED_PREF.format_sql,
shortcut: queryToolPref.format_sql,
options: {
callback: ()=>{formatSQL();}
}
@ -596,25 +515,25 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
onClose={onMenuClose}
label={gettext('Edit Menu')}
>
<PgMenuItem shortcut={FIXED_PREF.find}
<PgMenuItem shortcut={queryToolPref.find}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.replace}
<PgMenuItem shortcut={queryToolPref.replace}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.gotolinecol}
<PgMenuItem shortcut={queryToolPref.gotolinecol}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'gotoLineCol');}}>{gettext('Go to Line/Column')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={FIXED_PREF.indent}
<PgMenuItem shortcut={queryToolPref.indent}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentMore');}}>{gettext('Indent Selection')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.unindent}
<PgMenuItem shortcut={queryToolPref.unindent}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentLess');}}>{gettext('Unindent Selection')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.comment}
<PgMenuItem shortcut={queryToolPref.comment}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'toggleComment');}}>{gettext('Toggle Comment')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.toggle_case}
onClick={toggleCase}>{gettext('Toggle Case Of Selected Text')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.clear_query}
onClick={clearQuery}>{gettext('Clear Query')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={FIXED_PREF.format_sql}onClick={formatSQL}>{gettext('Format SQL')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.format_sql}onClick={formatSQL}>{gettext('Format SQL')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={filterMenuRef}

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/styles';
import React, {useContext, useCallback, useEffect } from 'react';
import React, {useContext, useCallback, useEffect, useMemo } from 'react';
import { format } from 'sql-formatter';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
@ -17,7 +17,7 @@ import { LayoutDockerContext, LAYOUT_EVENTS } from '../../../../../../static/js/
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
import gettext from 'sources/gettext';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
import { checkTrojanSource } from '../../../../../../static/js/utils';
import { checkTrojanSource, isShortcutValue, toCodeMirrorKey } from '../../../../../../static/js/utils';
import { parseApiError } from '../../../../../../static/js/api_instance';
import { usePgAdmin } from '../../../../../../static/js/BrowserComponent';
import ConfirmPromotionContent from '../dialogs/ConfirmPromotionContent';
@ -70,6 +70,8 @@ export default function Query() {
const pgAdmin = usePgAdmin();
const preferencesStore = usePreferences();
const queryToolPref = queryToolCtx.preferences.sqleditor;
const highlightError = (cmObj, {errormsg: result, data})=>{
let errorLineNo = 0,
startMarker = 0,
@ -255,38 +257,6 @@ export default function Query() {
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE, ()=>{
change();
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, ()=>{
let selection = true, sql = editor.current?.getSelection();
let sqlEditorPref = preferencesStore.getPreferencesForModule('sqleditor');
/* New library does not support capitalize casing
so if a user has set capitalize casing we will
use preserve casing which is default for the library.
*/
let formatPrefs = {
language: 'postgresql',
keywordCase: sqlEditorPref.keyword_case === 'capitalize' ? 'preserve' : sqlEditorPref.keyword_case,
identifierCase: sqlEditorPref.identifier_case === 'capitalize' ? 'preserve' : sqlEditorPref.identifier_case,
dataTypeCase: sqlEditorPref.data_type_case,
functionCase: sqlEditorPref.function_case,
logicalOperatorNewline: sqlEditorPref.logical_operator_new_line,
expressionWidth: sqlEditorPref.expression_width,
linesBetweenQueries: sqlEditorPref.lines_between_queries,
tabWidth: sqlEditorPref.tab_size,
useTabs: !sqlEditorPref.use_spaces,
denseOperators: !sqlEditorPref.spaces_around_operators,
newlineBeforeSemicolon: sqlEditorPref.new_line_before_semicolon
};
if(sql == '') {
sql = editor.current.getValue();
selection = false;
}
let formattedSql = format(sql,formatPrefs);
if(selection) {
editor.current.replaceSelection(formattedSql, 'around');
} else {
editor.current.setValue(formattedSql);
}
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_TOGGLE_CASE, ()=>{
let selectedText = editor.current?.getSelection();
if (!selectedText) return;
@ -334,7 +304,46 @@ export default function Query() {
/>
));
};
return eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE, warnSaveTextClose);
const formatSQL = ()=>{
let selection = true, sql = editor.current?.getSelection();
/* New library does not support capitalize casing
so if a user has set capitalize casing we will
use preserve casing which is default for the library.
*/
let formatPrefs = {
language: 'postgresql',
keywordCase: queryToolPref.keyword_case === 'capitalize' ? 'preserve' : queryToolPref.keyword_case,
identifierCase: queryToolPref.identifier_case === 'capitalize' ? 'preserve' : queryToolPref.identifier_case,
dataTypeCase: queryToolPref.data_type_case,
functionCase: queryToolPref.function_case,
logicalOperatorNewline: queryToolPref.logical_operator_new_line,
expressionWidth: queryToolPref.expression_width,
linesBetweenQueries: queryToolPref.lines_between_queries,
tabWidth: queryToolPref.tab_size,
useTabs: !queryToolPref.use_spaces,
denseOperators: !queryToolPref.spaces_around_operators,
newlineBeforeSemicolon: queryToolPref.new_line_before_semicolon
};
if(sql == '') {
sql = editor.current.getValue();
selection = false;
}
let formattedSql = format(sql,formatPrefs);
if(selection) {
editor.current.replaceSelection(formattedSql, 'around');
} else {
editor.current.setValue(formattedSql);
}
};
const unregisterFormatSQL = eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, formatSQL);
const unregisterWarn = eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE, warnSaveTextClose);
return ()=>{
unregisterFormatSQL();
unregisterWarn();
};
}, [queryToolCtx.preferences]);
useEffect(()=>{
@ -400,6 +409,33 @@ export default function Query() {
}
};
const shortcutOverrideKeys = useMemo(
()=>{
const queryToolPref = queryToolCtx.preferences.sqleditor;
return Object.values(queryToolPref)
.filter((p)=>isShortcutValue(p))
.map((p)=>({
key: toCodeMirrorKey(p), run: (_v, e)=>{
queryToolCtx.mainContainerRef?.current?.dispatchEvent(new KeyboardEvent('keydown', {
which: e.which,
keyCode: e.keyCode,
altKey: e.altKey,
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
}));
if(toCodeMirrorKey(p) == 'Mod-k') {
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL);
}
return true;
},
preventDefault: true,
stopPropagation: true,
}));
},
[queryToolCtx.preferences]
);
return <CodeMirror
currEditor={(obj)=>{
editor.current=obj;
@ -409,14 +445,6 @@ export default function Query() {
onCursorActivity={cursorActivity}
onChange={change}
autocomplete={true}
customKeyMap={[
{
key: 'Mod-k', run: (_view, e) => {
e.preventDefault();
e.stopPropagation();
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL);
},
}
]}
customKeyMap={shortcutOverrideKeys}
/>;
}