diff --git a/ui/frontend/Playground.tsx b/ui/frontend/Playground.tsx index a2da3023e..e33fb8bd7 100644 --- a/ui/frontend/Playground.tsx +++ b/ui/frontend/Playground.tsx @@ -11,6 +11,8 @@ import { Orientation } from './types'; import * as actions from './actions'; import styles from './Playground.module.css'; +import { useKeyDown } from './hooks/shortcuts'; +import { useAppDispatch } from './configureStore'; const TRACK_OPTION_NAME = { [Orientation.Horizontal]: 'rowGutters', @@ -88,7 +90,35 @@ const ResizableArea: React.FC = () => { }; const Playground: React.FC = () => { - const showNotifications = useSelector(selectors.anyNotificationsToShowSelector); + const showNotifications = useSelector( + selectors.anyNotificationsToShowSelector + ); + + const dispatch = useAppDispatch(); + const handleRustFmt = useCallback((_event) => { + dispatch(actions.performFormat()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleClippy = useCallback((_event) => { + dispatch(actions.performClippy()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleMiri = useCallback((_event) => { + dispatch(actions.performMiri()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleMacroExpansion = useCallback((_event) => { + dispatch(actions.performMacroExpansion()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const shortcutMap = new Map([ + [['Control', 'Alt', 'f'], handleRustFmt], + [['Control', 'Alt', 'c'], handleClippy], + [['Control', 'Alt', 'm'], handleMiri], + [['Control', 'Alt', 'x'], handleMacroExpansion], + ]); + useKeyDown(shortcutMap); return ( <> @@ -96,7 +126,7 @@ const Playground: React.FC = () => {
- { showNotifications && } + {showNotifications && } ); } diff --git a/ui/frontend/ToolsMenu.module.css b/ui/frontend/ToolsMenu.module.css new file mode 100644 index 000000000..74d170acd --- /dev/null +++ b/ui/frontend/ToolsMenu.module.css @@ -0,0 +1,5 @@ +.shortcut { + float: right; + background-color: #a0ffa0; + border: 2px solid #a0ffa0; +} diff --git a/ui/frontend/ToolsMenu.tsx b/ui/frontend/ToolsMenu.tsx index a0ed22c74..f47940ea1 100644 --- a/ui/frontend/ToolsMenu.tsx +++ b/ui/frontend/ToolsMenu.tsx @@ -9,6 +9,8 @@ import * as selectors from './selectors'; import * as actions from './actions'; import { useAppDispatch } from './configureStore'; +import styles from './ToolsMenu.module.css'; + interface ToolsMenuProps { close: () => void; } @@ -46,18 +48,21 @@ const ToolsMenu: React.FC = props => { + ⌘/Ctrl + Alt + f
Format this code with Rustfmt.
{rustfmtVersion} ({rustfmtVersionDetails})
+ ⌘/Ctrl + Alt + c
Catch common mistakes and improve the code using the Clippy linter.
{clippyVersion} ({clippyVersionDetails})
+ ⌘/Ctrl + Alt + m
Execute this program in the Miri interpreter to detect certain cases of undefined behavior (like out-of-bounds memory access). @@ -67,6 +72,7 @@ const ToolsMenu: React.FC = props => { + ⌘/Ctrl + Alt + x
Expand macros in code using the nightly compiler.
diff --git a/ui/frontend/hooks/shortcuts.tsx b/ui/frontend/hooks/shortcuts.tsx new file mode 100644 index 000000000..3eac84b32 --- /dev/null +++ b/ui/frontend/hooks/shortcuts.tsx @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useState } from 'react'; + +export const useKeyDown = ( + shortcutMap: Map, + node = document +) => { + const [currentShortcutKeys, setCurrentShortcutKeys] = useState([]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + // If key is already depressed, return early + if (currentShortcutKeys.includes(event.key)) { + return; + } + const newShortcutKeys = currentShortcutKeys.concat([event.key]); + for (const [keys, cb] of shortcutMap.entries()) { + // Note: this implementation cares about order of keys pressed + if ( + keys.length === newShortcutKeys.length && + keys.every((val, i) => newShortcutKeys[i] === val) + ) { + cb(event); + } + } + setCurrentShortcutKeys(newShortcutKeys); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [shortcutMap] + ); + + const handleKeyUp = (event: KeyboardEvent) => { + setCurrentShortcutKeys((prev) => { + const keyIndex = prev.indexOf(event.key); + if (keyIndex !== -1) { + return prev.slice(0, keyIndex).concat(prev.slice(keyIndex + 1)); + } + return prev; + }); + }; + + useEffect(() => { + node.addEventListener('keydown', handleKeyDown); + node.addEventListener('keyup', handleKeyUp); + return () => { + node.removeEventListener('keydown', handleKeyDown); + node.removeEventListener('keydown', handleKeyUp); + }; + }, [handleKeyDown, node]); +};