diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 71378f488e..1410008fed 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -1,12 +1,43 @@ import 'js-slang/dist/editors/ace/theme/source'; -import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { + Button, + ButtonGroup, + Card, + Classes, + Divider, + Icon, + Popover, + Pre, + Slider +} from '@blueprintjs/core'; import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; -import { IStepperPropContents } from 'js-slang/dist/stepper/stepper'; +import { IStepperPropContents } from 'js-slang/dist/tracer'; +import { StepperBaseNode } from 'js-slang/dist/tracer/interface'; +import { StepperExpression } from 'js-slang/dist/tracer/nodes'; +import { StepperArrayExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrayExpression'; +import { StepperArrowFunctionExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrowFunctionExpression'; +import { StepperBinaryExpression } from 'js-slang/dist/tracer/nodes/Expression/BinaryExpression'; +import { StepperConditionalExpression } from 'js-slang/dist/tracer/nodes/Expression/ConditionalExpression'; +import { StepperFunctionApplication } from 'js-slang/dist/tracer/nodes/Expression/FunctionApplication'; +import { StepperIdentifier } from 'js-slang/dist/tracer/nodes/Expression/Identifier'; +import { StepperLiteral } from 'js-slang/dist/tracer/nodes/Expression/Literal'; +import { StepperLogicalExpression } from 'js-slang/dist/tracer/nodes/Expression/LogicalExpression'; +import { StepperUnaryExpression } from 'js-slang/dist/tracer/nodes/Expression/UnaryExpression'; +import { StepperProgram } from 'js-slang/dist/tracer/nodes/Program'; +import { StepperBlockStatement } from 'js-slang/dist/tracer/nodes/Statement/BlockStatement'; +import { StepperExpressionStatement } from 'js-slang/dist/tracer/nodes/Statement/ExpressionStatement'; +import { StepperFunctionDeclaration } from 'js-slang/dist/tracer/nodes/Statement/FunctionDeclaration'; +import { StepperIfStatement } from 'js-slang/dist/tracer/nodes/Statement/IfStatement'; +import { StepperReturnStatement } from 'js-slang/dist/tracer/nodes/Statement/ReturnStatement'; +import { + StepperVariableDeclaration, + StepperVariableDeclarator +} from 'js-slang/dist/tracer/nodes/Statement/VariableDeclaration'; +import { astToString } from 'js-slang/dist/utils/ast/astToString'; import React, { useCallback, useEffect, useState } from 'react'; -import AceEditor from 'react-ace'; import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; @@ -57,22 +88,21 @@ const SubstCodeDisplay = (props: { content: string }) => { ); }; -type SubstVisualizerProps = { +type SubstVisualizerPropsAST = { content: IStepperPropContents[]; workspaceLocation: SideContentLocation; }; -const SideContentSubstVisualizer: React.FC = props => { +const SideContentSubstVisualizer: React.FC = props => { + console.log(props); const [stepValue, setStepValue] = useState(1); const lastStepValue = props.content.length; - const hasRunCode = lastStepValue !== 0; // 'content' property is initialised to '[]' by Playground component - + const hasRunCode = lastStepValue !== 0; const dispatch = useDispatch(); const alertSideContent = useCallback( () => dispatch(beginAlertSideContent(SideContentType.substVisualizer, props.workspaceLocation)), [props.workspaceLocation, dispatch] ); - // set source mode as 2 useEffect(() => { HighlightRulesSelector(2); @@ -87,49 +117,6 @@ const SideContentSubstVisualizer: React.FC = props => { } }, [props.content, setStepValue, alertSideContent]); - // Stepper function call helpers - const getPreviousFunctionCall = useCallback( - (value: number) => { - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = props.content[contIndex]?.function; - if (currentFunction === undefined) { - return null; - } - for (let i = contIndex - 1; i > -1; i--) { - const previousFunction = props.content[i].function; - if (previousFunction !== undefined && currentFunction === previousFunction) { - return i + 1; - } - } - return null; - }, - [lastStepValue, props.content] - ); - - const getNextFunctionCall = useCallback( - (value: number) => { - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = props.content[contIndex]?.function; - if (currentFunction === undefined) { - return null; - } - for (let i = contIndex + 1; i < props.content.length; i++) { - const nextFunction = props.content[i].function; - if (nextFunction !== undefined && currentFunction === nextFunction) { - return i + 1; - } - } - return null; - }, - [lastStepValue, props.content] - ); - - // Stepper handlers - const hasPreviousFunctionCall = getPreviousFunctionCall(stepValue) !== null; - const hasNextFunctionCall = getNextFunctionCall(stepValue) !== null; - const stepPreviousFunctionCall = () => - setStepValue(getPreviousFunctionCall(stepValue) ?? stepValue); - const stepNextFunctionCall = () => setStepValue(getNextFunctionCall(stepValue) ?? stepValue); const stepFirst = () => setStepValue(1); const stepLast = () => setStepValue(lastStepValue); const stepPrevious = () => setStepValue(Math.max(1, stepValue - 1)); @@ -151,66 +138,24 @@ const SideContentSubstVisualizer: React.FC = props => { ]; const hotkeyHandler = getHotkeyHandler(hotkeyBindings); - // Rendering helpers - const getText = useCallback( - (value: number) => { + const getExplanation = useCallback( + (value: number): string => { const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex; - const split = pathified.code.split('@redex'); - if (split.length > 1) { - let text = split[0]; - for (let i = 1; i < split.length; i++) { - text = text + redex + split[i]; - } - return text; + // Right now, prioritize the first marker + const markers = props.content[contIndex].markers; + if (markers === undefined || markers[0] === undefined) { + return '...'; } else { - return redexed; + return markers[0].explanation ?? '...'; } }, [lastStepValue, props.content] ); - const getDiffMarkers = useCallback( - (value: number) => { + const getAST = useCallback( + (value: number): IStepperPropContents => { const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex.split('\n'); - - const diffMarkers = [] as any[]; - if (redex.length > 0) { - const mainprog = redexed.split('@redex'); - let text = mainprog[0]; - let front = text.split('\n'); - - let startR = front.length - 1; - let startC = front[startR].length; - - for (let i = 0; i < mainprog.length - 1; i++) { - const endR = startR + redex.length - 1; - const endC = - redex.length === 1 - ? startC + redex[redex.length - 1].length - : redex[redex.length - 1].length; - - diffMarkers.push({ - startRow: startR, - startCol: startC, - endRow: endR, - endCol: endC, - className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', - type: 'background' - }); - - text = text + redex + mainprog[i + 1]; - front = text.split('\n'); - startR = front.length - 1; - startC = front[startR].length; - } - } - return diffMarkers; + return props.content[contIndex]; }, [lastStepValue, props.content] ); @@ -230,11 +175,7 @@ const SideContentSubstVisualizer: React.FC = props => { />
-
{' '}
- {hasRunCode ? ( - - ) : ( - - )} - {hasRunCode ? ( - - ) : null} + {hasRunCode ? : } + {hasRunCode ? : null} ); }; +/* + Custom AST renderer for Stepper (Inspired by astring library) + This custom AST renderer utilizing the recursive approach of handling rendering of various StepperNodes by + using nested
and . Unlike React-ace, using our own renderer make our stepper more customizable. For example, + we can add a code component that is hoverable by using with blueprint tooltip. +*/ + +/** RenderContext holds relevant information to handle rendering. This will be carried along the recursive renderNode function + * @params parentNode and isRight are used to dictate whether this node requires parenthesis or not + * @params styleWrapper composes the necessary styles being passed. + */ +interface RenderContext { + parentNode?: StepperBaseNode; + isRight?: boolean; // specified for binary expression + styleWrapper: StyleWrapper; +} + +/* + StyleWrapper is a function that returns a styling function based on the node. For example, + const wrapLiteral: StyleWrapper = (node) + => (preformatted) => node.type === "Literal" ?
preformatted
: preformatted; + makes the default result from renderNode(node) wrapped with className stepper-literal for literal AST. +*/ +type StyleWrapper = (node: StepperBaseNode) => (preformatted: React.ReactNode) => React.ReactNode; + +// composeStyleWrapper takes two style wrappers and merge its effect together. +function composeStyleWrapper( + first: StyleWrapper | undefined, + second: StyleWrapper | undefined +): StyleWrapper | undefined { + return first === undefined && second === undefined + ? undefined + : first === undefined + ? second + : second === undefined + ? first + : (node: StepperBaseNode) => (preformatted: React.ReactNode) => { + const afterFirstStyle = first(node)(preformatted); + return second(node)(afterFirstStyle); + }; +} + +/** + * renderNode renders Stepper AST to React ReactNode + * @param currentNode + * @param renderContext + */ +function renderNode(currentNode: StepperBaseNode, renderContext: RenderContext): React.ReactNode { + const styleWrapper = renderContext.styleWrapper; + + const renderers = { + Literal(node: StepperLiteral) { + return ( + {node.raw ? node.raw : JSON.stringify(node.value)} + ); + }, + Identifier(node: StepperIdentifier) { + return {node.name}; + }, + // Expressions + UnaryExpression(node: StepperUnaryExpression) { + return ( + + {`${node.operator}`} + {renderNode(node.argument, { parentNode: node, styleWrapper: styleWrapper })} + + ); + }, + BinaryExpression(node: StepperBinaryExpression) { + return ( + + {renderNode(node.left, { parentNode: node, isRight: false, styleWrapper: styleWrapper })} + {` ${node.operator} `} + {renderNode(node.right, { parentNode: node, isRight: true, styleWrapper: styleWrapper })} + + ); + }, + LogicalExpression(node: StepperLogicalExpression) { + return ( + + {renderNode(node.left, { parentNode: node, isRight: false, styleWrapper: styleWrapper })} + {` ${node.operator} `} + {renderNode(node.right, { parentNode: node, isRight: true, styleWrapper: styleWrapper })} + + ); + }, + ConditionalExpression(node: StepperConditionalExpression) { + return ( + + {renderNode(node.test, { styleWrapper: styleWrapper })} + {` ? `} + {renderNode(node.consequent, { styleWrapper: styleWrapper })} + {` : `} + {renderNode(node.alternate, { styleWrapper: styleWrapper })} + + ); + }, + ArrayExpression(node: StepperArrayExpression) { + // Render all arguments inside an array + const args: React.ReactNode[] = node.elements + .filter(arg => arg !== null) + .map(arg => renderNode(arg, { styleWrapper: styleWrapper })); + + const renderedArguments = args.slice(1).reduce( + (result, item) => ( + + {result} + {', '} + {item} + + ), + args[0] + ); + return ( + + {'['} + {renderedArguments} + {']'} + + ); + }, + ArrowFunctionExpression(node: StepperArrowFunctionExpression) { + /** + * Add hovering effect to children nodes only if it is an identifier with the name + * corresponding to the name of lambda expression + */ + function muTermStyleWrapper(targetNode: StepperBaseNode) { + if ( + targetNode.type === 'Identifier' && + (targetNode as StepperIdentifier).name === node.name + ) { + function addHovering(preprocessed: React.ReactNode): React.ReactNode { + const functionDefinition = astToString(node); + return ( + + + + {' Function definition'} +
+                        {functionDefinition}
+                      
+
+ } + > + {preprocessed} + + + ); + } + return addHovering; + } else { + // Do nothing + return (preprocessed: React.ReactNode) => preprocessed; + } + } + + // If the name is specified, render the name and add hovering for the body. + return node.name ? ( + + + + {' Function definition'} +
+                  {astToString(node)}
+                
+ + } + > + {node.name} +
+
+ ) : ( + + {renderFunctionArguments(node.params)} + {' => '} + {renderNode(node.body, { + styleWrapper: composeStyleWrapper(styleWrapper, muTermStyleWrapper)! + })} + + ); + }, + CallExpression(node: StepperFunctionApplication) { + let renderedCallee = renderNode(node.callee, { styleWrapper: styleWrapper }); + if (node.callee.type === 'ArrowFunctionExpression' && node.callee.name === undefined) { + renderedCallee = ( + + {'('} + {renderedCallee} + {')'} + + ); + } + return ( + + {renderedCallee} + {renderArguments(node.arguments)} + + ); + }, + Program(node: StepperProgram) { + return ( + + {node.body.map(ast => ( +
{renderNode(ast, { styleWrapper: styleWrapper })}
+ ))} +
+ ); + }, + IfStatement(node: StepperIfStatement) { + return ( + + + {'if '} + {'('} + {renderNode(node.test, { styleWrapper: styleWrapper })} + {') '} + + {renderNode(node.consequent, { styleWrapper: styleWrapper })} + {node.alternate && ( + + {' else '} + {renderNode(node.alternate!, { styleWrapper: styleWrapper })} + + )} + + ); + }, + ReturnStatement(node: StepperReturnStatement) { + return ( + + {'return '} + {node.argument && renderNode(node.argument, { styleWrapper: styleWrapper })} + {';'} + + ); + }, + BlockStatement(node: StepperBlockStatement) { + return ( + + {'{'} +
+ {node.body.map(ast => ( + + {renderNode(ast, { styleWrapper: styleWrapper })} +
+
+ ))} + {'}'} +
+ ); + }, + ExpressionStatement(node: StepperExpressionStatement) { + return ( + + {renderNode(node.expression, { styleWrapper: styleWrapper })} + {';'} + + ); + }, + FunctionDeclaration(node: StepperFunctionDeclaration) { + return ( + + {`function ${node.id.name}`} + {renderArguments(node.params)} + {renderNode(node.body, { styleWrapper: styleWrapper })} + + ); + }, + VariableDeclaration(node: StepperVariableDeclaration) { + return ( + + {node.kind} + {node.declarations.map((ast, idx) => ( + + {idx !== 0 && ', '} + {renderNode(ast, { styleWrapper: styleWrapper })} + + ))} + {';'} + + ); + }, + VariableDeclarator(node: StepperVariableDeclarator) { + return ( + + {renderNode(node.id, { styleWrapper: styleWrapper })} + {' = '} + {node.init ? renderNode(node.init, { styleWrapper: styleWrapper }) : 'undefined'} + + ); + } + }; + + // Additional renderers + const renderFunctionArguments = (nodes: StepperExpression[]) => { + const args: React.ReactNode[] = nodes.map(arg => + renderNode(arg, { styleWrapper: styleWrapper }) + ); + let renderedArguments = args.slice(1).reduce( + (result, item) => ( + + {result} + {', '} + {item} + + ), + args[0] + ); + if (args.length !== 1) { + renderedArguments = ( + + {'('} + {renderedArguments} + {')'} + + ); + } + return renderedArguments; + }; + + const renderArguments = (nodes: StepperExpression[]) => { + const args: React.ReactNode[] = nodes.map(arg => + renderNode(arg, { styleWrapper: styleWrapper }) + ); + let renderedArguments = args.slice(1).reduce( + (result, item) => ( + + {result} + {', '} + {item} + + ), + args[0] + ); + renderedArguments = ( + + {'('} + {renderedArguments} + {')'} + + ); + return renderedArguments; + }; + + // Entry point of rendering + const renderer = renderers[currentNode.type as keyof typeof renderers]; + const isParenthesis = expressionNeedsParenthesis( + currentNode, + renderContext.parentNode, + renderContext.isRight + ); + let result: React.ReactNode = renderer + ? // @ts-expect-error All subclasses of stepper base node has its corresponding renderes + renderer(currentNode) + : `<${currentNode.type}>`; // For debugging in case some AST renderer has not been implemented yet + if (isParenthesis) { + result = ( + + {'('} + {result} + {')'} + + ); + } + // custom wrapper style + if (styleWrapper) { + result = styleWrapper(currentNode)(result); + } + return result; +} +/////////////////////////////////// Custom AST Renderer for Stepper ////////////////////////////////// + +/** + * A React component that handles rendering + */ +function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { + const getDisplayedNode = useCallback((): React.ReactNode => { + function markerStyleWrapper(node: StepperBaseNode) { + return (renderNode: React.ReactNode) => { + if (props.markers === undefined) { + return renderNode; + } + let returnNode = {renderNode}; + props.markers.forEach(marker => { + if (marker.redex === node) { + returnNode = {returnNode}; + } + }); + return returnNode; + }; + } + return renderNode(props.ast, { + styleWrapper: markerStyleWrapper + }); + }, [props]); + return
{getDisplayedNode()}
; +} + +/** + * expressionNeedsParenthesis + * checking whether the there should be parentheses wrapped around the node or not + */ +function expressionNeedsParenthesis( + node: StepperBaseNode, + parentNode?: StepperBaseNode, + isRightHand?: boolean +) { + if (parentNode === undefined) { + return false; + } + + const nodePrecedence = EXPRESSIONS_PRECEDENCE[node.type as keyof typeof EXPRESSIONS_PRECEDENCE]; + if (nodePrecedence === NEEDS_PARENTHESES) { + return true; + } + const parentNodePrecedence = + EXPRESSIONS_PRECEDENCE[parentNode.type as keyof typeof EXPRESSIONS_PRECEDENCE]; + if (nodePrecedence === undefined || parentNodePrecedence === undefined) { + return false; + } + + if (nodePrecedence !== parentNodePrecedence) { + return ( + (!isRightHand && nodePrecedence === 15 && parentNodePrecedence === 14) || + nodePrecedence < parentNodePrecedence + ); + } + + if (!('operator' in node) || !('operator' in parentNode)) { + return false; + } + + if (nodePrecedence !== 13 && nodePrecedence !== 14) { + // Not a `LogicalExpression` or `BinaryExpression` + return false; + } + if (node.operator === '**' && parentNode.operator === '**') { + // Exponentiation operator has right-to-left associativity + return !isRightHand; + } + if ( + nodePrecedence === 13 && + parentNodePrecedence === 13 && + (node.operator === '??' || parentNode.operator === '??') + ) { + return true; + } + + const nodeOperatorPrecedence = + OPERATOR_PRECEDENCE[node.operator as keyof typeof OPERATOR_PRECEDENCE]; + const parentNodeOperatorPrecedence = + OPERATOR_PRECEDENCE[parentNode.operator as keyof typeof OPERATOR_PRECEDENCE]; + return isRightHand + ? nodeOperatorPrecedence <= parentNodeOperatorPrecedence + : nodeOperatorPrecedence <= parentNodeOperatorPrecedence; +} +const OPERATOR_PRECEDENCE = { + '||': 2, + '??': 3, + '&&': 4, + '|': 5, + '^': 6, + '&': 7, + '==': 8, + '!=': 8, + '===': 8, + '!==': 8, + '<': 9, + '>': 9, + '<=': 9, + '>=': 9, + in: 9, + instanceof: 9, + '<<': 10, + '>>': 10, + '>>>': 10, + '+': 11, + '-': 11, + '*': 12, + '%': 12, + '/': 12, + '**': 13 +}; +const NEEDS_PARENTHESES = 17; +const EXPRESSIONS_PRECEDENCE = { + // Definitions + ArrayExpression: 20, + TaggedTemplateExpression: 20, + ThisExpression: 20, + Identifier: 20, + PrivateIdentifier: 20, + Literal: 18, + TemplateLiteral: 20, + Super: 20, + SequenceExpression: 20, + // Operations + MemberExpression: 19, + ChainExpression: 19, + CallExpression: 19, + NewExpression: 19, + // Other definitions + ArrowFunctionExpression: NEEDS_PARENTHESES, + ClassExpression: NEEDS_PARENTHESES, + FunctionExpression: NEEDS_PARENTHESES, + ObjectExpression: NEEDS_PARENTHESES, + // Other operations + UpdateExpression: 16, + UnaryExpression: 15, + AwaitExpression: 15, + BinaryExpression: 14, + LogicalExpression: 13, + ConditionalExpression: 4, + AssignmentExpression: 3, + YieldExpression: 2, + RestElement: 1 +}; + export default SideContentSubstVisualizer; diff --git a/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx new file mode 100644 index 0000000000..71378f488e --- /dev/null +++ b/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx @@ -0,0 +1,291 @@ +import 'js-slang/dist/editors/ace/theme/source'; + +import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; +import classNames from 'classnames'; +import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; +import { IStepperPropContents } from 'js-slang/dist/stepper/stepper'; +import React, { useCallback, useEffect, useState } from 'react'; +import AceEditor from 'react-ace'; +import { useDispatch } from 'react-redux'; + +import { beginAlertSideContent } from '../SideContentActions'; +import { SideContentLocation, SideContentType } from '../SideContentTypes'; + +const SubstDefaultText = () => { + return ( +
+
+ Welcome to the Stepper! +
+
+ On this tab, the REPL will be hidden from view, so do check that your code has no errors + before running the stepper. You may use this tool by writing your program on the left, then + dragging the slider above to see its evaluation. +
+
+ On even-numbered steps, the part of the program that will be evaluated next is highlighted + in yellow. On odd-numbered steps, the result of the evaluation is highlighted in green. You + can change the maximum steps limit (500-5000, default 1000) in the control bar. +
+
+ + Some useful keyboard shortcuts: +
+
+ a: Move to the first step +
+ e: Move to the last step +
+ f: Move to the next step +
+ b: Move to the previous step +
+
+ Note that these shortcuts are only active when the browser focus is on this tab (click on or + above the explanation text). +
+
+ ); +}; + +const SubstCodeDisplay = (props: { content: string }) => { + return ( + +
{props.content}
+
+ ); +}; + +type SubstVisualizerProps = { + content: IStepperPropContents[]; + workspaceLocation: SideContentLocation; +}; + +const SideContentSubstVisualizer: React.FC = props => { + const [stepValue, setStepValue] = useState(1); + const lastStepValue = props.content.length; + const hasRunCode = lastStepValue !== 0; // 'content' property is initialised to '[]' by Playground component + + const dispatch = useDispatch(); + const alertSideContent = useCallback( + () => dispatch(beginAlertSideContent(SideContentType.substVisualizer, props.workspaceLocation)), + [props.workspaceLocation, dispatch] + ); + + // set source mode as 2 + useEffect(() => { + HighlightRulesSelector(2); + ModeSelector(2); + }, []); + + // reset stepValue when content changes + useEffect(() => { + setStepValue(1); + if (props.content.length > 0) { + alertSideContent(); + } + }, [props.content, setStepValue, alertSideContent]); + + // Stepper function call helpers + const getPreviousFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; + } + for (let i = contIndex - 1; i > -1; i--) { + const previousFunction = props.content[i].function; + if (previousFunction !== undefined && currentFunction === previousFunction) { + return i + 1; + } + } + return null; + }, + [lastStepValue, props.content] + ); + + const getNextFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; + } + for (let i = contIndex + 1; i < props.content.length; i++) { + const nextFunction = props.content[i].function; + if (nextFunction !== undefined && currentFunction === nextFunction) { + return i + 1; + } + } + return null; + }, + [lastStepValue, props.content] + ); + + // Stepper handlers + const hasPreviousFunctionCall = getPreviousFunctionCall(stepValue) !== null; + const hasNextFunctionCall = getNextFunctionCall(stepValue) !== null; + const stepPreviousFunctionCall = () => + setStepValue(getPreviousFunctionCall(stepValue) ?? stepValue); + const stepNextFunctionCall = () => setStepValue(getNextFunctionCall(stepValue) ?? stepValue); + const stepFirst = () => setStepValue(1); + const stepLast = () => setStepValue(lastStepValue); + const stepPrevious = () => setStepValue(Math.max(1, stepValue - 1)); + const stepNext = () => setStepValue(Math.min(props.content.length, stepValue + 1)); + + // Setup hotkey bindings + const hotkeyBindings: HotkeyItem[] = hasRunCode + ? [ + ['a', stepFirst], + ['f', stepNext], + ['b', stepPrevious], + ['e', stepLast] + ] + : [ + ['a', () => {}], + ['f', () => {}], + ['b', () => {}], + ['e', () => {}] + ]; + const hotkeyHandler = getHotkeyHandler(hotkeyBindings); + + // Rendering helpers + const getText = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex; + const split = pathified.code.split('@redex'); + if (split.length > 1) { + let text = split[0]; + for (let i = 1; i < split.length; i++) { + text = text + redex + split[i]; + } + return text; + } else { + return redexed; + } + }, + [lastStepValue, props.content] + ); + + const getDiffMarkers = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex.split('\n'); + + const diffMarkers = [] as any[]; + if (redex.length > 0) { + const mainprog = redexed.split('@redex'); + let text = mainprog[0]; + let front = text.split('\n'); + + let startR = front.length - 1; + let startC = front[startR].length; + + for (let i = 0; i < mainprog.length - 1; i++) { + const endR = startR + redex.length - 1; + const endC = + redex.length === 1 + ? startC + redex[redex.length - 1].length + : redex[redex.length - 1].length; + + diffMarkers.push({ + startRow: startR, + startCol: startC, + endRow: endR, + endCol: endC, + className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', + type: 'background' + }); + + text = text + redex + mainprog[i + 1]; + front = text.split('\n'); + startR = front.length - 1; + startC = front[startR].length; + } + } + return diffMarkers; + }, + [lastStepValue, props.content] + ); + + return ( +
+ +
+ +
{' '} +
+ {hasRunCode ? ( + + ) : ( + + )} + {hasRunCode ? ( + + ) : null} +
+ ); +}; + +export default SideContentSubstVisualizer; diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx index 6a726b4035..95aac5c5da 100644 --- a/src/pages/playground/PlaygroundTabs.tsx +++ b/src/pages/playground/PlaygroundTabs.tsx @@ -1,5 +1,4 @@ import { IconNames } from '@blueprintjs/icons'; -import { isStepperOutput } from 'js-slang/dist/stepper/stepper'; import { InterpreterOutput } from 'src/commons/application/ApplicationTypes'; import Markdown from 'src/commons/Markdown'; import SideContentRemoteExecution from 'src/commons/sideContent/content/remoteExecution/SideContentRemoteExecution'; @@ -49,8 +48,7 @@ export const makeSubstVisualizerTabFrom = ( editorOutput && editorOutput.type === 'result' && editorOutput.value instanceof Array && - editorOutput.value[0] === Object(editorOutput.value[0]) && - isStepperOutput(editorOutput.value[0]) + editorOutput.value[0] === Object(editorOutput.value[0]) ) { return editorOutput.value; } else { diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 63af80872f..df4cbddc40 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -418,18 +418,66 @@ $code-color-notification: #f9f0d7; margin: 15px; height: unset; + .stepper-display { + font: + 16px / normal 'Inconsolata', + 'Consolas', + monospace; + } + + .stepper-literal { + color: #ff6078; + } + + .stepper-operator { + color: #f89210; + } + + .stepper-identifier { + color: #f8d871; + } + + .stepper-mu-term { + pointer-events: auto; + cursor: pointer; + z-index: 20; + padding: 0px 3px; + border-radius: 5px; + background: rgba(255, 83, 83, 0.2); + } + + .stepper-mu-term:hover { + background: rgba(255, 83, 83, 0.5); + } + .beforeMarker { + position: relative; + -webkit-box-decoration-break: slice; + box-decoration-break: slice; background: rgba(179, 101, 57, 0.75); - position: absolute; + pointer-events: auto; + cursor: pointer; z-index: 20; } + .beforeMarker:hover { + background: rgba(100, 101, 57, 0.75); + } + .afterMarker { + position: relative; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; background: green; - position: absolute; + pointer-events: auto; + cursor: pointer; z-index: 20; } + .afterMarker:hover { + background: rgba(61, 101, 57, 0.75); + } + .#{$ns}-slider-label { width: -webkit-max-content; width: -moz-max-content;