diff --git a/server/main.wasm b/server/build.wasm similarity index 100% rename from server/main.wasm rename to server/build.wasm diff --git a/server/index.html b/server/index.html index c9acc26..1132633 100644 --- a/server/index.html +++ b/server/index.html @@ -1,13 +1,105 @@ + The WebAssembly Go Playground + + + + + + + - + + +
+ +
+
+ diff --git a/server/jquery-linedtextarea.js b/server/jquery-linedtextarea.js new file mode 100644 index 0000000..4431e95 --- /dev/null +++ b/server/jquery-linedtextarea.js @@ -0,0 +1,50 @@ +/** + * Adapted from jQuery Lined Textarea Plugin + * http://alan.blog-city.com/jquerylinedtextarea.htm + * + * Released under the MIT License: + * http://www.opensource.org/licenses/mit-license.php + */ +(function($) { + $.fn.linedtextarea = function() { + /* + * Helper function to make sure the line numbers are always kept up to + * the current system + */ + var fillOutLines = function(linesDiv, h, lineNo) { + while (linesDiv.height() < h) { + linesDiv.append("
" + lineNo + "
"); + lineNo++; + } + return lineNo; + }; + + return this.each(function() { + var lineNo = 1; + var textarea = $(this); + + /* Wrap the text area in the elements we need */ + textarea.wrap("
"); + textarea.width("97%"); + textarea.parent().prepend("
"); + var linesDiv = textarea.parent().find(".lines"); + + var scroll = function(tn) { + var domTextArea = $(this)[0]; + var scrollTop = domTextArea.scrollTop; + var clientHeight = domTextArea.clientHeight; + linesDiv.css({ + 'margin-top' : (-scrollTop) + "px" + }); + lineNo = fillOutLines(linesDiv, scrollTop + clientHeight, + lineNo); + }; + /* React to the scroll event */ + textarea.scroll(scroll); + $(window).resize(function() { textarea.scroll(); }); + /* We call scroll once to add the line numbers */ + textarea.scroll(); + }); + }; + +})(jQuery); diff --git a/server/playground.js b/server/playground.js new file mode 100644 index 0000000..0ab0b2f --- /dev/null +++ b/server/playground.js @@ -0,0 +1,544 @@ +// 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', { + 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; + } + if (!data.Events) { + data.Events = []; + } + if (data.VetErrors) { + // Inject errors from the vet as the first events in the output. + data.Events.unshift({Message: 'Go vet exited.\n\n', Kind: 'system', Delay: 0}); + data.Events.unshift({Message: data.VetErrors, Kind: 'stderr', Delay: 0}); + } + + if (!enableVet || data.VetOK || data.VetErrors) { + playing = playback(output, data); + return; + } + + // In case the server support doesn't support + // compile+vet in same request signaled by the + // 'withVet' parameter above, also try the old way. + // TODO: remove this when it falls out of use. + // It is 2019-05-13 now. + $.ajax("/vet", { + data: {"body": body}, + type: "POST", + dataType: "json", + success: function(dataVet) { + if (dataVet.Errors) { + // inject errors from the vet as the first events in the output + data.Events.unshift({Message: 'Go vet exited.\n\n', Kind: 'system', Delay: 0}); + data.Events.unshift({Message: dataVet.Errors, Kind: 'stderr', Delay: 0}); + } + playing = playback(output, data); + }, + error: function() { + 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, '>'); + + 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) { + // find the selection start and end + var start = code[0].selectionStart; + var end = code[0].selectionEnd; + // split the textarea content into two, and insert n tabs + var v = code[0].value; + var u = v.substr(0, start); + for (var i=0; i 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 = $('
').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 pushedEmpty = (window.location.pathname == "/");
+    function inputChanged() {
+      if (pushedEmpty) {
+        return;
+      }
+      pushedEmpty = true;
+      $(opts.shareURLEl).hide();
+      window.history.pushState(null, "", "/");
+    }
+    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 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 run() {
+      loading();
+      running = transport.Run(body(), highlightOutput(PlaygroundOutput(output[0])));
+    }
+
+    function fmt() {
+      loading();
+      var data = {"body": body()};
+      if ($(opts.fmtImportEl).is(":checked")) {
+        data["imports"] = "true";
+      }
+      $.ajax("/fmt", {
+        data: data,
+        type: "POST",
+        dataType: "json",
+        success: function(data) {
+          if (data.Error) {
+            setError(data.Error);
+          } else {
+            setBody(data.Body);
+            setError("");
+          }
+        }
+      });
+    }
+
+    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 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) {
+            alert("Server error; try again.");
+            return;
+          }
+          if (opts.shareRedirect) {
+            window.location = opts.shareRedirect + xhr.responseText;
+          }
+          var path = "/p/" + xhr.responseText;
+          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();
+
+            if (rewriteHistory) {
+              var historyData = {"code": sharingData};
+              window.history.pushState(historyData, "", path);
+              pushedEmpty = 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();
+      });
+    }
+
+    if (opts.toysEl !== null) {
+      $(opts.toysEl).bind('change', function() {
+        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);
+          }
+        });
+      });
+    }
+  }
+
+  window.playground = playground;
+})();
diff --git a/prebuilt/internal/bytealg.a b/server/prebuilt/internal/bytealg.a
similarity index 100%
rename from prebuilt/internal/bytealg.a
rename to server/prebuilt/internal/bytealg.a
diff --git a/prebuilt/internal/cpu.a b/server/prebuilt/internal/cpu.a
similarity index 100%
rename from prebuilt/internal/cpu.a
rename to server/prebuilt/internal/cpu.a
diff --git a/prebuilt/runtime.a b/server/prebuilt/runtime.a
similarity index 100%
rename from prebuilt/runtime.a
rename to server/prebuilt/runtime.a
diff --git a/prebuilt/runtime/internal/atomic.a b/server/prebuilt/runtime/internal/atomic.a
similarity index 100%
rename from prebuilt/runtime/internal/atomic.a
rename to server/prebuilt/runtime/internal/atomic.a
diff --git a/prebuilt/runtime/internal/math.a b/server/prebuilt/runtime/internal/math.a
similarity index 100%
rename from prebuilt/runtime/internal/math.a
rename to server/prebuilt/runtime/internal/math.a
diff --git a/prebuilt/runtime/internal/sys.a b/server/prebuilt/runtime/internal/sys.a
similarity index 100%
rename from prebuilt/runtime/internal/sys.a
rename to server/prebuilt/runtime/internal/sys.a
diff --git a/server/style.css b/server/style.css
new file mode 100644
index 0000000..1cd00e8
--- /dev/null
+++ b/server/style.css
@@ -0,0 +1,176 @@
+html {
+	height: 100%;
+}
+body {
+	color: black;
+	padding: 0;
+	margin: 0;
+	width: 100%;
+	height: 100%;
+}
+a {
+	color: #009;
+}
+#wrap,
+#about {
+	padding: 5px;
+	margin: 0;
+
+	position: absolute;
+	top: 50px;
+	bottom: 25%;
+	left: 0;
+	right: 0;
+
+	background: #FFD;
+}
+#about {
+	display: none;
+	z-index: 1;
+	padding: 10px 40px;
+	font-size: 16px;
+	font-family: sans-serif;
+	overflow: auto;
+}
+#about p {
+	max-width: 520px;
+}
+#about ul {
+	max-width: 480px;
+}
+#about li {
+	margin-bottom: 1em;
+}
+#code, #output, pre, .lines {
+	/* The default monospace font on OS X is ugly, so specify Menlo
+	 * instead. On other systems the default monospace font will be used. */
+	font-family: Menlo, monospace;
+	font-size: 11pt;
+}
+
+#code {
+	color: black;
+	background: inherit;
+
+	width: 100%;
+	height: 100%;
+	padding: 0; margin: 0;
+	border: none;
+	outline: none;
+	resize: none;
+	wrap: off;
+	float: right;
+}
+#output {
+	position: absolute;
+	top: 75%;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	padding: 8px;
+}
+#output .system, #output .loading {
+	color: #999;
+}
+#output .stderr, #output .error {
+	color: #900;
+}
+#output pre {
+	margin: 0;
+}
+#banner {
+	position: absolute;
+	left: 0;
+	right: 0;
+	top: 0;
+	height: 50px;
+	background-color: #E0EBF5;
+}
+#head {
+	float: left;
+	padding: 15px 10px;
+
+	font-size: 20px;
+	font-family: sans-serif;
+}
+#controls {
+	float: left;
+	padding: 10px 15px;
+	min-width: 245px;
+}
+#controls > input {
+	border-radius: 5px;
+}
+#aboutControls {
+	float: right;
+	padding: 10px 15px;
+}
+input[type=button],
+#importsBox {
+	height: 30px;
+	border: 1px solid #375EAB;
+	font-size: 16px;
+	font-family: sans-serif;
+	background: #375EAB;
+	color: white;
+	position: static;
+	top: 1px;
+	border-radius: 5px;
+}
+#importsBox {
+	position: relative;
+	display: inline;
+	padding: 5px 0;
+	margin-right: 5px;
+}
+#importsBox input {
+	position: relative;
+	top: -2px;
+	left: 1px;
+	height: 10px;
+	width: 10px;
+}
+#shareURL {
+	width: 280px;
+	font-size: 16px;
+	border: 1px solid #ccc;
+	background: #eee;
+	color: black;
+	height: 23px;
+}
+#embedLabel {
+	font-family: sans-serif;
+}
+.lines {
+	float: left;
+	overflow: hidden;
+	text-align: right;
+}
+.lines div {
+	padding-right: 5px;
+	color: lightgray;
+}
+.lineerror {
+	color: red;
+	background: #FDD;
+}
+.exit {
+	color: lightgray;
+}
+
+.embedded #banner {
+	display: none;
+}
+.embedded #wrap {
+	top: 0;
+}
+#embedRun {
+	display: none;
+}
+.embedded #embedRun {
+	display: block;
+	position: absolute;
+	z-index: 1;
+	top: 10px;
+	right: 10px;
+}
diff --git a/server/wasm_exec.js b/server/wasm_exec.js
index b26a17d..b23fdd3 100644
--- a/server/wasm_exec.js
+++ b/server/wasm_exec.js
@@ -17,7 +17,6 @@
 	const decoder = new TextDecoder('utf-8');
 
     const filesystem = {
-        '/main.go': encoder.encode("package main\nfunc main() {}"),
         '/importcfg.link': encoder.encode(
             "packagefile command-line-arguments=main.a\n" +
             "packagefile runtime=prebuilt/runtime.a\n" +
@@ -29,6 +28,26 @@
         ),
     };
 
