GoPLS Viewer

Home|gopls/godoc/static/playground.js
1// Copyright 2012 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5/*
6In the absence of any formal way to specify interfaces in JavaScript,
7here's a skeleton implementation of a playground transport.
8
9        function Transport() {
10                // Set up any transport state (eg, make a websocket connection).
11                return {
12                        Run: function(body, output, options) {
13                                // Compile and run the program 'body' with 'options'.
14                // Call the 'output' callback to display program output.
15                                return {
16                                        Kill: function() {
17                                                // Kill the running program.
18                                        }
19                                };
20                        }
21                };
22        }
23
24    // The output callback is called multiple times, and each time it is
25    // passed an object of this form.
26        var write = {
27                Kind: 'string', // 'start', 'stdout', 'stderr', 'end'
28                Body: 'string'  // content of write or end status message
29        }
30
31    // The first call must be of Kind 'start' with no body.
32    // Subsequent calls may be of Kind 'stdout' or 'stderr'
33    // and must have a non-null Body string.
34    // The final call should be of Kind 'end' with an optional
35    // Body string, signifying a failure ("killed", for example).
36
37    // The output callback must be of this form.
38    // See PlaygroundOutput (below) for an implementation.
39        function outputCallback(write) {
40        }
41*/
42
43// HTTPTransport is the default transport.
44// enableVet enables running vet if a program was compiled and ran successfully.
45// If vet returned any errors, display them before the output of a program.
46function HTTPTransport(enableVet) {
47  'use strict';
48
49  function playback(output, data) {
50    // Backwards compatibility: default values do not affect the output.
51    var events = data.Events || [];
52    var errors = data.Errors || '';
53    var status = data.Status || 0;
54    var isTest = data.IsTest || false;
55    var testsFailed = data.TestsFailed || 0;
56
57    var timeout;
58    output({ Kind: 'start' });
59    function next() {
60      if (!events || events.length === 0) {
61        if (isTest) {
62          if (testsFailed > 0) {
63            output({
64              Kind: 'system',
65              Body:
66                '\n' +
67                testsFailed +
68                ' test' +
69                (testsFailed > 1 ? 's' : '') +
70                ' failed.',
71            });
72          } else {
73            output({ Kind: 'system', Body: '\nAll tests passed.' });
74          }
75        } else {
76          if (status > 0) {
77            output({ Kind: 'end', Body: 'status ' + status + '.' });
78          } else {
79            if (errors !== '') {
80              // errors are displayed only in the case of timeout.
81              output({ Kind: 'end', Body: errors + '.' });
82            } else {
83              output({ Kind: 'end' });
84            }
85          }
86        }
87        return;
88      }
89      var e = events.shift();
90      if (e.Delay === 0) {
91        output({ Kind: e.Kind, Body: e.Message });
92        next();
93        return;
94      }
95      timeout = setTimeout(function() {
96        output({ Kind: e.Kind, Body: e.Message });
97        next();
98      }, e.Delay / 1000000);
99    }
100    next();
101    return {
102      Stop: function() {
103        clearTimeout(timeout);
104      },
105    };
106  }
107
108  function error(output, msg) {
109    output({ Kind: 'start' });
110    output({ Kind: 'stderr', Body: msg });
111    output({ Kind: 'end' });
112  }
113
114  function buildFailed(output, msg) {
115    output({ Kind: 'start' });
116    output({ Kind: 'stderr', Body: msg });
117    output({ Kind: 'system', Body: '\nGo build failed.' });
118  }
119
120  var seq = 0;
121  return {
122    Run: function(body, output, options) {
123      seq++;
124      var cur = seq;
125      var playing;
126      $.ajax('/compile', {
127        type: 'POST',
128        data: { version: 2, body: body, withVet: enableVet },
129        dataType: 'json',
130        success: function(data) {
131          if (seq != cur) return;
132          if (!data) return;
133          if (playing != null) playing.Stop();
134          if (data.Errors) {
135            if (data.Errors === 'process took too long') {
136              // Playback the output that was captured before the timeout.
137              playing = playback(output, data);
138            } else {
139              buildFailed(output, data.Errors);
140            }
141            return;
142          }
143          if (!data.Events) {
144            data.Events = [];
145          }
146          if (data.VetErrors) {
147            // Inject errors from the vet as the first events in the output.
148            data.Events.unshift({
149              Message: 'Go vet exited.\n\n',
150              Kind: 'system',
151              Delay: 0,
152            });
153            data.Events.unshift({
154              Message: data.VetErrors,
155              Kind: 'stderr',
156              Delay: 0,
157            });
158          }
159
160          if (!enableVet || data.VetOK || data.VetErrors) {
161            playing = playback(output, data);
162            return;
163          }
164
165          // In case the server support doesn't support
166          // compile+vet in same request signaled by the
167          // 'withVet' parameter above, also try the old way.
168          // TODO: remove this when it falls out of use.
169          // It is 2019-05-13 now.
170          $.ajax('/vet', {
171            data: { body: body },
172            type: 'POST',
173            dataType: 'json',
174            success: function(dataVet) {
175              if (dataVet.Errors) {
176                // inject errors from the vet as the first events in the output
177                data.Events.unshift({
178                  Message: 'Go vet exited.\n\n',
179                  Kind: 'system',
180                  Delay: 0,
181                });
182                data.Events.unshift({
183                  Message: dataVet.Errors,
184                  Kind: 'stderr',
185                  Delay: 0,
186                });
187              }
188              playing = playback(output, data);
189            },
190            error: function() {
191              playing = playback(output, data);
192            },
193          });
194        },
195        error: function() {
196          error(output, 'Error communicating with remote server.');
197        },
198      });
199      return {
200        Kill: function() {
201          if (playing != null) playing.Stop();
202          output({ Kind: 'end', Body: 'killed' });
203        },
204      };
205    },
206  };
207}
208
209function SocketTransport() {
210  'use strict';
211
212  var id = 0;
213  var outputs = {};
214  var started = {};
215  var websocket;
216  if (window.location.protocol == 'http:') {
217    websocket = new WebSocket('ws://' + window.location.host + '/socket');
218  } else if (window.location.protocol == 'https:') {
219    websocket = new WebSocket('wss://' + window.location.host + '/socket');
220  }
221
222  websocket.onclose = function() {
223    console.log('websocket connection closed');
224  };
225
226  websocket.onmessage = function(e) {
227    var m = JSON.parse(e.data);
228    var output = outputs[m.Id];
229    if (output === null) return;
230    if (!started[m.Id]) {
231      output({ Kind: 'start' });
232      started[m.Id] = true;
233    }
234    output({ Kind: m.Kind, Body: m.Body });
235  };
236
237  function send(m) {
238    websocket.send(JSON.stringify(m));
239  }
240
241  return {
242    Run: function(body, output, options) {
243      var thisID = id + '';
244      id++;
245      outputs[thisID] = output;
246      send({ Id: thisID, Kind: 'run', Body: body, Options: options });
247      return {
248        Kill: function() {
249          send({ Id: thisID, Kind: 'kill' });
250        },
251      };
252    },
253  };
254}
255
256function PlaygroundOutput(el) {
257  'use strict';
258
259  return function(write) {
260    if (write.Kind == 'start') {
261      el.innerHTML = '';
262      return;
263    }
264
265    var cl = 'system';
266    if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind;
267
268    var m = write.Body;
269    if (write.Kind == 'end') {
270      m = '\nProgram exited' + (m ? ': ' + m : '.');
271    }
272
273    if (m.indexOf('IMAGE:') === 0) {
274      // TODO(adg): buffer all writes before creating image
275      var url = 'data:image/png;base64,' + m.substr(6);
276      var img = document.createElement('img');
277      img.src = url;
278      el.appendChild(img);
279      return;
280    }
281
282    // ^L clears the screen.
283    var s = m.split('\x0c');
284    if (s.length > 1) {
285      el.innerHTML = '';
286      m = s.pop();
287    }
288
289    m = m.replace(/&/g, '&');
290    m = m.replace(/</g, '&lt;');
291    m = m.replace(/>/g, '&gt;');
292
293    var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight;
294
295    var span = document.createElement('span');
296    span.className = cl;
297    span.innerHTML = m;
298    el.appendChild(span);
299
300    if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight;
301  };
302}
303
304(function() {
305  function lineHighlight(error) {
306    var regex = /prog.go:([0-9]+)/g;
307    var r = regex.exec(error);
308    while (r) {
309      $('.lines div')
310        .eq(r[1] - 1)
311        .addClass('lineerror');
312      r = regex.exec(error);
313    }
314  }
315  function highlightOutput(wrappedOutput) {
316    return function(write) {
317      if (write.Body) lineHighlight(write.Body);
318      wrappedOutput(write);
319    };
320  }
321  function lineClear() {
322    $('.lineerror').removeClass('lineerror');
323  }
324
325  // opts is an object with these keys
326  //  codeEl - code editor element
327  //  outputEl - program output element
328  //  runEl - run button element
329  //  fmtEl - fmt button element (optional)
330  //  fmtImportEl - fmt "imports" checkbox element (optional)
331  //  shareEl - share button element (optional)
332  //  shareURLEl - share URL text input element (optional)
333  //  shareRedirect - base URL to redirect to on share (optional)
334  //  toysEl - toys select element (optional)
335  //  enableHistory - enable using HTML5 history API (optional)
336  //  transport - playground transport to use (default is HTTPTransport)
337  //  enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
338  //  enableVet - enable running vet and displaying its errors
339  function playground(opts) {
340    var code = $(opts.codeEl);
341    var transport = opts['transport'] || new HTTPTransport(opts['enableVet']);
342    var running;
343
344    // autoindent helpers.
345    function insertTabs(n) {
346      // find the selection start and end
347      var start = code[0].selectionStart;
348      var end = code[0].selectionEnd;
349      // split the textarea content into two, and insert n tabs
350      var v = code[0].value;
351      var u = v.substr(0, start);
352      for (var i = 0; i < n; i++) {
353        u += '\t';
354      }
355      u += v.substr(end);
356      // set revised content
357      code[0].value = u;
358      // reset caret position after inserted tabs
359      code[0].selectionStart = start + n;
360      code[0].selectionEnd = start + n;
361    }
362    function autoindent(el) {
363      var curpos = el.selectionStart;
364      var tabs = 0;
365      while (curpos > 0) {
366        curpos--;
367        if (el.value[curpos] == '\t') {
368          tabs++;
369        } else if (tabs > 0 || el.value[curpos] == '\n') {
370          break;
371        }
372      }
373      setTimeout(function() {
374        insertTabs(tabs);
375      }, 1);
376    }
377
378    // NOTE(cbro): e is a jQuery event, not a DOM event.
379    function handleSaveShortcut(e) {
380      if (e.isDefaultPrevented()) return false;
381      if (!e.metaKey && !e.ctrlKey) return false;
382      if (e.key != 'S' && e.key != 's') return false;
383
384      e.preventDefault();
385
386      // Share and save
387      share(function(url) {
388        window.location.href = url + '.go?download=true';
389      });
390
391      return true;
392    }
393
394    function keyHandler(e) {
395      if (opts.enableShortcuts && handleSaveShortcut(e)) return;
396
397      if (e.keyCode == 9 && !e.ctrlKey) {
398        // tab (but not ctrl-tab)
399        insertTabs(1);
400        e.preventDefault();
401        return false;
402      }
403      if (e.keyCode == 13) {
404        // enter
405        if (e.shiftKey) {
406          // +shift
407          run();
408          e.preventDefault();
409          return false;
410        }
411        if (e.ctrlKey) {
412          // +control
413          fmt();
414          e.preventDefault();
415        } else {
416          autoindent(e.target);
417        }
418      }
419      return true;
420    }
421    code.unbind('keydown').bind('keydown', keyHandler);
422    var outdiv = $(opts.outputEl).empty();
423    var output = $('<pre/>').appendTo(outdiv);
424
425    function body() {
426      return $(opts.codeEl).val();
427    }
428    function setBody(text) {
429      $(opts.codeEl).val(text);
430    }
431    function origin(href) {
432      return ('' + href)
433        .split('/')
434        .slice(0, 3)
435        .join('/');
436    }
437
438    var pushedEmpty = window.location.pathname == '/';
439    function inputChanged() {
440      if (pushedEmpty) {
441        return;
442      }
443      pushedEmpty = true;
444      $(opts.shareURLEl).hide();
445      window.history.pushState(null, '', '/');
446    }
447    function popState(e) {
448      if (e === null) {
449        return;
450      }
451      if (e && e.state && e.state.code) {
452        setBody(e.state.code);
453      }
454    }
455    var rewriteHistory = false;
456    if (
457      window.history &&
458      window.history.pushState &&
459      window.addEventListener &&
460      opts.enableHistory
461    ) {
462      rewriteHistory = true;
463      code[0].addEventListener('input', inputChanged);
464      window.addEventListener('popstate', popState);
465    }
466
467    function setError(error) {
468      if (running) running.Kill();
469      lineClear();
470      lineHighlight(error);
471      output
472        .empty()
473        .addClass('error')
474        .text(error);
475    }
476    function loading() {
477      lineClear();
478      if (running) running.Kill();
479      output.removeClass('error').text('Waiting for remote server...');
480    }
481    function run() {
482      loading();
483      running = transport.Run(
484        body(),
485        highlightOutput(PlaygroundOutput(output[0]))
486      );
487    }
488
489    function fmt() {
490      loading();
491      var data = { body: body() };
492      if ($(opts.fmtImportEl).is(':checked')) {
493        data['imports'] = 'true';
494      }
495      $.ajax('/fmt', {
496        data: data,
497        type: 'POST',
498        dataType: 'json',
499        success: function(data) {
500          if (data.Error) {
501            setError(data.Error);
502          } else {
503            setBody(data.Body);
504            setError('');
505          }
506        },
507      });
508    }
509
510    var shareURL; // jQuery element to show the shared URL.
511    var sharing = false; // true if there is a pending request.
512    var shareCallbacks = [];
513    function share(opt_callback) {
514      if (opt_callback) shareCallbacks.push(opt_callback);
515
516      if (sharing) return;
517      sharing = true;
518
519      var sharingData = body();
520      $.ajax('https://play.golang.org/share', {
521        processData: false,
522        data: sharingData,
523        type: 'POST',
524        contentType: 'text/plain; charset=utf-8',
525        complete: function(xhr) {
526          sharing = false;
527          if (xhr.status != 200) {
528            alert('Server error; try again.');
529            return;
530          }
531          if (opts.shareRedirect) {
532            window.location = opts.shareRedirect + xhr.responseText;
533          }
534          var path = '/p/' + xhr.responseText;
535          var url = origin(window.location) + path;
536
537          for (var i = 0; i < shareCallbacks.length; i++) {
538            shareCallbacks[i](url);
539          }
540          shareCallbacks = [];
541
542          if (shareURL) {
543            shareURL
544              .show()
545              .val(url)
546              .focus()
547              .select();
548
549            if (rewriteHistory) {
550              var historyData = { code: sharingData };
551              window.history.pushState(historyData, '', path);
552              pushedEmpty = false;
553            }
554          }
555        },
556      });
557    }
558
559    $(opts.runEl).click(run);
560    $(opts.fmtEl).click(fmt);
561
562    if (
563      opts.shareEl !== null &&
564      (opts.shareURLEl !== null || opts.shareRedirect !== null)
565    ) {
566      if (opts.shareURLEl) {
567        shareURL = $(opts.shareURLEl).hide();
568      }
569      $(opts.shareEl).click(function() {
570        share();
571      });
572    }
573
574    if (opts.toysEl !== null) {
575      $(opts.toysEl).bind('change', function() {
576        var toy = $(this).val();
577        $.ajax('/doc/play/' + toy, {
578          processData: false,
579          type: 'GET',
580          complete: function(xhr) {
581            if (xhr.status != 200) {
582              alert('Server error; try again.');
583              return;
584            }
585            setBody(xhr.responseText);
586          },
587        });
588      });
589    }
590  }
591
592  window.playground = playground;
593})();
594
MembersX
Members
X