From 425818ad44a848e031347f3161fa1a35001bdb84 Mon Sep 17 00:00:00 2001
From: Karan Yadav
Date: Thu, 10 Apr 2025 18:44:22 +0000
Subject: [PATCH 1/2] [#3444] Add keyboard shortcut update functionality
---
.../IDE/components/Editor/contexts.jsx | 38 ++++
.../modules/IDE/components/Editor/index.jsx | 81 ++++---
.../IDE/components/KeyboardShortcutItem.jsx | 79 +++++++
.../IDE/components/KeyboardShortcutModal.jsx | 14 +-
client/modules/IDE/pages/IDEView.jsx | 199 +++++++++---------
5 files changed, 277 insertions(+), 134 deletions(-)
create mode 100644 client/modules/IDE/components/Editor/contexts.jsx
create mode 100644 client/modules/IDE/components/KeyboardShortcutItem.jsx
diff --git a/client/modules/IDE/components/Editor/contexts.jsx b/client/modules/IDE/components/Editor/contexts.jsx
new file mode 100644
index 0000000000..7f1702c3b6
--- /dev/null
+++ b/client/modules/IDE/components/Editor/contexts.jsx
@@ -0,0 +1,38 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { createContext, useState } from 'react';
+
+export const EditorKeyMapsContext = createContext();
+
+export function EditorKeyMapProvider({ children }) {
+ const [keyMaps, setKeyMaps] = useState({ tidy: 'Shift-Ctrl-F' });
+
+ const updateKeyMap = (key, value) => {
+ if (key in keyMaps) {
+ setKeyMaps((prevKeyMaps) => ({
+ ...prevKeyMaps,
+ [key]: value
+ }));
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+EditorKeyMapProvider.propTypes = {
+ children: PropTypes.node.isRequired
+};
+
+export const useEditorKeyMap = () => {
+ const context = useContext(EditorKeyMapsContext);
+ if (!context) {
+ throw new Error(
+ 'useEditorKeyMap must be used within a EditorKeyMapProvider'
+ );
+ }
+ return context;
+};
diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index 404741591a..eef77209cf 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -72,6 +72,7 @@ import UnsavedChangesIndicator from '../UnsavedChangesIndicator';
import { EditorContainer, EditorHolder } from './MobileEditor';
import { FolderIcon } from '../../../../common/icons';
import IconButton from '../../../../common/IconButton';
+import { EditorKeyMapsContext } from './contexts';
emmet(CodeMirror);
@@ -104,6 +105,7 @@ class Editor extends React.Component {
this.showFind = this.showFind.bind(this);
this.showReplace = this.showReplace.bind(this);
this.getContent = this.getContent.bind(this);
+ this.updateKeyMaps = this.updateKeyMaps.bind(this);
}
componentDidMount() {
@@ -153,36 +155,9 @@ class Editor extends React.Component {
delete this._cm.options.lint.options.errors;
- const replaceCommand =
- metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`;
- this._cm.setOption('extraKeys', {
- Tab: (cm) => {
- if (!cm.execCommand('emmetExpandAbbreviation')) return;
- // might need to specify and indent more?
- const selection = cm.doc.getSelection();
- if (selection.length > 0) {
- cm.execCommand('indentMore');
- } else {
- cm.replaceSelection(' '.repeat(INDENTATION_AMOUNT));
- }
- },
- Enter: 'emmetInsertLineBreak',
- Esc: 'emmetResetAbbreviation',
- [`Shift-Tab`]: false,
- [`${metaKey}-Enter`]: () => null,
- [`Shift-${metaKey}-Enter`]: () => null,
- [`${metaKey}-F`]: 'findPersistent',
- [`Shift-${metaKey}-F`]: this.tidyCode,
- [`${metaKey}-G`]: 'findPersistentNext',
- [`Shift-${metaKey}-G`]: 'findPersistentPrev',
- [replaceCommand]: 'replace',
- // Cassie Tarakajian: If you don't set a default color, then when you
- // choose a color, it deletes characters inline. This is a
- // hack to prevent that.
- [`${metaKey}-K`]: (cm, event) =>
- cm.state.colorpicker.popup_color_picker({ length: 0 }),
- [`${metaKey}-.`]: 'toggleComment' // Note: most adblockers use the shortcut ctrl+.
- });
+ const { keyMaps } = this.context;
+
+ this.updateKeyMaps(keyMaps);
this.initializeDocuments(this.props.files);
this._cm.swapDoc(this._docs[this.props.file.id]);
@@ -236,6 +211,16 @@ class Editor extends React.Component {
}
componentDidUpdate(prevProps) {
+ const currentKeyMaps = this.context?.keyMaps;
+ const prevKeyMaps = this.prevKeyMapsRef?.current;
+
+ if (
+ prevKeyMaps &&
+ JSON.stringify(currentKeyMaps) !== JSON.stringify(prevKeyMaps)
+ ) {
+ this.updateKeyMaps(currentKeyMaps);
+ }
+ this.prevKeyMapsRef = { current: currentKeyMaps };
if (this.props.file.id !== prevProps.file.id) {
const fileMode = this.getFileMode(this.props.file.name);
if (fileMode === 'javascript') {
@@ -515,6 +500,42 @@ class Editor extends React.Component {
});
}
+ updateKeyMaps(keyMaps) {
+ const replaceCommand =
+ metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`;
+
+ this._cm.setOption('extraKeys', {
+ Tab: (cm) => {
+ if (!cm.execCommand('emmetExpandAbbreviation')) return;
+ // might need to specify and indent more?
+ const selection = cm.doc.getSelection();
+ if (selection.length > 0) {
+ cm.execCommand('indentMore');
+ } else {
+ cm.replaceSelection(' '.repeat(INDENTATION_AMOUNT));
+ }
+ },
+ Enter: 'emmetInsertLineBreak',
+ Esc: 'emmetResetAbbreviation',
+ [`Shift-Tab`]: false,
+ [`${metaKey}-Enter`]: () => null,
+ [`Shift-${metaKey}-Enter`]: () => null,
+ [`${metaKey}-F`]: 'findPersistent',
+ [`${keyMaps.tidy}`]: this.tidyCode,
+ [`${metaKey}-G`]: 'findPersistentNext',
+ [`Shift-${metaKey}-G`]: 'findPersistentPrev',
+ [replaceCommand]: 'replace',
+ // Cassie Tarakajian: If you don't set a default color, then when you
+ // choose a color, it deletes characters inline. This is a
+ // hack to prevent that.
+ [`${metaKey}-K`]: (cm, event) =>
+ cm.state.colorpicker.popup_color_picker({ length: 0 }),
+ [`${metaKey}-.`]: 'toggleComment' // Note: most adblockers use the shortcut ctrl+.
+ });
+ }
+
+ static contextType = EditorKeyMapsContext;
+
render() {
const editorSectionClass = classNames({
editor: true,
diff --git a/client/modules/IDE/components/KeyboardShortcutItem.jsx b/client/modules/IDE/components/KeyboardShortcutItem.jsx
new file mode 100644
index 0000000000..3c2926143c
--- /dev/null
+++ b/client/modules/IDE/components/KeyboardShortcutItem.jsx
@@ -0,0 +1,79 @@
+import React, { useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import { useEditorKeyMap } from './Editor/contexts';
+
+function KeyboardShortcutItem({ shortcut, desc }) {
+ const [edit, setEdit] = useState(false);
+ const pressedKeyCombination = useRef({});
+ const inputRef = useRef(null);
+ const { updateKeyMap } = useEditorKeyMap();
+
+ const handleEdit = (state) => {
+ setEdit(state);
+ if (state) {
+ inputRef.current.focus();
+ } else {
+ inputRef.current.blur();
+ updateKeyMap('tidy', inputRef.current.innerText);
+ }
+ };
+
+ return (
+
+
+ {
+ if (!edit) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ let { key } = event;
+ if (key === 'Control') {
+ key = 'Ctrl';
+ }
+ if (key === ' ') {
+ key = 'Space';
+ }
+
+ pressedKeyCombination.current[key] = true;
+
+ event.currentTarget.innerText = Object.keys(
+ pressedKeyCombination.current
+ ).join('-');
+ }}
+ onKeyUp={(event) => {
+ if (!edit) return;
+ event.preventDefault();
+ event.stopPropagation();
+ let { key } = event;
+ if (key === 'Control') {
+ key = 'Ctrl';
+ }
+ if (key === ' ') {
+ key = 'Space';
+ }
+
+ delete pressedKeyCombination.current[key];
+ }}
+ >
+ {shortcut}
+
+ {desc}
+
+ );
+}
+
+KeyboardShortcutItem.propTypes = {
+ shortcut: PropTypes.string.isRequired,
+ desc: PropTypes.string.isRequired
+};
+
+export default KeyboardShortcutItem;
diff --git a/client/modules/IDE/components/KeyboardShortcutModal.jsx b/client/modules/IDE/components/KeyboardShortcutModal.jsx
index fbca28a572..3baa59193e 100644
--- a/client/modules/IDE/components/KeyboardShortcutModal.jsx
+++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx
@@ -1,9 +1,13 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { metaKeyName, metaKey } from '../../../utils/metaKey';
+import KeyboardShortcutItem from './KeyboardShortcutItem';
+import { useEditorKeyMap } from './Editor/contexts';
function KeyboardShortcutModal() {
const { t } = useTranslation();
+ const { keyMaps } = useEditorKeyMap();
+
const replaceCommand =
metaKey === 'Ctrl' ? `${metaKeyName} + H` : `${metaKeyName} + ⌥ + F`;
const newFileCommand =
@@ -25,12 +29,10 @@ function KeyboardShortcutModal() {
.
- -
-
- {metaKeyName} + Shift + F
-
- {t('KeyboardShortcuts.CodeEditing.Tidy')}
-
+
-
{metaKeyName} + F
{t('KeyboardShortcuts.CodeEditing.FindText')}
diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx
index 4084facb77..3ce850b73e 100644
--- a/client/modules/IDE/pages/IDEView.jsx
+++ b/client/modules/IDE/pages/IDEView.jsx
@@ -27,6 +27,7 @@ import {
} from '../components/Editor/MobileEditor';
import IDEOverlays from '../components/IDEOverlays';
import useIsMobile from '../hooks/useIsMobile';
+import { EditorKeyMapProvider } from '../components/Editor/contexts';
function getTitle(project) {
const { id } = project;
@@ -167,121 +168,123 @@ const IDEView = () => {
return (
-
- {getTitle(project)}
-
- cmRef.current?.getContent()} />
-
-
-
-
-
- {isMobile ? (
- <>
-
-
- {
- setConsoleSize(size);
- setIsOverlayVisible(true);
- }}
- onDragFinished={() => {
- setIsOverlayVisible(false);
- }}
- allowResize={ide.consoleIsExpanded}
- className="editor-preview-subpanel"
- >
-
-
-
-
-
-
- {
- cmRef.current = ctl;
- }}
+
+
+ {getTitle(project)}
+
+ cmRef.current?.getContent()} />
+
+
+
+
+
+ {isMobile ? (
+ <>
+
-
- >
- ) : (
-
- {
- setSidebarSize(size);
- }}
- allowResize={ide.sidebarIsExpanded}
- minSize={150}
- >
-
- {
- setIsOverlayVisible(true);
- }}
- onDragFinished={() => {
- setIsOverlayVisible(false);
- }}
- resizerStyle={{
- borderLeftWidth: '2px',
- borderRightWidth: '2px',
- width: '2px',
- margin: '0px 0px'
- }}
- >
+
{
setConsoleSize(size);
+ setIsOverlayVisible(true);
+ }}
+ onDragFinished={() => {
+ setIsOverlayVisible(false);
}}
allowResize={ide.consoleIsExpanded}
className="editor-preview-subpanel"
>
- {
- cmRef.current = ctl;
- }}
+
-
-
-
- {t('Toolbar.Preview')}
-
-
-
-
+
+
+ {
+ cmRef.current = ctl;
+ }}
+ />
+
+ >
+ ) : (
+
+ {
+ setSidebarSize(size);
+ }}
+ allowResize={ide.sidebarIsExpanded}
+ minSize={150}
+ >
+
+ {
+ setIsOverlayVisible(true);
+ }}
+ onDragFinished={() => {
+ setIsOverlayVisible(false);
+ }}
+ resizerStyle={{
+ borderLeftWidth: '2px',
+ borderRightWidth: '2px',
+ width: '2px',
+ margin: '0px 0px'
+ }}
+ >
+ {
+ setConsoleSize(size);
+ }}
+ allowResize={ide.consoleIsExpanded}
+ className="editor-preview-subpanel"
+ >
+ {
+ cmRef.current = ctl;
+ }}
/>
-
-
+
+
+
+
+
+ {t('Toolbar.Preview')}
+
+
+
+
+
-
-
- )}
-
+
+ )}
+
+
);
};
From cbc82da83686f956423c431907bca0fefe5a9608 Mon Sep 17 00:00:00 2001
From: Karan Yadav
Date: Fri, 11 Apr 2025 20:33:22 +0000
Subject: [PATCH 2/2] [#3444] Add key-maps
This PR add more shortcuts to the map which
user can change.
---
.../IDE/components/Editor/contexts.jsx | 9 +-
.../modules/IDE/components/Editor/index.jsx | 8 +-
.../IDE/components/KeyboardShortcutItem.jsx | 149 ++++++++++++------
.../IDE/components/KeyboardShortcutModal.jsx | 40 ++---
.../components/_keyboard-shortcuts.scss | 4 +
5 files changed, 137 insertions(+), 73 deletions(-)
diff --git a/client/modules/IDE/components/Editor/contexts.jsx b/client/modules/IDE/components/Editor/contexts.jsx
index 7f1702c3b6..44bcfa12fa 100644
--- a/client/modules/IDE/components/Editor/contexts.jsx
+++ b/client/modules/IDE/components/Editor/contexts.jsx
@@ -1,11 +1,18 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { createContext, useState } from 'react';
+import { metaKey } from '../../../../utils/metaKey';
export const EditorKeyMapsContext = createContext();
export function EditorKeyMapProvider({ children }) {
- const [keyMaps, setKeyMaps] = useState({ tidy: 'Shift-Ctrl-F' });
+ const [keyMaps, setKeyMaps] = useState({
+ tidy: `Shift-${metaKey}-F`,
+ findPersistent: `${metaKey}-F`,
+ findPersistentNext: `${metaKey}-G`,
+ findPersistentPrev: `Shift-${metaKey}-G`,
+ colorPicker: `${metaKey}-K`
+ });
const updateKeyMap = (key, value) => {
if (key in keyMaps) {
diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index eef77209cf..3da7fcf69e 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -520,15 +520,15 @@ class Editor extends React.Component {
[`Shift-Tab`]: false,
[`${metaKey}-Enter`]: () => null,
[`Shift-${metaKey}-Enter`]: () => null,
- [`${metaKey}-F`]: 'findPersistent',
+ [`${keyMaps.findPersistent}`]: 'findPersistent',
[`${keyMaps.tidy}`]: this.tidyCode,
- [`${metaKey}-G`]: 'findPersistentNext',
- [`Shift-${metaKey}-G`]: 'findPersistentPrev',
+ [`${keyMaps.findPersistentNext}`]: 'findPersistentNext',
+ [`${keyMaps.findPersistentPrev}`]: 'findPersistentPrev',
[replaceCommand]: 'replace',
// Cassie Tarakajian: If you don't set a default color, then when you
// choose a color, it deletes characters inline. This is a
// hack to prevent that.
- [`${metaKey}-K`]: (cm, event) =>
+ [`${keyMaps.colorPicker}`]: (cm, event) =>
cm.state.colorpicker.popup_color_picker({ length: 0 }),
[`${metaKey}-.`]: 'toggleComment' // Note: most adblockers use the shortcut ctrl+.
});
diff --git a/client/modules/IDE/components/KeyboardShortcutItem.jsx b/client/modules/IDE/components/KeyboardShortcutItem.jsx
index 3c2926143c..75c831cbe1 100644
--- a/client/modules/IDE/components/KeyboardShortcutItem.jsx
+++ b/client/modules/IDE/components/KeyboardShortcutItem.jsx
@@ -2,27 +2,117 @@ import React, { useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useEditorKeyMap } from './Editor/contexts';
-function KeyboardShortcutItem({ shortcut, desc }) {
+function KeyboardShortcutItem({ desc, keyName }) {
const [edit, setEdit] = useState(false);
const pressedKeyCombination = useRef({});
const inputRef = useRef(null);
- const { updateKeyMap } = useEditorKeyMap();
+ const { updateKeyMap, keyMaps } = useEditorKeyMap();
- const handleEdit = (state) => {
+ if (!Object.keys(keyMaps).includes(keyName)) {
+ return null;
+ }
+
+ const cancelEdit = () => {
+ setEdit(false);
+ pressedKeyCombination.current = {};
+ inputRef.current.innerText = keyMaps[keyName];
+ };
+
+ const handleEdit = (state, key) => {
setEdit(state);
- if (state) {
- inputRef.current.focus();
- } else {
- inputRef.current.blur();
- updateKeyMap('tidy', inputRef.current.innerText);
+ if (!state) {
+ updateKeyMap(key, inputRef.current.innerText);
+ cancelEdit();
+ }
+ };
+
+ const handleKeyDown = (event) => {
+ if (!edit) return;
+ event.preventDefault();
+ event.stopPropagation();
+ let { key } = event;
+ if (key === 'Control') {
+ key = 'Ctrl';
+ }
+ if (key === ' ') {
+ key = 'Space';
+ }
+ if (key.length === 1 && key.match(/[a-z]/i)) {
+ key = key.toUpperCase();
+ }
+
+ pressedKeyCombination.current[key] = true;
+
+ const allKeys = Object.keys(pressedKeyCombination.current).filter(
+ (k) => !['Shift', 'Ctrl', 'Alt'].includes(k)
+ );
+
+ if (event.altKey) {
+ allKeys.unshift('Alt');
+ }
+ if (event.ctrlKey) {
+ allKeys.unshift('Ctrl');
+ }
+ if (event.shiftKey) {
+ allKeys.unshift('Shift');
}
+
+ event.currentTarget.innerText = allKeys.join('-');
+ };
+
+ const handleKeyUp = (event) => {
+ if (!edit) return;
+ event.preventDefault();
+ event.stopPropagation();
+ let { key } = event;
+ if (key === 'Control') {
+ key = 'Ctrl';
+ }
+ if (key === ' ') {
+ key = 'Space';
+ }
+ if (key.length === 1 && key.match(/[a-z]/i)) {
+ key = key.toUpperCase();
+ }
+
+ delete pressedKeyCombination.current[key];
};
return (
-
-
@@ -72,8 +131,8 @@ function KeyboardShortcutItem({ shortcut, desc }) {
}
KeyboardShortcutItem.propTypes = {
- shortcut: PropTypes.string.isRequired,
- desc: PropTypes.string.isRequired
+ desc: PropTypes.string.isRequired,
+ keyName: PropTypes.string.isRequired
};
export default KeyboardShortcutItem;
diff --git a/client/modules/IDE/components/KeyboardShortcutModal.jsx b/client/modules/IDE/components/KeyboardShortcutModal.jsx
index 3baa59193e..2aaed28bdd 100644
--- a/client/modules/IDE/components/KeyboardShortcutModal.jsx
+++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx
@@ -2,11 +2,9 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { metaKeyName, metaKey } from '../../../utils/metaKey';
import KeyboardShortcutItem from './KeyboardShortcutItem';
-import { useEditorKeyMap } from './Editor/contexts';
function KeyboardShortcutModal() {
const { t } = useTranslation();
- const { keyMaps } = useEditorKeyMap();
const replaceCommand =
metaKey === 'Ctrl' ? `${metaKeyName} + H` : `${metaKeyName} + ⌥ + F`;
@@ -30,25 +28,21 @@ function KeyboardShortcutModal() {
+
+
+
- -
- {metaKeyName} + F
- {t('KeyboardShortcuts.CodeEditing.FindText')}
-
- -
- {metaKeyName} + G
- {t('KeyboardShortcuts.CodeEditing.FindNextTextMatch')}
-
- -
-
- {metaKeyName} + Shift + G
-
-
- {t('KeyboardShortcuts.CodeEditing.FindPreviousTextMatch')}
-
-
-
{replaceCommand}
{t('KeyboardShortcuts.CodeEditing.ReplaceTextMatch')}
@@ -69,10 +63,10 @@ function KeyboardShortcutModal() {
{metaKeyName} + .
{t('KeyboardShortcuts.CodeEditing.CommentLine')}
- -
- {metaKeyName} + K
- {t('KeyboardShortcuts.CodeEditing.ColorPicker')}
-
+
-
{newFileCommand}
{t('KeyboardShortcuts.CodeEditing.CreateNewFile')}
diff --git a/client/styles/components/_keyboard-shortcuts.scss b/client/styles/components/_keyboard-shortcuts.scss
index ce0f4a344a..31a578635a 100644
--- a/client/styles/components/_keyboard-shortcuts.scss
+++ b/client/styles/components/_keyboard-shortcuts.scss
@@ -21,6 +21,10 @@
align-items: baseline;
}
+.keyboard-shortcut__edit {
+ margin-right: 2px;
+}
+
.keyboard-shortcut__command {
font-weight: bold;
text-align: right;