diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 2015c1007c..0743db122c 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -219,7 +219,7 @@ export const javaLanguages: SALanguage[] = [ variant: Variant.DEFAULT, displayName: 'Java', mainLanguage: SupportedLanguage.JAVA, - supports: {} + supports: { cseMachine: true } } ]; export const cLanguages: SALanguage[] = [ diff --git a/src/commons/controlBar/ControlBarChapterSelect.tsx b/src/commons/controlBar/ControlBarChapterSelect.tsx index aad0e84e19..11759c56ca 100644 --- a/src/commons/controlBar/ControlBarChapterSelect.tsx +++ b/src/commons/controlBar/ControlBarChapterSelect.tsx @@ -9,6 +9,7 @@ import { fullJSLanguage, fullTSLanguage, htmlLanguage, + javaLanguages, pyLanguages, SALanguage, schemeLanguages, @@ -87,7 +88,8 @@ export const ControlBarChapterSelect: React.FC<ControlBarChapterSelectProps> = ( // See https://github.com/source-academy/frontend/pull/2460#issuecomment-1528759912 ...(Constants.playgroundOnly ? [fullJSLanguage, fullTSLanguage, htmlLanguage] : []), ...schemeLanguages, - ...pyLanguages + ...pyLanguages, + ...javaLanguages ]; return ( diff --git a/src/commons/sagas/PlaygroundSaga.ts b/src/commons/sagas/PlaygroundSaga.ts index f8e1409145..e7acf4fe01 100644 --- a/src/commons/sagas/PlaygroundSaga.ts +++ b/src/commons/sagas/PlaygroundSaga.ts @@ -5,6 +5,7 @@ import qs from 'query-string'; import { SagaIterator } from 'redux-saga'; import { call, delay, put, race, select } from 'redux-saga/effects'; import CseMachine from 'src/features/cseMachine/CseMachine'; +import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine'; import { changeQueryString, @@ -100,12 +101,17 @@ export default function* PlaygroundSaga(): SagaIterator { if (newId !== SideContentType.cseMachine) { yield put(toggleUsingCse(false, workspaceLocation)); yield call([CseMachine, CseMachine.clearCse]); + yield call([JavaCseMachine, JavaCseMachine.clearCse]); yield put(updateCurrentStep(-1, workspaceLocation)); yield put(updateStepsTotal(0, workspaceLocation)); yield put(toggleUpdateCse(true, workspaceLocation)); yield put(setEditorHighlightedLines(workspaceLocation, 0, [])); } + if (playgroundSourceChapter === Chapter.FULL_JAVA && newId === SideContentType.cseMachine) { + yield put(toggleUsingCse(true, workspaceLocation)); + } + if ( isSourceLanguage(playgroundSourceChapter) && (newId === SideContentType.substVisualizer || newId === SideContentType.cseMachine) diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index 6dc00046f8..0dae58611d 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -250,6 +250,7 @@ export function* evalCode( let lastDebuggerResult = yield select( (state: OverallState) => state.workspaces[workspaceLocation].lastDebuggerResult ); + const isUsingCse = yield select((state: OverallState) => state.workspaces['playground'].usingCse); // Handles `console.log` statements in fullJS const detachConsole: () => void = @@ -266,7 +267,7 @@ export function* evalCode( : isC ? call(cCompileAndRun, entrypointCode, context) : isJava - ? call(javaRun, entrypointCode, context) + ? call(javaRun, entrypointCode, context, currentStep, isUsingCse) : call( runFilesInContext, isFolderModeEnabled diff --git a/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts b/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts index 7a0305091b..788e813ebf 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts @@ -1,24 +1,41 @@ +import { Chapter } from 'js-slang/dist/types'; import { SagaIterator } from 'redux-saga'; import { put, select } from 'redux-saga/effects'; import { OverallState } from '../../../application/ApplicationTypes'; import { actions } from '../../../utils/ActionsHelper'; +import { visualizeJavaCseMachine } from '../../../utils/JavaHelper'; import { visualizeCseMachine } from '../../../utils/JsSlangHelper'; import { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; export function* updateInspector(workspaceLocation: WorkspaceLocation): SagaIterator { try { - const lastDebuggerResult = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].lastDebuggerResult - ); - const row = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1; - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); - // We highlight only one row to show the current line - // If we highlight from start to end, the whole program block will be highlighted at the start - // since the first node is the program node - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [[row, row]])); - visualizeCseMachine(lastDebuggerResult); + const [lastDebuggerResult, chapter] = yield select((state: OverallState) => [ + state.workspaces[workspaceLocation].lastDebuggerResult, + state.workspaces[workspaceLocation].context.chapter + ]); + if (chapter === Chapter.FULL_JAVA) { + const controlItem = lastDebuggerResult.context.control.peek(); + let start = -1; + let end = -1; + if (controlItem?.srcNode?.location) { + const node = controlItem.srcNode; + start = node.location.startLine - 1; + end = node.location.endLine ? node.location.endLine - 1 : start; + } + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [[start, end]])); + visualizeJavaCseMachine(lastDebuggerResult); + } else { + const row = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1; + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + // We highlight only one row to show the current line + // If we highlight from start to end, the whole program block will be highlighted at the start + // since the first node is the program node + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [[row, row]])); + visualizeCseMachine(lastDebuggerResult); + } } catch (e) { // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); diff --git a/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap b/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap index 3ded2a0fb5..f33e89daec 100644 --- a/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap +++ b/src/commons/sideContent/__tests__/__snapshots__/SideContentCseMachine.tsx.snap @@ -352,17 +352,19 @@ exports[`CSE Machine component renders correctly 1`] = ` data-testid="cse-machine-default-text" id="cse-machine-default-text" > - The CSE machine generates control, stash and environment model diagrams following a notation introduced in - - <a - href="https://sourceacademy.org/sicpjs/3.2" - rel="noopener noreferrer" - target="_blank" - > - <i> - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, Section 2 - </i> - </a> + <span> + The CSE machine generates control, stash and environment model diagrams following a notation introduced in + + <a + href="https://sourceacademy.org/sicpjs/3.2" + rel="noopener noreferrer" + target="_blank" + > + <i> + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, Section 2 + </i> + </a> + </span> . <br /> <br /> diff --git a/src/commons/sideContent/content/SideContentCseMachine.tsx b/src/commons/sideContent/content/SideContentCseMachine.tsx index 009c2aec85..fe6adf3048 100644 --- a/src/commons/sideContent/content/SideContentCseMachine.tsx +++ b/src/commons/sideContent/content/SideContentCseMachine.tsx @@ -10,6 +10,7 @@ import { import { IconNames } from '@blueprintjs/icons'; import { Tooltip2 } from '@blueprintjs/popover2'; import classNames from 'classnames'; +import { Chapter } from 'js-slang/dist/types'; import { debounce } from 'lodash'; import React from 'react'; import { HotKeys } from 'react-hotkeys'; @@ -20,6 +21,7 @@ import type { PlaygroundWorkspaceState } from 'src/commons/workspace/WorkspaceTy import CseMachine from 'src/features/cseMachine/CseMachine'; import { CseAnimation } from 'src/features/cseMachine/CseMachineAnimation'; import { Layout } from 'src/features/cseMachine/CseMachineLayout'; +import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine'; import { InterpreterOutput, OverallState } from '../../application/ApplicationTypes'; import { HighlightedLines } from '../../editor/EditorTypes'; @@ -40,6 +42,7 @@ type State = { width: number; lastStep: boolean; stepLimitExceeded: boolean; + chapter: Chapter; }; type CseMachineProps = OwnProps & StateProps & DispatchProps; @@ -53,6 +56,7 @@ type StateProps = { changepointSteps: number[]; needCseUpdate: boolean; machineOutput: InterpreterOutput[]; + chapter: Chapter; }; type OwnProps = { @@ -85,25 +89,39 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> width: this.calculateWidth(props.editorWidth), height: this.calculateHeight(props.sideContentHeight), lastStep: false, - stepLimitExceeded: false + stepLimitExceeded: false, + chapter: props.chapter }; - CseMachine.init( - visualization => { - this.setState({ visualization }, () => CseAnimation.playAnimation()); - if (visualization) this.props.handleAlertSideContent(); - }, - this.state.width, - this.state.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 }); - } - ); + if (this.isJava()) { + JavaCseMachine.init( + visualization => this.setState({ visualization }), + (segments: [number, number][]) => { + props.setEditorHighlightedLines(0, segments); + } + ); + } else { + CseMachine.init( + visualization => { + this.setState({ visualization }, () => CseAnimation.playAnimation()); + if (visualization) this.props.handleAlertSideContent(); + }, + this.state.width, + this.state.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) { @@ -173,7 +191,11 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> } if (prevProps.needCseUpdate && !this.props.needCseUpdate) { this.stepFirst(); - CseMachine.clearCse(); + if (this.isJava()) { + JavaCseMachine.clearCse(); + } else { + CseMachine.clearCse(); + } } } @@ -210,45 +232,53 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> onRelease={this.sliderRelease} value={this.state.value < 0 ? 0 : this.state.value} /> - <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> - <ButtonGroup> - <Tooltip2 content="Control and Stash" compact> - <AnchorButton - onMouseUp={() => { - if (this.state.visualization) { - CseMachine.toggleControlStash(); - CseMachine.redraw(); - } - }} - icon="layers" - disabled={!this.state.visualization} - > - <Checkbox - checked={CseMachine.getControlStash()} + <div + style={{ + display: 'flex', + justifyContent: this.isJava() ? 'center' : 'space-between', + alignItems: 'center' + }} + > + {!this.isJava() && ( + <ButtonGroup> + <Tooltip2 content="Control and Stash" compact> + <AnchorButton + onMouseUp={() => { + if (this.state.visualization) { + CseMachine.toggleControlStash(); + CseMachine.redraw(); + } + }} + icon="layers" disabled={!this.state.visualization} - style={{ margin: 0 }} - /> - </AnchorButton> - </Tooltip2> - <Tooltip2 content="Truncate Control" compact> - <AnchorButton - onMouseUp={() => { - if (this.state.visualization) { - CseMachine.toggleStackTruncated(); - CseMachine.redraw(); - } - }} - icon="minimize" - disabled={!this.state.visualization} - > - <Checkbox - checked={CseMachine.getStackTruncated()} + > + <Checkbox + checked={CseMachine.getControlStash()} + disabled={!this.state.visualization} + style={{ margin: 0 }} + /> + </AnchorButton> + </Tooltip2> + <Tooltip2 content="Truncate Control" compact> + <AnchorButton + onMouseUp={() => { + if (this.state.visualization) { + CseMachine.toggleStackTruncated(); + CseMachine.redraw(); + } + }} + icon="minimize" disabled={!this.state.visualization} - style={{ margin: 0 }} - /> - </AnchorButton> - </Tooltip2> - </ButtonGroup> + > + <Checkbox + checked={CseMachine.getStackTruncated()} + disabled={!this.state.visualization} + style={{ margin: 0 }} + /> + </AnchorButton> + </Tooltip2> + </ButtonGroup> + )} <ButtonGroup> <Button disabled={!this.state.visualization} @@ -259,13 +289,19 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> disabled={!this.state.visualization} icon="chevron-left" onClick={ - CseMachine.getControlStash() ? this.stepPrevious : this.stepPrevChangepoint + this.isJava() || CseMachine.getControlStash() + ? this.stepPrevious + : this.stepPrevChangepoint } /> <Button disabled={!this.state.visualization} icon="chevron-right" - onClick={CseMachine.getControlStash() ? this.stepNext : this.stepNextChangepoint} + onClick={ + this.isJava() || CseMachine.getControlStash() + ? this.stepNext + : this.stepNextChangepoint + } /> <Button disabled={!this.state.visualization} @@ -273,33 +309,35 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> onClick={this.stepNextBreakpoint} /> </ButtonGroup> - <ButtonGroup> - <Tooltip2 content="Print" compact> - <AnchorButton - onMouseUp={() => { - if (this.state.visualization) { - CseMachine.togglePrintableMode(); - CseMachine.redraw(); - } - }} - icon="print" - disabled={!this.state.visualization} - > - <Checkbox + {!this.isJava() && ( + <ButtonGroup> + <Tooltip2 content="Print" compact> + <AnchorButton + onMouseUp={() => { + if (this.state.visualization) { + CseMachine.togglePrintableMode(); + CseMachine.redraw(); + } + }} + icon="print" disabled={!this.state.visualization} - checked={CseMachine.getPrintableMode()} - style={{ margin: 0 }} + > + <Checkbox + disabled={!this.state.visualization} + checked={CseMachine.getPrintableMode()} + style={{ margin: 0 }} + /> + </AnchorButton> + </Tooltip2> + <Tooltip2 content="Save" compact> + <AnchorButton + icon="floppy-disk" + disabled={!this.state.visualization} + onClick={Layout.exportImage} /> - </AnchorButton> - </Tooltip2> - <Tooltip2 content="Save" compact> - <AnchorButton - icon="floppy-disk" - disabled={!this.state.visualization} - onClick={Layout.exportImage} - /> - </Tooltip2> - </ButtonGroup> + </Tooltip2> + </ButtonGroup> + )} </div> </div>{' '} {this.state.visualization && @@ -331,14 +369,34 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> className={Classes.RUNNING_TEXT} data-testid="cse-machine-default-text" > - The CSE machine generates control, stash and environment model diagrams following a - notation introduced in{' '} - <a href={Links.textbookChapter3_2} rel="noopener noreferrer" target="_blank"> - <i> - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, - Section 2 - </i> - </a> + {this.isJava() ? ( + <span> + The CSEC machine generates control, stash, environment and class model diagrams + adapted from the notation introduced in{' '} + <a href={Links.textbookChapter3_2} rel="noopener noreferrer" target="_blank"> + <i> + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter + 3, Section 2 + </i> + </a> + {'. '} + You have chosen the sublanguage{' '} + <a href={`${Links.sourceDocs}java_csec/`} rel="noopener noreferrer" target="_blank"> + <i>Java CSEC</i> + </a> + </span> + ) : ( + <span> + The CSE machine generates control, stash and environment model diagrams following a + notation introduced in{' '} + <a href={Links.textbookChapter3_2} rel="noopener noreferrer" target="_blank"> + <i> + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter + 3, Section 2 + </i> + </a> + </span> + )} . <br /> <br /> On this tab, the REPL will be hidden from view, so do check that your code has no @@ -371,13 +429,13 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> <Button icon="plus" disabled={!this.state.visualization} - onClick={() => Layout.zoomStage(true, 5)} + onClick={() => this.zoomStage(true, 5)} style={{ marginBottom: '5px', borderRadius: '3px' }} /> <Button icon="minus" disabled={!this.state.visualization} - onClick={() => Layout.zoomStage(false, 5)} + onClick={() => this.zoomStage(false, 5)} style={{ borderRadius: '3px' }} /> </ButtonGroup> @@ -385,6 +443,14 @@ class SideContentCseMachineBase extends React.Component<CseMachineProps, State> ); } + private zoomStage = (isZoomIn: boolean, multiplier: number) => { + if (this.isJava()) { + JavaCseMachine.zoomStage(isZoomIn, multiplier); + } else { + Layout.zoomStage(isZoomIn, multiplier); + } + }; + private sliderRelease = (newValue: number) => { if (newValue === this.props.stepsTotal) { this.setState({ lastStep: true }); @@ -509,7 +575,8 @@ const mapStateToProps: MapStateToProps<StateProps, OwnProps, OverallState> = ( breakpointSteps: workspace.breakpointSteps, changepointSteps: workspace.changepointSteps, needCseUpdate: workspace.updateCse, - machineOutput: workspace.output + machineOutput: workspace.output, + chapter: workspace.context.chapter }; }; diff --git a/src/commons/utils/JavaHelper.ts b/src/commons/utils/JavaHelper.ts index 42eea82175..fab36ed3c3 100644 --- a/src/commons/utils/JavaHelper.ts +++ b/src/commons/utils/JavaHelper.ts @@ -1,14 +1,21 @@ -import { compileFromSource, typeCheck } from 'java-slang'; +import { compileFromSource, ECE, typeCheck } from 'java-slang'; import { BinaryWriter } from 'java-slang/dist/compiler/binary-writer'; import setupJVM, { parseBin } from 'java-slang/dist/jvm'; import { createModuleProxy, loadCachedFiles } from 'java-slang/dist/jvm/utils/integration'; import { Context } from 'js-slang'; import loadSourceModules from 'js-slang/dist/modules/loader'; +import { ErrorSeverity, ErrorType, Result, SourceError } from 'js-slang/dist/types'; +import { CseMachine } from '../../features/cseMachine/java/CseMachine'; import Constants from './Constants'; import DisplayBufferService from './DisplayBufferService'; -export async function javaRun(javaCode: string, context: Context) { +export async function javaRun( + javaCode: string, + context: Context, + targetStep: number, + isUsingCse: boolean +) { let compiled = {}; const stderr = (type: 'TypeCheck' | 'Compile' | 'Runtime', msg: string) => { @@ -99,6 +106,8 @@ export async function javaRun(javaCode: string, context: Context) { } }; + if (isUsingCse) return await runJavaCseMachine(javaCode, targetStep, context); + // load cached classfiles from IndexedDB return loadCachedFiles(() => // Initial loader to fetch commonly used classfiles @@ -148,3 +157,44 @@ export async function javaRun(javaCode: string, context: Context) { return { status: 'error' }; }); } + +export function visualizeJavaCseMachine({ context }: { context: ECE.Context }) { + try { + CseMachine.drawCse(context); + } catch (err) { + throw new Error('Java CSE machine is not enabled'); + } +} + +export async function runJavaCseMachine(code: string, targetStep: number, context: Context) { + const convertJavaErrorToJsError = (e: ECE.SourceError): SourceError => ({ + type: ErrorType.RUNTIME, + severity: ErrorSeverity.ERROR, + // TODO update err source node location once location info is avail + location: { + start: { + line: 0, + column: 0 + }, + end: { + line: 0, + column: 0 + } + }, + explain: () => e.explain(), + elaborate: () => e.explain() + }); + context.executionMethod = 'cse-machine'; + return ECE.runECEvaluator(code, targetStep) + .then(result => { + context.runtime.envStepsTotal = result.context.totalSteps; + if (result.status === 'error') { + context.errors = result.context.errors.map(e => convertJavaErrorToJsError(e)); + } + return result; + }) + .catch(e => { + console.error(e); + return { status: 'error' } as Result; + }); +} diff --git a/src/features/cseMachine/java/CseMachine.tsx b/src/features/cseMachine/java/CseMachine.tsx new file mode 100644 index 0000000000..d259b4e021 --- /dev/null +++ b/src/features/cseMachine/java/CseMachine.tsx @@ -0,0 +1,152 @@ +import { ECE } from 'java-slang'; +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Layer, Rect, Stage } from 'react-konva'; + +import { Config, ShapeDefaultProps } from './../CseMachineConfig'; +import { Control } from './components/Control'; +import { Environment } from './components/Environment'; +import { Stash } from './components/Stash'; + +type SetVis = (vis: React.ReactNode) => void; +type SetEditorHighlightedLines = (segments: [number, number][]) => void; + +export class CseMachine { + /** the unique key assigned to each node */ + static key: number = 0; + + /** callback function to update the visualization state in the SideContentCseMachine component */ + private static setVis: SetVis; + /** function to highlight editor lines */ + public static setEditorHighlightedLines: SetEditorHighlightedLines; + + public static stageRef: RefObject<any> = React.createRef(); + /** scale factor for zooming and out of canvas */ + public static scaleFactor = 1.02; + + static environment: Environment | undefined; + static control: Control | undefined; + static stash: Stash | undefined; + + static init(setVis: SetVis, setEditorHighlightedLines: (segments: [number, number][]) => void) { + this.setVis = setVis; + this.setEditorHighlightedLines = setEditorHighlightedLines; + } + + /** updates the visualization state in the SideContentCseMachine component based on + * the Java Slang context passed in */ + static drawCse(context: ECE.Context) { + if (!this.setVis || !context.environment || !context.control || !context.stash) { + throw new Error('Java CSE Machine not initialized'); + } + + CseMachine.environment = new Environment(context.environment); + CseMachine.control = new Control(context.control); + CseMachine.stash = new Stash(context.stash); + + this.setVis(this.draw()); + + // Set icon to blink. + const icon = document.getElementById('env_visualizer-icon'); + icon && icon.classList.add('side-content-tab-alert'); + } + + static clearCse() { + if (this.setVis) { + this.setVis(undefined); + CseMachine.environment = undefined; + CseMachine.control = undefined; + CseMachine.stash = undefined; + } + } + + /** + * Updates the scale of the stage after the user inititates a zoom in or out + * by scrolling or by the trackpad. + */ + static zoomStage(event: KonvaEventObject<WheelEvent> | boolean, multiplier: number = 1) { + typeof event != 'boolean' && event.evt.preventDefault(); + if (CseMachine.stageRef.current) { + const stage = CseMachine.stageRef.current; + const oldScale = stage.scaleX(); + const { x: pointerX, y: pointerY } = stage.getPointerPosition(); + const mousePointTo = { + x: (pointerX - stage.x()) / oldScale, + y: (pointerY - stage.y()) / oldScale + }; + + // zoom in or zoom out + const direction = + typeof event != 'boolean' ? (event.evt.deltaY > 0 ? -1 : 1) : event ? 1 : -1; + + // Check if the zoom limits have been reached + if ((direction > 0 && oldScale < 3) || (direction < 0 && oldScale > 0.4)) { + const newScale = + direction > 0 + ? oldScale * CseMachine.scaleFactor ** multiplier + : oldScale / CseMachine.scaleFactor ** multiplier; + stage.scale({ x: newScale, y: newScale }); + if (typeof event !== 'boolean') { + const newPos = { + x: pointerX - mousePointTo.x * newScale, + y: pointerY - mousePointTo.y * newScale + }; + stage.position(newPos); + stage.batchDraw(); + } + } + } + } + + static draw(): React.ReactNode { + const layout = ( + <div className={'sa-cse-machine'} data-testid="sa-cse-machine"> + <div + id="scroll-container" + style={{ + width: window.innerWidth - 50, + height: window.innerHeight - 150, + overflow: 'hidden' + }} + > + <div + id="large-container" + style={{ + width: Config.CanvasMinWidth, + height: Config.CanvasMinHeight, + overflow: 'hidden', + backgroundColor: Config.SA_BLUE + }} + > + <Stage + width={+Config.CanvasMinWidth} + height={+Config.CanvasMinHeight} + ref={this.stageRef} + draggable + onWheel={CseMachine.zoomStage} + className="draggable" + > + <Layer> + <Rect + {...ShapeDefaultProps} + x={0} + y={0} + width={Config.CanvasMinWidth} + height={Config.CanvasMinHeight} + fill={Config.SA_BLUE} + key={CseMachine.key++} + listening={false} + /> + {this.control?.draw()} + {this.stash?.draw()} + {this.environment?.draw()} + </Layer> + </Stage> + </div> + </div> + </div> + ); + + return layout; + } +} diff --git a/src/features/cseMachine/java/components/Arrow.tsx b/src/features/cseMachine/java/components/Arrow.tsx new file mode 100644 index 0000000000..ddbb8dfba7 --- /dev/null +++ b/src/features/cseMachine/java/components/Arrow.tsx @@ -0,0 +1,67 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import { Arrow as KonvaArrow, Group as KonvaGroup, Path as KonvaPath } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + setHoveredCursor, + setHoveredStyle, + setUnhoveredCursor, + setUnhoveredStyle +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +/** this class encapsulates an Arrow to be drawn between 2 points */ +export class Arrow extends Visible implements IHoverable { + private static readonly TO_X_INDEX = 2; + private readonly _points: number[] = []; + + constructor(fromX: number, fromY: number, toX: number, toY: number) { + super(); + this._points.push(fromX, fromY, toX, toY); + } + + setToX(x: number) { + this._points[Arrow.TO_X_INDEX] = x; + } + + onMouseEnter(e: KonvaEventObject<MouseEvent>) { + setHoveredStyle(e.currentTarget); + setHoveredCursor(e.currentTarget); + } + + onMouseLeave(e: KonvaEventObject<MouseEvent>) { + setUnhoveredStyle(e.currentTarget); + setUnhoveredCursor(e.currentTarget); + } + + draw() { + const path = `M ${this._points[0]} ${this._points[1]} L ${this._points[2]} ${this._points[3]}`; + return ( + <KonvaGroup + key={CseMachine.key++} + onMouseEnter={e => this.onMouseEnter(e)} + onMouseLeave={e => this.onMouseLeave(e)} + > + <KonvaPath + {...ShapeDefaultProps} + stroke={String(Config.SA_WHITE)} + strokeWidth={Number(Config.ArrowStrokeWidth)} + hitStrokeWidth={Number(Config.ArrowHitStrokeWidth)} + data={path} + key={CseMachine.key++} + /> + <KonvaArrow + {...ShapeDefaultProps} + points={this._points.slice(this._points.length - 4)} + fill={String(Config.SA_WHITE)} + strokeEnabled={false} + pointerWidth={Number(Config.ArrowHeadSize)} + pointerLength={Number(Config.ArrowHeadSize)} + key={CseMachine.key++} + /> + </KonvaGroup> + ); + } +} diff --git a/src/features/cseMachine/java/components/Binding.tsx b/src/features/cseMachine/java/components/Binding.tsx new file mode 100644 index 0000000000..6ecdb6cb47 --- /dev/null +++ b/src/features/cseMachine/java/components/Binding.tsx @@ -0,0 +1,96 @@ +import { ECE } from 'java-slang'; +import React from 'react'; + +import { Visible } from '../../components/Visible'; +import { Config } from '../../CseMachineConfig'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Method } from './Method'; +import { Text } from './Text'; +import { Variable } from './Variable'; + +/** a Binding is a key-value pair in a Frame */ +export class Binding extends Visible { + private readonly _name: Text; + + private readonly _value: Variable | Method | Text; + // Only Method has arrow. + private readonly _arrow: Arrow | undefined; + + constructor(name: ECE.Name, value: ECE.Value, x: number, y: number) { + super(); + + // Position. + this._x = x; + this._y = y; + + if (value.kind === ECE.StructType.CLOSURE) { + // Name. + this._name = new Text( + name + Config.VariableColon, // := is part of name + this.x(), + this.y() + ); + // Value. + this._value = new Method( + this._name.x() + this._name.width(), + this.y() + this._name.height() / 2, + value + ); + this._arrow = new Arrow( + this._name.x() + this._name.width(), + this._name.y() + this._name.height() / 2, + this._value.x(), + this._value.y() + ); + } else if (value.kind === ECE.StructType.VARIABLE) { + // Name. + this._name = new Text( + name + Config.VariableColon, // := is part of name + this.x(), + this.y() + Config.FontSize + Config.TextPaddingX + ); + // Value. + this._value = new Variable(this._name.x() + this._name.width(), this.y(), value); + } /*if (value.kind === StructType.CLASS)*/ else { + // Dummy value as class will nvr be drawn. + // Name. + this._name = new Text( + name + Config.VariableColon, // := is part of name + this.x(), + this.y() + Config.FontSize + Config.TextPaddingX + ); + // Value. + this._value = new Text( + '', + this._name.x() + this._name.width() + Config.TextPaddingX, + this.y() + Config.TextPaddingX + ); + } + + // Height and width. + this._height = Math.max(this._name.height(), this._value.height()); + this._width = this._value.x() + this._value.width() - this._name.x(); + } + + get value() { + return this._value; + } + + setArrowToX(x: number) { + this._arrow?.setToX(x); + } + + draw(): React.ReactNode { + return ( + <React.Fragment key={CseMachine.key++}> + {/* Name */} + {this._name.draw()} + + {/* Value */} + {this._value.draw()} + {this._arrow?.draw()} + </React.Fragment> + ); + } +} diff --git a/src/features/cseMachine/java/components/Control.tsx b/src/features/cseMachine/java/components/Control.tsx new file mode 100644 index 0000000000..8305ec1551 --- /dev/null +++ b/src/features/cseMachine/java/components/Control.tsx @@ -0,0 +1,197 @@ +import { astToString, ECE } from 'java-slang'; +import { Group } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { CseMachine } from '../CseMachine'; +import { ControlItem } from './ControlItem'; + +export class Control extends Visible { + private readonly _controlItems: ControlItem[] = []; + + constructor(control: ECE.Control) { + super(); + + // Position. + this._x = ControlStashConfig.ControlPosX; + this._y = + ControlStashConfig.ControlPosY + + ControlStashConfig.StashItemHeight + + ControlStashConfig.StashItemTextPadding * 2; + + // Create each ControlItem. + let controlItemY: number = this._y; + control.getStack().forEach((controlItem, index) => { + const controlItemText = this.getControlItemString(controlItem); + + const controlItemStroke = + index === control.getStack().length - 1 + ? Config.SA_CURRENT_ITEM + : ControlStashConfig.SA_WHITE; + + // TODO reference draw ltr? + const controlItemReference = + ECE.isInstr(controlItem) && controlItem.instrType === ECE.InstrType.ENV + ? CseMachine.environment?.frames.find(f => f.frame === (controlItem as ECE.EnvInstr).env) + : undefined; + + const controlItemTooltip = this.getControlItemTooltip(controlItem); + this.getControlItemTooltip(controlItem); + + const node = ECE.isNode(controlItem) ? controlItem : controlItem.srcNode; + const highlightOnHover = () => { + let start = -1; + let end = -1; + if (node.location) { + start = node.location.startLine - 1; + end = node.location.endLine ? node.location.endLine - 1 : start; + } + CseMachine.setEditorHighlightedLines([[start, end]]); + }; + const unhighlightOnHover = () => CseMachine.setEditorHighlightedLines([]); + + const currControlItem = new ControlItem( + controlItemY, + controlItemText, + controlItemStroke, + controlItemReference, + controlItemTooltip, + highlightOnHover, + unhighlightOnHover + ); + + this._controlItems.push(currControlItem); + controlItemY += currControlItem.height(); + }); + + // Height and width. + this._height = controlItemY - this._y; + // TODO cal real width? + this._width = ControlStashConfig.ControlItemWidth; + } + + draw(): React.ReactNode { + return ( + <Group key={CseMachine.key++} ref={this.ref}> + {this._controlItems.map(c => c.draw())} + </Group> + ); + } + + private getControlItemString = (controlItem: ECE.ControlItem): string => { + if (ECE.isNode(controlItem)) { + return astToString(controlItem); + } + + switch (controlItem.instrType) { + case ECE.InstrType.RESET: + return 'return'; + case ECE.InstrType.ASSIGNMENT: + return 'asgn'; + case ECE.InstrType.BINARY_OP: + const binOpInstr = controlItem as ECE.BinOpInstr; + return binOpInstr.symbol; + case ECE.InstrType.POP: + return 'pop'; + case ECE.InstrType.INVOCATION: + const appInstr = controlItem as ECE.InvInstr; + return `invoke ${appInstr.arity}`; + case ECE.InstrType.ENV: + return 'env'; + case ECE.InstrType.MARKER: + return 'mark'; + case ECE.InstrType.EVAL_VAR: + const evalVarInstr = controlItem as ECE.EvalVarInstr; + return `name ${evalVarInstr.symbol}`; + case ECE.InstrType.NEW: + const newInstr = controlItem as ECE.NewInstr; + return `new ${newInstr.c.frame.name}`; + case ECE.InstrType.RES_TYPE: + const resTypeInstr = controlItem as ECE.ResTypeInstr; + return `res_type ${ + resTypeInstr.value.kind === 'Class' + ? resTypeInstr.value.frame.name + : astToString(resTypeInstr.value) + }`; + case ECE.InstrType.RES_TYPE_CONT: + const resTypeContInstr = controlItem as ECE.ResTypeContInstr; + return `res_type_cont ${resTypeContInstr.name}`; + case ECE.InstrType.RES_OVERLOAD: + const resOverloadInstr = controlItem as ECE.ResOverloadInstr; + return `res_overload ${resOverloadInstr.name} ${resOverloadInstr.arity}`; + case ECE.InstrType.RES_OVERRIDE: + return `res_override`; + case ECE.InstrType.RES_CON_OVERLOAD: + const resConOverloadInstr = controlItem as ECE.ResConOverloadInstr; + return `res_con_overload ${resConOverloadInstr.arity}`; + case ECE.InstrType.RES: + const resInstr = controlItem as ECE.ResInstr; + return `res ${resInstr.name}`; + case ECE.InstrType.DEREF: + return 'deref'; + default: + return 'INSTRUCTION'; + } + }; + + private getControlItemTooltip = (controlItem: ECE.ControlItem): string => { + if (ECE.isNode(controlItem)) { + return astToString(controlItem); + } + + switch (controlItem.instrType) { + case ECE.InstrType.RESET: + return 'Skip control items until marker instruction is reached'; + case ECE.InstrType.ASSIGNMENT: + return 'Assign value on top of stash to location on top of stash'; + case ECE.InstrType.BINARY_OP: + const binOpInstr = controlItem as ECE.BinOpInstr; + return `Perform ${binOpInstr.symbol} on top 2 stash values`; + case ECE.InstrType.POP: + return 'Pop most recently pushed value from stash'; + case ECE.InstrType.INVOCATION: + const appInstr = controlItem as ECE.InvInstr; + return `Invoke method with ${appInstr.arity} argument${appInstr.arity === 1 ? '' : 's'}`; + case ECE.InstrType.ENV: + return 'Set current environment to this environment'; + case ECE.InstrType.MARKER: + return 'Mark return address'; + case ECE.InstrType.EVAL_VAR: + const evalVarInstr = controlItem as ECE.EvalVarInstr; + return `name ${evalVarInstr.symbol}`; + case ECE.InstrType.NEW: + const newInstr = controlItem as ECE.NewInstr; + return `Create new instance of class ${newInstr.c.frame.name}`; + case ECE.InstrType.RES_TYPE: + const resTypeInstr = controlItem as ECE.ResTypeInstr; + return `Resolve type of ${ + resTypeInstr.value.kind === 'Class' + ? resTypeInstr.value.frame.name + : astToString(resTypeInstr.value) + }`; + case ECE.InstrType.RES_TYPE_CONT: + const resTypeContInstr = controlItem as ECE.ResTypeContInstr; + return `Resolve type of ${resTypeContInstr.name} in most recently pushed type from stash`; + case ECE.InstrType.RES_OVERLOAD: + const resOverloadInstr = controlItem as ECE.ResOverloadInstr; + return `Resolve overloading of method ${resOverloadInstr.name} with ${ + resOverloadInstr.arity + } argument${resOverloadInstr.arity === 1 ? '' : 's'}`; + case ECE.InstrType.RES_OVERRIDE: + return 'Resolve overriding of resolved method on top of stash'; + case ECE.InstrType.RES_CON_OVERLOAD: + const resConOverloadInstr = controlItem as ECE.ResConOverloadInstr; + return `Resolve constructor overloading of class on stash with ${ + resConOverloadInstr.arity + } argument${resConOverloadInstr.arity === 1 ? '' : 's'}`; + case ECE.InstrType.RES: + const resInstr = controlItem as ECE.ResInstr; + return `Resolve field ${resInstr.name} of most recently pushed value from stash`; + case ECE.InstrType.DEREF: + return 'Dereference most recently pushed value from stash'; + default: + return 'INSTRUCTION'; + } + }; +} diff --git a/src/features/cseMachine/java/components/ControlItem.tsx b/src/features/cseMachine/java/components/ControlItem.tsx new file mode 100644 index 0000000000..55fa748887 --- /dev/null +++ b/src/features/cseMachine/java/components/ControlItem.tsx @@ -0,0 +1,151 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Label, Tag, Text } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + getTextHeight, + setHoveredCursor, + setHoveredStyle, + setUnhoveredCursor, + setUnhoveredStyle, + truncateText +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Frame } from './Frame'; + +export class ControlItem extends Visible implements IHoverable { + private readonly _arrow: Arrow | undefined; + private readonly _tooltipRef: RefObject<any>; + + constructor( + y: number, + + private readonly _text: string, + private readonly _stroke: string, + + reference: Frame | undefined, + + private readonly _tooltip: string, + private readonly highlightOnHover: () => void, + private readonly unhighlightOnHover: () => void + ) { + super(); + + // Position. + this._x = ControlStashConfig.ControlPosX; + this._y = y; + + // Text. + this._text = truncateText( + this._text, + ControlStashConfig.ControlMaxTextWidth, + ControlStashConfig.ControlMaxTextHeight + ); + + // Tooltip. + this._tooltipRef = React.createRef(); + + // Height and width. + this._height = + getTextHeight(this._text, ControlStashConfig.ControlMaxTextWidth) + + ControlStashConfig.ControlItemTextPadding * 2; + this._width = ControlStashConfig.ControlItemWidth; + + // Arrow + if (reference) { + this._arrow = new Arrow( + this._x + this._width, + this._y + this._height / 2, + reference.x(), + reference.y() + reference.height() / 2 + reference.name.height() + ); + } + } + + private isCurrentItem = (): boolean => { + return this._stroke === Config.SA_CURRENT_ITEM; + }; + + onMouseEnter = (e: KonvaEventObject<MouseEvent>): void => { + this.highlightOnHover(); + !this.isCurrentItem() && setHoveredStyle(e.currentTarget); + setHoveredCursor(e.currentTarget); + this._tooltipRef.current.show(); + }; + + onMouseLeave = (e: KonvaEventObject<MouseEvent>): void => { + this.unhighlightOnHover(); + !this.isCurrentItem() && setUnhoveredStyle(e.currentTarget); + setUnhoveredCursor(e.currentTarget); + this._tooltipRef.current.hide(); + }; + + draw(): React.ReactNode { + const textProps = { + fill: ControlStashConfig.SA_WHITE, + padding: ControlStashConfig.ControlItemTextPadding, + fontFamily: ControlStashConfig.FontFamily, + fontSize: ControlStashConfig.FontSize, + fontStyle: ControlStashConfig.FontStyle, + fontVariant: ControlStashConfig.FontVariant + }; + const tagProps = { + stroke: this._stroke, + cornerRadius: ControlStashConfig.ControlItemCornerRadius + }; + return ( + <React.Fragment key={CseMachine.key++}> + {/* Text */} + <Label + x={this.x()} + y={this.y()} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + key={CseMachine.key++} + > + <Tag {...ShapeDefaultProps} {...tagProps} key={CseMachine.key++} /> + <Text + {...ShapeDefaultProps} + {...textProps} + text={this._text} + width={this.width()} + height={this.height()} + key={CseMachine.key++} + /> + </Label> + + {/* Tooltip */} + <Label + x={this.x() + this.width() + ControlStashConfig.TooltipMargin} + y={this.y() + ControlStashConfig.TooltipMargin} + visible={false} + ref={this._tooltipRef} + key={CseMachine.key++} + > + <Tag + {...ShapeDefaultProps} + stroke="black" + fill={'black'} + opacity={ControlStashConfig.TooltipOpacity} + key={CseMachine.key++} + /> + <Text + {...ShapeDefaultProps} + {...textProps} + text={this._tooltip} + padding={ControlStashConfig.TooltipPadding} + key={CseMachine.key++} + /> + </Label> + + {/* Arrow */} + {this._arrow?.draw()} + </React.Fragment> + ); + } +} diff --git a/src/features/cseMachine/java/components/Environment.tsx b/src/features/cseMachine/java/components/Environment.tsx new file mode 100644 index 0000000000..f8be475a31 --- /dev/null +++ b/src/features/cseMachine/java/components/Environment.tsx @@ -0,0 +1,217 @@ +import { ECE } from 'java-slang'; +import { Group } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Frame } from './Frame'; +import { Line } from './Line'; +import { Obj } from './Object'; +import { Variable } from './Variable'; + +export class Environment extends Visible { + private readonly _methodFrames: Frame[] = []; + private readonly _objects: Obj[] = []; + private readonly _classFrames: Frame[] = []; + private readonly _lines: Line[] = []; + + constructor(environment: ECE.Environment) { + super(); + + // Position. + this._x = + ControlStashConfig.ControlPosX + + ControlStashConfig.ControlItemWidth + + 2 * Config.CanvasPaddingX; + this._y = + ControlStashConfig.StashPosY + ControlStashConfig.StashItemHeight + 2 * Config.CanvasPaddingY; + + // Create method frames. + const methodFramesX = this._x; + let methodFramesY: number = this._y; + let methodFramesWidth = Number(Config.FrameMinWidth); + environment.global.children.forEach(env => { + if (env.name.includes('(')) { + let currEnv: ECE.EnvNode | undefined = env; + let parentFrame; + while (currEnv) { + const stroke = currEnv === environment.current ? Config.SA_CURRENT_ITEM : Config.SA_WHITE; + const frame = new Frame(currEnv, methodFramesX, methodFramesY, stroke); + this._methodFrames.push(frame); + methodFramesY += frame.height() + Config.FramePaddingY; + methodFramesWidth = Math.max(methodFramesWidth, frame.width()); + parentFrame && frame.setParent(parentFrame); + + parentFrame = frame; + currEnv = currEnv.children.length ? currEnv.children[0] : undefined; + } + } + }); + + // Create objects. + const objectFramesX = methodFramesX + methodFramesWidth + Config.FrameMinWidth; + let objectFramesY: number = this._y; + let objectFramesWidth = Number(Config.FrameMinWidth); + environment.objects.forEach(obj => { + const objectFrames: Frame[] = []; + let objectFrameWidth = Number(Config.FrameMinWidth); + + // Get top env. + let env: ECE.EnvNode | undefined = obj.frame; + while (env.parent) { + env = env.parent; + } + + // Create frame top-down. + while (env) { + const stroke = env === environment.current ? Config.SA_CURRENT_ITEM : Config.SA_WHITE; + const frame = new Frame(env, objectFramesX, objectFramesY, stroke); + // No padding btwn obj frames thus no arrows required. + objectFramesY += frame.height(); + objectFramesWidth = Math.max(objectFramesWidth, frame.width()); + + env = env.children.length ? env.children[0] : undefined; + + objectFrames.push(frame); + objectFrameWidth = Math.max(objectFrameWidth, frame.width()); + } + + // Standardize obj frames width. + objectFrames.forEach(o => o.setWidth(objectFrameWidth)); + + // Only add padding btwn objects. + objectFramesY += Config.FramePaddingY; + + this._objects.push(new Obj(objectFrames, obj)); + }); + + // Create class frames. + const classFramesX = objectFramesX + objectFramesWidth + Config.FrameMinWidth; + let classFramesY = this._y; + for (const c of environment.global.frame.values()) { + const classEnv = (c as ECE.Class).frame; + const classFrameStroke = + classEnv === environment.current ? Config.SA_CURRENT_ITEM : Config.SA_WHITE; + const highlightOnHover = () => { + const node = (c as ECE.Class).classDecl; + let start = -1; + let end = -1; + if (node.location) { + start = node.location.startLine - 1; + end = node.location.endLine ? node.location.endLine - 1 : start; + } + CseMachine.setEditorHighlightedLines([[start, end]]); + }; + const unhighlightOnHover = () => CseMachine.setEditorHighlightedLines([]); + const classFrame = new Frame( + classEnv, + classFramesX, + classFramesY, + classFrameStroke, + '', + highlightOnHover, + unhighlightOnHover + ); + const superClassName = (c as ECE.Class).superclass?.frame.name; + if (superClassName) { + const parentFrame = this._classFrames.find(f => f.name.text === superClassName)!; + classFrame.setParent(parentFrame); + } + this._classFrames.push(classFrame); + classFramesY += classFrame.height() + Config.FramePaddingY; + } + + // Draw arrow for var ref in mtd frames to corresponding obj. + this._methodFrames.forEach(mf => { + mf.bindings.forEach(b => { + if (b.value instanceof Variable && b.value.variable.value.kind === ECE.StructType.OBJECT) { + const objFrame = b.value.variable.value.frame; + const matchingObj = this._objects.filter(o => o.getFrame().frame === objFrame)[0]; + b.value.value = new Arrow( + b.value.x() + b.value.width() / 2, + b.value.y() + b.value.type.height() + (b.value.height() - b.value.type.height()) / 2, + matchingObj.x(), + matchingObj.y() + matchingObj.height() / 2 + ); + } + }); + }); + + // Draw arrow for var ref in obj frames to corresponding var or obj. + this._objects + .flatMap(obj => obj.frames) + .forEach(of => { + of.bindings.forEach(b => { + if ( + b.value instanceof Variable && + b.value.variable.value.kind === ECE.StructType.VARIABLE + ) { + const variable = b.value.variable.value; + const matchingVariable = this._classFrames + .flatMap(c => c.bindings) + .filter(b => b.value instanceof Variable && b.value.variable === variable)[0] + .value as Variable; + b.value.value = new Arrow( + b.value.x() + b.value.width() / 2, + b.value.y() + b.value.type.height() + (b.value.height() - b.value.type.height()) / 2, + matchingVariable.x(), + matchingVariable.y() + matchingVariable.type.height() + ); + } + if ( + b.value instanceof Variable && + b.value.variable.value.kind === ECE.StructType.OBJECT + ) { + const obj = b.value.variable.value.frame; + const matchingObj = this._objects.find(o => o.getFrame().frame === obj)!; + // Variable always has a box. + b.value.value = new Arrow( + b.value.x() + b.value.width() / 2, + b.value.y() + b.value.type.height() + (b.value.height() - b.value.type.height()) / 2, + matchingObj.x(), + matchingObj.y() + matchingObj.height() / 2 + ); + } + }); + }); + + // Draw line for obj to class. + this._objects.forEach(obj => { + const matchingClass = this._classFrames.find( + c => c.name.text === obj.object.class.frame.name + )!; + const line = new Line( + obj.x() + obj.width(), + obj.y() + obj.height() / 2, + matchingClass.x(), + matchingClass.y() + matchingClass.height() / 2 + matchingClass.name.height() + ); + this._lines.push(line); + }); + } + + get classes() { + return this._classFrames; + } + + get objects() { + return this._objects.flatMap(obj => obj.frames); + } + + get frames() { + return this._methodFrames; + } + + draw(): React.ReactNode { + return ( + <Group key={CseMachine.key++}> + {this._methodFrames.map(f => f.draw())} + {this._objects.flatMap(obj => obj.frames).map(f => f.draw())} + {this._classFrames.map(f => f.draw())} + {this._lines.map(f => f.draw())} + </Group> + ); + } +} diff --git a/src/features/cseMachine/java/components/Frame.tsx b/src/features/cseMachine/java/components/Frame.tsx new file mode 100644 index 0000000000..2c7ec35570 --- /dev/null +++ b/src/features/cseMachine/java/components/Frame.tsx @@ -0,0 +1,153 @@ +import { ECE } from 'java-slang'; +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Group, Label, Rect, Tag, Text as KonvaText } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { setHoveredCursor, setUnhoveredCursor } from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Binding } from './Binding'; +import { Method } from './Method'; +import { Text } from './Text'; + +export class Frame extends Visible implements IHoverable { + readonly tooltipRef: RefObject<any>; + + readonly bindings: Binding[] = []; + readonly name: Text; + private parent: Frame | undefined; + + constructor( + readonly frame: ECE.EnvNode, + x: number, + y: number, + readonly stroke: string, + + readonly tooltip?: string, + readonly highlightOnHover?: () => void, + readonly unhighlightOnHover?: () => void + ) { + super(); + + this._x = x; + this._y = y; + + this.name = new Text(frame.name, this._x + Config.FramePaddingX, this._y); + + this._width = Math.max(Config.FrameMinWidth, this.name.width() + 2 * Config.FramePaddingX); + this._height = Config.FramePaddingY + this.name.height(); + + // Create binding for each key-value pair + let bindingY: number = this._y + this.name.height() + Config.FramePaddingY; + for (const [key, data] of frame.frame) { + const currBinding: Binding = new Binding(key, data, this._x + Config.FramePaddingX, bindingY); + this.bindings.push(currBinding); + bindingY += currBinding.height() + Config.FramePaddingY; + this._width = Math.max(this._width, currBinding.width() + 2 * Config.FramePaddingX); + this._height += currBinding.height() + Config.FramePaddingY; + } + + // Set x of Method aft knowing frame width. + this.bindings + .filter(b => b.value instanceof Method) + .forEach(b => { + (b.value as Method).setX(this._x + this._width + Config.FramePaddingX); + b.setArrowToX(this._x + this._width + Config.FramePaddingX); + }); + + this.tooltipRef = React.createRef(); + } + + setWidth(width: number) { + this._width = width; + } + + setParent(parent: Frame) { + this.parent = parent; + } + + onMouseEnter = (e: KonvaEventObject<MouseEvent>) => { + this.highlightOnHover && this.highlightOnHover(); + (this.tooltip || this.highlightOnHover) && setHoveredCursor(e.currentTarget); + this.tooltip && this.tooltipRef.current.show(); + }; + + onMouseLeave = (e: KonvaEventObject<MouseEvent>) => { + this.unhighlightOnHover && this.unhighlightOnHover(); + (this.tooltip || this.unhighlightOnHover) && setUnhoveredCursor(e.currentTarget); + this.tooltip && this.tooltipRef.current.hide(); + }; + + draw(): React.ReactNode { + const textProps = { + fill: ControlStashConfig.SA_WHITE.toString(), + padding: Number(ControlStashConfig.ControlItemTextPadding), + fontFamily: ControlStashConfig.FontFamily.toString(), + fontSize: Number(ControlStashConfig.FontSize), + fontStyle: ControlStashConfig.FontStyle.toString(), + fontVariant: ControlStashConfig.FontVariant.toString() + }; + + return ( + <Group key={CseMachine.key++}> + <Rect + {...ShapeDefaultProps} + x={this.x()} + y={this.y() + this.name.height()} + width={this.width()} + height={this.height()} + stroke={this.stroke} + cornerRadius={Number(Config.FrameCornerRadius)} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + key={CseMachine.key++} + /> + {/* Frame name */} + {this.name.draw()} + + {/* Frame */} + {this.bindings.map(binding => binding.draw())} + + {/* Frame parent */} + {this.parent && + new Arrow( + this._x + Config.FramePaddingX / 2, + this._y + this.name.height(), + this.parent.x() + Config.FramePaddingX / 2, + // TODO WHY NEED TO ADD NAME HEIGHT? + this.parent.y() + this.parent.height() + this.name?.height() + ).draw()} + + {/* Frame tooltip */} + {this.tooltip && ( + <Label + x={this.x() + this.width() + ControlStashConfig.TooltipMargin} + y={this.y() + ControlStashConfig.TooltipMargin} + visible={false} + ref={this.tooltipRef} + key={CseMachine.key++} + > + <Tag + {...ShapeDefaultProps} + stroke="black" + fill={'black'} + opacity={Number(ControlStashConfig.TooltipOpacity)} + key={CseMachine.key++} + /> + <KonvaText + {...ShapeDefaultProps} + {...textProps} + text={this.tooltip} + padding={Number(ControlStashConfig.TooltipPadding)} + key={CseMachine.key++} + /> + </Label> + )} + </Group> + ); + } +} diff --git a/src/features/cseMachine/java/components/Line.tsx b/src/features/cseMachine/java/components/Line.tsx new file mode 100644 index 0000000000..67323c32b5 --- /dev/null +++ b/src/features/cseMachine/java/components/Line.tsx @@ -0,0 +1,58 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import { Group, Path } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + setHoveredCursor, + setHoveredStyle, + setUnhoveredCursor, + setUnhoveredStyle +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +/** this class encapsulates a Line to be drawn between 2 points */ +export class Line extends Visible implements IHoverable { + private static readonly TO_X_INDEX = 2; + private readonly _points: number[] = []; + + constructor(fromX: number, fromY: number, toX: number, toY: number) { + super(); + this._points.push(fromX, fromY, toX, toY); + } + + setToX(x: number) { + this._points[Line.TO_X_INDEX] = x; + } + + onMouseEnter(e: KonvaEventObject<MouseEvent>) { + setHoveredStyle(e.currentTarget); + setHoveredCursor(e.currentTarget); + } + + onMouseLeave(e: KonvaEventObject<MouseEvent>) { + setUnhoveredStyle(e.currentTarget); + setUnhoveredCursor(e.currentTarget); + } + + draw() { + const path = `M ${this._points[0]} ${this._points[1]} L ${this._points[2]} ${this._points[3]}`; + return ( + <Group + key={CseMachine.key++} + onMouseEnter={e => this.onMouseEnter(e)} + onMouseLeave={e => this.onMouseLeave(e)} + > + <Path + {...ShapeDefaultProps} + stroke={String(Config.SA_WHITE)} + strokeWidth={Number(Config.ArrowStrokeWidth)} + hitStrokeWidth={Number(Config.ArrowHitStrokeWidth)} + data={path} + key={CseMachine.key++} + /> + </Group> + ); + } +} diff --git a/src/features/cseMachine/java/components/Method.tsx b/src/features/cseMachine/java/components/Method.tsx new file mode 100644 index 0000000000..45846e10e6 --- /dev/null +++ b/src/features/cseMachine/java/components/Method.tsx @@ -0,0 +1,118 @@ +import { astToString, ECE } from 'java-slang'; +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Circle, Group, Label, Tag, Text } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + getTextHeight, + getTextWidth, + setHoveredCursor, + setUnhoveredCursor +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +export class Method extends Visible implements IHoverable { + private _centerX: number; + + private readonly _tooltipRef: RefObject<any>; + private readonly _tooltip: string; + + constructor( + x: number, + y: number, + private readonly _method: ECE.Closure + ) { + super(); + + // Position. + this._x = x; + this._y = y; + + // Circle. + this._centerX = this._x + Config.FnRadius * 2; + + // Tooltip. + this._tooltipRef = React.createRef(); + this._tooltip = astToString(this._method.mtdOrCon); + } + + get method() { + return this._method; + } + + setX(x: number) { + this._x = x; + this._centerX = this._x + Config.FnRadius * 2; + } + + onMouseEnter = (e: KonvaEventObject<MouseEvent>) => { + setHoveredCursor(e.currentTarget); + this._tooltipRef.current.show(); + }; + + onMouseLeave = (e: KonvaEventObject<MouseEvent>) => { + setUnhoveredCursor(e.currentTarget); + this._tooltipRef.current.hide(); + }; + + draw(): React.ReactNode { + return ( + <React.Fragment key={CseMachine.key++}> + <Group + onMouseEnter={e => this.onMouseEnter(e)} + onMouseLeave={e => this.onMouseLeave(e)} + // ref={this.ref} + key={CseMachine.key++} + > + {/* Left outer */} + <Circle + {...ShapeDefaultProps} + key={CseMachine.key++} + x={this._centerX - Config.FnRadius} + y={this.y()} + radius={Config.FnRadius} + stroke={ControlStashConfig.SA_WHITE} + /> + {/* Left inner */} + <Circle + {...ShapeDefaultProps} + key={CseMachine.key++} + x={this._centerX - Config.FnRadius} + y={this.y()} + radius={Config.FnInnerRadius} + fill={String(ControlStashConfig.SA_WHITE)} + /> + </Group> + + {/* Tooltip */} + <Label + x={this.x() + Config.FnRadius * 2 + Config.TextPaddingX * 2} + y={this.y() - getTextHeight(this._tooltip, getTextWidth(this._tooltip)) / 2} + visible={false} + ref={this._tooltipRef} + key={CseMachine.key++} + > + <Tag + stroke="black" + fill={'black'} + opacity={Config.FnTooltipOpacity} + key={CseMachine.key++} + /> + <Text + text={this._tooltip} + fontFamily={ControlStashConfig.FontFamily} + fontSize={ControlStashConfig.FontSize} + fontStyle={ControlStashConfig.FontStyle} + fill={ControlStashConfig.SA_WHITE} + padding={5} + key={CseMachine.key++} + /> + </Label> + </React.Fragment> + ); + } +} diff --git a/src/features/cseMachine/java/components/Object.tsx b/src/features/cseMachine/java/components/Object.tsx new file mode 100644 index 0000000000..73760e232f --- /dev/null +++ b/src/features/cseMachine/java/components/Object.tsx @@ -0,0 +1,43 @@ +import { ECE } from 'java-slang'; +import React from 'react'; +import { Group } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { CseMachine } from '../CseMachine'; +import { Frame } from './Frame'; + +export class Obj extends Visible { + constructor( + private readonly _frames: Frame[], + private readonly _object: ECE.Object + ) { + super(); + + // Position. + this._x = _frames[0].x(); + this._y = _frames[0].y(); + + // Height and width. + this._height = this._frames.reduce((accHeight, currFrame) => accHeight + currFrame.height(), 0); + this._width = this._frames.reduce( + (maxWidth, currFrame) => Math.max(maxWidth, currFrame.width()), + 0 + ); + } + + get frames() { + return this._frames; + } + + get object() { + return this._object; + } + + getFrame(): Frame { + return this._frames[this._frames.length - 1]; + } + + draw(): React.ReactNode { + return <Group key={CseMachine.key++}>{this._frames.map(f => f.draw())}</Group>; + } +} diff --git a/src/features/cseMachine/java/components/Stash.tsx b/src/features/cseMachine/java/components/Stash.tsx new file mode 100644 index 0000000000..263fd097ac --- /dev/null +++ b/src/features/cseMachine/java/components/Stash.tsx @@ -0,0 +1,89 @@ +import { ECE } from 'java-slang'; +import React from 'react'; +import { Group } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { CseMachine } from '../CseMachine'; +import { Method } from './Method'; +import { StashItem } from './StashItem'; +import { Variable } from './Variable'; + +export class Stash extends Visible { + private readonly _stashItems: StashItem[] = []; + + constructor(stash: ECE.Stash) { + super(); + + // Position. + this._x = ControlStashConfig.StashPosX; + this._y = ControlStashConfig.StashPosY; + + // Create each StashItem. + let stashItemX: number = this._x; + for (const stashItem of stash.getStack()) { + const stashItemText = this.getStashItemString(stashItem); + const stashItemStroke = ControlStashConfig.SA_WHITE; + const stashItemReference = this.getStashItemRef(stashItem); + const currStashItem = new StashItem( + stashItemX, + stashItemText, + stashItemStroke, + stashItemReference + ); + + this._stashItems.push(currStashItem); + stashItemX += currStashItem.width(); + } + + // Height and width. + this._height = ControlStashConfig.StashItemHeight; + this._width = stashItemX - this._x; + } + + draw(): React.ReactNode { + return <Group key={CseMachine.key++}>{this._stashItems.map(s => s.draw())}</Group>; + } + + private getStashItemString = (stashItem: ECE.StashItem): string => { + switch (stashItem.kind) { + case 'Literal': + return stashItem.literalType.value; + case ECE.StructType.VARIABLE: + return 'location'; + case ECE.StructType.TYPE: + return stashItem.type; + default: + return stashItem.kind.toLowerCase(); + } + }; + + private getStashItemRef = (stashItem: ECE.StashItem) => { + return stashItem.kind === ECE.StructType.CLOSURE + ? CseMachine.environment && + (CseMachine.environment.classes + .flatMap(c => c.bindings) + .find(b => b.value instanceof Method && b.value.method === stashItem)?.value as Method) + : stashItem.kind === ECE.StructType.VARIABLE + ? CseMachine.environment && + ((CseMachine.environment.frames + .flatMap(c => c.bindings) + .find(b => b.value instanceof Variable && b.value.variable === stashItem) + ?.value as Variable) || + (CseMachine.environment.classes + .flatMap(c => c.bindings) + .find(b => b.value instanceof Variable && b.value.variable === stashItem) + ?.value as Variable) || + (CseMachine.environment.objects + .flatMap(o => o.bindings) + .find(b => b.value instanceof Variable && b.value.variable === stashItem) + ?.value as Variable)) + : stashItem.kind === ECE.StructType.CLASS + ? CseMachine.environment && + CseMachine.environment.classes.find(c => c.frame === stashItem.frame) + : stashItem.kind === ECE.StructType.OBJECT + ? CseMachine.environment && + CseMachine.environment.objects.find(o => o.frame === stashItem.frame) + : undefined; + }; +} diff --git a/src/features/cseMachine/java/components/StashItem.tsx b/src/features/cseMachine/java/components/StashItem.tsx new file mode 100644 index 0000000000..27646641e6 --- /dev/null +++ b/src/features/cseMachine/java/components/StashItem.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { + Group as KonvaGroup, + Label as KonvaLabel, + Tag as KonvaTag, + Text as KonvaText +} from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { getTextWidth } from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Frame } from './Frame'; +import { Method } from './Method'; +import { Variable } from './Variable'; + +export class StashItem extends Visible { + private readonly _arrow: Arrow | undefined; + + constructor( + x: number, + private readonly _text: string, + private readonly _stroke: string, + reference?: Method | Frame | Variable + ) { + super(); + + // Position. + this._x = x; + this._y = ControlStashConfig.StashPosY; + + // Height and width. + this._height = ControlStashConfig.StashItemHeight + ControlStashConfig.StashItemTextPadding * 2; + this._width = ControlStashConfig.StashItemTextPadding * 2 + getTextWidth(this._text); + + // Arrow + if (reference) { + const toY = + reference instanceof Frame + ? reference.y() + reference.name.height() + : reference instanceof Method + ? reference.y() + : reference.y() + reference.type.height(); + this._arrow = new Arrow( + this._x + this._width / 2, + this._y + this._height, + reference.x(), + toY + ); + } + } + + draw(): React.ReactNode { + const textProps = { + fill: ControlStashConfig.SA_WHITE, + padding: ControlStashConfig.StashItemTextPadding, + fontFamily: ControlStashConfig.FontFamily, + fontSize: ControlStashConfig.FontSize, + fontStyle: ControlStashConfig.FontStyle, + fontVariant: ControlStashConfig.FontVariant + }; + + const tagProps = { + stroke: this._stroke, + cornerRadius: ControlStashConfig.StashItemCornerRadius + }; + + return ( + <KonvaGroup key={CseMachine.key++}> + {/* Text */} + <KonvaLabel x={this.x()} y={this.y()} key={CseMachine.key++}> + <KonvaTag {...ShapeDefaultProps} {...tagProps} key={CseMachine.key++} /> + <KonvaText + {...ShapeDefaultProps} + {...textProps} + text={this._text} + key={CseMachine.key++} + /> + </KonvaLabel> + + {/* Arrow */} + {this._arrow?.draw()} + </KonvaGroup> + ); + } +} diff --git a/src/features/cseMachine/java/components/Text.tsx b/src/features/cseMachine/java/components/Text.tsx new file mode 100644 index 0000000000..db11a00abe --- /dev/null +++ b/src/features/cseMachine/java/components/Text.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Group as KonvaGroup, Label as KonvaLabel, Text as KonvaText } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { getTextWidth } from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +/** this class encapsulates a string to be drawn onto the canvas */ +export class Text extends Visible { + constructor( + private readonly _text: string, + x: number, + y: number + ) { + super(); + + // Position + this._x = x; + this._y = y; + + // Height and width + this._height = Config.FontSize; + this._width = getTextWidth(this._text); + } + + get text() { + return this._text; + } + + setY(y: number) { + this._y = y; + } + + draw(): React.ReactNode { + const props = { + fontFamily: Config.FontFamily, + fontSize: Config.FontSize, + fontStyle: Config.FontStyle, + fill: Config.SA_WHITE + }; + + return ( + <KonvaGroup key={CseMachine.key++}> + <KonvaLabel x={this.x()} y={this.y()} key={CseMachine.key++}> + <KonvaText {...ShapeDefaultProps} key={CseMachine.key++} text={this._text} {...props} /> + </KonvaLabel> + </KonvaGroup> + ); + } +} diff --git a/src/features/cseMachine/java/components/Variable.tsx b/src/features/cseMachine/java/components/Variable.tsx new file mode 100644 index 0000000000..380a7df816 --- /dev/null +++ b/src/features/cseMachine/java/components/Variable.tsx @@ -0,0 +1,109 @@ +import { ECE } from 'java-slang'; +import React from 'react'; +import { Group, Rect } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Text } from './Text'; + +export interface TextOptions { + maxWidth: number; + fontSize: number; + fontFamily: string; + fontStyle: string; + fontVariant: string; + isStringIdentifiable: boolean; +} + +export const defaultOptions: TextOptions = { + maxWidth: Number.MAX_VALUE, // maximum width this text should be + fontFamily: Config.FontFamily.toString(), // default is Arial + fontSize: Number(Config.FontSize), // in pixels. Default is 12 + fontStyle: Config.FontStyle.toString(), // can be normal, bold, or italic. Default is normal + fontVariant: Config.FontVariant.toString(), // can be normal or small-caps. Default is normal + isStringIdentifiable: false // if true, contain strings within double quotation marks "". Default is false +}; + +export class Variable extends Visible { + private readonly _type: Text; + private _value: Text | Arrow; + + constructor( + x: number, + y: number, + private readonly _variable: ECE.Variable + ) { + super(); + + // Position. + this._x = x; + this._y = y; + + // Type. + this._type = new Text(this._variable.type, this._x, this._y); + + // Value. + if (this.variable.value.kind === 'Literal') { + this._value = new Text( + this.variable.value.literalType.value, + this._x + Config.TextPaddingX, + this._y + this._type.height() + Config.TextPaddingX + ); + } else if (this.variable.value.kind === ECE.StructType.SYMBOL) { + this._value = new Text( + '', + this._x + Config.TextPaddingX, + this._y + this._type.height() + Config.TextPaddingX + ); + } else { + this._value = new Text( + '', + this._x + Config.TextPaddingX, + this._y + this._type.height() + Config.TextPaddingX + ); + } + + // Height and width. + this._height = this._type.height() + this._value.height() + 2 * Config.TextPaddingX; + this._width = Math.max(this._type.width(), this._value.width() + 2 * Config.TextPaddingX); + } + + get variable() { + return this._variable; + } + + set value(value: Arrow) { + this._value = value; + this._height = this._type.height() + Config.FontSize + 2 * Config.TextPaddingX; + this._width = Math.max(this._type.width(), Config.TextMinWidth); + } + + get type() { + return this._type; + } + + draw(): React.ReactNode { + return ( + <Group key={CseMachine.key++}> + {/* Type */} + {this._type.draw()} + + {/* Box */} + <Rect + {...ShapeDefaultProps} + x={this._x} + y={this._y + this._type.height()} + width={this._width} + height={this._height - this._type.height()} + stroke={Config.SA_WHITE} + key={CseMachine.key++} + /> + + {/* Text */} + {this._value.draw()} + </Group> + ); + } +} diff --git a/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap b/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap index e0ee8352d6..561f62ea1a 100644 --- a/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap +++ b/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap @@ -979,17 +979,19 @@ and also the data-testid="cse-machine-default-text" id="cse-machine-default-text" > - The CSE machine generates control, stash and environment model diagrams following a notation introduced in - - <a - href="https://sourceacademy.org/sicpjs/3.2" - rel="noopener noreferrer" - target="_blank" - > - <i> - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, Section 2 - </i> - </a> + <span> + The CSE machine generates control, stash and environment model diagrams following a notation introduced in + + <a + href="https://sourceacademy.org/sicpjs/3.2" + rel="noopener noreferrer" + target="_blank" + > + <i> + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, Section 2 + </i> + </a> + </span> . <br /> <br />