From cda498f77994c6e695018f2efc1a1e1631fbfb2a Mon Sep 17 00:00:00 2001 From: Pravesh Sharma Date: Fri, 13 Jun 2025 15:48:54 +0530 Subject: [PATCH] Added support for customizing keyboard shortcuts in the Query Tool's Edit menu. #2659 --- .../js/components/ReactCodeMirror/index.jsx | 58 +++++- .../js/components/QueryToolComponent.jsx | 50 ------ .../js/components/QueryToolConstants.js | 1 - .../js/components/sections/MainToolBar.jsx | 13 +- .../static/js/components/sections/Query.jsx | 168 ++++++------------ .../sqleditor/utils/query_tool_preferences.py | 120 ++++++++++++- 6 files changed, 228 insertions(+), 182 deletions(-) diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx index 1b198d996..6af2f4cdc 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx @@ -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, }; diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx index df59bcf19..a0d450aff 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx @@ -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}) { diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js index 3b41efcf2..a60d2bf11 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js @@ -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', diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx index d87b4a838..da0e64ad5 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx @@ -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')} {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')} - {executeCmd('gotoLineCol');}}>{gettext('Go to Line/Column')} {gettext('Clear Query')} - {gettext('Format SQL')} + {executeCmd('formatSql');}}>{gettext('Format SQL')} { @@ -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; diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index 13c9bbd70..aa5312d87 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -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',