blob: 06204c78717f200783fdd3cd8778efb97240bdd0 [file] [log] [blame]
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
In the absence of any formal way to specify interfaces in JavaScript,
here's a skeleton implementation of a playground transport.
function Transport() {
// Set up any transport state (eg, make a websocket connection).
return {
Run: function(body, output, options) {
// Compile and run the program 'body' with 'options'.
// Call the 'output' callback to display program output.
return {
Kill: function() {
// Kill the running program.
}
};
}
};
}
// The output callback is called multiple times, and each time it is
// passed an object of this form.
var write = {
Kind: 'string', // 'start', 'stdout', 'stderr', 'end'
Body: 'string' // content of write or end status message
}
// The first call must be of Kind 'start' with no body.
// Subsequent calls may be of Kind 'stdout' or 'stderr'
// and must have a non-null Body string.
// The final call should be of Kind 'end' with an optional
// Body string, signifying a failure ("killed", for example).
// The output callback must be of this form.
// See PlaygroundOutput (below) for an implementation.
function outputCallback(write) {
}
*/
// HTTPTransport is the default transport.
// enableVet enables running vet if a program was compiled and ran successfully.
// If vet returned any errors, display them before the output of a program.
function HTTPTransport(enableVet) {
'use strict';
function playback(output, data) {
// Backwards compatibility: default values do not affect the output.
var events = data.Events || [];
var errors = data.Errors || '';
var status = data.Status || 0;
var isTest = data.IsTest || false;
var testsFailed = data.TestsFailed || 0;
var timeout;
output({ Kind: 'start' });
function next() {
if (!events || events.length === 0) {
if (isTest) {
if (testsFailed > 0) {
output({
Kind: 'system',
Body:
'\n' +
testsFailed +
' test' +
(testsFailed > 1 ? 's' : '') +
' failed.',
});
} else {
output({ Kind: 'system', Body: '\nAll tests passed.' });
}
} else {
if (status > 0) {
output({ Kind: 'end', Body: 'status ' + status + '.' });
} else {
if (errors !== '') {
// errors are displayed only in the case of timeout.
output({ Kind: 'end', Body: errors + '.' });
} else {
output({ Kind: 'end' });
}
}
}
return;
}
var e = events.shift();
if (e.Delay === 0) {
output({ Kind: e.Kind, Body: e.Message });
next();
return;
}
timeout = setTimeout(function() {
output({ Kind: e.Kind, Body: e.Message });
next();
}, e.Delay / 1000000);
}
next();
return {
Stop: function() {
clearTimeout(timeout);
},
};
}
function error(output, msg) {
output({ Kind: 'start' });
output({ Kind: 'stderr', Body: msg });
output({ Kind: 'end' });
}
function buildFailed(output, msg) {
output({ Kind: 'start' });
output({ Kind: 'stderr', Body: msg });
output({ Kind: 'system', Body: '\nGo build failed.' });
}
var seq = 0;
return {
Run: function(body, output, options) {
seq++;
var cur = seq;
var playing;
$.ajax('/_/compile?backend=' + (options.backend || ''), {
type: 'POST',
data: { version: 2, body: body, withVet: enableVet },
dataType: 'json',
success: function(data) {
if (seq != cur) return;
if (!data) return;
if (playing != null) playing.Stop();
if (data.Errors) {
if (data.Errors === 'process took too long') {
// Playback the output that was captured before the timeout.
playing = playback(output, data);
} else {
buildFailed(output, data.Errors);
}
return;
}
playing = playback(output, data);
},
error: function() {
error(output, 'Error communicating with remote server.');
},
});
return {
Kill: function() {
if (playing != null) playing.Stop();
output({ Kind: 'end', Body: 'killed' });
},
};
},
};
}
function SocketTransport() {
'use strict';
var id = 0;
var outputs = {};
var started = {};
var websocket;
if (window.location.protocol == 'http:') {
websocket = new WebSocket('ws://' + window.location.host + '/socket');
} else if (window.location.protocol == 'https:') {
websocket = new WebSocket('wss://' + window.location.host + '/socket');
}
websocket.onclose = function() {
console.log('websocket connection closed');
};
websocket.onmessage = function(e) {
var m = JSON.parse(e.data);
var output = outputs[m.Id];
if (output === null) return;
if (!started[m.Id]) {
output({ Kind: 'start' });
started[m.Id] = true;
}
output({ Kind: m.Kind, Body: m.Body });
};
function send(m) {
websocket.send(JSON.stringify(m));
}
return {
Run: function(body, output, options) {
var thisID = id + '';
id++;
outputs[thisID] = output;
send({ Id: thisID, Kind: 'run', Body: body, Options: options });
return {
Kill: function() {
send({ Id: thisID, Kind: 'kill' });
},
};
},
};
}
function PlaygroundOutput(el) {
'use strict';
return function(write) {
if (write.Kind == 'start') {
el.innerHTML = '';
return;
}
var cl = 'system';
if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind;
var m = write.Body;
if (write.Kind == 'end') {
m = '\nProgram exited' + (m ? ': ' + m : '.');
}
if (m.indexOf('IMAGE:') === 0) {
// TODO(adg): buffer all writes before creating image
var url = 'data:image/png;base64,' + m.substr(6);
var img = document.createElement('img');
img.src = url;
el.appendChild(img);
return;
}
// ^L clears the screen.
var s = m.split('\x0c');
if (s.length > 1) {
el.innerHTML = '';
m = s.pop();
}
m = m.replace(/&/g, '&');
m = m.replace(/</g, '&lt;');
m = m.replace(/>/g, '&gt;');
var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight;
var span = document.createElement('span');
span.className = cl;
span.innerHTML = m;
el.appendChild(span);
if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight;
};
}
(function() {
function lineHighlight(error) {
var regex = /prog.go:([0-9]+)/g;
var r = regex.exec(error);
while (r) {
$('.lines div')
.eq(r[1] - 1)
.addClass('lineerror');
r = regex.exec(error);
}
}
function highlightOutput(wrappedOutput) {
return function(write) {
if (write.Body) lineHighlight(write.Body);
wrappedOutput(write);
};
}
function lineClear() {
$('.lineerror').removeClass('lineerror');
}
// opts is an object with these keys
// codeEl - code editor element
// outputEl - program output element
// runEl - run button element
// fmtEl - fmt button element (optional)
// fmtImportEl - fmt "imports" checkbox element (optional)
// shareEl - share button element (optional)
// shareURLEl - share URL text input element (optional)
// shareRedirect - base URL to redirect to on share (optional)
// toysEl - toys select element (optional)
// enableHistory - enable using HTML5 history API (optional)
// transport - playground transport to use (default is HTTPTransport)
// enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
// enableVet - enable running vet and displaying its errors
function playground(opts) {
var code = $(opts.codeEl);
var transport = opts['transport'] || new HTTPTransport(opts['enableVet']);
var running;
// autoindent helpers.
function insertTabs(n) {
// Without the n > 0 check, Safari cannot type a blank line at the bottom of a playground snippet.
// See go.dev/issue/49794.
if (n > 0) {
document.execCommand('insertText', false, '\t'.repeat(n));
}
}
function autoindent(el) {
var curpos = el.selectionStart;
var tabs = 0;
while (curpos > 0) {
curpos--;
if (el.value[curpos] == '\t') {
tabs++;
} else if (tabs > 0 || el.value[curpos] == '\n') {
break;
}
}
setTimeout(function() {
insertTabs(tabs);
}, 1);
}
// NOTE(cbro): e is a jQuery event, not a DOM event.
function handleSaveShortcut(e) {
if (e.isDefaultPrevented()) return false;
if (!e.metaKey && !e.ctrlKey) return false;
if (e.key != 'S' && e.key != 's') return false;
e.preventDefault();
// Share and save
share(function(url) {
window.location.href = url + '.go?download=true';
});
return true;
}
function keyHandler(e) {
if (opts.enableShortcuts && handleSaveShortcut(e)) return;
if (e.keyCode == 9 && !e.ctrlKey) {
// tab (but not ctrl-tab)
insertTabs(1);
e.preventDefault();
return false;
}
if (e.keyCode == 13) {
// enter
if (e.shiftKey) {
// +shift
run();
e.preventDefault();
return false;
}
if (e.ctrlKey) {
// +control
fmt();
e.preventDefault();
} else {
autoindent(e.target);
}
}
return true;
}
code.unbind('keydown').bind('keydown', keyHandler);
var outdiv = $(opts.outputEl).empty();
var output = $('<pre/>').appendTo(outdiv);
function body() {
return $(opts.codeEl).val();
}
function setBody(text) {
$(opts.codeEl).val(text);
}
function origin(href) {
return ('' + href)
.split('/')
.slice(0, 3)
.join('/');
}
var pushedPlay = window.location.pathname == '/play/';
function inputChanged() {
if (pushedPlay) {
return;
}
pushedPlay = true;
$(opts.shareURLEl).hide();
$(opts.toysEl).show();
var path = window.location.pathname;
var i = path.indexOf('/play/');
var p = path.substr(0, i+6);
if (opts.versionEl !== null) {
var v = $(opts.versionEl).val();
if (v != '') {
p += '?v=' + v;
}
}
window.history.pushState(null, '', p);
}
function popState(e) {
if (e === null) {
return;
}
if (e && e.state && e.state.code) {
setBody(e.state.code);
}
}
var rewriteHistory = false;
if (
window.history &&
window.history.pushState &&
window.addEventListener &&
opts.enableHistory
) {
rewriteHistory = true;
code[0].addEventListener('input', inputChanged);
window.addEventListener('popstate', popState);
}
function backend() {
if (!opts.versionEl) {
return '';
}
var vers = $(opts.versionEl);
if (!vers) {
return '';
}
return vers.val();
}
function setError(error) {
if (running) running.Kill();
lineClear();
lineHighlight(error);
output
.empty()
.addClass('error')
.text(error);
}
function loading() {
lineClear();
if (running) running.Kill();
output.removeClass('error').text('Waiting for remote server...');
}
function runOnly() {
loading();
running = transport.Run(
body(),
highlightOutput(PlaygroundOutput(output[0])),
{backend: backend()},
);
}
function fmtAnd(run) {
loading();
var data = { body: body() };
data['imports'] = 'true';
$.ajax('/_/fmt?backend='+backend(), {
data: data,
type: 'POST',
dataType: 'json',
success: function(data) {
if (data.Error) {
setError(data.Error);
} else {
setBody(data.Body);
setError('');
}
run();
},
error: function() {
setError('Error communicating with remote server.');
},
});
}
function loadShare(id) {
$.ajax('/_/share?id='+id, {
processData: false,
type: 'GET',
complete: function(xhr) {
if(xhr.status != 200) {
setBody('Cannot load shared snippet; try again.');
return;
}
setBody(xhr.responseText);
},
})
}
function fmt() {
fmtAnd(function(){});
}
function run() {
fmtAnd(runOnly);
}
var shareURL; // jQuery element to show the shared URL.
var sharing = false; // true if there is a pending request.
var shareCallbacks = [];
function share(opt_callback) {
if (opt_callback) shareCallbacks.push(opt_callback);
if (sharing) return;
sharing = true;
var errorMessages = {
413: 'Snippet is too large to share.'
};
var sharingData = body();
$.ajax('/_/share', {
processData: false,
data: sharingData,
type: 'POST',
contentType: 'text/plain; charset=utf-8',
complete: function(xhr) {
sharing = false;
if (xhr.status != 200) {
var alertMsg = errorMessages[xhr.status] ? errorMessages[xhr.status] : 'Server error; try again.';
alert(alertMsg);
return;
}
if (opts.shareRedirect) {
window.location = opts.shareRedirect + xhr.responseText;
}
var path = '/play/p/' + xhr.responseText;
if (opts.versionEl !== null && $(opts.versionEl).val() != "") {
path += "?v=" + $(opts.versionEl).val();
}
var url = origin(window.location) + path;
for (var i = 0; i < shareCallbacks.length; i++) {
shareCallbacks[i](url);
}
shareCallbacks = [];
if (shareURL) {
shareURL
.show()
.val(url)
.focus()
.select();
$(opts.toysEl).hide();
if (rewriteHistory) {
var historyData = { code: sharingData };
window.history.pushState(historyData, '', path);
pushedPlay = false;
}
}
},
});
}
$(opts.runEl).click(run);
$(opts.fmtEl).click(fmt);
if (
opts.shareEl !== null &&
(opts.shareURLEl !== null || opts.shareRedirect !== null)
) {
if (opts.shareURLEl) {
shareURL = $(opts.shareURLEl).hide();
}
$(opts.shareEl).click(function() {
share();
});
}
var path = window.location.pathname;
var toyDisable = false;
if (path.startsWith('/go.dev/')) {
path = path.slice(7);
}
if (path.startsWith('/play/p/')) {
var id = path.slice(8);
id = id.replace(/\.go$/, "");
loadShare(id);
toyDisable = true;
}
if (opts.toysEl !== null) {
$(opts.toysEl).bind('change', function() {
if (toyDisable) {
toyDisable = false;
return;
}
var toy = $(this).val();
$.ajax('/doc/play/' + toy, {
processData: false,
type: 'GET',
complete: function(xhr) {
if (xhr.status != 200) {
alert('Server error; try again.');
return;
}
setBody(xhr.responseText);
if (toy.includes('-dev') && opts.versionEl !== null) {
$(opts.versionEl).val('gotip');
}
run();
},
});
});
}
if (opts.versionEl !== null) {
var select = $(opts.versionEl);
var v = (new URL(window.location)).searchParams.get('v');
if (v !== null && v != "") {
select.val(v);
if (select.val() != v) {
select.append($('<option>', {value: v, text: 'Backend: ' + v}));
select.val(v);
}
}
if (opts.enableHistory) {
select.bind('change', inputChanged);
}
}
}
window.playground = playground;
})();