From 804336e3b10d7a806d0c3a482e7ff8ac4f677666 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:18:31 +0800 Subject: [PATCH 01/10] Migrate CSE side content to FC --- .../content/SideContentCseMachine.tsx | 799 ++++++++---------- 1 file changed, 375 insertions(+), 424 deletions(-) diff --git a/src/commons/sideContent/content/SideContentCseMachine.tsx b/src/commons/sideContent/content/SideContentCseMachine.tsx index 71aa0c31ca..ec05561454 100644 --- a/src/commons/sideContent/content/SideContentCseMachine.tsx +++ b/src/commons/sideContent/content/SideContentCseMachine.tsx @@ -14,7 +14,7 @@ import { bindActionCreators } from '@reduxjs/toolkit'; import classNames from 'classnames'; import { Chapter } from 'js-slang/dist/types'; import { debounce } from 'lodash'; -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import HotKeys from 'src/commons/hotkeys/HotKeys'; import { Output } from 'src/commons/repl/Repl'; @@ -32,18 +32,6 @@ import { beginAlertSideContent } from '../SideContentActions'; import { getLocation } from '../SideContentHelper'; import { NonStoryWorkspaceLocation, SideContentTab, SideContentType } from '../SideContentTypes'; -type State = { - visualization: React.ReactNode; - value: number; - height: number; - width: number; - lastStep: boolean; - stepLimitExceeded: boolean; - chapter: Chapter; -}; - -type CseMachineProps = OwnProps & StateProps & DispatchProps; - type StateProps = { editorWidth?: string; sideContentHeight?: number; @@ -70,470 +58,433 @@ type DispatchProps = { handleAlertSideContent: () => void; }; -class SideContentCseMachineBase extends React.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 +const 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); +}; + +const 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); +}; + +type Props = OwnProps & StateProps & DispatchProps; + +const SideContentCseMachineBase: React.FC = props => { + const [visualization, setVisualization] = useState(null); + const [value, setValue] = useState(-1); + const [width, setWidth] = useState(calculateWidth(props.editorWidth)); + const [height, setHeight] = useState(calculateHeight(props.sideContentHeight)); + const [lastStep, setLastStep] = useState(false); + const [stepLimitExceeded, setStepLimitExceeded] = useState(false); + + const isJava = useCallback(() => props.chapter === Chapter.FULL_JAVA, [props.chapter]); + + const handleResize = useCallback( + debounce(() => { + const newWidth = calculateWidth(props.editorWidth); + const newHeight = calculateHeight(props.sideContentHeight); + if (newWidth !== width || newHeight !== height) { + setWidth(newWidth); + setHeight(newHeight); + CseMachine.updateDimensions(newWidth, newHeight); + } + }, 300), + [props.editorWidth, props.sideContentHeight, width, height] + ); + + useEffect(() => { + handleResize(); + window.addEventListener('resize', handleResize); + CseMachine.redraw(); + + return () => { + handleResize.cancel(); + window.removeEventListener('resize', handleResize); }; - if (this.isJava()) { - JavaCseMachine.init( - visualization => this.setState({ visualization }), - (segments: [number, number][]) => { - props.setEditorHighlightedLines(0, segments); - } - ); + }, [handleResize]); + + // TODO: STOPPED CHECKING HERE + + useEffect(() => { + if (isJava()) { + JavaCseMachine.init(setVisualization, (segments: [number, number][]) => { + props.setEditorHighlightedLines(0, segments); + }); } else { CseMachine.init( visualization => { - this.setState({ visualization }, () => CseAnimation.playAnimation()); - if (visualization) this.props.handleAlertSideContent(); + setVisualization(visualization); + CseAnimation.playAnimation(); + if (visualization) props.handleAlertSideContent(); }, - 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); }, - // We shouldn't be able to move slider to a step number beyond the step limit - isControlEmpty => { - this.setState({ stepLimitExceeded: false }); - } - ); - } - } - - 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 + () => setStepLimitExceeded(false) ); } - return Math.min(width, maxWidth); - } + }, [isJava, props, width, height]); - 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); - } - - 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); - CseMachine.redraw(); - } - - componentWillUnmount() { - this.handleResize.cancel(); - window.removeEventListener('resize', this.handleResize); - } - - 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) { - this.stepFirst(); - if (this.isJava()) { + useEffect(() => { + if (props.needCseUpdate) { + stepFirst(); + if (isJava()) { JavaCseMachine.clearCse(); } else { CseMachine.clearCse(); } } - } - - public render() { - 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 ( - -
- -
- {!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} - > - - - - - )} - -
-
{' '} - {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 - ) - ) : ( -
- {this.isJava() ? ( - - The CSEC machine generates control, stash, environment and class model diagrams - adapted from the notation introduced in{' '} - - - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter - 3, Section 2 - - - {'. '} - You have chosen the sublanguage{' '} - - Java CSEC - - - ) : ( - - The CSE machine generates control, stash and environment model diagrams following a - notation introduced in{' '} - - - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter - 3, Section 2 - - - - )} - . -
-
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 running your program and - then dragging the slider above to see the state of the control, stash and environment at - different stages in the evaluation of your program. Clicking on the fast-forward button - (double chevron) will take you to the next breakpoint in your program -
-
- - 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. -
- )} - -