Skip to content

Commit 156a2d3

Browse files
LeowWBmartin-henz
authored andcommitted
Substitution model visualizer (#803)
* Add SUBST button Modify class definitions, instantiations to incorporate SUBST button Remove SUBST button from controlbar Create SubstTimeline and SubstVisualizer Add a SUBST tab to the sidebar Fix problem with range input not sliding in SUBST visualizer tab 22 march 2019 Remove compile error; confirm can generate code from tree and display Add functionality to handle slider change Add SUBST visualizer functionality to maximum extent permissable by current state of SUBST substitutor Remove excess code and comments left over from testing Add functionality for SUBST to return the actual correct output in the repl output display Remove redundant code Add code to make SUBST slider default to max value Change SUBST display to automatically update to match slider default value Add buttons to SUBST timeline to allow for fine-grained control over stepping through substitution Add checks to disable SUBST timeline control buttons when appropriate "When appropriate" -> for example, all buttons are disabled when the program has not yet been run, and hence there is nothing to step through; button to go to previous step is disabled when already at first step. Add code such that SUBST interpreter is only used when the SUBST tab of the sidebar is open Add keyboard controls to SUBST timeline buttons Add code to transfer default focus to SUBST slider after program evaluation Override default behavior of HTML input slider for SUBST Rationale: default HTML input slider binds up and down to increase and decrease, respectively. This runs counter to the intuition provided by course textbook. Make SUBST slider inline Make SUBST slider inline (via sass) * Fix Playground errors * Shift useSubst variable location * Rewrite components to make use of new substituter interface w js slang * Only use subst interpreter when the subst tab is open * Run format * Fix linting * Fix final answer displayed by subst * Fix Redux Saga tests * Refactor of Substituter component * SubstVisualizer and SubstTimeline merged into one component * Tooltip changed to 'Substituter' * Use Blueprint Slider instead of HTML ones * REPL now checks Redux state to evaluate results for Subst * New actions set up to update Redux state * Minor CSS style changes * Changed use of 'bp3' literals to constants * Updated tests * Formatting * Fix for controlBar reference * Hide REPL from Substituter * CSS changes for code display style * Updated REPL snapshot * Style and interface improvements * Implemented use of breakpoints to activate SUBST * SUBST now only enabled for Chapter 1 and 2 * Hide REPL, disable resizing on SUBST tab * Minor style changes for SUBST tab, slider * Fix bug where SUBST would be active outside Playground * Implemented blinking style for SUBST tab * Updated snapshots * Revert package-lock.json * Set SUBST tab icon to 'Flow: Review' * CSS style fixes * Fix for code text being justified in SUBST * SUBST tab now scrolls properly on Chrome * Added vendor prefixes for SUBST CSS * Change occurences of 'code' => 'your program' * Updated snapshots * Formatting for SCSS * Merge branch 'master' of https://github.com/source-academy/cadet-frontend into leow-subst * Fix for crash when REPL processes an array output from js-slang * Updated tests * Changes to the way subst slider responds to arrow key input * Modified SUBST hotkeys * SUBST slider can now be moved with < and > keys * Use of SUBST now clears the REPL to prevent display of raw data * Arrow keys no longer used as hotkeys to prevent conflict * Update js-slang version * Formatting * Updated tests
1 parent 6ffaca1 commit 156a2d3

File tree

27 files changed

+503
-84
lines changed

27 files changed

+503
-84
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"draft-js": "^0.10.5",
4545
"flexboxgrid": "^6.3.1",
4646
"flexboxgrid-helpers": "^1.1.3",
47-
"js-slang": "^0.2.6",
47+
"js-slang": "^0.3.0",
4848
"jwt-decode": "^2.2.0",
4949
"lodash": "^4.17.13",
5050
"lz-string": "^1.4.4",

src/actions/__tests__/playground.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as actionTypes from '../actionTypes';
2-
import { changeQueryString, generateLzString } from '../playground';
2+
import { changeQueryString, generateLzString, toggleUsingSubst } from '../playground';
33

44
test('generateLzString generates correct action object', () => {
55
const action = generateLzString();
@@ -16,3 +16,11 @@ test('changeQueryString generates correct action object', () => {
1616
payload: queryString
1717
});
1818
});
19+
20+
test('toggleUsingSubst generates correct action object', () => {
21+
const action = toggleUsingSubst(true);
22+
expect(action).toEqual({
23+
type: actionTypes.TOGGLE_USING_SUBST,
24+
payload: true
25+
});
26+
});

src/actions/actionTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const LOG_OUT = 'LOG_OUT';
77
/** Playground */
88
export const CHANGE_QUERY_STRING = 'CHANGE_QUERY_STRING';
99
export const GENERATE_LZ_STRING = 'GENERATE_LZ_STRING';
10+
export const TOGGLE_USING_SUBST = 'TOGGLE_USING_SUBST';
1011

1112
/** Interpreter */
1213
export const BEGIN_INTERRUPT_EXECUTION = 'BEGIN_INTERRUPT_EXECUTION';

src/actions/playground.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ import * as actionTypes from './actionTypes';
44

55
export const generateLzString = () => action(actionTypes.GENERATE_LZ_STRING);
66

7+
export const toggleUsingSubst = (usingSubst: boolean) =>
8+
action(actionTypes.TOGGLE_USING_SUBST, usingSubst);
9+
710
export const changeQueryString = (queryString: string) =>
811
action(actionTypes.CHANGE_QUERY_STRING, queryString);

src/components/Playground.tsx

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { SideContentTab } from './workspace/side-content';
2424
import EnvVisualizer from './workspace/side-content/EnvVisualizer';
2525
import Inspector from './workspace/side-content/Inspector';
2626
import ListVisualizer from './workspace/side-content/ListVisualizer';
27+
import SubstVisualizer from './workspace/side-content/SubstVisualizer';
2728
import VideoDisplay from './workspace/side-content/VideoDisplay';
2829

2930
const CHAP = '\xa7';
@@ -68,6 +69,7 @@ export interface IStateProps {
6869
sourceChapter: number;
6970
websocketStatus: number;
7071
externalLibraryName: string;
72+
usingSubst: boolean;
7173
}
7274

7375
export interface IDispatchProps {
@@ -93,6 +95,7 @@ export interface IDispatchProps {
9395
handleSetEditorSessionId: (editorSessionId: string) => void;
9496
handleSetWebsocketStatus: (websocketStatus: number) => void;
9597
handleSideContentHeightChange: (heightChange: number) => void;
98+
handleUsingSubst: (usingSubst: boolean) => void;
9699
handleDebuggerPause: () => void;
97100
handleDebuggerResume: () => void;
98101
handleDebuggerReset: () => void;
@@ -101,20 +104,33 @@ export interface IDispatchProps {
101104

102105
type PlaygroundState = {
103106
isGreen: boolean;
107+
selectedTab: SideContentType;
108+
hasBreakpoints: boolean;
104109
};
105110

106111
class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
107112
private keyMap = { goGreen: 'h u l k' };
108-
109113
private handlers = { goGreen: () => {} };
110114

111115
constructor(props: IPlaygroundProps) {
112116
super(props);
113-
this.state = { isGreen: false };
117+
this.state = {
118+
isGreen: false,
119+
selectedTab: SideContentType.introduction,
120+
hasBreakpoints: false
121+
};
114122
this.handlers.goGreen = this.toggleIsGreen.bind(this);
123+
(window as any).thePlayground = this;
115124
}
116125

117126
public render() {
127+
const substVisualizerTab: SideContentTab = {
128+
label: 'Substituter',
129+
iconName: IconNames.FLOW_REVIEW,
130+
body: <SubstVisualizer content={this.processArrayOutput(this.props.output)} />,
131+
id: SideContentType.substVisualizer
132+
};
133+
118134
const autorunButtons = (
119135
<AutorunButtons
120136
handleDebuggerPause={this.props.handleDebuggerPause}
@@ -130,8 +146,19 @@ class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
130146
/>
131147
);
132148

133-
const chapterSelectHandler = ({ chapter }: { chapter: number }, e: any) =>
149+
const chapterSelectHandler = ({ chapter }: { chapter: number }, e: any) => {
150+
if (
151+
(chapter <= 2 && this.state.hasBreakpoints) ||
152+
this.state.selectedTab === SideContentType.substVisualizer
153+
) {
154+
this.props.handleUsingSubst(true);
155+
}
156+
if (chapter > 2) {
157+
this.props.handleReplOutputClear();
158+
this.props.handleUsingSubst(false);
159+
}
134160
this.props.handleChapterSelect(chapter);
161+
};
135162
const chapterSelect = (
136163
<ChapterSelect
137164
handleChapterSelect={chapterSelectHandler}
@@ -140,17 +167,19 @@ class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
140167
/>
141168
);
142169

143-
const clearButton = (
144-
<ClearButton handleReplOutputClear={this.props.handleReplOutputClear} key="clear_repl" />
145-
);
170+
const clearButton =
171+
this.state.selectedTab === SideContentType.substVisualizer ? null : (
172+
<ClearButton handleReplOutputClear={this.props.handleReplOutputClear} key="clear_repl" />
173+
);
146174

147-
const evalButton = (
148-
<EvalButton
149-
handleReplEval={this.props.handleReplEval}
150-
isRunning={this.props.isRunning}
151-
key="eval_repl"
152-
/>
153-
);
175+
const evalButton =
176+
this.state.selectedTab === SideContentType.substVisualizer ? null : (
177+
<EvalButton
178+
handleReplEval={this.props.handleReplEval}
179+
isRunning={this.props.isRunning}
180+
key="eval_repl"
181+
/>
182+
);
154183

155184
const changeExecutionTimeHandler = (execTime: number) =>
156185
this.props.handleChangeExecTime(execTime);
@@ -212,6 +241,10 @@ class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
212241
tabs.push(envVisualizerTab);
213242
}
214243

244+
if (this.props.sourceChapter <= 2) {
245+
tabs.push(substVisualizerTab);
246+
}
247+
215248
const workspaceProps: WorkspaceProps = {
216249
controlBarProps: {
217250
editorButtons: [
@@ -235,7 +268,36 @@ class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
235268
isEditorAutorun: this.props.isEditorAutorun,
236269
breakpoints: this.props.breakpoints,
237270
highlightedLines: this.props.highlightedLines,
238-
handleEditorUpdateBreakpoints: this.props.handleEditorUpdateBreakpoints,
271+
handleEditorUpdateBreakpoints: (breakpoints: string[]) => {
272+
// get rid of holes in array
273+
const numberOfBreakpoints = breakpoints.filter(arrayItem => !!arrayItem).length;
274+
if (numberOfBreakpoints > 0) {
275+
this.setState({
276+
...this.state,
277+
hasBreakpoints: true
278+
});
279+
if (this.props.sourceChapter <= 2) {
280+
/**
281+
* There are breakpoints set on Source Chapter 2, so we set the
282+
* Redux state for the editor to evaluate to the substituter
283+
*/
284+
285+
this.props.handleUsingSubst(true);
286+
}
287+
}
288+
if (numberOfBreakpoints === 0) {
289+
this.setState({
290+
...this.state,
291+
hasBreakpoints: false
292+
});
293+
294+
if (this.state.selectedTab !== SideContentType.substVisualizer) {
295+
this.props.handleReplOutputClear();
296+
this.props.handleUsingSubst(false);
297+
}
298+
}
299+
this.props.handleEditorUpdateBreakpoints(breakpoints);
300+
},
239301
handleSetWebsocketStatus: this.props.handleSetWebsocketStatus
240302
},
241303
editorHeight: this.props.editorHeight,
@@ -249,14 +311,18 @@ class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
249311
handleBrowseHistoryDown: this.props.handleBrowseHistoryDown,
250312
handleBrowseHistoryUp: this.props.handleBrowseHistoryUp,
251313
handleReplEval: this.props.handleReplEval,
252-
handleReplValueChange: this.props.handleReplValueChange
314+
handleReplValueChange: this.props.handleReplValueChange,
315+
hidden: this.state.selectedTab === SideContentType.substVisualizer,
316+
usingSubst: this.props.usingSubst
253317
},
254318
sideContentHeight: this.props.sideContentHeight,
255319
sideContentProps: {
256-
defaultSelectedTabId: SideContentType.introduction,
320+
defaultSelectedTabId: this.state.selectedTab,
257321
handleActiveTabChange: this.props.handleActiveTabChange,
322+
onChange: this.onChangeTabs,
258323
tabs
259-
}
324+
},
325+
sideContentIsResizeable: this.state.selectedTab !== SideContentType.substVisualizer
260326
};
261327

262328
return (
@@ -274,8 +340,44 @@ class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
274340
);
275341
}
276342

343+
private onChangeTabs = (
344+
newTabId: SideContentType,
345+
prevTabId: SideContentType,
346+
event: React.MouseEvent<HTMLElement>
347+
) => {
348+
if (newTabId === prevTabId) {
349+
return;
350+
}
351+
352+
if (this.props.sourceChapter <= 2 && newTabId === SideContentType.substVisualizer) {
353+
this.props.handleUsingSubst(true);
354+
}
355+
356+
if (prevTabId === SideContentType.substVisualizer && !this.state.hasBreakpoints) {
357+
this.props.handleReplOutputClear();
358+
this.props.handleUsingSubst(false);
359+
}
360+
361+
this.setState({
362+
...this.state,
363+
selectedTab: newTabId
364+
});
365+
};
366+
367+
private processArrayOutput = (output: InterpreterOutput[]) => {
368+
const editorOutput = output[0];
369+
if (editorOutput && editorOutput.type === 'result' && editorOutput.value instanceof Array) {
370+
return editorOutput.value;
371+
} else {
372+
return [];
373+
}
374+
};
375+
277376
private toggleIsGreen() {
278-
this.setState({ isGreen: !this.state.isGreen });
377+
this.setState({
378+
...this.state,
379+
isGreen: !this.state.isGreen
380+
});
279381
}
280382
}
281383

src/components/__tests__/Playground.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const baseProps = {
2525
output: [],
2626
replValue: '',
2727
websocketStatus: 0,
28+
usingSubst: false,
2829
handleActiveTabChange: (activeTab: SideContentType) => {},
2930
handleBrowseHistoryDown: () => {},
3031
handleBrowseHistoryUp: () => {},
@@ -48,6 +49,7 @@ const baseProps = {
4849
handleSetWebsocketStatus: (websocketStatus: number) => {},
4950
handleSideContentHeightChange: (h: number) => {},
5051
handleToggleEditorAutorun: () => {},
52+
handleUsingSubst: (usingSubst: boolean) => {},
5153
handleDebuggerPause: () => {},
5254
handleDebuggerResume: () => {},
5355
handleDebuggerReset: () => {}

src/components/__tests__/__snapshots__/Playground.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
exports[`Playground renders correctly 1`] = `
44
"<HotKeys className=\\"Playground bp3-dark\\" keyMap={{...}} handlers={{...}}>
5-
<Workspace controlBarProps={{...}} editorProps={{...}} editorHeight={[undefined]} editorWidth=\\"50%\\" handleEditorHeightChange={[Function: handleEditorHeightChange]} handleEditorWidthChange={[Function: handleEditorWidthChange]} handleSideContentHeightChange={[Function: handleSideContentHeightChange]} replProps={{...}} sideContentHeight={40} sideContentProps={{...}} />
5+
<Workspace controlBarProps={{...}} editorProps={{...}} editorHeight={[undefined]} editorWidth=\\"50%\\" handleEditorHeightChange={[Function: handleEditorHeightChange]} handleEditorWidthChange={[Function: handleEditorWidthChange]} handleSideContentHeightChange={[Function: handleSideContentHeightChange]} replProps={{...}} sideContentHeight={40} sideContentProps={{...}} sideContentIsResizeable={true} />
66
</HotKeys>"
77
`;
88

99
exports[`Playground with link renders correctly 1`] = `
1010
"<HotKeys className=\\"Playground bp3-dark\\" keyMap={{...}} handlers={{...}}>
11-
<Workspace controlBarProps={{...}} editorProps={{...}} editorHeight={[undefined]} editorWidth=\\"50%\\" handleEditorHeightChange={[Function: handleEditorHeightChange]} handleEditorWidthChange={[Function: handleEditorWidthChange]} handleSideContentHeightChange={[Function: handleSideContentHeightChange]} replProps={{...}} sideContentHeight={40} sideContentProps={{...}} />
11+
<Workspace controlBarProps={{...}} editorProps={{...}} editorHeight={[undefined]} editorWidth=\\"50%\\" handleEditorHeightChange={[Function: handleEditorHeightChange]} handleEditorWidthChange={[Function: handleEditorWidthChange]} handleSideContentHeightChange={[Function: handleSideContentHeightChange]} replProps={{...}} sideContentHeight={40} sideContentProps={{...}} sideContentIsResizeable={true} />
1212
</HotKeys>"
1313
`;

src/components/assessment/AssessmentWorkspace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ class AssessmentWorkspace extends React.Component<
195195
>
196196
<div className={Classes.DIALOG_BODY}>
197197
<Markdown content="Are you sure you want to reset the template?" />
198-
<Markdown content="*Note this will not affect the saved copy of your code, unless you save over it.*" />
198+
<Markdown content="*Note this will not affect the saved copy of your program, unless you save over it.*" />
199199
</div>
200200
<div className={Classes.DIALOG_FOOTER}>
201201
<ButtonGroup>

src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ exports[`AssessmentWorkspace page with MCQ question renders correctly 1`] = `
1313
<Blueprint3.Dialog className=\\"assessment-reset\\" icon=\\"error\\" isCloseButtonShown={false} isOpen={false} title=\\"Confirmation: Reset editor?\\" canOutsideClickClose={true}>
1414
<div className=\\"bp3-dialog-body\\">
1515
<Markdown content=\\"Are you sure you want to reset the template?\\" />
16-
<Markdown content=\\"*Note this will not affect the saved copy of your code, unless you save over it.*\\" />
16+
<Markdown content=\\"*Note this will not affect the saved copy of your program, unless you save over it.*\\" />
1717
</div>
1818
<div className=\\"bp3-dialog-footer\\">
1919
<Blueprint3.ButtonGroup>
@@ -41,7 +41,7 @@ exports[`AssessmentWorkspace page with overdue assessment renders correctly 1`]
4141
<Blueprint3.Dialog className=\\"assessment-reset\\" icon=\\"error\\" isCloseButtonShown={false} isOpen={false} title=\\"Confirmation: Reset editor?\\" canOutsideClickClose={true}>
4242
<div className=\\"bp3-dialog-body\\">
4343
<Markdown content=\\"Are you sure you want to reset the template?\\" />
44-
<Markdown content=\\"*Note this will not affect the saved copy of your code, unless you save over it.*\\" />
44+
<Markdown content=\\"*Note this will not affect the saved copy of your program, unless you save over it.*\\" />
4545
</div>
4646
<div className=\\"bp3-dialog-footer\\">
4747
<Blueprint3.ButtonGroup>
@@ -69,7 +69,7 @@ exports[`AssessmentWorkspace page with programming question renders correctly 1`
6969
<Blueprint3.Dialog className=\\"assessment-reset\\" icon=\\"error\\" isCloseButtonShown={false} isOpen={false} title=\\"Confirmation: Reset editor?\\" canOutsideClickClose={true}>
7070
<div className=\\"bp3-dialog-body\\">
7171
<Markdown content=\\"Are you sure you want to reset the template?\\" />
72-
<Markdown content=\\"*Note this will not affect the saved copy of your code, unless you save over it.*\\" />
72+
<Markdown content=\\"*Note this will not affect the saved copy of your program, unless you save over it.*\\" />
7373
</div>
7474
<div className=\\"bp3-dialog-footer\\">
7575
<Blueprint3.ButtonGroup>
@@ -97,7 +97,7 @@ exports[`AssessmentWorkspace renders Grading tab correctly if the question has b
9797
<Blueprint3.Dialog className=\\"assessment-reset\\" icon=\\"error\\" isCloseButtonShown={false} isOpen={false} title=\\"Confirmation: Reset editor?\\" canOutsideClickClose={true}>
9898
<div className=\\"bp3-dialog-body\\">
9999
<Markdown content=\\"Are you sure you want to reset the template?\\" />
100-
<Markdown content=\\"*Note this will not affect the saved copy of your code, unless you save over it.*\\" />
100+
<Markdown content=\\"*Note this will not affect the saved copy of your program, unless you save over it.*\\" />
101101
</div>
102102
<div className=\\"bp3-dialog-footer\\">
103103
<Blueprint3.ButtonGroup>

0 commit comments

Comments
 (0)