From 2dac2d0e21ae1a71e9ee345a6bc85f8d1ad63727 Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Mon, 1 Sep 2025 15:03:16 +0530 Subject: [PATCH] Fixed an issue where editor shortcuts fail when using Option key combinations on macOS, due to macOS treating Option+Key as a different key input. #9116 --- docs/en_US/release_notes_9_8.rst | 3 +- .../js/components/ReactCodeMirror/index.jsx | 63 ++++++++++--------- web/pgadmin/static/js/utils.js | 44 +++++++++++-- .../static/js/components/sections/Query.jsx | 3 +- 4 files changed, 79 insertions(+), 34 deletions(-) diff --git a/docs/en_US/release_notes_9_8.rst b/docs/en_US/release_notes_9_8.rst index 76ac4aa65..986b4d566 100644 --- a/docs/en_US/release_notes_9_8.rst +++ b/docs/en_US/release_notes_9_8.rst @@ -34,4 +34,5 @@ Bug fixes ********* | `Issue #9090 `_ - Pin Paramiko to version 3.5.1 to fix the DSSKey error introduced in the latest release. - | `Issue #9095 `_ - Fixed an issue where pgAdmin config migration was failing while upgrading to v9.7. \ No newline at end of file + | `Issue #9095 `_ - Fixed an issue where pgAdmin config migration was failing while upgrading to v9.7. + | `Issue #9116 `_ - Fixed an issue where editor shortcuts fail when using Option key combinations on macOS, due to macOS treating Option+Key as a different key input. \ No newline at end of file diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx index 63979a006..3456ec0d0 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx @@ -25,7 +25,7 @@ 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'; +import { parseKeyEventValue, parseShortcutValue } from '../../utils'; const Root = styled('div')(() => ({ position: 'relative', @@ -103,39 +103,46 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu } }; - const finalCustomKeyMap = useMemo(()=>[{ - key: toCodeMirrorKey(editorPrefs.find), run: () => { - setShowFind(prevVal => [true, false, !prevVal[2]]); + // We're not using CodeMirror keymap and using any instead + // because on Mac, the alt key combination creates special + // chars and https://github.com/codemirror/view/commit/3cea8dba19845fe75bea4eae756c6103694f49f3 + const customShortcuts = { + [parseShortcutValue(editorPrefs.find, true)]: () => { + setTimeout(()=>{ + setShowFind(prevVal => [true, false, !prevVal[2]]); + }, 0); }, - preventDefault: true, - stopPropagation: true, - }, { - key: toCodeMirrorKey(editorPrefs.replace), run: () => { - setShowFind(prevVal => [true, true, !prevVal[2]]); + [parseShortcutValue(editorPrefs.replace, true)]: () => { + setTimeout(()=>{ + setShowFind(prevVal => [true, true, !prevVal[2]]); + }, 0); }, - preventDefault: true, - stopPropagation: true, - }, { - key: toCodeMirrorKey(editorPrefs.goto_line_col), run: () => { + [parseShortcutValue(editorPrefs.goto_line_col, true)]: () => { setShowGoto(true); }, - preventDefault: true, - stopPropagation: true, - }, { - key: toCodeMirrorKey(editorPrefs.comment), run: () => { + [parseShortcutValue(editorPrefs.comment, true)]: () => { editor.current?.execCommand('toggleComment'); }, - preventDefault: true, - stopPropagation: true, - },{ - key: toCodeMirrorKey(editorPrefs.format_sql), run: formatSQL, - preventDefault: true, - stopPropagation: true, - },{ - key: toCodeMirrorKey(preferences.auto_complete), run: startCompletion, - preventDefault: true, - }, - ...customKeyMap], [customKeyMap]); + [parseShortcutValue(editorPrefs.format_sql, true)]: formatSQL, + [parseShortcutValue(preferences.auto_complete, true)]: startCompletion, + }; + + const finalCustomKeyMap = useMemo(() => [ + { + any: (view, e) => { + const eventStr = parseKeyEventValue(e, true); + const callback = customShortcuts[eventStr]; + if(callback) { + e.preventDefault(); + e.stopPropagation(); + callback(view); + return true; + } + return false; + } + }, + ...customKeyMap + ], [customKeyMap]); const closeFind = () => { setShowFind([false, false, false]); diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 065d16bf7..a0c55842e 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -18,7 +18,7 @@ import pgAdmin from 'sources/pgadmin'; import { isMac } from './keyboard_shortcuts'; import { WORKSPACES } from '../../browser/static/js/constants'; -export function parseShortcutValue(obj) { +export function parseShortcutValue(obj, useKeyboardCode=false) { let shortcut = ''; if (!obj){ return null; @@ -27,11 +27,11 @@ export function parseShortcutValue(obj) { if (obj.shift) { shortcut += 'shift+'; } if (isMac() && obj.ctrl_is_meta) { shortcut += 'meta+'; } else if (obj.control) { shortcut += 'ctrl+'; } - shortcut += obj?.key.char?.toLowerCase(); + shortcut += useKeyboardCode ? shortcutCharToCode(obj?.key.char) : obj?.key.char?.toLowerCase(); return shortcut; } -export function parseKeyEventValue(e) { +export function parseKeyEventValue(e, useKeyboardCode=false) { let shortcut = ''; if(!e) { return null; @@ -40,7 +40,7 @@ export function parseKeyEventValue(e) { if (e.shiftKey) { shortcut += 'shift+'; } if (isMac() && e.metaKey) { shortcut += 'meta+'; } else if (e.ctrlKey) { shortcut += 'ctrl+'; } - shortcut += e.key.toLowerCase(); + shortcut += useKeyboardCode? e.code : e.key.toLowerCase(); return shortcut; } @@ -49,6 +49,42 @@ export function isShortcutValue(obj) { return [obj.alt, obj.control, obj?.key, obj?.key?.char].every((k)=>!_.isUndefined(k)); } +// Map shortcut character to key code +export function shortcutCharToCode(char) { + const punctuationMap = { + '`': 'Backquote', + '-': 'Minus', + '=': 'Equal', + '[': 'BracketLeft', + ']': 'BracketRight', + '\\': 'Backslash', + ';': 'Semicolon', + '\'': 'Quote', + ',': 'Comma', + '.': 'Period', + '/': 'Slash', + ' ': 'Space', + }; + + const mappedCode = punctuationMap[char.toLowerCase()]; + if (mappedCode) { + return mappedCode; + } + + // Fallback for alphanumeric keys (A-Z, 0-9) + const isAlphanumeric = /^[a-z0-9]$/i.test(char); + if (isAlphanumeric) { + if (char.length === 1 && /[a-zA-Z]/.test(char)) { + return `Key${char.toUpperCase()}`; + } + if (char.length === 1 && /[0-9]/.test(char)) { + return `Digit${char}`; + } + } + + return char; +} + export function getEnterKeyHandler(clickHandler) { return (e)=>{ diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx index 1ebedab62..3b26bdb56 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -15,7 +15,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, isShortcutValue, parseKeyEventValue, parseShortcutValue } from '../../../../../../static/js/utils'; +import { checkTrojanSource, isShortcutValue, parseKeyEventValue, parseShortcutValue, shortcutCharToCode } from '../../../../../../static/js/utils'; import { usePgAdmin } from '../../../../../../static/js/PgAdminProvider'; import ConfirmPromotionContent from '../dialogs/ConfirmPromotionContent'; import ConfirmExecuteQueryContent from '../dialogs/ConfirmExecuteQueryContent'; @@ -296,6 +296,7 @@ export default function Query({onTextSelect, setQtStatePartial}) { // this function creates a key object from the shortcut preference let key = { keyCode: pref.key.key_code, + code: shortcutCharToCode(pref.key.char), metaKey: false, ctrlKey: pref.control, shiftKey: pref.shift,