Added support for customizing keyboard shortcuts in the Query Tool's Edit menu. #2659

pull/8849/head
Pravesh Sharma 2025-06-13 15:48:54 +05:30 committed by GitHub
parent 67c18cb082
commit cda498f779
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 228 additions and 182 deletions

View File

@ -12,6 +12,8 @@ import { styled } from '@mui/material/styles';
import FileCopyRoundedIcon from '@mui/icons-material/FileCopyRounded';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import PropTypes from 'prop-types';
import { startCompletion } from '@codemirror/autocomplete';
import { format } from 'sql-formatter';
import gettext from 'sources/gettext';
import { PgIconButton } from '../Buttons';
@ -22,6 +24,8 @@ import Editor from './components/Editor';
import CustomPropTypes from '../../custom_prop_types';
import FindDialog from './components/FindDialog';
import GotoDialog from './components/GotoDialog';
import usePreferences from '../../../../preferences/static/js/store';
import { toCodeMirrorKey } from '../../utils';
const Root = styled('div')(() => ({
position: 'relative',
@ -64,25 +68,71 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu
const [[showFind, isReplace, findKey], setShowFind] = useState([false, false, false]);
const [showGoto, setShowGoto] = useState(false);
const [showCopy, setShowCopy] = useState(false);
const preferences = usePreferences().getPreferencesForModule('sqleditor');
const formatSQL = (view)=>{
let selection = true, sql = view.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: preferences.keyword_case === 'capitalize' ? 'preserve' : preferences.keyword_case,
identifierCase: preferences.identifier_case === 'capitalize' ? 'preserve' : preferences.identifier_case,
dataTypeCase: preferences.data_type_case,
functionCase: preferences.function_case,
logicalOperatorNewline: preferences.logical_operator_new_line,
expressionWidth: preferences.expression_width,
linesBetweenQueries: preferences.lines_between_queries,
tabWidth: preferences.tab_size,
useTabs: !preferences.use_spaces,
denseOperators: !preferences.spaces_around_operators,
newlineBeforeSemicolon: preferences.new_line_before_semicolon
};
if(sql == '') {
sql = view.getValue();
selection = false;
}
let formattedSql = format(sql,formatPrefs);
if(selection) {
view.replaceSelection(formattedSql);
} else {
view.setValue(formattedSql);
}
};
const finalCustomKeyMap = useMemo(()=>[{
key: 'Mod-f', run: () => {
key: toCodeMirrorKey(preferences.find), run: () => {
setShowFind(prevVal => [true, false, !prevVal[2]]);
},
preventDefault: true,
stopPropagation: true,
}, {
key: 'Mod-Alt-f', run: () => {
key: toCodeMirrorKey(preferences.replace), run: () => {
setShowFind(prevVal => [true, true, !prevVal[2]]);
},
preventDefault: true,
stopPropagation: true,
}, {
key: 'Mod-l', run: () => {
key: toCodeMirrorKey(preferences.goto_line_col), run: () => {
setShowGoto(true);
},
preventDefault: true,
stopPropagation: true,
}, {
key: toCodeMirrorKey(preferences.comment), run: () => {
editor.current?.execCommand('toggleComment');
},
preventDefault: true,
stopPropagation: true,
},{
key: toCodeMirrorKey(preferences.format_sql), run: formatSQL,
preventDefault: true,
stopPropagation: true,
},{
key: toCodeMirrorKey(preferences.autocomplete), run: startCompletion,
preventDefault: true,
},
...customKeyMap], [customKeyMap]);
@ -148,5 +198,5 @@ CodeMirror.propTypes = {
className: CustomPropTypes.className,
showCopyBtn: PropTypes.bool,
customKeyMap: PropTypes.array,
onTextSelect:PropTypes.func
onTextSelect:PropTypes.func,
};

View File

@ -92,36 +92,6 @@ function setPanelTitle(docker, panelId, title, qtState, dirty=false) {
}
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,
@ -140,26 +110,6 @@ const FIXED_PREF = {
'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}) {

View File

@ -26,7 +26,6 @@ export const QUERY_TOOL_EVENTS = {
TRIGGER_INCLUDE_EXCLUDE_FILTER: 'TRIGGER_INCLUDE_EXCLUDE_FILTER',
TRIGGER_REMOVE_FILTER: 'TRIGGER_REMOVE_FILTER',
TRIGGER_SET_LIMIT: 'TRIGGER_SET_LIMIT',
TRIGGER_FORMAT_SQL: 'TRIGGER_FORMAT_SQL',
TRIGGER_GRAPH_VISUALISER: 'TRIGGER_GRAPH_VISUALISER',
TRIGGER_SELECT_ALL: 'TRIGGER_SELECT_ALL',
TRIGGER_SAVE_QUERY_TOOL_DATA: 'TRIGGER_SAVE_QUERY_TOOL_DATA',

View File

@ -291,9 +291,6 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
setLimit(e.target.value);
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SET_LIMIT,e.target.value);
};
const formatSQL=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL);
};
const toggleCase=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_TOGGLE_CASE);
};
@ -444,12 +441,6 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
callback: ()=>{clearQuery();}
}
},
{
shortcut: queryToolPref.format_sql,
options: {
callback: ()=>{formatSQL();}
}
},
], containerRef);
/* Macro shortcuts */
@ -598,7 +589,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.replace}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.gotolinecol}
<PgMenuItem shortcut={queryToolPref.goto_line_col}
onClick={()=>{executeCmd('gotoLineCol');}}>{gettext('Go to Line/Column')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={queryToolPref.indent}
@ -612,7 +603,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
<PgMenuItem shortcut={queryToolPref.clear_query}
onClick={clearQuery}>{gettext('Clear Query')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={queryToolPref.format_sql} onClick={formatSQL}>{gettext('Format SQL')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.format_sql} onClick={()=>{executeCmd('formatSql');}}>{gettext('Format SQL')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={filterMenuRef}

View File

@ -7,10 +7,9 @@
//
//////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useMemo, useState } from 'react';
import { format } from 'sql-formatter';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
import { PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import { PANELS, QUERY_TOOL_EVENTS, MODAL_DIALOGS } from '../QueryToolConstants';
import url_for from 'sources/url_for';
import { LayoutDockerContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
@ -24,7 +23,6 @@ import ConfirmExecuteQueryContent from '../dialogs/ConfirmExecuteQueryContent';
import usePreferences from '../../../../../../preferences/static/js/store';
import { getTitle } from '../../sqleditor_title';
import PropTypes from 'prop-types';
import { MODAL_DIALOGS } from '../QueryToolConstants';
import { useApplicationState, getToolData } from '../../../../../../settings/static/ApplicationStateProvider';
import { useDelayDebounce } from '../../../../../../static/js/custom_hooks';
@ -68,7 +66,6 @@ export default function Query({onTextSelect, setQtStatePartial}) {
const pgAdmin = usePgAdmin();
const {saveToolData, isSaveToolDataEnabled} = useApplicationState();
const preferencesStore = usePreferences();
const queryToolPref = queryToolCtx.preferences.sqleditor;
const modalId = MODAL_DIALOGS.QT_CONFIRMATIONS;
const highlightError = (cmObj, {errormsg: result, data}, executeCursor)=>{
@ -234,24 +231,6 @@ export default function Query({onTextSelect, setQtStatePartial}) {
});
});
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);
eventBus.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.QUERY);
@ -261,20 +240,6 @@ export default function Query({onTextSelect, setQtStatePartial}) {
}, 250);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, (replace=false)=>{
editor.current?.focus();
let key = {
keyCode: 70, metaKey: false, ctrlKey: true, shiftKey: false, altKey: replace,
};
if(isMac()) {
key.metaKey = true;
key.ctrlKey = false;
key.shiftKey = false;
key.altKey = replace;
}
editor.current?.fireDOMEvent(new KeyboardEvent('keydown', key));
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, (value, focus=true)=>{
focus && editor.current?.focus();
editor.current?.setValue(value, !queryToolCtx.params.is_query_tool);
@ -282,40 +247,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
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.CHANGE_EOL, (lineSep)=>{
// Set the new EOL character in the editor.
editor.current?.setEOL(lineSep);
@ -384,44 +316,58 @@ export default function Query({onTextSelect, setQtStatePartial}) {
), {id:modalId});
};
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
const createKeyObjectFromShortcut = (pref)=>{
// this function creates a key object from the shortcut preference
let key = {
keyCode: pref.key.key_code,
metaKey: pref.ctrl_is_meta,
ctrlKey: pref.control,
shiftKey: pref.shift,
altKey: pref.alt,
};
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);
if(isMac()) {
key.metaKey = true;
key.ctrlKey = false;
}
return key;
};
const unregisterFormatSQL = eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, formatSQL);
const unregisterEditorExecCmd = eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, (cmd='')=>{
let key = {}, gotolinecol = queryToolCtx.preferences.sqleditor.goto_line_col,
formatSql = queryToolCtx.preferences.sqleditor.format_sql;
switch(cmd) {
case 'gotoLineCol':
key = createKeyObjectFromShortcut(gotolinecol);
break;
case 'formatSql':
key = createKeyObjectFromShortcut(formatSql);
break;
default:
editor.current?.execCommand(cmd);
return;
}
editor.current?.fireDOMEvent(new KeyboardEvent('keydown', key));
});
const unregisterFindReplace = eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, (replace=false)=>{
let findShortcut = queryToolCtx.preferences.sqleditor.find;
let replaceShortcut = queryToolCtx.preferences.sqleditor.replace;
let key ={};
editor.current?.focus();
if (!replace) {
key = createKeyObjectFromShortcut(findShortcut);
} else {
key = createKeyObjectFromShortcut(replaceShortcut);
}
editor.current?.fireDOMEvent(new KeyboardEvent('keydown', key));
});
const unregisterWarn = eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE, warnSaveTextClose);
return ()=>{
unregisterFormatSQL();
unregisterWarn();
unregisterEditorExecCmd();
unregisterFindReplace();
};
}, [queryToolCtx.preferences]);
@ -531,7 +477,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
const shortcutOverrideKeys = useMemo(
()=>{
// omit CM internal shortcuts
const queryToolPref = _.omit(queryToolCtx.preferences.sqleditor, ['indent', 'unindent', 'comment']);
const queryToolPref = _.omit(queryToolCtx.preferences.sqleditor, ['indent', 'unindent']);
const queryToolShortcuts = Object.values(queryToolPref)
.filter((p)=>isShortcutValue(p))
.map((p)=>parseShortcutValue(p));
@ -540,18 +486,14 @@ export default function Query({onTextSelect, setQtStatePartial}) {
any: (_v, e)=>{
const eventStr = parseKeyEventValue(e);
if(queryToolShortcuts.includes(eventStr)) {
if((isMac() && eventStr == 'meta+k') || eventStr == 'ctrl+k') {
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL);
} else {
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,
}));
}
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,
}));
e.preventDefault();
e.stopPropagation();
return true;

