pyret-embed 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,862 @@
1
+ /*
2
+ TS-compiler flavor of cpo-main.js: the same editor/REPL UI, but with
3
+ compilation done by the TypeScript compiler bundle (window.PyretTSCompiler)
4
+ instead of the Pyret-hosted compiler modules. Execution still goes
5
+ through the builtin load-lib against the page realm, so results,
6
+ check blocks, and runtime errors render exactly as in cpo-main.js.
7
+
8
+ The long list of builtin requires below exists so that the built
9
+ ts jarr instantiates the same builtin:// realm as cpo-main.jarr:
10
+ user programs compiled at runtime link against these instances.
11
+
12
+ The UI sections of this file (the top of theModule and everything in
13
+ withRepl) are copied verbatim from cpo-main.js; the compiler plumbing
14
+ in between is TS-specific (see ts-compiler-lib.js).
15
+ */
16
+ ({
17
+ requires: [
18
+ { "import-type": "builtin",
19
+ name: "runtime-lib"
20
+ },
21
+ { "import-type": "builtin",
22
+ name: "load-lib"
23
+ },
24
+ { "import-type": "dependency",
25
+ protocol: "js-file",
26
+ args: ["./ts-compiler-lib"]
27
+ },
28
+ { "import-type": "dependency",
29
+ protocol: "js-file",
30
+ args: ["./repl-ui"]
31
+ },
32
+ { "import-type": "dependency",
33
+ protocol: "js-file",
34
+ args: ["./text-handlers"]
35
+ },
36
+ { "import-type": "builtin", name: "arrays" },
37
+ { "import-type": "builtin", name: "ast" },
38
+ { "import-type": "builtin", name: "base" },
39
+ { "import-type": "builtin", name: "builtin-modules" },
40
+ { "import-type": "builtin", name: "chart" },
41
+ { "import-type": "builtin", name: "chart-lib" },
42
+ { "import-type": "builtin", name: "charts" },
43
+ { "import-type": "builtin", name: "charts-lib" },
44
+ { "import-type": "builtin", name: "charts-util" },
45
+ { "import-type": "builtin", name: "checker" },
46
+ { "import-type": "builtin", name: "color" },
47
+ { "import-type": "builtin", name: "constants" },
48
+ { "import-type": "builtin", name: "contracts" },
49
+ { "import-type": "builtin", name: "cpo-builtins" },
50
+ { "import-type": "builtin", name: "csv" },
51
+ { "import-type": "builtin", name: "csv-lib" },
52
+ { "import-type": "builtin", name: "d3-lib" },
53
+ { "import-type": "builtin", name: "d3-lib-list" },
54
+ { "import-type": "builtin", name: "data-source" },
55
+ { "import-type": "builtin", name: "dcic2024" },
56
+ { "import-type": "builtin", name: "either" },
57
+ { "import-type": "builtin", name: "empty-context" },
58
+ { "import-type": "builtin", name: "equality" },
59
+ { "import-type": "builtin", name: "error" },
60
+ { "import-type": "builtin", name: "error-display" },
61
+ { "import-type": "builtin", name: "essentials2020" },
62
+ { "import-type": "builtin", name: "essentials2021" },
63
+ { "import-type": "builtin", name: "essentials2024" },
64
+ { "import-type": "builtin", name: "fetch" },
65
+ { "import-type": "builtin", name: "ffi" },
66
+ { "import-type": "builtin", name: "file" },
67
+ { "import-type": "builtin", name: "filelib" },
68
+ { "import-type": "builtin", name: "filesystem" },
69
+ { "import-type": "builtin", name: "filesystem-internal" },
70
+ { "import-type": "builtin", name: "format" },
71
+ { "import-type": "builtin", name: "gdrive-sheets" },
72
+ { "import-type": "builtin", name: "global" },
73
+ { "import-type": "builtin", name: "image" },
74
+ { "import-type": "builtin", name: "image-lib" },
75
+ { "import-type": "builtin", name: "image-structs" },
76
+ { "import-type": "builtin", name: "image-typed" },
77
+ { "import-type": "builtin", name: "internal-image-shared" },
78
+ { "import-type": "builtin", name: "internal-image-typed" },
79
+ { "import-type": "builtin", name: "internal-image-untyped" },
80
+ { "import-type": "builtin", name: "json" },
81
+ { "import-type": "builtin", name: "json-structs" },
82
+ { "import-type": "builtin", name: "lists" },
83
+ { "import-type": "builtin", name: "make-image" },
84
+ { "import-type": "builtin", name: "math" },
85
+ { "import-type": "builtin", name: "matrices" },
86
+ { "import-type": "builtin", name: "matrix-util" },
87
+ { "import-type": "builtin", name: "multiple-regression" },
88
+ { "import-type": "builtin", name: "option" },
89
+ { "import-type": "builtin", name: "parse-pyret" },
90
+ { "import-type": "builtin", name: "pathlib" },
91
+ { "import-type": "builtin", name: "pick" },
92
+ { "import-type": "builtin", name: "plot" },
93
+ { "import-type": "builtin", name: "plot-lib" },
94
+ { "import-type": "builtin", name: "plot-lib-list" },
95
+ { "import-type": "builtin", name: "plot-list" },
96
+ { "import-type": "builtin", name: "pprint" },
97
+ { "import-type": "builtin", name: "reactor-events" },
98
+ { "import-type": "builtin", name: "reactors" },
99
+ { "import-type": "builtin", name: "render-error-display" },
100
+ { "import-type": "builtin", name: "s-exp" },
101
+ { "import-type": "builtin", name: "s-exp-structs" },
102
+ { "import-type": "builtin", name: "sets" },
103
+ { "import-type": "builtin", name: "sha" },
104
+ { "import-type": "builtin", name: "source-map-lib" },
105
+ { "import-type": "builtin", name: "srcloc" },
106
+ { "import-type": "builtin", name: "starter2024" },
107
+ { "import-type": "builtin", name: "statistics" },
108
+ { "import-type": "builtin", name: "str-dict" },
109
+ { "import-type": "builtin", name: "string-dict" },
110
+ { "import-type": "builtin", name: "table" },
111
+ { "import-type": "builtin", name: "tables" },
112
+ { "import-type": "builtin", name: "timing" },
113
+ { "import-type": "builtin", name: "type-logger" },
114
+ { "import-type": "builtin", name: "valueskeleton" },
115
+ { "import-type": "builtin", name: "vector-util" },
116
+ { "import-type": "builtin", name: "world" },
117
+ { "import-type": "builtin", name: "world-lib" }
118
+ ],
119
+ nativeRequires: [
120
+ "cpo/cpo-builtin-modules",
121
+ "cpo/modal-prompt"
122
+ ],
123
+ provides: {
124
+ values: {
125
+ "repl": "tany"
126
+ }
127
+ },
128
+ theModule: function(runtime, namespace, uri,
129
+ runtimeLib, loadLib, tsLib, replUI, textHandlers
130
+ /* , ...parity builtins, cpoModules, modalPrompt */) {
131
+ // The parity builtin requires and the native requires arrive
132
+ // positionally after the named parameters; only the natives are used.
133
+ var allArgs = Array.prototype.slice.call(arguments);
134
+ var cpoModules = allArgs[allArgs.length - 2];
135
+ var modalPrompt = allArgs[allArgs.length - 1];
136
+
137
+ var replContainer = $("<div>").addClass("repl");
138
+ replContainer.attr("tabindex", "-1").attr('role', 'application');
139
+ //replContainer.attr("aria-hidden", "true");
140
+ $("#REPL").append(replContainer);
141
+
142
+ var logDetailedOption = $("#detailed-logging");
143
+
144
+ if(localSettings.getItem('log-detailed') !== null) {
145
+ logDetailedOption.prop("checked",
146
+ localSettings.getItem('log-detailed') == 'true');
147
+ } else {
148
+ localSettings.setItem('log-detailed', false);
149
+ }
150
+
151
+ logDetailedOption.on('change', function () {
152
+ localSettings.setItem('log-detailed', this.checked);
153
+ });
154
+
155
+ localSettings.change("log-detailed", function(_, newValue) {
156
+ logDetailedOption[0].checked = newValue == 'true';
157
+ logDetailedOption.attr('aria-pressed', '' + (newValue == 'true'));
158
+ });
159
+
160
+ runtime.setParam("imgUrlProxy", function(s) {
161
+ var a = document.createElement("a");
162
+ a.href = s;
163
+ if(a.origin === window.APP_BASE_URL) {
164
+ return s;
165
+ }
166
+ else if(window.IMAGE_PROXY_BYPASS) {
167
+ return s;
168
+ }
169
+ /*
170
+ else if(a.hostname === "drive.google.com" && a.pathname === "/uc") {
171
+ return s;
172
+ }
173
+ */
174
+ else {
175
+ return window.APP_BASE_URL + "/downloadImg?" + s;
176
+ }
177
+ });
178
+
179
+ var gf = runtime.getField;
180
+ var gmf = function(m, f) { return gf(gf(m, "values"), f); };
181
+
182
+ // NOTE(joe): This line is "cheating" by mixing runtime levels,
183
+ // and uses the same runtime for the compiler and running code.
184
+ // Usually you can only get a new Runtime by calling create, but
185
+ // here we magic the current runtime into one.
186
+ var pyRuntime = gf(gf(runtimeLib, "internal").brandRuntime, "brand").app(
187
+ runtime.makeObject({
188
+ "runtime": runtime.makeOpaque(runtime)
189
+ }));
190
+ var pyRealm = gf(loadLib, "internal").makeRealm(cpoModules.getRealm());
191
+
192
+ var T = tsLib.tsCompiler();
193
+ var builtinSupport = tsLib.makeBuiltinSupport(runtime.getParam("staticModules"));
194
+ var executor = tsLib.makeExecutor(pyRuntime);
195
+
196
+ // Mirrors the stock editor's finder configuration (cpo-main.js): the
197
+ // url-file mode comes from the host page, and the root compile context
198
+ // seeds the load-path at "." (the open tab's directory, which is what
199
+ // the embedding host resolves every fs path against).
200
+ var urlFileMode = window.URL_FILE_MODE || "all-remote";
201
+ var tsRepl = T.repl.makeRepl(
202
+ executor,
203
+ builtinSupport.loadables,
204
+ pyRealm,
205
+ { "load-path": "." },
206
+ tsLib.makeFinderFactory(builtinSupport, urlFileMode));
207
+
208
+ function tsOptions(options) {
209
+ var o = Object.assign({}, T.compileStructs.defaultCompileOptions);
210
+ o.checkMode = true;
211
+ o.checks = "main";
212
+ o.checkAll = !!options.checkAll;
213
+ o.typeCheck = !!options.typeCheck;
214
+ o.displayProgress = false;
215
+ o.log = function(_s, _toClear) {};
216
+ o.logError = function(s) { console.error(s); };
217
+ o.onCompile = function(_locator, loadable, _trace) { return loadable; };
218
+ return o;
219
+ }
220
+
221
+ var currentCancellation = null;
222
+ function newCancellation() {
223
+ currentCancellation = T.repl.makeCancellation();
224
+ return currentCancellation;
225
+ }
226
+
227
+ /*
228
+ A cancelled run is a user break that happened to land before the program
229
+ became a Pyret computation, so it is reported as one: the same
230
+ ffi.userBreak the runtime raises.
231
+ */
232
+ function reportRunEnd(ret, err) {
233
+ if (err instanceof T.repl.Cancelled) { tsLib.resolveWithUserBreak(ret); }
234
+ else { tsLib.resolveWithError(ret, err); }
235
+ }
236
+
237
+ var jsRepl = {
238
+ runtime: runtime,
239
+ /*
240
+ This should not be called while a Pyret stack is running
241
+ */
242
+ restartInteractions: function(source, options) {
243
+ var ret = Q.defer();
244
+ var cancel = newCancellation();
245
+ setTimeout(function() {
246
+ var opts = tsOptions(options);
247
+ var defsLocator = tsRepl.makeDefinitionsLocator(
248
+ function() { return source; },
249
+ T.compileStructs.standardGlobals);
250
+ tsRepl.restartInteractions(defsLocator, opts, cancel)
251
+ .then(function(either) { tsLib.resolveWithEither(ret, either); })
252
+ .catch(function(err) { reportRunEnd(ret, err); });
253
+ }, 0);
254
+ return ret.promise;
255
+ },
256
+ run: function(str, name) {
257
+ var ret = Q.defer();
258
+ var cancel = newCancellation();
259
+ setTimeout(function() {
260
+ var locator = tsRepl.makeInteractionLocator(function() { return str; });
261
+ tsRepl.runInteraction(locator, cancel)
262
+ .then(function(either) { tsLib.resolveWithEither(ret, either); })
263
+ .catch(function(err) { reportRunEnd(ret, err); });
264
+ }, 0);
265
+ return ret.promise;
266
+ },
267
+ pause: function(afterPause) {
268
+ runtime.schedulePause(function(resumer) {
269
+ afterPause(resumer);
270
+ });
271
+ },
272
+ /*
273
+ breakAll() kills all Pyret computation; the cancellation
274
+ is for pure JS computation (the compiler) that is no longer
275
+ Pyret code on a break-able Pyret stack
276
+ */
277
+ stop: function() {
278
+ if (currentCancellation !== null) { currentCancellation.cancel(); }
279
+ runtime.breakAll();
280
+ }
281
+ };
282
+
283
+ return withRepl(jsRepl);
284
+
285
+ function withRepl(repl) {
286
+ var runButton = $("#runButton");
287
+
288
+ var replWidget =
289
+ replUI.makeRepl(replContainer, repl, runtime, {
290
+ breakButton: $("#breakButton"),
291
+ runButton: runButton,
292
+ runDropdown: $('#runDropdown')
293
+ });
294
+
295
+ // NOTE(joe): assigned on window for embedding API in events.js, and for debugging
296
+ // NOTE(joe): Some of the CPO internals use Q promises. The withResolvers pattern
297
+ // promotes these to real JS promises that will work with e.g. async functions.
298
+ window.RUN_CODE = CPO.RUN_CODE = async function(src) {
299
+ const result = doRunAction(src, true);
300
+ const { promise, resolve, reject } = Promise.withResolvers();
301
+ result.then(resolve);
302
+ result.catch(reject);
303
+ return promise;
304
+ };
305
+ window.RUN_INTERACTION = CPO.RUN_INTERACTION = async function(src) {
306
+ const result = replWidget.runner(src, true);
307
+ const { promise, resolve, reject } = Promise.withResolvers();
308
+ result.then(resolve);
309
+ result.catch(reject);
310
+ return promise;
311
+ };
312
+ window.replWidget = CPO.replWidget = replWidget;
313
+
314
+ /*
315
+ $("#runDropdown").click(function() {
316
+ $("#run-dropdown-content").toggle();
317
+ });
318
+ */
319
+
320
+ // CPO.editor is set in beforePyret.js
321
+ var editor = CPO.editor;
322
+ var currentAction = "run";
323
+
324
+ $("#select-run").click(function() {
325
+ runButton.text("Run");
326
+ currentAction = "run";
327
+ doRunAction(editor.cm.getValue(), false);
328
+ $('#runDropdown').attr('aria-expanded', 'false');
329
+ $("#run-dropdown-content").attr('aria-hidden', 'true').hide();
330
+ });
331
+
332
+ $("#select-tc-run").click(function() {
333
+ runButton.text("Type-check and Run");
334
+ currentAction = "tc-and-run";
335
+ doRunAction(editor.cm.getValue(), false);
336
+ $('#runDropdown').attr('aria-expanded', 'false');
337
+ $("#run-dropdown-content").attr('aria-hidden', 'true').hide();
338
+ });
339
+ /*
340
+ $("#select-scsh").click(function() {
341
+ highlightMode = "scsh"; $("#run-dropdown-content").hide();});
342
+ $("#select-scmh").click(function() {
343
+ highlightMode = "scmh"; $("#run-dropdown-content").hide();});
344
+ $("#select-mcmh").click(function() {
345
+ highlightMode = "mcmh"; $("#run-dropdown-content").hide();});
346
+ */
347
+ function doRunAction(src, synthetic) {
348
+ if(!synthetic) {
349
+ CPO.events.triggerOnRun();
350
+ }
351
+ editor.cm.operation(function() {
352
+ editor.cm.clearGutter("test-marker-gutter");
353
+ var marks = editor.cm.getAllMarks();
354
+ document.getElementById("main").dataset.highlights = "";
355
+ editor.cm.eachLine(function(lh){
356
+ editor.cm.removeLineClass(lh, "background");});
357
+ for(var i = 0; i < marks.length; i++) {
358
+ const attribs = marks[i].attributes;
359
+ if(!(attribs && attribs.useline)) {
360
+ marks[i].clear();
361
+ }
362
+ }
363
+ });
364
+ var sheet = document.getElementById("highlight-styles").sheet;
365
+ for(var i=0; i< sheet.cssRules.length; i++) {
366
+ sheet.deleteRule(i);
367
+ }
368
+ switch (currentAction) {
369
+ case "run":
370
+ return replWidget.runCode(src, {check: true, cm: editor.cm});
371
+ case "tc-and-run":
372
+ return replWidget.runCode(src, {check: true, cm: editor.cm, "type-check": true});
373
+ }
374
+ }
375
+
376
+ runButton.on("click", function() { doRunAction(editor.cm.getValue(), false); });
377
+
378
+ $(window).on("keyup", function(e) {
379
+ if(e.keyCode === 27) { // "ESC"
380
+ $("#help-keys").fadeOut(500);
381
+ e.stopImmediatePropagation();
382
+ e.preventDefault();
383
+ }
384
+ });
385
+
386
+ /* Documentation Overlay */
387
+ /*
388
+ NOTE(joe): Skipping this for now, until HTTPS solution for docs worked out
389
+ $("#docs").on("click", function(e){
390
+ $("#doc-containment").toggle();
391
+ e.stopImmediatePropagation();
392
+ e.preventDefault();
393
+ });
394
+ */
395
+
396
+ $("#doc-close").on("click", function(e){
397
+ $("#doc-containment").toggle();
398
+ e.stopImmediatePropagation();
399
+ e.preventDefault();
400
+ });
401
+
402
+ $("#doc-overlay").draggable({
403
+ start: fixIframe,
404
+ stop: fixIframe,
405
+ handle: "#doc-bar",
406
+ cancel: "#doc-close"
407
+ });
408
+
409
+ $("#doc-overlay").resizable({
410
+ handles: {
411
+ s:"#doc-bottom",
412
+ e: "#doc-right",
413
+ w:"#doc-left",
414
+ sw: "#doc-sw-corner",
415
+ se:"#doc-se-corner"},
416
+ start: fixIframe,
417
+ stop: fixIframe,
418
+ containment: "#doc-containment",
419
+ scroll: false
420
+ });
421
+
422
+ function fixIframe() {
423
+ $("#doc-cover").toggle();
424
+ }
425
+
426
+ $('#font-plus').click(changeFont);
427
+ $('#font-minus').click(changeFont);
428
+
429
+ function changeFont(e){
430
+ fontSize = parseInt($('#main').css("font-size"));
431
+ if ($(e.target).is("#font-plus")) {
432
+ if (fontSize < 32) {
433
+ $('#main').css('font-size', '+=2');
434
+ } else if (fontSize < 55) {
435
+ $('#main').css('font-size', '+=4');
436
+ }
437
+ } else if ($(e.target).is("#font-minus")) {
438
+ if (fontSize > 32) {
439
+ $('#main').css('font-size', '-=4');
440
+ } else if (fontSize > 10) {
441
+ $('#main').css('font-size', '-=2');
442
+ }
443
+ }
444
+ editor.refresh();
445
+ replWidget.refresh();
446
+ updateFontSizeMenuText();
447
+ }
448
+ function formatFontSizeMenuText(size) {
449
+ return "Font size: " + Math.round(parseFloat(size));
450
+ }
451
+ function updateFontSizeMenuText() {
452
+ $('#font-label').text(formatFontSizeMenuText($('#main').css("font-size")));
453
+ }
454
+ updateFontSizeMenuText();
455
+
456
+ var curTheme = document.getElementById("theme-select").value;
457
+ var themeSelect = $("#theme-select");
458
+
459
+ function applyTheme(theme) {
460
+ themeSelect.val(theme);
461
+ $("body").removeClass(curTheme).addClass(theme);
462
+ curTheme = theme;
463
+ }
464
+
465
+ applyTheme(curTheme);
466
+
467
+ $("#theme").change(function(e) {
468
+ var value = e.target.value;
469
+ applyTheme(value);
470
+
471
+ // track theme in local settings
472
+ localSettings.setItem('theme', curTheme);
473
+ });
474
+
475
+ localSettings.change("theme", function(_, newTheme) {
476
+ applyTheme(newTheme);
477
+ });
478
+
479
+ $('.notificationArea').click(function() {$('.notificationArea span').fadeOut(1000);});
480
+
481
+ editor.cm.on('beforeChange', function(instance, changeObj){textHandlers.autoCorrect(instance, changeObj, editor.cm);});
482
+
483
+ // Resizable
484
+ var replHeight = $( "#REPL" ).height();
485
+ var editorEvenSplit = true;
486
+ $( "#REPL" ).resizable({
487
+ maxHeight: replHeight,
488
+ maxWidth: window.innerWidth - 128,
489
+ minHeight: replHeight,
490
+ minWidth: 100,
491
+ handles: {"w": "#handle"}});
492
+
493
+ $( "#REPL" ).on( "resize", leftResize);
494
+ $( "#REPL" ).on( "resize", function() {editorEvenSplit = false;});
495
+
496
+ function leftResize(event, ui) {
497
+ var leftWidth = (window.innerWidth - ui.size.width)
498
+ $(".replMain").css("width", leftWidth + "px");
499
+ editor.refresh();
500
+ replWidget.refresh();
501
+ }
502
+
503
+ $( "#REPL" ).on( "resizestop", toPercent);
504
+
505
+ var rightResizePct = 50;
506
+ var leftResizePct = 50;
507
+
508
+ function toPercent(event, ui) {
509
+ rightResizePct = (ui.size.width / window.innerWidth) * 100
510
+ leftResizePct = 100 - rightResizePct
511
+ setEditorSize(leftResizePct, rightResizePct);
512
+ }
513
+
514
+ $( window ).resize( function() {
515
+ $( "#REPL" ).resizable( "option", "maxWidth", window.innerWidth - 128);
516
+ });
517
+ // End Resizable
518
+
519
+ function setEditorSize(leftPct, rightPct) {
520
+ $( "#REPL" ).css( "width", rightPct + "%");
521
+ $( "#REPL" ).css( "left", leftPct + "%");
522
+ $(".replMain").css("width", leftPct + "%");
523
+ }
524
+
525
+ function toggleEditorSize() {
526
+ if(editorEvenSplit) {
527
+ editorEvenSplit = false;
528
+ setEditorSize(leftResizePct, rightResizePct);
529
+ }
530
+ else {
531
+ editorEvenSplit = true;
532
+ setEditorSize("50", "50");
533
+ }
534
+ }
535
+
536
+ // save
537
+ // On Mac mod ends up mapping to command+s whereas on Windows and Linux it maps to ctrl+s.
538
+ // Saving has a special condition: when embedded we want the Ctrl-S to
539
+ // propagate up. We could fire a special “save” event, but for contexts
540
+ // like VScode it is nice to have the “real” Cmd-S event fire to get
541
+ // good default behavior
542
+ if(!PYRET_IS_EMBEDDED) {
543
+ Mousetrap.bindGlobal('mod+s', function(e) {
544
+ CPO.save();
545
+ e.stopImmediatePropagation();
546
+ e.preventDefault();
547
+ });
548
+ }
549
+
550
+ // resize, Toggle sizing of the editor window between 50% and last resize
551
+ Mousetrap.bindGlobal('ctrl+m', function(e){
552
+ toggleEditorSize();
553
+ e.stopImmediatePropagation();
554
+ e.preventDefault();
555
+ });
556
+
557
+ // run the definitions area
558
+ Mousetrap.bindGlobal('ctrl+enter', function(e){
559
+ doRunAction(editor.cm.getValue(), false);
560
+ CPO.autoSave();
561
+ e.stopImmediatePropagation();
562
+ e.preventDefault();
563
+ });
564
+
565
+ function reciteHelp() {
566
+ CPO.sayAndForget(
567
+ "Press Escape to exit help. " +
568
+ "Control question mark: recite help. " +
569
+ "Control s: save. " +
570
+ "F6 and shift-F6: cycle focus through regions. " +
571
+ "F7 or Control enter: run the code in the definitions window. " +
572
+ "F11: insert image. " +
573
+ "Control left: move cursor left by one word. " +
574
+ "Control right: move cursor right by one word. " +
575
+ "Alt left: if cursor is just before a right parenthesis or end keyword, " +
576
+ "move left to matching delimiter, " +
577
+ "otherwise move left by one word. " +
578
+ "Alt right: like alt left, but move right. " +
579
+ "Escape left: synonym for alt left, in case alt key is used by browser. " +
580
+ "Escape right: synonym for alt right."
581
+ );
582
+ }
583
+
584
+ // pull up help menu
585
+ Mousetrap.bindGlobal('ctrl+shift+/', function(e) {
586
+ $("#help-keys").fadeIn(100);
587
+ reciteHelp();
588
+ e.stopImmediatePropagation();
589
+ e.preventDefault();
590
+ });
591
+
592
+ $('#ctrl-question').click(function() {
593
+ $('#help-keys').fadeIn(100);
594
+ reciteHelp();
595
+ });
596
+
597
+ Mousetrap.bindGlobal('f6', function(e) {
598
+ // cycle focus (forward)
599
+ CPO.cycleFocus();
600
+ e.stopImmediatePropagation();
601
+ e.preventDefault();
602
+ });
603
+
604
+ Mousetrap.bindGlobal('shift+f6', function(e) {
605
+ // cycle focus backward
606
+ CPO.cycleFocus(true);
607
+ e.stopImmediatePropagation();
608
+ e.preventDefault();
609
+ });
610
+
611
+ Mousetrap.bindGlobal('shift+tab', function(e) {
612
+ // cycle focus backward
613
+ //console.log('mouse shift+tab')
614
+ CPO.cycleFocus(true);
615
+ e.stopImmediatePropagation();
616
+ e.preventDefault();
617
+ });
618
+
619
+ Mousetrap.bindGlobal('f7', function(e) {
620
+ doRunAction(editor.cm.getValue(), false);
621
+ CPO.autoSave();
622
+ e.stopImmediatePropagation();
623
+ e.preventDefault();
624
+ });
625
+
626
+ Mousetrap.bindGlobal('f8', function(e) {
627
+ $('#breakButton').click();
628
+ e.stopImmediatePropagation();
629
+ e.preventDefault();
630
+ });
631
+
632
+ Mousetrap.bindGlobal('f9', function(e) {
633
+ var sc = $('#shareContainer');
634
+ if (sc) {
635
+ var sl = sc[0].childNodes[0];
636
+ sl.click();
637
+ }
638
+ e.stopImmediatePropagation();
639
+ e.preventDefault();
640
+ });
641
+
642
+ Mousetrap.bindGlobal('f11', function(e) {
643
+ $('#insert').click();
644
+ e.stopImmediatePropagation();
645
+ e.preventDefault();
646
+ });
647
+
648
+ // Used for image definition naming (identifier: "img" + curImg)
649
+ var curImg = 0;
650
+
651
+ /**
652
+ * Sets curImg to a value which will not clash with the code in
653
+ * contents (note: this is done conservatively -- the estimation
654
+ * is "dumb" in that it pays no attention to token types)
655
+ */
656
+ function inferCurImg(contents) {
657
+ var query = /img([0-9]+)[\s\n\r]*=/g;
658
+ var maxSoFar = 0;
659
+ var res;
660
+ while((res = query.exec(contents)) !== null) {
661
+ maxSoFar = Math.max(maxSoFar, Number(res[1]));
662
+ }
663
+ curImg = maxSoFar + 1;
664
+ }
665
+
666
+ var photoPrompt = function(count) {
667
+ var plural = count > 1;
668
+ return new modalPrompt({
669
+ title: "Import options",
670
+ style: "radio",
671
+ submitText: "Import",
672
+ cancelText: "Close",
673
+ options: [
674
+ {
675
+ message: "Value" + (plural ? "s" : ""),
676
+ value: "values",
677
+ example: 'image-url("<URL>")'
678
+ + (plural ? '\n'
679
+ + (count > 2 ? '# ' + (count-2) + ' more...\n': '')
680
+ + 'image-url("<URL>")'
681
+ : '')
682
+ },
683
+ {
684
+ message: "Definition" + (plural ? "s" : ""),
685
+ value: "defs",
686
+ example: 'image' + (plural ? '0' : '') + ' = image-url("<URL>")'
687
+ + (plural ?
688
+ (count > 2 ? '\n# ' + (count-2) + ' more...' : '')
689
+ + '\nimage' + (count-1) + ' = image-url("<URL>")'
690
+ : '')
691
+ },
692
+ {
693
+ message: "List",
694
+ value: "list",
695
+ example: '[list: image-url("<URL>")'
696
+ + (plural ?
697
+ ',\n' + (count > 2 ? ' # ' + (count-2) + ' more...\n' : '')
698
+ + ' image-url("<URL>")]'
699
+ : ']')
700
+ }]
701
+ });
702
+ }
703
+
704
+ var lastSave = 0;
705
+ function handlePickerData(documents, picker, drive) {
706
+ // File loaded
707
+ if (documents[0][picker.Document.TYPE] === "file") {
708
+ var id = documents[0][picker.Document.ID];
709
+ function load(here) {
710
+ if(here) {
711
+
712
+ window.CPO.save().then(function() {
713
+ var p = drive.getFileById(id);
714
+
715
+ window.CPO.showShareContainer(p);
716
+ history.pushState(null, null, "#program=" + id);
717
+ window.CPO.loadProgram(p).then(function(contents) {
718
+ window.CPO.editor.cm.setValue(contents);
719
+ window.CPO.editor.cm.clearHistory();
720
+ });
721
+ })
722
+ .fail(function(err) {
723
+ window.flashMessage("Currently unable to save, try opening that file in a new tab");
724
+ });
725
+ }
726
+ else {
727
+ window.open(window.APP_BASE_URL + "/editor#program=" + id, "_blank");
728
+ }
729
+ }
730
+ function openFile(id) {
731
+ var filePrompt = new modalPrompt({
732
+ title: "Where would you like to open the file?",
733
+ style: "tiles",
734
+ hideSubmit: true,
735
+ options: [
736
+ {
737
+ message: "Open here",
738
+ details: "The current file will be saved first",
739
+ on: {click: function() {
740
+ load(true);
741
+ filePrompt.onClose();
742
+ }}
743
+ },
744
+ {
745
+ message: "Open in new tab",
746
+ details: "The current file will remain open in this tab",
747
+ on: {click: function() {
748
+ load(false);
749
+ filePrompt.onClose();
750
+ }}
751
+ }]
752
+ });
753
+ filePrompt.show();
754
+ }
755
+ openFile(documents[0][picker.Document.ID]);
756
+ }
757
+ // Picture loaded
758
+ else if (documents[0][picker.Document.TYPE] === picker.Type.PHOTO) {
759
+
760
+ try {
761
+ photoPrompt(documents.length).show(function(res) {
762
+ // Name of event for CM undo history
763
+ var origin = "+insertImage" + curImg;
764
+ var asValues = (res === "values");
765
+ var asDefs = (res === "defs");
766
+ var asList = (res === "list");
767
+ if (!(asValues || asDefs || asList)) {
768
+ // Check for garbage and log it
769
+ if (res !== null) {
770
+ console.warn("Unknown photoPrompt response: ", res);
771
+ }
772
+ return;
773
+ }
774
+ // http://stackoverflow.com/questions/23733455/inserting-a-new-text-at-given-cursor-position
775
+ var cm = CPO.editor.cm;
776
+ var doc = cm.getDoc();
777
+ function placeInEditor(str) {
778
+ var cursor = doc.getCursor();
779
+ var line = doc.getLine(cursor.line);
780
+ var pos = {
781
+ line: cursor.line,
782
+ ch: line.length
783
+ };
784
+ doc.replaceRange(str, pos, undefined, origin);
785
+ reindent(cursor.line);
786
+ }
787
+ function reindent(line) {
788
+ cm.indentLine(line || doc.getCursor().line);
789
+ }
790
+ function emitNewline() {
791
+ var cursor = doc.getCursor();
792
+ placeInEditor('\n');
793
+ // FIXME: Dunno why this happens.
794
+ if (cursor.line === doc.getCursor().line) {
795
+ doc.setCursor({line: cursor.line + 1, ch: 0});
796
+ }
797
+ }
798
+ function emitLn(s) {
799
+ placeInEditor(s);
800
+ emitNewline();
801
+ }
802
+ function onEmptyLine() {
803
+ var cursor = doc.getCursor("to");
804
+ var line = doc.getLine(cursor.line);
805
+ return (/^\s*$/.test(line));
806
+ }
807
+ // Make newline at cursor position if we are not on an empty line
808
+ if (onEmptyLine()) {
809
+ reindent();
810
+ } else {
811
+ emitNewline();
812
+ }
813
+ if (asList) {
814
+ placeInEditor("[list:");
815
+ }
816
+ documents.forEach(function(d, idx) {
817
+ var pathToImg = '"' + window.APP_BASE_URL + "/shared-image-contents?sharedImageId="
818
+ + d.id + '"';
819
+ var outstr = asDefs ? ("img" + curImg + " = ") : "";
820
+ ++curImg;
821
+ outstr += "image-url(" + pathToImg + ")";
822
+ var isLast = (idx === (documents.length - 1));
823
+ if (asList) {
824
+ if (idx === 0) {
825
+ // The space after ":" gets eaten, so we need to enter it here
826
+ outstr = ' ' + outstr;
827
+ }
828
+ outstr += isLast ? "]" : ",";
829
+ }
830
+ if (isLast) {
831
+ placeInEditor(outstr);
832
+ } else {
833
+ emitLn(outstr);
834
+ }
835
+ });
836
+ });
837
+ }
838
+ catch(e) {
839
+ console.error("The show() function failed: ", e);
840
+ }
841
+ } else {
842
+ flashError("Invalid file type: " + documents[0][picker.Document.TYPE]);
843
+ }
844
+ }
845
+ var insertPicker = new FilePicker({
846
+ onLoaded: function() {
847
+ $("#insert").attr("disabled", false);
848
+ insertPicker.openOn($("#insert")[0], "click");
849
+ },
850
+ onSelect: handlePickerData,
851
+ onError: flashError,
852
+ onInternalError: stickError,
853
+ views: ["imageView"],
854
+ title: "Select images"
855
+ });
856
+
857
+ return runtime.makeModuleReturn({
858
+ repl: runtime.makeOpaque(repl)
859
+ }, {});
860
+ }
861
+ }
862
+ })