Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui/frontend): add shortcuts for tools #871

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions ui/frontend/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -88,15 +90,43 @@ 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],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, shift+alt+f is the default VSCode shortcut for formatting a document on macOS and Windows (but not Linux, it seems?), so users of the Monaco editor might find that more familiar, although it doesn't use Control like the shortcuts propsed here.

On macOS at least (and I would guess on other OSes as well), some of these shortcuts seem to have uses already within the editor:

  • cmd+alt+f seems to open a find+replace dialog in ace and monaco
  • cmd+alt+c toggles case-sensitivity for searches in monaco

This PR looks like it has been open a while without much movement, but I just wanted to provide some feedback on the choice of shortcuts. If there is any interest in reviving this I think it would definitely be a nice feature to have!

[['Control', 'Alt', 'c'], handleClippy],
[['Control', 'Alt', 'm'], handleMiri],
[['Control', 'Alt', 'x'], handleMacroExpansion],
]);
useKeyDown(shortcutMap);

return (
<>
<div className={styles.container}>
<Header />
<ResizableArea />
</div>
{ showNotifications && <Notifications />}
{showNotifications && <Notifications />}
</>
);
}
Expand Down
5 changes: 5 additions & 0 deletions ui/frontend/ToolsMenu.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.shortcut {
float: right;
background-color: #a0ffa0;
border: 2px solid #a0ffa0;
}
6 changes: 6 additions & 0 deletions ui/frontend/ToolsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -46,18 +48,21 @@ const ToolsMenu: React.FC<ToolsMenuProps> = props => {
<ButtonMenuItem
name="Rustfmt"
onClick={format}>
<span className={styles.shortcut}>⌘/Ctrl + Alt + f</span>
<div>Format this code with Rustfmt.</div>
<MenuAside>{rustfmtVersion} ({rustfmtVersionDetails})</MenuAside>
</ButtonMenuItem>
<ButtonMenuItem
name="Clippy"
onClick={clippy}>
<span className={styles.shortcut}>⌘/Ctrl + Alt + c</span>
<div>Catch common mistakes and improve the code using the Clippy linter.</div>
<MenuAside>{clippyVersion} ({clippyVersionDetails})</MenuAside>
</ButtonMenuItem>
<ButtonMenuItem
name="Miri"
onClick={miri}>
<span className={styles.shortcut}>⌘/Ctrl + Alt + m</span>
<div>
Execute this program in the Miri interpreter to detect certain
cases of undefined behavior (like out-of-bounds memory access).
Expand All @@ -67,6 +72,7 @@ const ToolsMenu: React.FC<ToolsMenuProps> = props => {
<ButtonMenuItem
name="Expand macros"
onClick={expandMacros}>
<span className={styles.shortcut}>⌘/Ctrl + Alt + x</span>
<div>
Expand macros in code using the nightly compiler.
</div>
Expand Down
49 changes: 49 additions & 0 deletions ui/frontend/hooks/shortcuts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useCallback, useEffect, useState } from 'react';

export const useKeyDown = (
shortcutMap: Map<string[], Function>,
node = document
) => {
const [currentShortcutKeys, setCurrentShortcutKeys] = useState<string[]>([]);

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]);
};