Skip to content

Commit 600ba75

Browse files
committed
Merge branch 'sa2122-achievements' of https://github.com/source-academy/frontend into sa2122-achievements
2 parents af6d773 + 71f39de commit 600ba75

File tree

9 files changed

+124
-52
lines changed

9 files changed

+124
-52
lines changed

src/commons/sagas/AchievementSaga.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export default function* AchievementSaga(): SagaIterator {
233233
const eventGoalUuids = goals
234234
.filter((goal: AchievementGoal) => goalIncludesEvents(goal, events))
235235
.map((goal: AchievementGoal) => goal.uuid);
236+
236237
eventGoalUuids.forEach((uuid: string) => {
237238
incrementCount(uuid, inferencer);
238239
updatedGoals.add(uuid);

src/features/envVisualizer/EnvVisualizerLayout.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Context } from 'js-slang';
22
import { Frame } from 'js-slang/dist/types';
3-
import { cloneDeep } from 'lodash';
43
import React from 'react';
54
import { Rect } from 'react-konva';
65
import { Layer, Stage } from 'react-konva';
@@ -14,6 +13,7 @@ import { Value } from './components/values/Value';
1413
import { Config, ShapeDefaultProps } from './EnvVisualizerConfig';
1514
import { Data, EnvTree, EnvTreeNode, ReferenceType } from './EnvVisualizerTypes';
1615
import {
16+
deepCopyTree,
1717
isArray,
1818
isEmptyEnvironment,
1919
isFn,
@@ -49,7 +49,8 @@ export class Layout {
4949
Layout.values.clear();
5050
Layout.levels = [];
5151
Layout.key = 0;
52-
Layout.environmentTree = cloneDeep(context.runtime.environmentTree as EnvTree);
52+
// deep copy so we don't mutate the context
53+
Layout.environmentTree = deepCopyTree(context.runtime.environmentTree as EnvTree);
5354
Layout.globalEnvNode = Layout.environmentTree.root;
5455

5556
// remove program environment and merge bindings into global env
@@ -65,6 +66,7 @@ export class Layout {
6566
Config.CanvasMinHeight,
6667
lastLevel.y + lastLevel.height + Config.CanvasPaddingY
6768
);
69+
6870
Layout.width = Math.max(
6971
Config.CanvasMinWidth,
7072
Layout.levels.reduce<number>((maxWidth, level) => Math.max(maxWidth, level.width), 0) +
@@ -151,19 +153,24 @@ export class Layout {
151153
return [c];
152154
}
153155
};
156+
154157
let frontier: EnvTreeNode[] = [Layout.globalEnvNode];
155158
let prevLevel: Level | null = null;
159+
let currLevel: Level;
160+
156161
while (frontier.length > 0) {
157-
const currLevel: Level = new Level(prevLevel, frontier);
162+
currLevel = new Level(prevLevel, frontier);
158163
this.levels.push(currLevel);
159164
const nextFrontier: EnvTreeNode[] = [];
165+
160166
frontier.forEach(e => {
161167
e.children.forEach(c => {
162168
const nextChildren = getNextChildren(c as EnvTreeNode);
163169
nextChildren.forEach(c => (c.parent = e));
164170
nextFrontier.push(...nextChildren);
165171
});
166172
});
173+
167174
prevLevel = currLevel;
168175
frontier = nextFrontier;
169176
}

src/features/envVisualizer/EnvVisualizerUtils.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
import { Environment } from 'js-slang/dist/types';
12
import { Node } from 'konva/types/Node';
3+
import { cloneDeep } from 'lodash';
24

35
import { Value } from './components/values/Value';
46
import { Config } from './EnvVisualizerConfig';
57
import {
68
Data,
79
EmptyObject,
810
Env,
11+
EnvTree,
12+
EnvTreeNode,
913
FnTypes,
1014
PrimitiveTypes,
1115
ReferenceType
1216
} from './EnvVisualizerTypes';
1317

18+
// TODO: can make use of lodash
1419
/** checks if `x` is an object */
1520
export function isObject(x: any): x is object {
1621
return x === Object(x);
@@ -21,6 +26,21 @@ export function isEmptyObject(object: Object): object is EmptyObject {
2126
return Object.keys(object).length === 0;
2227
}
2328

29+
/** checks if `object` is `Environment` */
30+
export function isEnvironment(object: Object): object is Environment {
31+
return 'head' in object && 'tail' in object && 'name' in object;
32+
}
33+
34+
/** checks if `object` is `EnvTreeNode` */
35+
export function isEnvTreeNode(object: Object): object is EnvTreeNode {
36+
return 'parent' in object && 'children' in object;
37+
}
38+
39+
/** checks if `object` is `EnvTree` */
40+
export function isEnvTree(object: Object): object is EnvTree {
41+
return 'root' in object;
42+
}
43+
2444
/** checks if `env` is empty (that is, head of env is an empty object) */
2545
export function isEmptyEnvironment(env: Env): env is Env & { head: EmptyObject } {
2646
return env === null || isEmptyObject(env.head);
@@ -93,7 +113,11 @@ export function getTextWidth(
93113
): number {
94114
const canvas = document.createElement('canvas');
95115
const context = canvas.getContext('2d');
96-
if (!context) return 0;
116+
117+
if (!context || !text) {
118+
return 0;
119+
}
120+
97121
context.font = font;
98122
const longestLine = text
99123
.split('\n')
@@ -186,3 +210,42 @@ export function getNonEmptyEnv(environment: Env): Env {
186210
return environment;
187211
}
188212
}
213+
214+
/**
215+
* Given any objects, this function will find the underlying `Environment` objects
216+
* and perform copying of property descriptors from source frames to destination frames.
217+
* Property descriptors are important for us to distinguish between constants and variables.
218+
*/
219+
export function copyOwnPropertyDescriptors(source: any, destination: any) {
220+
// TODO: use lodash cloneDeepWith customizer?
221+
if (isFunction(source) || isPrimitiveData(source)) {
222+
return;
223+
}
224+
if (isEnvTree(source) && isEnvTree(destination)) {
225+
copyOwnPropertyDescriptors(source.root, destination.root);
226+
} else if (isEnvTreeNode(source) && isEnvTreeNode(destination)) {
227+
// recurse only on children and environment
228+
copyOwnPropertyDescriptors(source.children, destination.children);
229+
copyOwnPropertyDescriptors(source.environment, destination.environment);
230+
} else if (isArray(source) && isArray(destination)) {
231+
// recurse on array items
232+
source.forEach((item, i) => copyOwnPropertyDescriptors(item, destination[i]));
233+
} else if (isEnvironment(source) && isEnvironment(destination)) {
234+
// copy descriptors from source frame to destination frame
235+
Object.defineProperties(destination.head, Object.getOwnPropertyDescriptors(source.head));
236+
// recurse on tail
237+
copyOwnPropertyDescriptors(source.tail, destination.tail);
238+
}
239+
}
240+
241+
/**
242+
* creates a deep clone of `EnvTree`
243+
*
244+
* TODO: move this function to EnvTree class
245+
* so we can invoke like so: environmentTree.deepCopy()
246+
*/
247+
export function deepCopyTree(value: EnvTree): EnvTree {
248+
const clone = cloneDeep(value);
249+
copyOwnPropertyDescriptors(value, clone);
250+
return clone;
251+
}

src/features/envVisualizer/__tests__/__snapshots__/EnvVisualizer.tsx.snap

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ Array [
136136
},
137137
},
138138
},
139-
"fn: ",
139+
"fn:= ",
140140
[Function],
141-
"x: ",
141+
"x:= ",
142142
Array [
143143
"long string",
144144
Array [
@@ -162,7 +162,7 @@ Array [
162162
5,
163163
6,
164164
],
165-
"y: ",
165+
"y:= ",
166166
Array [
167167
Array [
168168
[Function],
@@ -352,9 +352,9 @@ Array [
352352
},
353353
},
354354
},
355-
"fn: ",
355+
"fn:= ",
356356
[Function],
357-
"x: ",
357+
"x:= ",
358358
Array [
359359
1,
360360
Array [
@@ -366,7 +366,7 @@ Array [
366366
],
367367
4,
368368
],
369-
"l: ",
369+
"l:= ",
370370
Array [
371371
1,
372372
Array [
@@ -530,7 +530,7 @@ Array [
530530
},
531531
},
532532
},
533-
"x1: ",
533+
"x1:= ",
534534
Array [
535535
1,
536536
Array [
@@ -544,7 +544,7 @@ Array [
544544
],
545545
],
546546
],
547-
"x2: ",
547+
"x2:= ",
548548
Array [
549549
3,
550550
Array [
@@ -706,7 +706,7 @@ Array [
706706
},
707707
},
708708
},
709-
"e: ",
709+
"e:= ",
710710
Array [
711711
Array [
712712
3,
@@ -735,12 +735,12 @@ Array [
735735
],
736736
],
737737
],
738-
"f: ",
738+
"f:= ",
739739
Array [
740740
1,
741741
2,
742742
],
743-
"g: ",
743+
"g:= ",
744744
Array [
745745
Array [
746746
[Function],
@@ -769,7 +769,7 @@ Array [
769769
],
770770
],
771771
],
772-
"y: ",
772+
"y:= ",
773773
Array [
774774
Array [
775775
1,
@@ -866,9 +866,9 @@ Array [
866866
0,
867867
"y: ",
868868
10,
869-
"f: ",
869+
"f:= ",
870870
[Function],
871-
"z: ",
871+
"z:= ",
872872
[Function],
873873
Object {
874874
"callExpression": Object {
@@ -1101,7 +1101,7 @@ Array [
11011101
},
11021102
},
11031103
},
1104-
"x: ",
1104+
"x:= ",
11051105
Array [
11061106
1,
11071107
Array [
@@ -1188,7 +1188,7 @@ Array [
11881188
},
11891189
},
11901190
},
1191-
"x: ",
1191+
"x:= ",
11921192
Array [],
11931193
]
11941194
`;

src/features/envVisualizer/components/Binding.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export class Binding implements Visible {
3333
/** frame this binding is in */
3434
readonly frame: Frame,
3535
/** previous binding (the binding above it) */
36-
readonly prevBinding: Binding | null
36+
readonly prevBinding: Binding | null,
37+
readonly isConstant: boolean = false
3738
) {
3839
// derive the coordinates from the binding above it
3940
if (this.prevBinding) {
@@ -44,7 +45,7 @@ export class Binding implements Visible {
4445
this.y = this.frame.y + Config.FramePaddingY;
4546
}
4647

47-
this.keyString += Config.VariableColon;
48+
this.keyString += isConstant ? Config.ConstantColon : Config.VariableColon;
4849
this.value = Layout.createValue(data, this);
4950

5051
const keyYOffset =

src/features/envVisualizer/components/Frame.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class Frame implements Visible, Hoverable {
7474
let maxBindingWidth = 0;
7575
for (const [key, data] of Object.entries(this.environment.head)) {
7676
const bindingWidth =
77-
Math.max(Config.TextMinWidth, getTextWidth(String(key + Config.VariableColon))) +
77+
Math.max(Config.TextMinWidth, getTextWidth(key + Config.ConstantColon)) +
7878
Config.TextPaddingX +
7979
(isUnassigned(data)
8080
? Math.max(Config.TextMinWidth, getTextWidth(Config.UnassignedData.toString()))
@@ -88,8 +88,11 @@ export class Frame implements Visible, Hoverable {
8888
// initializes bindings (keys + values)
8989
let prevBinding: Binding | null = null;
9090
let totalWidth = this.width;
91-
for (const [key, data] of Object.entries(this.environment.head)) {
92-
const currBinding: Binding = new Binding(String(key), data, this, prevBinding);
91+
92+
const descriptors = Object.getOwnPropertyDescriptors(this.environment.head);
93+
94+
for (const [key, data] of Object.entries(descriptors)) {
95+
const currBinding: Binding = new Binding(key, data.value, this, prevBinding, !data.writable);
9396
this.bindings.push(currBinding);
9497
prevBinding = currBinding;
9598
totalWidth = Math.max(totalWidth, currBinding.width + Config.FramePaddingX);

src/features/envVisualizer/components/Text.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Label as KonvaLabel, Tag as KonvaTag, Text as KonvaText } from 'react-k
44

55
import { Config, ShapeDefaultProps } from '../EnvVisualizerConfig';
66
import { Layout } from '../EnvVisualizerLayout';
7-
import { Hoverable, Visible } from '../EnvVisualizerTypes';
7+
import { Data, Hoverable, Visible } from '../EnvVisualizerTypes';
88
import { getTextWidth } from '../EnvVisualizerUtils';
99

1010
export interface TextOptions {
@@ -30,14 +30,14 @@ export class Text implements Visible, Hoverable {
3030
readonly height: number;
3131
readonly width: number;
3232

33-
readonly fullStr: string;
33+
readonly partialStr: string; // truncated string representation of data
34+
readonly fullStr: string; // full string representation of data
3435

3536
readonly options: TextOptions = defaultOptions;
3637
private labelRef: RefObject<any> = React.createRef();
3738

3839
constructor(
39-
/** text */
40-
readonly data: any,
40+
readonly data: Data,
4141
readonly x: number,
4242
readonly y: number,
4343
/** additional options (for customization of text) */
@@ -47,20 +47,22 @@ export class Text implements Visible, Hoverable {
4747

4848
const { fontSize, fontStyle, fontFamily, maxWidth, isStringIdentifiable } = this.options;
4949

50-
this.fullStr = this.data = isStringIdentifiable ? JSON.stringify(data) : String(data);
50+
this.fullStr = this.partialStr = isStringIdentifiable
51+
? JSON.stringify(data) || String(data)
52+
: String(data);
5153
this.height = fontSize;
5254

5355
const widthOf = (s: string) => getTextWidth(s, `${fontStyle} ${fontSize}px ${fontFamily}`);
54-
if (widthOf(this.data) > maxWidth) {
56+
if (widthOf(this.partialStr) > maxWidth) {
5557
let truncatedText = Config.Ellipsis.toString();
5658
let i = 0;
57-
while (widthOf(this.data.substr(0, i) + Config.Ellipsis.toString()) < maxWidth) {
58-
truncatedText = this.data.substr(0, i++) + Config.Ellipsis.toString();
59+
while (widthOf(this.partialStr.substr(0, i) + Config.Ellipsis.toString()) < maxWidth) {
60+
truncatedText = this.partialStr.substr(0, i++) + Config.Ellipsis.toString();
5961
}
6062
this.width = widthOf(truncatedText);
61-
this.data = truncatedText;
63+
this.partialStr = truncatedText;
6264
} else {
63-
this.width = Math.max(Config.TextMinWidth, widthOf(this.data));
65+
this.width = Math.max(Config.TextMinWidth, widthOf(this.partialStr));
6466
}
6567
}
6668

@@ -94,7 +96,7 @@ export class Text implements Visible, Hoverable {
9496
onMouseEnter={this.onMouseEnter}
9597
onMouseLeave={this.onMouseLeave}
9698
>
97-
<KonvaText {...ShapeDefaultProps} key={Layout.key++} text={this.data} {...props} />
99+
<KonvaText {...ShapeDefaultProps} key={Layout.key++} text={this.partialStr} {...props} />
98100
</KonvaLabel>
99101
<KonvaLabel
100102
x={this.x}

0 commit comments

Comments
 (0)