+    let workingDirectory = '/';
+
+    let absPath = (path) => {
+        if (path[0] == '/') {
+            return path;
+        }
+        return workingDirectory + path.replace(/^\.\/?/, '');
+    };
+
+    global.readFromGoFilesystem = (path) => filesystem[absPath(path)];
+    global.writeToGoFilesystem = (path, content) => {
+        if (typeof content === 'string') {
+            filesystem[absPath(path)] = encoder.encode(content);
+        } else {
+            filesystem[absPath(path)] = content;
+        }
+    };
+    global.goStdout = (buf) => {};
+    global.goStderr = (buf) => {};
+
     const openFiles = new Map();
     let nextFd = 1000;
 
@@ -60,15 +79,6 @@
         });
     };
 
-    let workingDirectory = '/';
-
-    let absPath = (path) => {
-        if (path[0] == '/') {
-            return path;
-        }
-        return workingDirectory + path.replace(/^\.\/?/, '');
-    };
-
     const constants = {
         O_WRONLY: 1 << 0,
         O_RDWR: 1 << 1,
@@ -82,33 +92,30 @@
     global.fs = {
         constants,
         writeSync(fd, buf) {
-            if (fd <= 3) {
-                outputBuf += decoder.decode(buf);
-                const nl = outputBuf.lastIndexOf("\n");
-                if (nl != -1) {
-                    console.log(outputBuf.substr(0, nl));
-                    outputBuf = outputBuf.substr(nl + 1);
+            if (fd === 2) {
+                global.goStdout(buf);
+            } else if (fd === 3) {
+                global.goStderr(buf);
+            } else {
+                const file = openFiles[fd];
+                const source = filesystem[file.path];
+                let destLength = source.length + buf.length;
+                if (file.offset < source.length) {
+                    destLength = file.offset + buf.length;
+                    if (destLength < source.length) {
+                        destLength = source.length;
+                    }
                 }
-                return buf.length;
-            }
-            const file = openFiles[fd];
-            const source = filesystem[file.path];
-            let destLength = source.length + buf.length;
-            if (file.offset < source.length) {
-                destLength = file.offset + buf.length;
-                if (destLength < source.length) {
-                    destLength = source.length;
+                const dest = new Uint8Array(destLength);
+                for (let i = 0; i < source.length; ++i) {
+                    dest[i] = source[i];
                 }
+                for (let i = 0; i < buf.length; ++i) {
+                    dest[file.offset + i] = buf[i];
+                }
+                openFiles[fd].offset += buf.length;
+                filesystem[file.path] = dest;
             }
-            const dest = new Uint8Array(destLength);
-            for (let i = 0; i < source.length; ++i) {
-                dest[i] = source[i];
-            }
-            for (let i = 0; i < buf.length; ++i) {
-                dest[file.offset + i] = buf[i];
-            }
-            openFiles[fd].offset += buf.length;
-            filesystem[file.path] = dest;
         },
         write(fd, buf, offset, length, position, callback) {
             if (offset !== 0 || length !== buf.length) {
@@ -144,7 +151,6 @@
             callback(null, fd);
         },
         read(fd, buffer, offset, length, position, callback) {
-            console.log('read(' + fd + ', ' + length + ', ' + position + ')');
             if (offset !== 0) {
                 throw new Error('read not fully implemented: ' + offset);
             }