diff --git a/main.go b/main.go index 1796823..56b0866 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "errors" "flag" @@ -80,7 +81,7 @@ func compileAndRunCode(w http.ResponseWriter, tempDir string) (string, bool) { return "", false } -func postHandler(w http.ResponseWriter, r *http.Request) { +func compileHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -98,21 +99,50 @@ func postHandler(w http.ResponseWriter, r *http.Request) { os.Chmod(tempDir, 0777) codePath := filepath.Join(tempDir, "main.jule") - codeInput, _ := io.ReadAll(r.Body) - os.WriteFile(codePath, codeInput, 0644) + inputCode, _ := io.ReadAll(r.Body) + os.WriteFile(codePath, inputCode, 0644) if outputMessage, ok := compileAndRunCode(w, tempDir); ok { fmt.Fprint(w, outputMessage) } } +func formatHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + inputCode, _ := io.ReadAll(r.Body) + + // When an error occurs, julefmt writes to stderr but still returns 0 as an exit code. + // Therefore you have to check directly stderr and stdout content. + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd := exec.Command("julefmt") + cmd.Stdin = strings.NewReader(string(inputCode)) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + if err != nil { + errorMessage := fmt.Sprintf("Exit error: %v\n", err) + http.Error(w, errorMessage, 500) + } else if stderr.Len() != 0 { + http.Error(w, stderr.String(), 500) + } else if stdout.Len() != 0 { + fmt.Fprint(w, stdout.String()) + } +} + func main() { port := flag.Int("port", 8080, "server port") flag.Parse() fs := http.FileServer(http.Dir("./public")) http.Handle("/playground/", http.StripPrefix("/playground/", fs)) - http.HandleFunc("/playground/compile", postHandler) + http.HandleFunc("/playground/compile", compileHandler) + http.HandleFunc("/playground/format", formatHandler) addr := fmt.Sprintf(":%d", *port) fmt.Println("http://0.0.0.0" + addr + "/playground/") log.Fatal(http.ListenAndServe(addr, nil)) diff --git a/public/bundle.js b/public/bundle.js index 2d4782e..12218cb 100644 --- a/public/bundle.js +++ b/public/bundle.js @@ -20509,8 +20509,11 @@ var basicSetup = /* @__PURE__ */ (() => [ // public/playground.js var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent); +var runButton = document.getElementById("run-button"); +var formatButton = document.getElementById("format-button"); if (isMobile) { - document.getElementById("run-button").innerText = "Run"; + runButton.innerText = "Run"; + formatButton.innerText = "Format"; } var types2 = [ "bool", @@ -20629,7 +20632,7 @@ var jule = StreamLanguage.define({ } }); var helloWorldCode = `fn main() { - println("Hello World!") + println("Hello World!") }`; var editor = new EditorView({ doc: helloWorldCode, @@ -20637,14 +20640,15 @@ var editor = new EditorView({ basicSetup, jule, syntaxHighlighting(style), - keymap.of([indentWithTab]) + keymap.of([indentWithTab]), + indentUnit.of("\t") ], parent: document.getElementById("editor") }); var isCompiling = false; -var runButton = document.getElementById("run-button"); +var isFormatting = false; runButton.onclick = () => { - if (isCompiling) { + if (isCompiling || isFormatting) { return; } isCompiling = true; @@ -20673,6 +20677,45 @@ document.addEventListener("keydown", (e) => { runButton.click(); } }, { capture: true }); +formatButton.onclick = () => { + if (isFormatting || isCompiling) { + return; + } + isFormatting = true; + const outputElement = document.getElementById("output"); + const inputCode = editor.state.doc.toString(); + fetch("/playground/format", { + method: "POST", + body: inputCode, + headers: { "Content-Type": "text/plain" } + }).then(async (res) => { + if (res.status >= 500) { + const message = await res.text(); + throw message; + } + return res.text(); + }).then((formattedCode) => { + editor.dispatch({ + changes: { + from: 0, + to: editor.state.doc.length, + insert: formattedCode + } + }); + outputElement.textContent = "Code formatted successfully."; + isFormatting = false; + }).catch((err) => { + outputElement.textContent = err; + isFormatting = false; + }); + isFormatting = false; +}; +document.addEventListener("keydown", (e) => { + if (e.shiftKey && e.key === "Enter") { + e.preventDefault(); + formatButton.click(); + } +}, { capture: true }); var examples = document.getElementById("examples"); examples.onchange = (e) => { const value = e.target.value; @@ -20680,67 +20723,66 @@ examples.onchange = (e) => { switch (value) { case "fizzbuzz": newCode = `fn main() { - mut i := 1 - for i <= 16; i++ { - if i % 15 == 0 { - println("FizzBuzz") - } else if i % 3 == 0 { - println("Fizz") - } else if i % 5 == 0 { - println("Buzz") - } - } + mut i := 1 + for i <= 16; i++ { + if i%15 == 0 { + println("FizzBuzz") + } else if i%3 == 0 { + println("Fizz") + } else if i%5 == 0 { + println("Buzz") + } + } }`; break; case "randomness": newCode = `use "std/fmt" use "std/math/rand" -use "std/time" fn main() { - // Constants are compile-time known values - const min = 1 - const max = 10 + // Constants are compile-time known values + const min = 1 + const max = 10 - random_number := rand::IntN(max - min) + min + random_number := rand::IntN(max-min) + min - // print[ln] doesn't accept multiple arguments, so you have to use fmt::Print - fmt::Print("Here is a number between ", min, " and ", max, ": ") - println(random_number) + // print[ln] doesn't accept multiple arguments, so you have to use fmt::Print + fmt::Print("Here is a number between ", min, " and ", max, ": ") + println(random_number) }`; break; case "comptime-matching": newCode = `fn printKind[T](value: T) { - const match type T { - | *int: - println("int pointer") - | &int: - println("int reference") - | u32: - println("u32") - | i32: - println("i32") - | u8: - println("u8") - | cmplx128: - println("cmplx128") - | cmplx64: - println("cmplx64") - | []int: - println("slice of ints") - | [5]int: - println("array of 5 ints") - |: - panic("unexpected type") - } + const match type T { + | *int: + println("int pointer") + | &int: + println("int reference") + | u32: + println("u32") + | i32: + println("i32") + | u8: + println("u8") + | cmplx128: + println("cmplx128") + | cmplx64: + println("cmplx64") + | []int: + println("slice of ints") + | [5]int: + println("array of 5 ints") + |: + panic("unexpected type") + } } fn main() { - let x: [5]int = [1, 2, 3, 4, 5] - printKind(x) - printKind(3+4i) - slice := [2, 3, 4] - printKind(slice) + let x: [5]int = [1, 2, 3, 4, 5] + printKind(x) + printKind(3 + 4i) + slice := [2, 3, 4] + printKind(slice) }`; break; } diff --git a/public/index.html b/public/index.html index 0c0388a..e051f9b 100644 --- a/public/index.html +++ b/public/index.html @@ -20,7 +20,10 @@

Jule Playground

- +
+ + +
 clang version 21.1.2
 jule0.2.0
diff --git a/public/playground.js b/public/playground.js index 92aef28..d9b7830 100644 --- a/public/playground.js +++ b/public/playground.js @@ -1,6 +1,7 @@ import { indentWithTab } from "@codemirror/commands"; import { HighlightStyle, + indentUnit, StreamLanguage, syntaxHighlighting, } from "@codemirror/language"; @@ -9,8 +10,11 @@ import { tags } from "@lezer/highlight"; import { basicSetup, EditorView } from "codemirror"; const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent); +const runButton = document.getElementById("run-button"); +const formatButton = document.getElementById("format-button"); if (isMobile) { - document.getElementById("run-button").innerText = "Run"; + runButton.innerText = "Run"; + formatButton.innerText = "Format"; } const types = [ @@ -136,7 +140,7 @@ const jule = StreamLanguage.define({ }); const helloWorldCode = `fn main() { - println("Hello World!") + println("Hello World!") }`; const editor = new EditorView({ @@ -145,15 +149,17 @@ const editor = new EditorView({ basicSetup, jule, syntaxHighlighting(style), - keymap.of([indentWithTab]), + keymap.of([indentWithTab]), // Handles the tab key + indentUnit.of("\t"), // To add because by default indentations add spaces ], parent: document.getElementById("editor"), }); let isCompiling = false; -const runButton = document.getElementById("run-button"); +let isFormatting = false; + runButton.onclick = () => { - if (isCompiling) { + if (isCompiling || isFormatting) { return; } @@ -197,6 +203,56 @@ document.addEventListener( { capture: true }, ); +formatButton.onclick = () => { + if (isFormatting || isCompiling) { + return; + } + isFormatting = true; + const outputElement = document.getElementById("output"); + const inputCode = editor.state.doc.toString(); + + fetch("/playground/format", { + method: "POST", + body: inputCode, + headers: { "Content-Type": "text/plain" }, + }) + .then(async (res) => { + if (res.status >= 500) { + const message = await res.text(); + throw message; + } + return res.text(); + }) + .then((formattedCode) => { + editor.dispatch({ + changes: { + from: 0, + to: editor.state.doc.length, + insert: formattedCode, + }, + }); + outputElement.textContent = "Code formatted successfully."; + isFormatting = false; + }) + .catch((err) => { + outputElement.textContent = err; + isFormatting = false; + }); + + isFormatting = false; +}; + +document.addEventListener( + "keydown", + (e) => { + if (e.shiftKey && e.key === "Enter") { + e.preventDefault(); + formatButton.click(); + } + }, + { capture: true }, +); + const examples = document.getElementById("examples"); examples.onchange = (e) => { const value = e.target.value; @@ -205,67 +261,66 @@ examples.onchange = (e) => { switch (value) { case "fizzbuzz": newCode = `fn main() { - mut i := 1 - for i <= 16; i++ { - if i % 15 == 0 { - println("FizzBuzz") - } else if i % 3 == 0 { - println("Fizz") - } else if i % 5 == 0 { - println("Buzz") - } - } + mut i := 1 + for i <= 16; i++ { + if i%15 == 0 { + println("FizzBuzz") + } else if i%3 == 0 { + println("Fizz") + } else if i%5 == 0 { + println("Buzz") + } + } }`; break; case "randomness": newCode = `use "std/fmt" use "std/math/rand" -use "std/time" fn main() { - // Constants are compile-time known values - const min = 1 - const max = 10 + // Constants are compile-time known values + const min = 1 + const max = 10 - random_number := rand::IntN(max - min) + min + random_number := rand::IntN(max-min) + min - // print[ln] doesn't accept multiple arguments, so you have to use fmt::Print - fmt::Print("Here is a number between ", min, " and ", max, ": ") - println(random_number) + // print[ln] doesn't accept multiple arguments, so you have to use fmt::Print + fmt::Print("Here is a number between ", min, " and ", max, ": ") + println(random_number) }`; break; case "comptime-matching": newCode = `fn printKind[T](value: T) { - const match type T { - | *int: - println("int pointer") - | &int: - println("int reference") - | u32: - println("u32") - | i32: - println("i32") - | u8: - println("u8") - | cmplx128: - println("cmplx128") - | cmplx64: - println("cmplx64") - | []int: - println("slice of ints") - | [5]int: - println("array of 5 ints") - |: - panic("unexpected type") - } + const match type T { + | *int: + println("int pointer") + | &int: + println("int reference") + | u32: + println("u32") + | i32: + println("i32") + | u8: + println("u8") + | cmplx128: + println("cmplx128") + | cmplx64: + println("cmplx64") + | []int: + println("slice of ints") + | [5]int: + println("array of 5 ints") + |: + panic("unexpected type") + } } fn main() { - let x: [5]int = [1, 2, 3, 4, 5] - printKind(x) - printKind(3+4i) - slice := [2, 3, 4] - printKind(slice) + let x: [5]int = [1, 2, 3, 4, 5] + printKind(x) + printKind(3 + 4i) + slice := [2, 3, 4] + printKind(slice) }`; break; } diff --git a/public/style.css b/public/style.css index 933de5d..17dffd9 100644 --- a/public/style.css +++ b/public/style.css @@ -55,14 +55,23 @@ button { height: 2em; color: #333; border-radius: 4px; - width: 10em; + width: 12em; +} + +button:hover { + background-color: #ddd; } #output-wrapper { height: 25vh; display: flex; flex-direction: column; - /*justify-content: flex-end; /* forces the div to be at the bottom of the page */ +} + +#output-wrapper #buttons { + display: flex; + flex-direction: row; + gap: 8px; } pre#output {