Skip to content

Commit f767322

Browse files
committed
Merge branch 'gh-pages' of https://github.com/LivelyKernel/lively4-core into gh-pages
2 parents fdb5959 + e666d4f commit f767322

File tree

22 files changed

+732
-278
lines changed

22 files changed

+732
-278
lines changed

demos/markdown/mle-workspaces.md

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,154 @@
11
# Workspace Example
22

33
This is an Example of a workspace embedded in HTML/Markdown that will persist it's code in browser local storage.
4-
<lively-script><script>import focalStorage from 'src/external/focalStorage.js'; import {SocketSingleton} from 'src/components/mle/socket.js'; const idMap = new Map(); const enclosingDiv = <div />; var mle = {}; async function startUp(){ await SocketSingleton.get(); let i = 0; while(await focalStorage.getItem(`markdown_workspace_${i}`)){ await new Pane(i, await focalStorage.getItem(`markdown_workspace_${i}_kind`)).create(); i++; } } class Pane { constructor(id, kind){ if(id !== undefined){ this._id = id; this.kind = kind; } else { this._id = idMap.size; } idMap.set(this.textStorageId, this); } onDoIt() { this.saveText() this.workspace.tryBoundEval(this.workspace.value) } async onReset() { if(this.kind) this.socket = await SocketSingleton.reset(); this.logarea.value = "LOGS"; this.drawarea.innerHTML = ""; } get defaultText() { return "sql`SELECT * FROM students`" } get textStorageId() { return `markdown_workspace_${this._id}` } async loadText() { var loaded = await focalStorage.getItem(this.textStorageId); if (loaded) return loaded; return this.defaultText; } async saveText() { focalStorage.setItem(this.textStorageId, this.workspace.value); focalStorage.setItem(`${this.textStorageId}_kind`, this.kind); } log(s) { this.logarea.value += s + ""; } async create() { // #TODO #META style and pre tags are problematic in Markdown scripts this.kind = this.kind === undefined ? await lively.confirm("MLE Workspace? Ok for yes, Cancel for No.") : this.kind; var style = document.createElement("style"); style.textContent = ` lively-code-mirror { border: 1px solid gray; flex: 4; }`; var buttons = <div> <button click={() => {this.onDoIt()} }>DoIt</button> <button click={() => {this.onReset()} }>reset</button> </div>; this.workspace = await (<lively-code-mirror></lively-code-mirror>); this.workspace.value = await this.loadText(); this.workspace.doitContext = this; this.logarea = <textarea disabled style="flex: 2;"/>; this.logarea.value = "LOGS"; this.drawarea = <div></div>; if(this.kind){ this.workspace.boundEval = async function(s) { this.socket = this.socket || await SocketSingleton.get(); this.socket.emit("test", {id: this.textStorageId, func: "evaluate", parameters: [s]}); const value = await new Promise((res) => { this.socket.on("result", r => { if(!r) return; if(r.id !== this.textStorageId) return; console.log(r.data) res(JSON.parse(r.data)); }); }); this.outData = value; Object.defineProperty(window, `$${this._id}`, {configurable: true, value}); this.log(JSON.stringify(value)); this.saveText(); return {value}; } this.workspace.boundEval = this.workspace.boundEval.bind(this); } enclosingDiv.appendChild( <div style="padding: 10px; width:90%;"> {style} <h4>{this.kind ? "MLE" : "DOM"} Workspace ${this._id}</h4> {buttons} <div style="display: flex;"> {this.workspace} {this.logarea} </div> {this.drawarea} </div> ); } } startUp().then(t => <div> <button click={() => new Pane().create()}>New workspace</button> {enclosingDiv} </div>)</script> </lively-script>
4+
<script>
5+
import focalStorage from 'src/external/focalStorage.js';
6+
import {SocketSingleton} from 'src/components/mle/socket.js';
7+
const idMap = new Map();
8+
const enclosingDiv = <div />;
9+
var mle = {};
10+
async function startUp(){
11+
await SocketSingleton.get();
12+
let i = 0;
13+
while(await focalStorage.getItem(`markdown_workspace_${i}`)){
14+
await new Pane(i, await focalStorage.getItem(`markdown_workspace_${i}_kind`)).create();
15+
i++;
16+
}
17+
}
18+
class Pane {
19+
constructor(id, kind){
20+
if(id !== undefined){
21+
this._id = id;
22+
this.kind = kind;
23+
} else {
24+
this._id = idMap.size;
25+
}
26+
idMap.set(this.textStorageId, this);
27+
}
28+
29+
onDoIt() {
30+
this.saveText();
31+
this.workspace.tryBoundEval(this.workspace.value);
32+
}
33+
34+
async onReset() {
35+
if(this.kind) this.socket = await SocketSingleton.reset();
36+
this.logarea.value = "LOGS";
37+
this.drawarea.innerHTML = "";
38+
}
39+
40+
get defaultText() {
41+
return "sql`SELECT * FROM students`"
42+
}
43+
44+
get textStorageId() {
45+
return `markdown_workspace_${this._id}`
46+
}
47+
48+
async loadText() {
49+
var loaded = await focalStorage.getItem(this.textStorageId);
50+
if (loaded) return loaded;
51+
return this.defaultText;
52+
}
53+
54+
async saveText() {
55+
focalStorage.setItem(this.textStorageId, this.workspace.value);
56+
focalStorage.setItem(`${this.textStorageId}_kind`, this.kind);
57+
}
58+
59+
log(s) {
60+
this.logarea.value += s + "";
61+
}
62+
63+
async create() {
64+
// #TODO #META style and pre tags are problematic in Markdown scripts
65+
this.kind = this.kind === undefined ? await lively.confirm("MLE Workspace? Ok for yes, Cancel for No.") : this.kind;
66+
var style = document.createElement("style");
67+
style.textContent = ` lively-code-mirror { border: 1px solid gray; flex: 4; }`;
68+
var buttons = <div>
69+
<button click={() => {this.onDoIt()} }>DoIt</button>
70+
<button click={() => {this.onReset()} }>reset</button>
71+
</div>;
72+
this.workspace = await (<lively-code-mirror></lively-code-mirror>);
73+
this.workspace.value = await this.loadText();
74+
this.workspace.doitContext = this;
75+
this.logarea = <textarea disabled style="flex: 2;"/>;
76+
this.logarea.value = "LOGS";
77+
this.drawarea = <div></div>;
78+
if(this.kind){
79+
this.workspace.boundEval = async function(s) {
80+
this.socket = this.socket || await SocketSingleton.get();
81+
this.socket.emit("test", {
82+
id: this.textStorageId,
83+
func: "evaluate",
84+
parameters: [s]
85+
});
86+
const value = await new Promise((res) => {
87+
this.socket.on("result", r => {
88+
if(!r) return;
89+
if(r.id !== this.textStorageId) return;
90+
res(JSON.parse(r.data));
91+
});
92+
});
93+
this.outData = value;
94+
Object.defineProperty(window, `$${this._id}`, {configurable: true, value});
95+
this.log(JSON.stringify(value));
96+
this.saveText();
97+
return {value};
98+
}
99+
this.workspace.boundEval = this.workspace.boundEval.bind(this);
100+
}
101+
102+
enclosingDiv.appendChild(<div style="padding: 10px; width:90%;">
103+
{style}
104+
<h4>{this.kind ? "MLE" : "DOM"} Workspace ${this._id}</h4>
105+
{buttons}
106+
<div style="display: flex;">
107+
{this.workspace}
108+
{this.logarea}
109+
</div>
110+
{this.drawarea}
111+
</div>);
112+
}
113+
}
114+
115+
startUp().then(t => <div>
116+
<button click={() => new Pane().create()}>New workspace</button>
117+
{enclosingDiv}
118+
</div>)
119+
</script>
120+
121+
Example Query:
122+
```
123+
const projects = sql`SELECT * FROM projects where id < 20`.map(
124+
({id, url}) => {
125+
const prs = sql`SELECT * FROM pull_requests where head_repo_id=${id}`.map(({id}) => {
126+
const events = sql`SELECT * FROM pull_request_history where pull_request_id=${id}`.reduce(
127+
({opened, last}, {created_at, action}) => {
128+
if(action.startsWith("opened")){
129+
if(!opened || opened > created_at) {
130+
return {opened: created_at, last};
131+
}
132+
}
133+
if(action.startsWith("closed") || action.startsWith("merged")){
134+
if(!last || last < created_at){
135+
return {opened, last: created_at}
136+
}
137+
}
138+
return {opened, last};
139+
}, {opened: false, last: false});
140+
return events.last - events.opened;
141+
}).reduce(({count, sum}, curr) => ({count: count+1, sum: sum+curr}), {count: 0, sum: 0});
142+
return {repo: url, avg: prs.sum/prs.count};
143+
}
144+
).reduce((prev, curr) => {
145+
if(!curr.avg || curr.avg <0) return prev;
146+
if(prev.length < 3) return [...prev, curr];
147+
const t = prev.sort((a, b) => a.avg-b.avg);
148+
if(t[0].avg < curr.avg){
149+
[...t.slice(1), curr]
150+
}
151+
return t;
152+
}, []).map(({repo, ...t}) => ({...t, repo:repo.split("/").slice(4).join("/")}));
153+
projects
154+
```

demos/tom/Events.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class ASTChangeEvent {
5151
}
5252

5353
getNode(id, astNode) {
54-
if (astNode.type) {
54+
if (astNode && astNode.type) {
5555
const isSearchedNode = value => value && value.traceID !== undefined && value.traceID.nodeID === id.nodeID;
5656

5757
if (isSearchedNode(astNode)) {
@@ -97,6 +97,9 @@ export class ASTChangeEvent {
9797
}
9898

9999
resolve(copy, ast) {
100+
if (!copy) {
101+
return copy;
102+
}
100103

101104
// Todo: optimize by caching: traceID -> ASTNode
102105
if(copy.isTraceID) {

demos/tom/TraceLogParser.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,6 @@ export default class TraceLogParser {
7676
const returnEntry = this.consume();
7777
section.addEntry(this.instantiateEvent(returnEntry));
7878

79-
if(returnEntry.position === this.peek().position) {
80-
this.match('left');
81-
}
82-
debugger
83-
8479
throw new EarlyReturn('function');
8580
} else if(this.matchPeek('leave')) {
8681
const leaveEntry = this.consume();
@@ -91,10 +86,8 @@ export default class TraceLogParser {
9186
}
9287
else if(this.matchPeek('enterPlugin')) {
9388
section.addEntry(this.parsePlugin());
94-
continue;
9589
} else if(this.matchPeek('aboutToEnter')) {
9690
this.parseFunction(section, [...higherSections, section]);
97-
continue;
9891
} else if(this.matchPeek('enterFunction')) {
9992
if(!section.isFunction() || (section.isFunction() && section._name !== this.peek().data)) {
10093
// entered a function that got called from native code or babel
@@ -103,10 +96,8 @@ export default class TraceLogParser {
10396
} else {
10497
this.match('enterFunction');
10598
}
106-
continue;
10799
} else if(this.matchPeek('beginCondition')) {
108100
this.parseCondition(section, [...higherSections, section]);
109-
continue;
110101
} else if(this.matchPeek('astChangeEvent')) {
111102
const change = this.consumeAsEvent();
112103

@@ -115,18 +106,17 @@ export default class TraceLogParser {
115106
}
116107

117108
section.addChange(change);
118-
continue;
119109
} else if(this.matchPeek('error')) {
120110
section.addEntry(this.consumeAsEvent());
121-
// break out from nesting
111+
122112
throw new EarlyReturn('error');
123113
} else if(this.match('endCondition')) {
124114
return section;
125115
} else if(this.match('left')) {
126116
return section;
117+
} else {
118+
section.addEntry(this.consumeAsEvent());
127119
}
128-
129-
section.addEntry(this.consumeAsEvent());
130120
}
131121

132122
return section;
@@ -165,6 +155,11 @@ export default class TraceLogParser {
165155
}
166156
}
167157

158+
// Functions that got entered from native code or babel get not left event
159+
if(fun.position === this.peek().position) {
160+
this.match('left');
161+
}
162+
168163
return fun;
169164
}
170165

demos/tom/babel-plugin-tracer.js

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ export default function({ types: t }) {
2222
}
2323

2424
function callOnTrace(methodName, args = [], shouldBeStatement = false) {
25-
let call = t.callExpression(t.memberExpression(t.identifier(Trace.traceIdentifierName), t.identifier(methodName)),
26-
args);
25+
let call = t.callExpression(t.memberExpression(t.identifier(Trace.traceIdentifierName), t.identifier(methodName)), args);
2726
if (shouldBeStatement) {
2827
call = t.expressionStatement(call);
2928
}
@@ -74,7 +73,7 @@ export default function({ types: t }) {
7473
},
7574
post() {
7675
if(pluginDefinedTrace) {
77-
delete window[Trace.traceIdentifierName];
76+
window[Trace.traceIdentifierName] = undefined;
7877
}
7978
},
8079
visitor: {
@@ -86,32 +85,41 @@ export default function({ types: t }) {
8685
return;
8786
}
8887

89-
9088
path.node.alreadyVisited = true;
9189
let callee = path.get('callee');
9290
let name;
9391

9492
if (t.isMemberExpression(callee)) {
95-
callee.node.computed = true;
96-
callee = path.get('callee.property');
93+
if(callee.node.computed) {
94+
callee = path.get('callee.property');
9795

98-
name = callee.node.name || 'anonymous function';
96+
name = callee.node;
97+
} else {
98+
callee.node.computed = true;
99+
callee = path.get('callee.property');
100+
101+
name = t.stringLiteral(callee.node.name || 'anonymous function');
102+
}
103+
104+
105+
106+
const loc = location(callee.node, this);
99107

100108
const aboutToEnter = callOnTrace('aboutToEnter',
101109
[
102-
location(callee.node, this),
103-
t.stringLiteral(name)
110+
loc,
111+
name
104112
]);
105-
callee.replaceWith(t.stringLiteral(name));
113+
callee.replaceWith(name);
106114
callee.insertBefore(aboutToEnter);
107115

108116
const left = callOnTrace('left',
109117
[
110-
location(path.node, this),
118+
loc,
111119
path.node
112120
]);
113121

114-
path.replaceWith(left)
122+
path.replaceWith(left)
115123
return;
116124
} else if (t.isFunctionExpression(callee)) {
117125
name = callee.node.id.name || 'anonymous function';
@@ -154,7 +162,7 @@ export default function({ types: t }) {
154162
]));
155163
} else {
156164
modifyFunction('anonymous function', path, this);
157-
}
165+
}
158166
},
159167
"ClassMethod|ObjectMethod"(path) {
160168
const key = path.node.key;

demos/tom/copyAST.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
1-
function copyArrayOfRound(arr, roundNumber) {
2-
const copy = [];
3-
4-
for(const entry of arr) {
5-
copy.push(copyASTPartsOfRound(entry, roundNumber));
6-
}
7-
8-
return copy;
1+
import { wrapCurrentASTNode } from 'demos/tom/wrapAST.js';
2+
3+
function copyArray(array, observer) {
4+
return array.map(elm => copyAndWrapUnkownSubtree(elm, observer));
95
}
106

11-
// Todo: merge wrap and copy => copy all nodes without traceID
12-
// roundNumber could result in unncessary copies as in one round there could be multiple edits
13-
export default function copyASTPartsOfRound(object, roundNumber) {
7+
// Copies subtree of new ASTNodes. Every ASTNode it visits gets wrapped and a traceID assigned.
8+
export default function copyAndWrapUnkownSubtree(object, observer) {
149
// simply check if the object is an astNode
1510
if (object && object.type) {
1611
// if already in AST return only a reference
17-
if (object.traceID && object.traceID.pluginRoundID !== roundNumber) {
12+
if (object.traceID) {
1813
return object.traceID;
1914
}
2015

16+
wrapCurrentASTNode(object, observer);
17+
2118
const objectCopy = {};
2219
const keys = Object.keys(object).filter(key => key[0] !== '_');
2320
for (const key of keys) {
2421
const value = object[key];
2522

2623
if (Array.isArray(value)) {
27-
objectCopy[key] = copyArrayOfRound(value, roundNumber);
24+
objectCopy[key] = copyArray(value, observer);
2825

2926
continue;
3027
}
3128

32-
if (value === null) {
29+
if (!value) {
3330
objectCopy[key] = value;
3431
continue;
3532
}
@@ -39,7 +36,7 @@ export default function copyASTPartsOfRound(object, roundNumber) {
3936
// ignore functions
4037
break;
4138
case 'object':
42-
objectCopy[key] = copyASTPartsOfRound(value, roundNumber);
39+
objectCopy[key] = copyAndWrapUnkownSubtree(value, observer);
4340
break;
4441
default:
4542
objectCopy[key] = value;
@@ -49,7 +46,9 @@ export default function copyASTPartsOfRound(object, roundNumber) {
4946
return objectCopy;
5047
} else {
5148
if(Array.isArray(object)) {
52-
return copyArrayOfRound(object, roundNumber);
49+
return copyArray(object, observer);
50+
} else if (!object) {
51+
return object;
5352
} else {
5453
// do a simple copy
5554
return JSON.parse(JSON.stringify(object));

0 commit comments

Comments
 (0)