diff --git a/Makefile b/Makefile index d3f1b85..84764fb 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ build.wasm: GOOS=js GOARCH=wasm go build -o target/wasm/gotemplate.wasm ./cmds/gotemplate GOOS=js GOARCH=wasm go build -o target/wasm/timestamps.wasm ./cmds/timestamps GOOS=js GOARCH=wasm go build -o target/wasm/android-icons.wasm ./cmds/android-icons + GOOS=js GOARCH=wasm go build -o target/wasm/golines.wasm ./cmds/golines cp $(GOROOT)/lib/wasm/wasm_exec.js target/wasm/. .Phony: build.site diff --git a/cmds/golines/format.go b/cmds/golines/format.go new file mode 100644 index 0000000..dc4ca14 --- /dev/null +++ b/cmds/golines/format.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + + "github.com/golangci/golines/shorten" +) + +func formatSource(source string, maxLen int) (output []byte, err error) { + // Dependency panics must not escape the JS callback and terminate the WASM runtime. + defer func() { + if recovered := recover(); recovered != nil { + output = nil + err = fmt.Errorf("golines could not format this input (internal panic: %v). Ensure the input is a complete Go file, including a package declaration", recovered) + } + }() + + tabLen := 4 + if maxLen < 80 { + tabLen = 2 + } + return shorten.NewShortener(&shorten.Config{ + MaxLen: maxLen, + TabLen: tabLen, + ShortenComments: true, + ReformatTags: true, + ChainSplitDots: true, + }).Process([]byte(source)) +} diff --git a/cmds/golines/format_test.go b/cmds/golines/format_test.go new file mode 100644 index 0000000..5d3c770 --- /dev/null +++ b/cmds/golines/format_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "go/parser" + "go/token" + "strings" + "testing" +) + +func TestFormatWidths(t *testing.T) { + source := `package main +func main() { + println("first argument", "second argument", "third argument", "fourth argument") +} +` + var narrow, wide string + for width := 40; width <= 180; width += 5 { + output, err := formatSource(source, width) + if err != nil { + t.Fatalf("width %d: %v", width, err) + } + if _, err := parser.ParseFile(token.NewFileSet(), "output.go", output, parser.AllErrors); err != nil { + t.Fatalf("width %d produced invalid Go: %v", width, err) + } + if width == 40 { + narrow = string(output) + } + if width == 180 { + wide = string(output) + } + } + if strings.Count(narrow, "\n") <= strings.Count(wide, "\n") { + t.Fatal("narrow width did not split the call") + } +} + +func TestInvalidSource(t *testing.T) { + if _, err := formatSource("package main\nfunc {", 100); err == nil { + t.Fatal("expected a syntax error") + } + // A failed edit must not prevent subsequent formatting. + if _, err := formatSource("package main\nfunc main() {}", 100); err != nil { + t.Fatal(err) + } +} diff --git a/cmds/golines/main.go b/cmds/golines/main.go new file mode 100644 index 0000000..7580354 --- /dev/null +++ b/cmds/golines/main.go @@ -0,0 +1,40 @@ +//go:build js && wasm + +package main + +import "syscall/js" + +func main() { + document := js.Global().Get("document") + element := func(id string) js.Value { return document.Call("getElementById", id) } + source, width := element("source"), element("max-len") + output, message := element("output"), element("message") + render := func() { + maxLen := width.Get("valueAsNumber").Int() + element("width-value").Set("textContent", maxLen) + tabLen := 4 + if maxLen < 80 { + tabLen = 2 + } + element("tab-value").Set("textContent", tabLen) + source.Get("style").Set("tabSize", tabLen) + output.Get("style").Set("tabSize", tabLen) + formatted, err := formatSource(source.Get("value").String(), maxLen) + if err != nil { + output.Set("value", "") + message.Set("textContent", err.Error()) + source.Call("setAttribute", "aria-invalid", "true") + return + } + source.Call("removeAttribute", "aria-invalid") + output.Set("value", string(formatted)) + message.Set("textContent", "") + } + listener := js.FuncOf(func(this js.Value, args []js.Value) any { render(); return nil }) + source.Call("addEventListener", "input", listener) + width.Call("addEventListener", "input", listener) + source.Set("disabled", false) + width.Set("disabled", false) + render() + select {} +} diff --git a/cmds/golines/recovery_test.go b/cmds/golines/recovery_test.go new file mode 100644 index 0000000..94410c9 --- /dev/null +++ b/cmds/golines/recovery_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "strings" + "testing" +) + +func TestFormatterPanicRecovery(t *testing.T) { + // go/format accepts fragments, but dst v0.27.3 panics when golines + // attempts to decorate this function without a package declaration. + source := `func example() { + println("first argument", "second argument", "third argument", "fourth argument") +}` + output, err := formatSource(source, 40) + if err == nil || !strings.Contains(err.Error(), "internal panic") { + t.Fatalf("expected recovered dependency panic, got output %q, error %v", output, err) + } + if output != nil { + t.Fatalf("unexpected output after panic: %q", output) + } + output, err = formatSource("package main\n"+source, 40) + if err != nil || !strings.Contains(string(output), "func example()") { + t.Fatalf("formatting after panic failed: output %q, error %v", output, err) + } +} diff --git a/go.mod b/go.mod index 4e30240..ef865e7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,15 @@ module github.com/lmika/webtools go 1.25.0 require ( - github.com/alecthomas/participle/v2 v2.1.4 // indirect - golang.org/x/image v0.37.0 // indirect + github.com/alecthomas/participle/v2 v2.1.4 + github.com/golangci/golines v0.15.0 + golang.org/x/image v0.37.0 +) + +require ( + github.com/dave/dst v0.27.3 // indirect + github.com/ldez/structtags v0.6.1 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/tools v0.41.0 // indirect ) diff --git a/go.sum b/go.sum index 8e1c4f0..cc2d6ae 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,36 @@ +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= +github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= +github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/site/golines/index.html b/site/golines/index.html new file mode 100644 index 0000000..5a9b36e --- /dev/null +++ b/site/golines/index.html @@ -0,0 +1,41 @@ + + + + + + + Golines Playground - Tools + + + + +
+

Golines Playground

+

Format Go code with golines. Everything runs in your browser.

+
+
+ + +

Tab width: 4. Comment shortening, struct tag formatting, and splitting method chains at dots are enabled.

+
+
+ + +
+
+ + +
+
+

Loading formatter…

+
+ + + diff --git a/site/golines/main.js b/site/golines/main.js new file mode 100644 index 0000000..ad1b85d --- /dev/null +++ b/site/golines/main.js @@ -0,0 +1,12 @@ +try { + await import("/wasm/wasm_exec.js"); + const go = new Go(); + const response = await fetch("/wasm/golines.wasm"); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const result = await WebAssembly.instantiate(await response.arrayBuffer(), go.importObject); + await go.run(result.instance); +} catch (error) { + document.getElementById("message").textContent = `Unable to load the formatter: ${error.message}. Please reload to try again.`; + document.getElementById("source").disabled = true; + document.getElementById("max-len").disabled = true; +} diff --git a/site/golines/style.css b/site/golines/style.css new file mode 100644 index 0000000..2c2bdc9 --- /dev/null +++ b/site/golines/style.css @@ -0,0 +1,13 @@ +textarea { + height: 28rem; + font-family: monospace; + font-size: 0.8rem; + white-space: pre; + overflow-wrap: normal; + tab-size: 4; +} + +#message { + white-space: pre-wrap; + overflow-wrap: anywhere; +} diff --git a/site/index.html b/site/index.html index 437258e..9b2fd6b 100644 --- a/site/index.html +++ b/site/index.html @@ -20,6 +20,7 @@