@@ -0,0 +1,457 @@
1+
2+(function() {
3+'use strict';
4+
5+Elm.Debugger = null;
6+Elm.debuggerAttach = function(module, hotSwapState /* =undefined */) {
7+ return {
8+ make: function(runtime) {
9+ var wrappedModule = debugModule(module, runtime);
10+ Elm.Debugger = debuggerInit(wrappedModule, runtime, hotSwapState);
11+ dispatchElmDebuggerInit();
12+ return wrappedModule.moduleInstance;
13+ }
14+ };
15+};
16+
17+function dispatchElmDebuggerInit() {
18+ if (parent.window) {
19+ var dispatch = function(_) {
20+ parent.window.postMessage("elmDebuggerInit", window.location.origin);
21+ };
22+ if (parent.window.document.readyState === "complete") {
23+ dispatch();
24+ }
25+ else {
26+ parent.window.addEventListener("load", dispatch);
27+ }
28+ }
29+}
30+
31+function debugModule(module, runtime) {
32+ var programPaused = false;
33+ var recordedEvents = [];
34+ var asyncCallbacks = [];
35+
36+ function wrapNotify(id, v) {
37+ var timestep = Date.now();
38+
39+ if (programPaused) {
40+ // ignore async events generated while playing back
41+ // or user events while program is paused
42+ return false;
43+ }
44+ else {
45+ var changed = runtime.notify(id, v, timestep);
46+ recordEvent(id, v, timestep);
47+ if (parent.window) {
48+ parent.window.postMessage("elmNotify", window.location.origin);
49+ }
50+ return changed;
51+ }
52+ };
53+
54+ function wrapRunDelayed(func, delayMs) {
55+ var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
56+ var timerId = setTimeout(function() {
57+ cbObj.executed = true;
58+ func();
59+ }, delayMs);
60+ cbObj.timerId = timerId;
61+ asyncCallbacks.push(cbObj);
62+ return timerId;
63+ }
64+
65+ function recordEvent(id, v, timestep) {
66+ recordedEvents.push({ id:id, value:v, timestep:timestep });
67+ }
68+
69+ function clearAsyncCallbacks() {
70+ asyncCallbacks.forEach(function(timer) {
71+ if (!timer.executed) {
72+ clearTimeout(timer.timerId);
73+ }
74+ });
75+ }
76+
77+ function clearRecordedEvents() {
78+ recordedEvents = [];
79+ }
80+
81+ function getRecordedEventsLength() {
82+ return recordedEvents.length;
83+ }
84+
85+ function getRecordedEventAt(i) {
86+ return recordedEvents[i];
87+ }
88+
89+ function copyRecordedEvents() {
90+ return recordedEvents.slice();
91+ }
92+
93+ function loadRecordedEvents(events) {
94+ recordedEvents = events.slice();
95+ }
96+
97+ function setPaused(v) {
98+ programPaused = v;
99+ if (programPaused) {
100+ clearAsyncCallbacks();
101+ tracePath.stopRecording();
102+ }
103+ else {
104+ // executeCallbacks should only be called when continuing, not when restarting.
105+ //executeCallbacks(asyncCallbacks, false);
106+ tracePath.startRecording();
107+ }
108+ }
109+
110+ function getPaused() {
111+ return programPaused;
112+ }
113+
114+ // runtime is the prototype of wrappedRuntime
115+ // so we can access all runtime properties too
116+ var wrappedRuntime = Object.create(runtime);
117+ wrappedRuntime.notify = wrapNotify;
118+ wrappedRuntime.runDelayed = wrapRunDelayed;
119+
120+ var assignedPropTracker = Object.create(wrappedRuntime);
121+ var moduleInstance = module.make(assignedPropTracker);
122+ // The main module stores imported modules onto the runtime.
123+ // To ensure only one instance of each module is created,
124+ // we assign them back on the original runtime object.
125+ Object.keys(assignedPropTracker).forEach(function(key) {
126+ runtime[key] = assignedPropTracker[key];
127+ });
128+
129+ var moduleNodes = flattenNodes(wrappedRuntime.inputs);
130+ var tracePath = tracePathInit(runtime, moduleInstance.main);
131+
132+ return {
133+ moduleInstance: moduleInstance,
134+ moduleNodes: moduleNodes,
135+ initialNodeValues: saveNodeValues(moduleNodes),
136+ initialAsyncCallbacks: asyncCallbacks.slice(),
137+ // API functions
138+ clearAsyncCallbacks: clearAsyncCallbacks,
139+ clearRecordedEvents: clearRecordedEvents,
140+ getRecordedEventsLength: getRecordedEventsLength,
141+ getRecordedEventAt: getRecordedEventAt,
142+ copyRecordedEvents: copyRecordedEvents,
143+ loadRecordedEvents: loadRecordedEvents,
144+ getPaused: getPaused,
145+ setPaused: setPaused,
146+ tracePath: tracePath
147+ };
148+}
149+
150+function debuggerInit(debugModule, runtime, hotSwapState /* =undefined */) {
151+ var eventCounter = 0;
152+
153+ function resetProgram() {
154+ debugModule.clearAsyncCallbacks();
155+ restoreNodeValues(debugModule.moduleNodes, debugModule.initialNodeValues);
156+ redrawGraphics();
157+ }
158+
159+ function restartProgram() {
160+ resetProgram();
161+ debugModule.tracePath.clearTraces();
162+ debugModule.clearRecordedEvents();
163+ debugModule.setPaused(false);
164+ executeCallbacks(debugModule.initialAsyncCallbacks, true);
165+ }
166+
167+ function pauseProgram() {
168+ debugModule.setPaused(true);
169+ eventCounter = debugModule.getRecordedEventsLength();
170+ }
171+
172+ function continueProgram() {
173+ if (debugModule.getPaused())
174+ {
175+ debugModule.setPaused(false);
176+ }
177+ }
178+
179+ function stepTo(index) {
180+ if (!debugModule.getPaused()) {
181+ debugModule.setPaused(true);
182+ resetProgram();
183+ }
184+
185+ if (index < 0 || index > getMaxSteps()) {
186+ throw "Index out of bounds: " + index;
187+ }
188+
189+ if (index < eventCounter) {
190+ resetProgram();
191+ eventCounter = 0;
192+ }
193+
194+ assert(index >= eventCounter, "index must be bad");
195+ while (eventCounter < index) {
196+ var nextEvent = debugModule.getRecordedEventAt(eventCounter);
197+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
198+
199+ eventCounter += 1;
200+ }
201+ assert(eventCounter == index, "while loop didn't work");
202+ }
203+
204+ function getMaxSteps() {
205+ return debugModule.getRecordedEventsLength();
206+ }
207+
208+ function doPlayback(eventList) {
209+ var x = eventList.shift();
210+ var time = x[2];
211+ runtime.notify(x[0], x[1], x[2]);
212+
213+ if (eventList.length > 0) {
214+ var delta = eventList[0][2] - time;
215+ setTimeout(function() { doPlayback(eventList); }, delta);
216+ }
217+ }
218+
219+ function redrawGraphics() {
220+ var graphicsNode = debugModule.moduleInstance.main.kids[0];
221+ graphicsNode.recv(Date.now(), true, debugModule.moduleInstance.main.id);
222+ }
223+
224+ function getHotSwapState() {
225+ var counter = eventCounter;
226+ if (!debugModule.getPaused()) {
227+ counter = getMaxSteps();
228+ }
229+ return {
230+ recordedEvents: debugModule.copyRecordedEvents(),
231+ eventCounter: counter
232+ };
233+ }
234+
235+ function dispose() {
236+ var parentNode = runtime.node.parentNode;
237+ parentNode.removeChild(debugModule.tracePath.canvas);
238+ parentNode.removeChild(runtime.node);
239+ }
240+
241+ if (hotSwapState) {
242+ debugModule.setPaused(true);
243+ debugModule.loadRecordedEvents(hotSwapState.recordedEvents);
244+
245+ // draw new trace path
246+ debugModule.tracePath.startRecording();
247+ stepTo(getMaxSteps());
248+ debugModule.tracePath.stopRecording();
249+
250+ stepTo(hotSwapState.eventCounter);
251+ }
252+
253+ runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
254+
255+ var elmDebugger = {
256+ restart: restartProgram,
257+ pause: pauseProgram,
258+ kontinue: continueProgram,
259+ getMaxSteps: getMaxSteps,
260+ stepTo: stepTo,
261+ getPaused: debugModule.getPaused,
262+ getHotSwapState: getHotSwapState,
263+ dispose: dispose,
264+ allNodes: debugModule.moduleNodes,
265+ mainNode: debugModule.moduleInstance.main
266+ };
267+
268+ return elmDebugger;
269+}
270+
271+function Point(x, y) {
272+ this.x = x;
273+ this.y = y;
274+
275+ this.translate = function(x, y) {
276+ return new Point(this.x + x, this.y + y);
277+ }
278+
279+ this.equals = function(p) {
280+ return this.x == p.x && this.y == p.y;
281+ }
282+}
283+
284+function tracePathInit(runtime, mainNode) {
285+ var List = Elm.List.make(runtime);
286+ var Signal = Elm.Signal.make(runtime);
287+ var tracePathNode = A2(Signal.lift, graphicsUpdate, mainNode);
288+ var tracePathCanvas = createCanvas();
289+ var tracePositions = {};
290+ var recordingTraces = true;
291+
292+ function findPositions(currentScene) {
293+ var positions = {};
294+ function processElement(elem, offset) {
295+ if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
296+ {
297+ List.map(F2(processForm)(offset))(elem.element.model.forms);
298+ }
299+ else if (elem.element.ctor == "Image" && elem.props.debugTracePathId)
300+ {
301+ positions[elem.props.debugTracePathId] = new Point(offset.x, offset.y);
302+ }
303+ }
304+
305+ function processForm(offset, form) {
306+ if (form.form.ctor == "FElement")
307+ {
308+ processElement(form.form._0, offset.translate(form.x, -form.y));
309+ }
310+ }
311+
312+ processElement(currentScene, new Point(0, 0));
313+ return positions;
314+ }
315+
316+ function appendPositions(positions) {
317+ for (var id in positions) {
318+ var pos = positions[id];
319+ if (tracePositions.hasOwnProperty(id)) {
320+ var points = tracePositions[id];
321+ if (!pos.equals(points[points.length-1])) {
322+ points.push(pos);
323+ }
324+ }
325+ else {
326+ tracePositions[id] = [pos];
327+ }
328+ }
329+ }
330+
331+ function graphicsUpdate(currentScene) {
332+ if (!recordingTraces) {
333+ return;
334+ }
335+
336+ var ctx = tracePathCanvas.getContext('2d');
337+ ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
338+
339+ ctx.save();
340+ ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
341+ appendPositions(findPositions(currentScene));
342+ for (var id in tracePositions)
343+ {
344+ ctx.beginPath();
345+ var points = tracePositions[id];
346+ for (var i=0; i < points.length; i++)
347+ {
348+ var p = points[i];
349+ if (i == 0) {
350+ ctx.moveTo(p.x, p.y);
351+ }
352+ else {
353+ ctx.lineTo(p.x, p.y);
354+ }
355+ }
356+ ctx.lineWidth = 1;
357+ ctx.strokeStyle = "#888a85";
358+ ctx.stroke();
359+
360+ for (var i=0; i < points.length; i++)
361+ {
362+ var p = points[i];
363+ ctx.beginPath();
364+ ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
365+ ctx.fillStyle = "#2e3436";
366+ ctx.fill();
367+ }
368+ }
369+
370+ ctx.restore();
371+ }
372+
373+ function clearTraces() {
374+ tracePositions = {};
375+ }
376+
377+ function stopRecording() {
378+ recordingTraces = false;
379+ }
380+
381+ function startRecording() {
382+ recordingTraces = true;
383+ }
384+
385+ return {
386+ graphicsUpdate: graphicsUpdate,
387+ canvas: tracePathCanvas,
388+ clearTraces: clearTraces,
389+ stopRecording: stopRecording,
390+ startRecording: startRecording
391+ }
392+}
393+
394+
395+function executeCallbacks(callbacks, reexecute) {
396+ callbacks.forEach(function(timer) {
397+ if (reexecute || !timer.executed) {
398+ var func = timer.func;
399+ func();
400+ }
401+ });
402+}
403+
404+function createCanvas() {
405+ var c = document.createElement('canvas');
406+ c.width = window.innerWidth;
407+ c.height = window.innerHeight;
408+ c.style.position = "absolute";
409+ c.style.top = "0";
410+ c.style.left = "0";
411+ return c;
412+}
413+
414+function assert(bool, msg) {
415+ if (!bool) {
416+ throw "Assertion error: " + msg;
417+ }
418+}
419+
420+function saveNodeValues(allNodes) {
421+ var nodeValues = [];
422+
423+ allNodes.forEach(function(node) {
424+ nodeValues.push({ value: node.value, id: node.id });
425+ });
426+
427+ return nodeValues;
428+};
429+
430+
431+function restoreNodeValues(allNodes, nodeStates) {
432+ assert(allNodes.length == nodeStates.length, "saved program state has wrong length");
433+ for (var i=0; i < allNodes.length; i++) {
434+ var node = allNodes[i];
435+ var state = nodeStates[i];
436+ assert(node.id == state.id, "the nodes moved position");
437+
438+ node.value = state.value;
439+ }
440+}
441+
442+function flattenNodes(nodes) {
443+ var nodesById = {};
444+
445+ function addAllToDict(node) {
446+ nodesById[node.id] = node;
447+ node.kids.forEach(addAllToDict);
448+ }
449+ nodes.forEach(addAllToDict);
450+
451+ var allNodes = Object.keys(nodesById).sort().map(function(key) {
452+ return nodesById[key];
453+ });
454+ return allNodes;
455+};
456+
457+}());