From 62f5a837f090ae779300bd1530eb80913e23c34d Mon Sep 17 00:00:00 2001 From: En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 3 May 2024 12:49:52 +0800 Subject: [PATCH] Migrate out of react-hotkeys to @mantine/hooks (#2968) * Migrate playground hulk hotkey bindings to alt+shift+h * Migrate SubstVisualizer hotkey bindings * Migrate Data Viz hotkey bindings * Migrate CSE machine hotkey bindings * Remove react-hotkeys from package.json * Fix PR comments * Fix snapshots * Use Blueprint Card component instead of div * Move documentation from props to component * Fix format and snapshots --------- Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> --- package.json | 1 - .../AssessmentWorkspace.tsx.snap | 9 - src/commons/editor/Editor.tsx | 16 +- src/commons/hotkeys/HotKeys.tsx | 39 ++ src/commons/repl/Repl.tsx | 15 +- .../__tests__/__snapshots__/Repl.tsx.snap | 13 +- .../SideContentCseMachine.tsx.snap | 6 +- .../content/SideContentCseMachine.tsx | 39 +- .../content/SideContentDataVisualizer.tsx | 39 +- .../content/SideContentSubstVisualizer.tsx | 517 +++++++-------- .../sourceRecorder/SourceRecorderEditor.tsx | 16 +- src/pages/playground/Playground.tsx | 28 +- .../__snapshots__/Playground.tsx.snap | 589 +++++++++--------- yarn.lock | 7 - 14 files changed, 602 insertions(+), 732 deletions(-) create mode 100644 src/commons/hotkeys/HotKeys.tsx diff --git a/package.json b/package.json index 1d6b04cc62..edef0c8f50 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "react-drag-drop-files": "^2.3.10", "react-draggable": "^4.4.5", "react-dropzone": "^14.2.3", - "react-hotkeys": "^2.0.0", "react-i18next": "^14.1.0", "react-konva": "^18.2.10", "react-latex-next": "^3.0.0", diff --git a/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap b/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap index 2525ef6a9b..e0ea3c38bc 100644 --- a/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap +++ b/src/commons/assessmentWorkspace/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap @@ -220,7 +220,6 @@ exports[`AssessmentWorkspace AssessmentWorkspace page with ContestVoting questio >
{ editor.renderer.scrollCursorIntoView(position, 0.5); }; -/* Override handler, so does not trigger when focus is in editor */ -const handlers = { - goGreen: () => {} -}; - const EditorBase = React.memo((props: EditorProps & LocalStateProps) => { const reactAceRef: React.MutableRefObject = React.useRef(null); const [filePath, setFilePath] = React.useState(undefined); @@ -652,14 +645,11 @@ const EditorBase = React.memo((props: EditorProps & LocalStateProps) => { }, []); return ( - +
-
+ ); }); diff --git a/src/commons/hotkeys/HotKeys.tsx b/src/commons/hotkeys/HotKeys.tsx new file mode 100644 index 0000000000..0baf92ee20 --- /dev/null +++ b/src/commons/hotkeys/HotKeys.tsx @@ -0,0 +1,39 @@ +import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; +import React, { PropsWithChildren } from 'react'; + +type HotKeysProps = { + bindings: HotkeyItem[]; +}; + +/** + * This HOC was created to facilitate the migration out of react-hotkeys in favor of @mantine/hooks useHotkeys, + * as SideContentCseMachine.tsx and SideContentDataVisualizer still use class-based React. + * + * NOTE: + * - New hotkey implementations should NOT use this component. Use functional React and the useHotkeys hook + * from @mantine/hooks directly. + * + * TODO: + * - Eventually migrate out of class-based React in the aforementioned components and use useHotkeys directly. + */ +const HotKeys: React.FC< + PropsWithChildren< + HotKeysProps & { + style?: React.CSSProperties; + } + > +> = ({ bindings, children, style }) => { + const handler = getHotkeyHandler(bindings); + + return ( +
+ {children} +
+ ); +}; + +export default HotKeys; diff --git a/src/commons/repl/Repl.tsx b/src/commons/repl/Repl.tsx index 4cc233ab6e..ad9ed588b5 100644 --- a/src/commons/repl/Repl.tsx +++ b/src/commons/repl/Repl.tsx @@ -1,10 +1,9 @@ -import { Card, Classes, Pre } from '@blueprintjs/core'; +import { Card, Pre } from '@blueprintjs/core'; import { Ace } from 'ace-builds'; import classNames from 'classnames'; import { parseError } from 'js-slang'; import { Chapter, Variant } from 'js-slang/dist/types'; import React from 'react'; -import { HotKeys } from 'react-hotkeys'; import { InterpreterOutput } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; @@ -52,12 +51,9 @@ const Repl: React.FC = props => {
{cards} {!props.inputHidden && ( - + - + )}
@@ -133,9 +129,4 @@ export const Output: React.FC = props => { } }; -/* Override handler, so does not trigger when focus is in editor */ -const handlers = { - goGreen: () => {} -}; - export default Repl; diff --git a/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap b/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap index ffd0f69976..3d26fde8e7 100644 --- a/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap +++ b/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap @@ -135,13 +135,10 @@ exports[`Repl renders correctly 1`] = ` } usingSubst={false} /> - - +
`; diff --git a/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap b/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap index f33e89daec..628fdc3378 100644 --- a/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap +++ b/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap @@ -2,18 +2,14 @@ exports[`CSE Machine component renders correctly 1`] = `
void; }; -const cseMachineKeyMap = { - FIRST_STEP: 'a', - NEXT_STEP: 'f', - PREVIOUS_STEP: 'b', - LAST_STEP: 'e' -}; - class SideContentCseMachineBase extends React.Component { constructor(props: CseMachineProps) { super(props); @@ -200,24 +194,23 @@ class SideContentCseMachineBase extends React.Component } public render() { - const cseMachineHandlers = this.state.visualization - ? { - FIRST_STEP: this.stepFirst, - NEXT_STEP: this.stepNext, - PREVIOUS_STEP: this.stepPrevious, - LAST_STEP: this.stepLast(this.props.stepsTotal) - } - : { - FIRST_STEP: () => {}, - NEXT_STEP: () => {}, - PREVIOUS_STEP: () => {}, - LAST_STEP: () => {} - }; + const hotkeyBindings: HotkeyItem[] = this.state.visualization + ? [ + ['a', this.stepFirst], + ['f', this.stepNext], + ['b', this.stepPrevious], + ['e', this.stepLast(this.props.stepsTotal)] + ] + : [ + ['a', () => {}], + ['f', () => {}], + ['b', () => {}], + ['e', () => {}] + ]; return ( void; }; -const dataVisualizerKeyMap = { - PREVIOUS_STEP: 'left', - NEXT_STEP: 'right' -}; - /** * This class is responsible for the visualization of data structures via the * data_data function in Source. It adds a listener to the DataVisualizer singleton @@ -49,29 +45,18 @@ class SideContentDataVisualizerBase extends React.Component boolean = () => this.state.currentStep === 0; const finalStep: () => boolean = () => !this.state.steps || this.state.currentStep === this.state.steps.length - 1; - const dataVisualizerHandlers = { - PREVIOUS_STEP: this.onPrevButtonClick, - NEXT_STEP: this.onNextButtonClick - }; - - configure({ - ignoreEventsCondition: event => { - return ( - (event.key === 'ArrowLeft' && firstStep()) || (event.key === 'ArrowRight' && finalStep()) - ); - }, - ignoreRepeatedEventsWhenKeyHeldDown: false, - stopEventPropagationAfterIgnoring: false - }); - - const step: Step | undefined = this.state.steps[this.state.currentStep]; + const hotkeyBindings: HotkeyItem[] = [ + ['ArrowLeft', this.onPrevButtonClick], + ['ArrowRight', this.onNextButtonClick] + ]; return ( - +
{this.state.steps.length > 1 ? (
)}
- + ); } private onPrevButtonClick = () => { + const firstStep = 0; this.setState(state => { - return { currentStep: state.currentStep - 1 }; + return { currentStep: Math.max(firstStep, state.currentStep - 1) }; }); }; private onNextButtonClick = () => { + const finalStep = this.state.steps.length - 1; this.setState(state => { - return { currentStep: state.currentStep + 1 }; + return { currentStep: Math.min(finalStep, state.currentStep + 1) }; }); }; } diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 7ef04bedf5..71378f488e 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -1,17 +1,16 @@ -/* eslint-disable simple-import-sort/imports */ -import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; -import React from 'react'; -import AceEditor from 'react-ace'; -import { HotKeys } from 'react-hotkeys'; -import { MapDispatchToProps, connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; +import 'js-slang/dist/editors/ace/theme/source'; +import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; -import 'js-slang/dist/editors/ace/theme/source'; import { IStepperPropContents } from 'js-slang/dist/stepper/stepper'; -import { SideContentLocation, SideContentType } from '../SideContentTypes'; +import React, { useCallback, useEffect, useState } from 'react'; +import AceEditor from 'react-ace'; +import { useDispatch } from 'react-redux'; + import { beginAlertSideContent } from '../SideContentActions'; +import { SideContentLocation, SideContentType } from '../SideContentTypes'; const SubstDefaultText = () => { return ( @@ -50,13 +49,6 @@ const SubstDefaultText = () => { ); }; -const substKeyMap = { - FIRST_STEP: 'a', - NEXT_STEP: 'f', - PREVIOUS_STEP: 'b', - LAST_STEP: 'e' -}; - const SubstCodeDisplay = (props: { content: string }) => { return ( @@ -65,314 +57,235 @@ const SubstCodeDisplay = (props: { content: string }) => { ); }; -type SubstVisualizerProps = OwnProps & DispatchProps; - -type OwnProps = { +type SubstVisualizerProps = { content: IStepperPropContents[]; workspaceLocation: SideContentLocation; }; -type State = { - value: number; -}; +const SideContentSubstVisualizer: React.FC = props => { + const [stepValue, setStepValue] = useState(1); + const lastStepValue = props.content.length; + const hasRunCode = lastStepValue !== 0; // 'content' property is initialised to '[]' by Playground component -type DispatchProps = { - alertSideContent: () => void; -}; - -class SideContentSubstVisualizerBase extends React.Component { - constructor(props: SubstVisualizerProps) { - super(props); - this.state = { - value: 1 - }; + const dispatch = useDispatch(); + const alertSideContent = useCallback( + () => dispatch(beginAlertSideContent(SideContentType.substVisualizer, props.workspaceLocation)), + [props.workspaceLocation, dispatch] + ); - // set source mode as 2 + // set source mode as 2 + useEffect(() => { HighlightRulesSelector(2); ModeSelector(2); - } + }, []); - componentDidUpdate(prevProps: OwnProps, prevState: State) { - if (prevProps.content !== this.props.content) { - this.setState((state: State) => { - return { value: 1 }; - }); - - if (this.props.content.length > 0) { - this.props.alertSideContent(); - } + // reset stepValue when content changes + useEffect(() => { + setStepValue(1); + if (props.content.length > 0) { + alertSideContent(); } - } - - public render() { - const lastStepValue = this.props.content.length; - // 'content' property is initialised to '[]' by Playground component - const hasRunCode = lastStepValue !== 0; - const substHandlers = hasRunCode - ? { - FIRST_STEP: this.stepFirst, - NEXT_STEP: this.stepNext, - PREVIOUS_STEP: this.stepPrevious, - LAST_STEP: this.stepLast(lastStepValue) - } - : { - FIRST_STEP: () => {}, - NEXT_STEP: () => {}, - PREVIOUS_STEP: () => {}, - LAST_STEP: () => {} - }; - - return ( - -
-
- -
- -
{' '} -
- {hasRunCode ? ( - - ) : ( - - )} - {hasRunCode ? ( - - ) : null} -
-
-
- ); - } - - private getDiffMarkers = (value: number) => { - const lastStepValue = this.props.content.length; - const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = this.props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex.split('\n'); - - const diffMarkers = [] as any[]; - if (redex.length > 0) { - const mainprog = redexed.split('@redex'); - let text = mainprog[0]; - let front = text.split('\n'); - - let startR = front.length - 1; - let startC = front[startR].length; - - for (let i = 0; i < mainprog.length - 1; i++) { - const endR = startR + redex.length - 1; - const endC = - redex.length === 1 - ? startC + redex[redex.length - 1].length - : redex[redex.length - 1].length; - - diffMarkers.push({ - startRow: startR, - startCol: startC, - endRow: endR, - endCol: endC, - className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', - type: 'background' - }); - - text = text + redex + mainprog[i + 1]; - front = text.split('\n'); - startR = front.length - 1; - startC = front[startR].length; + }, [props.content, setStepValue, alertSideContent]); + + // Stepper function call helpers + const getPreviousFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; } - } - return diffMarkers; - }; - - private getText(value: number) { - const lastStepValue = this.props.content.length; - const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = this.props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex; - const split = pathified.code.split('@redex'); - if (split.length > 1) { - let text = split[0]; - for (let i = 1; i < split.length; i++) { - text = text + redex + split[i]; - } - return text; - } else { - return redexed; - } - } - - private sliderShift = (newValue: number) => { - this.setState((state: State) => { - return { value: newValue }; - }); - }; - - private stepFirst = () => { - // Move to the first step - this.sliderShift(1); - }; - - private stepLast = (lastStepValue: number) => () => { - // Move to the last step - this.sliderShift(lastStepValue); - }; - - private stepPrevious = () => { - if (this.state.value !== 1) { - this.sliderShift(this.state.value - 1); - } - }; - - private stepNext = () => { - const lastStepValue = this.props.content.length; - if (this.state.value !== lastStepValue) { - this.sliderShift(this.state.value + 1); - } - }; - - private stepPreviousFunctionCall = (value: number) => () => { - const previousFunctionCall = this.getPreviousFunctionCall(value); - if (previousFunctionCall !== null) { - this.sliderShift(previousFunctionCall); - } - }; - - private stepNextFunctionCall = (value: number) => () => { - const nextFunctionCall = this.getNextFunctionCall(value); - if (nextFunctionCall !== null) { - this.sliderShift(nextFunctionCall); - } - }; - - private hasPreviousFunctionCall = (value: number) => { - const lastStepValue = this.props.content.length; - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = this.props.content[contIndex].function; - if (currentFunction === undefined) { - return false; - } else { for (let i = contIndex - 1; i > -1; i--) { - const previousFunction = this.props.content[i].function; + const previousFunction = props.content[i].function; if (previousFunction !== undefined && currentFunction === previousFunction) { - return true; + return i + 1; } } - return false; - } - }; + return null; + }, + [lastStepValue, props.content] + ); - private hasNextFunctionCall = (value: number) => { - const lastStepValue = this.props.content.length; - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = this.props.content[contIndex].function; - if (currentFunction === undefined) { - return false; - } else { - for (let i = contIndex + 1; i < this.props.content.length; i++) { - const nextFunction = this.props.content[i].function; + const getNextFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; + } + for (let i = contIndex + 1; i < props.content.length; i++) { + const nextFunction = props.content[i].function; if (nextFunction !== undefined && currentFunction === nextFunction) { - return true; + return i + 1; } } - return false; - } - }; - - private getPreviousFunctionCall = (value: number) => { - const lastStepValue = this.props.content.length; - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = this.props.content[contIndex].function; - if (currentFunction === undefined) { return null; - } - for (let i = contIndex - 1; i > -1; i--) { - const previousFunction = this.props.content[i].function; - if (previousFunction !== undefined && currentFunction === previousFunction) { - return i + 1; - } - } - return null; - }; + }, + [lastStepValue, props.content] + ); - private getNextFunctionCall = (value: number) => { - const lastStepValue = this.props.content.length; - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = this.props.content[contIndex].function; - if (currentFunction === undefined) { - return null; - } - for (let i = contIndex + 1; i < this.props.content.length; i++) { - const nextFunction = this.props.content[i].function; - if (nextFunction !== undefined && currentFunction === nextFunction) { - return i + 1; + // Stepper handlers + const hasPreviousFunctionCall = getPreviousFunctionCall(stepValue) !== null; + const hasNextFunctionCall = getNextFunctionCall(stepValue) !== null; + const stepPreviousFunctionCall = () => + setStepValue(getPreviousFunctionCall(stepValue) ?? stepValue); + const stepNextFunctionCall = () => setStepValue(getNextFunctionCall(stepValue) ?? stepValue); + const stepFirst = () => setStepValue(1); + const stepLast = () => setStepValue(lastStepValue); + const stepPrevious = () => setStepValue(Math.max(1, stepValue - 1)); + const stepNext = () => setStepValue(Math.min(props.content.length, stepValue + 1)); + + // Setup hotkey bindings + const hotkeyBindings: HotkeyItem[] = hasRunCode + ? [ + ['a', stepFirst], + ['f', stepNext], + ['b', stepPrevious], + ['e', stepLast] + ] + : [ + ['a', () => {}], + ['f', () => {}], + ['b', () => {}], + ['e', () => {}] + ]; + const hotkeyHandler = getHotkeyHandler(hotkeyBindings); + + // Rendering helpers + const getText = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex; + const split = pathified.code.split('@redex'); + if (split.length > 1) { + let text = split[0]; + for (let i = 1; i < split.length; i++) { + text = text + redex + split[i]; + } + return text; + } else { + return redexed; } - } - return null; - }; -} + }, + [lastStepValue, props.content] + ); -const mapDispatchToProps: MapDispatchToProps = ( - dispatch, - { workspaceLocation } -) => - bindActionCreators( - { - alertSideContent: () => - beginAlertSideContent(SideContentType.substVisualizer, workspaceLocation) + const getDiffMarkers = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex.split('\n'); + + const diffMarkers = [] as any[]; + if (redex.length > 0) { + const mainprog = redexed.split('@redex'); + let text = mainprog[0]; + let front = text.split('\n'); + + let startR = front.length - 1; + let startC = front[startR].length; + + for (let i = 0; i < mainprog.length - 1; i++) { + const endR = startR + redex.length - 1; + const endC = + redex.length === 1 + ? startC + redex[redex.length - 1].length + : redex[redex.length - 1].length; + + diffMarkers.push({ + startRow: startR, + startCol: startC, + endRow: endR, + endCol: endC, + className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', + type: 'background' + }); + + text = text + redex + mainprog[i + 1]; + front = text.split('\n'); + startR = front.length - 1; + startC = front[startR].length; + } + } + return diffMarkers; }, - dispatch + [lastStepValue, props.content] + ); + + return ( +
+ +
+ +
{' '} +
+ {hasRunCode ? ( + + ) : ( + + )} + {hasRunCode ? ( + + ) : null} +
); -export default connect(null, mapDispatchToProps)(SideContentSubstVisualizerBase); +}; + +export default SideContentSubstVisualizer; diff --git a/src/commons/sourceRecorder/SourceRecorderEditor.tsx b/src/commons/sourceRecorder/SourceRecorderEditor.tsx index ea933a0c48..0f7b9af4d0 100644 --- a/src/commons/sourceRecorder/SourceRecorderEditor.tsx +++ b/src/commons/sourceRecorder/SourceRecorderEditor.tsx @@ -2,13 +2,11 @@ import 'ace-builds/src-noconflict/ext-searchbox'; import 'ace-builds/src-noconflict/mode-javascript'; import 'js-slang/dist/editors/ace/theme/source'; -import { Classes } from '@blueprintjs/core'; +import { Card } from '@blueprintjs/core'; import { Ace } from 'ace-builds'; -import classNames from 'classnames'; import { isEqual } from 'lodash'; import React from 'react'; import AceEditor, { IAceEditorProps } from 'react-ace'; -import { HotKeys } from 'react-hotkeys'; import { CodeDelta, @@ -208,10 +206,7 @@ class SourcecastEditor extends React.PureComponent +
- +
); } @@ -326,9 +321,4 @@ class SourcecastEditor extends React.PureComponent {} -}; - export default SourcecastEditor; diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index 4c7d48ac7c..33eff6549b 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -1,5 +1,6 @@ import { Classes } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import { HotkeyItem, useHotkeys } from '@mantine/hooks'; import { Ace, Range } from 'ace-builds'; import { FSModule } from 'browserfs/dist/node/core/FS'; import classNames from 'classnames'; @@ -7,7 +8,6 @@ import { Chapter, Variant } from 'js-slang/dist/types'; import { isEqual } from 'lodash'; import { decompressFromEncodedURIComponent } from 'lz-string'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { HotKeys } from 'react-hotkeys'; import { useDispatch, useStore } from 'react-redux'; import { useLocation, useNavigate } from 'react-router'; import { AnyAction, Dispatch } from 'redux'; @@ -143,8 +143,6 @@ export type PlaygroundProps = { handleCloseEditor?: () => void; }; -const keyMap = { goGreen: 'h u l k' }; - export async function handleHash( hash: string, handlers: { @@ -306,7 +304,6 @@ const Playground: React.FC = props => { } const [lastEdit, setLastEdit] = useState(new Date()); - const [isGreen, setIsGreen] = useState(false); const { selectedTab, setSelectedTab } = useSideContent( workspaceLocation, shouldAddDevice ? SideContentType.remoteExecution : SideContentType.introduction @@ -320,6 +317,14 @@ const Playground: React.FC = props => { }) ); + // Playground hotkeys + const [isGreen, setIsGreen] = useState(false); + const playgroundHotkeyBindings: HotkeyItem[] = useMemo( + () => [['alt+shift+h', () => setIsGreen(v => !v)]], + [setIsGreen] + ); + useHotkeys(playgroundHotkeyBindings); + const remoteExecutionTab: SideContentTab = useMemo( () => makeRemoteExecutionTabFrom(deviceSecret, setDeviceSecret), [deviceSecret] @@ -395,13 +400,6 @@ const Playground: React.FC = props => { } }, [isMobileBreakpoint, selectedTab, setSelectedTab]); - const handlers = useMemo( - () => ({ - goGreen: () => setIsGreen(!isGreen) - }), - [isGreen] - ); - const onEditorValueChange = React.useCallback( (editorTabIndex: number, newEditorValue: string) => { setLastEdit(new Date()); @@ -1051,13 +1049,9 @@ const Playground: React.FC = props => {
) : ( - +
- +
); }; diff --git a/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap b/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap index af338f9c12..1bae777cd8 100644 --- a/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap +++ b/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap @@ -4,7 +4,6 @@ exports[`Playground tests Playground renders correctly 1`] = `
-

- The data visualizer helps you to visualize data structures. -
-
- It is activated by calling the function - - - draw_data(x - - 1 - - , x - - 2 - - , ... x - - n - - ) - - , where - - - x - - k - - - - would be the - - - k - - th - - - - data structure that you want to visualize and - - n - - is the number of structures. -
-
- The data visualizer uses box-and-pointer diagrams, as introduced in - - - - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 2, Section 2 - - - . -

+ The data visualizer helps you to visualize data structures. +
+
+ It is activated by calling the function + + + draw_data(x + + 1 + + , x + + 2 + + , ... x + + n + + ) + + , where + + + x + + k + + + + would be the + + + k + + th + + + + data structure that you want to visualize and + + n + + is the number of structures. +
+
+ The data visualizer uses box-and-pointer diagrams, as introduced in + + + + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 2, Section 2 + + + . +

+
@@ -1391,7 +1393,6 @@ and also the >
-

- The data visualizer helps you to visualize data structures. -
-
- It is activated by calling the function - - - draw_data(x - - 1 - - , x - - 2 - - , ... x - - n - - ) - - , where - - - x - - k - - - - would be the - - - k - - th - - - - data structure that you want to visualize and - - n - - is the number of structures. -
-
- The data visualizer uses box-and-pointer diagrams, as introduced in - - - - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 2, Section 2 - - - . -

+ The data visualizer helps you to visualize data structures. +
+
+ It is activated by calling the function + + + draw_data(x + + 1 + + , x + + 2 + + , ... x + + n + + ) + + , where + + + x + + k + + + + would be the + + + k + + th + + + + data structure that you want to visualize and + + n + + is the number of structures. +
+
+ The data visualizer uses box-and-pointer diagrams, as introduced in + + + + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 2, Section 2 + + + . +

+
@@ -2450,190 +2453,185 @@ and also the class="side-content-text" >
-
+
+
+
+
+
+ + + 1 + + +
+
+
+ - - + - + -
-
- + + + + +
+
+ +
+
+
+ Welcome to the Stepper!
-
-
- Welcome to the Stepper! -
-
- On this tab, the REPL will be hidden from view, so do check that your code has no errors before running the stepper. You may use this tool by writing your program on the left, then dragging the slider above to see its evaluation. -
-
- On even-numbered steps, the part of the program that will be evaluated next is highlighted in yellow. On odd-numbered steps, the result of the evaluation is highlighted in green. You can change the maximum steps limit (500-5000, default 1000) in the control bar. -
-
-
- Some useful keyboard shortcuts: -
-
- a: Move to the first step -
- e: Move to the last step -
- f: Move to the next step -
- b: Move to the previous step -
-
- Note that these shortcuts are only active when the browser focus is on this tab (click on or above the explanation text). -
-
+
+ On this tab, the REPL will be hidden from view, so do check that your code has no errors before running the stepper. You may use this tool by writing your program on the left, then dragging the slider above to see its evaluation. +
+
+ On even-numbered steps, the part of the program that will be evaluated next is highlighted in yellow. On odd-numbered steps, the result of the evaluation is highlighted in green. You can change the maximum steps limit (500-5000, default 1000) in the control bar. +
+
+
+ Some useful keyboard shortcuts: +
+
+ a: Move to the first step +
+ e: Move to the last step +
+ f: Move to the next step +
+ b: Move to the previous step +
+
+ Note that these shortcuts are only active when the browser focus is on this tab (click on or above the explanation text).
@@ -2705,7 +2703,6 @@ and also the >