diff --git a/src/commons/sideContent/content/SideContentCseMachine.tsx b/src/commons/sideContent/content/SideContentCseMachine.tsx index 7f806c7608..6d31955e30 100644 --- a/src/commons/sideContent/content/SideContentCseMachine.tsx +++ b/src/commons/sideContent/content/SideContentCseMachine.tsx @@ -12,20 +12,17 @@ import { } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import type { HotkeyItem } from '@mantine/hooks'; -import { bindActionCreators } from '@reduxjs/toolkit'; import classNames from 'classnames'; import { t } from 'i18next'; import { Chapter } from 'js-slang/dist/langs'; import { debounce } from 'lodash'; -import { Component } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { connect, type MapDispatchToProps, type MapStateToProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import HotKeys from 'src/commons/hotkeys/HotKeys'; import { Output } from 'src/commons/repl/Repl'; -import { - type PlaygroundWorkspaceState, - type WorkspaceLocation, -} from 'src/commons/workspace/WorkspaceTypes'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; +import type { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; import { ClearDeadFramesAnimation } from 'src/features/cseMachine/animationComponents/ClearDeadFramesAnimation'; import CseMachine from 'src/features/cseMachine/CseMachine'; import { CseAnimation } from 'src/features/cseMachine/CseMachineAnimation'; @@ -34,7 +31,6 @@ import type { ArrowOriginFilterKey } from 'src/features/cseMachine/CseMachineTyp import { computeFramesCoordChange } from 'src/features/cseMachine/CseMachineUtils'; import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine'; -import type { InterpreterOutput, OverallState } from '../../application/ApplicationTypes'; import type { HighlightedLines } from '../../editor/EditorTypes'; import Constants, { Links } from '../../utils/Constants'; import WorkspaceActions from '../../workspace/WorkspaceActions'; @@ -51,668 +47,642 @@ const ALL_ARROW_FILTER_KEYS: ArrowOriginFilterKey[] = [ 'stash', ]; -type State = { - visualization: React.ReactNode; - value: number; - height: number; - width: number; - lastStep: boolean; - stepLimitExceeded: boolean; - chapter: Chapter; - clearDeadFrames: boolean; - arrowFilterOpen: boolean; +const calculateWidth = (editorWidthProp?: string) => { + const horizontalPadding = 50; + const maxWidth = 5000; + let w; + if (editorWidthProp === undefined) { + w = window.innerWidth - horizontalPadding; + } else { + w = Math.min( + maxWidth, + (window.innerWidth * (100 - parseFloat(editorWidthProp))) / 100 - horizontalPadding, + ); + } + return Math.min(w, maxWidth); }; -type CseMachineProps = OwnProps & StateProps & DispatchProps; +const calculateHeight = (sideContentHeightProp?: number) => { + const verticalPadding = 150; + const maxHeight = 5000; // limit for visible diagram height for huge screens + let h; + if (window.innerWidth < Constants.mobileBreakpoint) { + // mobile mode + h = window.innerHeight - verticalPadding; + } else if (sideContentHeightProp === undefined) { + h = window.innerHeight - verticalPadding; + } else { + h = sideContentHeightProp - verticalPadding; + } + return Math.min(h, maxHeight); +}; -type StateProps = { +type Props = { + workspaceLocation: WorkspaceLocation; editorWidth?: string; sideContentHeight?: number; - stepsTotal: number; - currentStep: number; - breakpointSteps: number[]; - changepointSteps: number[]; - needCseUpdate: boolean; - machineOutput: InterpreterOutput[]; - chapter: Chapter; }; -type OwnProps = { - workspaceLocation: WorkspaceLocation; -}; +const SideContentCseMachine: React.FC = ({ + workspaceLocation, + editorWidth, + sideContentHeight, +}) => { + const [visualization, setVisualization] = useState(null); + const [value, setValue] = useState(-1); + const [width] = useState(() => calculateWidth(editorWidth)); + const [height] = useState(() => calculateHeight(sideContentHeight)); + const [, setLastStep] = useState(false); + const [stepLimitExceeded, setStepLimitExceeded] = useState(false); + const [clearDeadFrames, setClearDeadFrames] = useState(false); + const [arrowFilterOpen, setArrowFilterOpen] = useState(false); + + const valueRef = useRef(-1); + const breakpointStepsRef = useRef([]); + const changepointStepsRef = useRef([]); + const clearDeadFramesRef = useRef(false); + const isInitializedRef = useRef(false); + const prevUpdateCseRef = useRef(false); + const stepsTotalRef = useRef(0); + + const [loc] = getLocation(workspaceLocation); + const workspace = useTypedSelector( + store => store.workspaces[loc === 'sicp' ? 'sicp' : 'playground'], + ); -type DispatchProps = { - handleStepUpdate: (steps: number) => void; - handleEditorEval: () => void; - setEditorHighlightedLines: ( - editorTabIndex: number, - newHighlightedLines: HighlightedLines[], - ) => void; - handleAlertSideContent: () => void; -}; + const dispatch = useDispatch(); -class SideContentCseMachineBase extends Component { - constructor(props: CseMachineProps) { - super(props); - this.state = { - visualization: null, - value: -1, - width: this.calculateWidth(props.editorWidth), - height: this.calculateHeight(props.sideContentHeight), - lastStep: false, - stepLimitExceeded: false, - chapter: props.chapter, - clearDeadFrames: false, - arrowFilterOpen: false, - }; - if (this.isJava()) { + const chapter = workspace.context.chapter; + const stepsTotal = workspace.stepsTotal; + const breakpointSteps = workspace.breakpointSteps; + const changepointSteps = workspace.changepointSteps; + const updateCse = workspace.updateCse; + const machineOutput = workspace.output; + + const isJava = chapter === Chapter.FULL_JAVA; + + useEffect(() => { + valueRef.current = value; + }, [value]); + + useEffect(() => { + breakpointStepsRef.current = breakpointSteps; + }, [breakpointSteps]); + + useEffect(() => { + changepointStepsRef.current = changepointSteps; + }, [changepointSteps]); + + useEffect(() => { + clearDeadFramesRef.current = clearDeadFrames; + }, [clearDeadFrames]); + + useEffect(() => { + stepsTotalRef.current = stepsTotal; + }, [stepsTotal]); + + const handleEditorEval = useCallback(() => { + dispatch(WorkspaceActions.evalEditor(workspaceLocation)); + }, [dispatch, workspaceLocation]); + + const handleStepUpdate = useCallback( + (steps: number) => { + dispatch(WorkspaceActions.updateCurrentStep(steps, workspaceLocation)); + }, + [dispatch, workspaceLocation], + ); + + const handleAlertSideContent = useCallback(() => { + dispatch(beginAlertSideContent(SideContentType.cseMachine, workspaceLocation)); + }, [dispatch, workspaceLocation]); + + const setEditorHighlightedLines = useCallback( + (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => { + dispatch( + WorkspaceActions.setEditorHighlightedLinesControl( + workspaceLocation, + editorTabIndex, + newHighlightedLines, + ), + ); + }, + [dispatch, workspaceLocation], + ); + + const sliderRelease = useCallback( + (newValue: number) => { + if (newValue === stepsTotalRef.current) { + setLastStep(true); + } else { + setLastStep(false); + } + handleEditorEval(); + }, + [handleEditorEval], + ); + + const sliderShift = useCallback( + (newValue: number) => { + if (clearDeadFramesRef.current) { + CseMachine.setClearDeadFrames(false); + CseMachine.clearLiveLayouts(); + CseMachine.redraw(); + } + handleStepUpdate(newValue); + valueRef.current = newValue; + setValue(newValue); + setClearDeadFrames(false); + }, + [handleStepUpdate], + ); + + useEffect(() => { + if (isInitializedRef.current) { + return; + } + isInitializedRef.current = true; + + if (isJava) { JavaCseMachine.init( - visualization => this.setState({ visualization }), + newVisualization => setVisualization(newVisualization), (segments: [number, number][]) => { - props.setEditorHighlightedLines(0, segments); + setEditorHighlightedLines(0, segments); }, ); } else { CseMachine.init( - visualization => { - this.setState({ visualization }, () => CseAnimation.playAnimation()); - if (visualization) this.props.handleAlertSideContent(); + newVisualization => { + setVisualization(newVisualization); + if (newVisualization) { + handleAlertSideContent(); + } + CseAnimation.playAnimation(); }, - this.state.width, - this.state.height, + width, + height, (segments: [number, number][]) => { // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. // This comment is copied over from workspace saga - props.setEditorHighlightedLines(0, segments); + setEditorHighlightedLines(0, segments); }, // We shouldn't be able to move slider to a step number beyond the step limit isControlEmpty => { - const isAtLastStep = this.state.value === this.props.stepsTotal; - - this.setState({ - stepLimitExceeded: !isControlEmpty && isAtLastStep, - }); + const isAtLastStep = valueRef.current === stepsTotalRef.current; + setStepLimitExceeded(!isControlEmpty && isAtLastStep); }, ); } - } - - private isJava(): boolean { - return this.props.chapter === Chapter.FULL_JAVA; - } - - private calculateWidth(editorWidth?: string) { - const horizontalPadding = 50; - const maxWidth = 5000; // limit for visible diagram width for huge screens - let width; - if (editorWidth === undefined) { - width = window.innerWidth - horizontalPadding; - } else { - width = Math.min( - maxWidth, - (window.innerWidth * (100 - parseFloat(editorWidth))) / 100 - horizontalPadding, - ); - } - return Math.min(width, maxWidth); - } - private calculateHeight(sideContentHeight?: number) { - const verticalPadding = 150; - const maxHeight = 5000; // limit for visible diagram height for huge screens - let height; - if (window.innerWidth < Constants.mobileBreakpoint) { - // mobile mode - height = window.innerHeight - verticalPadding; - } else if (sideContentHeight === undefined) { - height = window.innerHeight - verticalPadding; - } else { - height = sideContentHeight - verticalPadding; - } - return Math.min(height, maxHeight); - } + const resizeHandler = debounce(() => { + const w = calculateWidth(editorWidth); + const h = calculateHeight(sideContentHeight); + CseMachine.updateDimensions(w, h); + }, 300); - handleResize = debounce(() => { - const newWidth = this.calculateWidth(this.props.editorWidth); - const newHeight = this.calculateHeight(this.props.sideContentHeight); - if (newWidth !== this.state.width || newHeight !== this.state.height) { - this.setState({ - height: newHeight, - width: newWidth, - }); - CseMachine.updateDimensions(newWidth, newHeight); - } - }, 300); - - componentDidMount() { - this.handleResize(); - window.addEventListener('resize', this.handleResize); + window.addEventListener('resize', resizeHandler); CseMachine.redraw(); - } - componentWillUnmount() { - this.handleResize.cancel(); - window.removeEventListener('resize', this.handleResize); - if (!this.isJava()) { - CseMachine.resetArrowOriginFilters(); - } - } - - componentDidUpdate(prevProps: { - editorWidth?: string; - sideContentHeight?: number; - stepsTotal: number; - needCseUpdate: boolean; - }) { - if ( - prevProps.sideContentHeight !== this.props.sideContentHeight || - prevProps.editorWidth !== this.props.editorWidth - ) { - this.handleResize(); - } - if (prevProps.needCseUpdate && !this.props.needCseUpdate) { + return () => { + resizeHandler.cancel(); + window.removeEventListener('resize', resizeHandler); + }; + }, [ + isJava, + width, + height, + editorWidth, + sideContentHeight, + handleAlertSideContent, + setEditorHighlightedLines, + ]); + + useEffect(() => { + if (prevUpdateCseRef.current && !updateCse) { CseMachine.resetArrowOriginFilters(); - this.setState({ arrowFilterOpen: false }); - this.stepFirst(); - if (this.isJava()) { + setArrowFilterOpen(false); + sliderShift(0); + sliderRelease(0); + if (isJava) { JavaCseMachine.clearCse(); } else { CseMachine.clearCse(); } } - } - - public render() { - const arrowFilters = CseMachine.getArrowOriginFilters(); - const areAllArrowFiltersSelected = ALL_ARROW_FILTER_KEYS.every(key => arrowFilters[key]); - 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', () => {}], - ]; - - const currentStep = Math.max(0, this.state.value); - const isAtFirstStep = currentStep < 1; - const isAtLastStep = currentStep >= this.props.stepsTotal; - const isNavDisabled = !this.state.visualization; - - return ( - -
- -
- {!this.isJava() && ( - - - { - if (this.state.visualization) { - CseMachine.toggleControlStash(); - CseMachine.redraw(); - } - }} - icon="layers" - disabled={!this.state.visualization} - > - - - - - { - if (this.state.visualization) { - CseMachine.toggleStackTruncated(); - CseMachine.redraw(); - } - }} - icon="minimize" - disabled={!this.state.visualization} - > - - - - - - { - if (this.state.visualization) { - CseMachine.toggleCenterAlignment(); - CseMachine.redraw(); - } - }} - icon="eye-open" - disabled={!this.state.visualization} - > - - - - - this.setState({ arrowFilterOpen: nextOpen })} - position={Position.BOTTOM_LEFT} - content={ -
-
Filter Arrows
- - this.toggleArrowFilter('text')} - /> - this.toggleArrowFilter('frame')} - /> - this.toggleArrowFilter('function')} - /> - this.toggleArrowFilter('array')} - /> - this.toggleArrowFilter('control')} - /> - this.toggleArrowFilter('stash')} - /> -
- } - > - -
-
-
- )} - -
-
{' '} - {this.state.visualization && - this.props.machineOutput.length && - this.props.machineOutput[0].type === 'errors' ? ( - this.props.machineOutput.map((slice, index) => ( - - )) - ) : ( -
- )} - {this.state.visualization ? ( - this.state.stepLimitExceeded ? ( -
- Maximum number of steps exceeded. - - Please increase the step limit if you would like to see futher evaluation. -
- ) : ( - this.state.visualization - ) - ) : ( - - )} - - + toggleArrowFilter('text')} + /> + toggleArrowFilter('frame')} + /> + toggleArrowFilter('function')} + /> + toggleArrowFilter('array')} + /> + toggleArrowFilter('control')} + /> + toggleArrowFilter('stash')} + /> + + } + > + + + + + )} + +