golang.org/x/tools@v0.21.0/godoc/static/playground.js (about)

     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  /*
     6  In the absence of any formal way to specify interfaces in JavaScript,
     7  here'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.
    46  function 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  
   209  function 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  
   256  function 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  })();