diff --git a/client/modules/IDE/components/Editor/contexts.jsx b/client/modules/IDE/components/Editor/contexts.jsx
new file mode 100644
index 0000000000..44bcfa12fa
--- /dev/null
+++ b/client/modules/IDE/components/Editor/contexts.jsx
@@ -0,0 +1,45 @@
+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-${metaKey}-F`,
+ findPersistent: `${metaKey}-F`,
+ findPersistentNext: `${metaKey}-G`,
+ findPersistentPrev: `Shift-${metaKey}-G`,
+ colorPicker: `${metaKey}-K`
+ });
+
+ 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..3da7fcf69e 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,
+ [`${keyMaps.findPersistent}`]: 'findPersistent',
+ [`${keyMaps.tidy}`]: this.tidyCode,
+ [`${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.
+ [`${keyMaps.colorPicker}`]: (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..75c831cbe1
--- /dev/null
+++ b/client/modules/IDE/components/KeyboardShortcutItem.jsx
@@ -0,0 +1,138 @@
+import React, { useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import { useEditorKeyMap } from './Editor/contexts';
+
+function KeyboardShortcutItem({ desc, keyName }) {
+ const [edit, setEdit] = useState(false);
+ const pressedKeyCombination = useRef({});
+ const inputRef = useRef(null);
+ const { updateKeyMap, keyMaps } = useEditorKeyMap();
+
+ 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) {
+ 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 (
+
+
+
+
+
+ {keyMaps[keyName]}
+
+ {desc}
+
+ );
+}
+
+KeyboardShortcutItem.propTypes = {
+ 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 fbca28a572..2aaed28bdd 100644
--- a/client/modules/IDE/components/KeyboardShortcutModal.jsx
+++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx
@@ -1,9 +1,11 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { metaKeyName, metaKey } from '../../../utils/metaKey';
+import KeyboardShortcutItem from './KeyboardShortcutItem';
function KeyboardShortcutModal() {
const { t } = useTranslation();
+
const replaceCommand =
metaKey === 'Ctrl' ? `${metaKeyName} + H` : `${metaKeyName} + ⌥ + F`;
const newFileCommand =
@@ -25,28 +27,22 @@ function KeyboardShortcutModal() {
.
- -
-
- {metaKeyName} + Shift + F
-
- {t('KeyboardShortcuts.CodeEditing.Tidy')}
-
- -
- {metaKeyName} + F
- {t('KeyboardShortcuts.CodeEditing.FindText')}
-
- -
- {metaKeyName} + G
- {t('KeyboardShortcuts.CodeEditing.FindNextTextMatch')}
-
- -
-
- {metaKeyName} + Shift + G
-
-
- {t('KeyboardShortcuts.CodeEditing.FindPreviousTextMatch')}
-
-
+
+
+
+
-
{replaceCommand}
{t('KeyboardShortcuts.CodeEditing.ReplaceTextMatch')}
@@ -67,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/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')}
+
+
+
+
+
-
-
- )}
-
+
+ )}
+
+
);
};
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;