View File

@ -551,7 +551,7 @@ def register_query_tool_preferences(self):
'control': True,
'key': {
'key_code': 76,
'char': 'L'
'char': 'l'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
@ -561,7 +561,7 @@ def register_query_tool_preferences(self):
self.preference.register(
'keyboard_shortcuts',
'download_results',
gettext('Download Results'),
gettext('Download results'),
'keyboardshortcut',
{
'alt': False,
@ -615,7 +615,7 @@ def register_query_tool_preferences(self):
self.preference.register(
'keyboard_shortcuts',
'switch_panel',
gettext('Switch Panel'),
gettext('Switch panel'),
'keyboardshortcut',
{
'alt': True,
@ -873,6 +873,120 @@ def register_query_tool_preferences(self):
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'find',
gettext('Find'),
'keyboardshortcut',
{
'alt': False,
'shift': False,
'control': True,
'ctrl_is_meta': True,
'key': {
'key_code': 70,
'char': 'f'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'replace',
gettext('Replace'),
'keyboardshortcut',
{
'alt': True,
'shift': False,
'control': True,
'ctrl_is_meta': True,
'key': {
'key_code': 70,
'char': 'f'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'goto_line_col',
gettext('Go to line/column'),
'keyboardshortcut',
{
'alt': False,
'shift': False,
'control': True,
'ctrl_is_meta': True,
'key': {
'key_code': 76,
'char': 'l'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'comment',
gettext('Toggle comment'),
'keyboardshortcut',
{
'alt': False,
'shift': False,
'control': True,
'ctrl_is_meta': True,
'key': {
'key_code': 191,
'char': '/'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'format_sql',
gettext('Format SQL'),
'keyboardshortcut',
{
'alt': False,
'shift': False,
'control': True,
'ctrl_is_meta': True,
'key': {
'key_code': 75,
'char': 'k'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'auto_complete',
gettext('Auto complete'),
'keyboardshortcut',
{
'alt': False,
'shift': False,
'control': True,
'ctrl_is_meta': False,
'key': {
'key_code': 32,
'char': 'Space'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.keyword_case = self.preference.register(
'editor', 'keyword_case',
gettext("Keyword case"), 'radioModern', 'upper',