From 7452a67ceeb31de5b496378b54e9b907ff557582 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 17 Jul 2024 15:28:33 -0400 Subject: [PATCH 01/10] fix: fix colliding var name with pkg name --- cmd/cmd.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index d51d087..b9e19dc 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -18,11 +18,11 @@ func GnoplsCmd() *cobra.Command { SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { slog.Info("Initializing Server...") - env := &env.Env{ + procEnv := &env.Env{ GNOROOT: os.Getenv("GNOROOT"), GNOHOME: env.GnoHome(), } - err := lsp.RunServer(cmd.Context(), env) + err := lsp.RunServer(cmd.Context(), procEnv) if err != nil { return err } From 520dea626007087452647f66b39d21fcad0b63fa Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 17 Jul 2024 18:13:35 -0400 Subject: [PATCH 02/10] feat: add js connector --- internal/env/conn_js.go | 12 +++++ internal/env/conn_other.go | 15 ++++++ internal/js/conn_js.go | 22 +++++++++ internal/js/imports_js.go | 12 +++++ internal/js/listener_js.go | 96 ++++++++++++++++++++++++++++++++++++++ internal/js/reader.go | 62 ++++++++++++++++++++++++ internal/js/writer_js.go | 27 +++++++++++ internal/lsp/run.go | 15 +++--- 8 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 internal/env/conn_js.go create mode 100644 internal/env/conn_other.go create mode 100644 internal/js/conn_js.go create mode 100644 internal/js/imports_js.go create mode 100644 internal/js/listener_js.go create mode 100644 internal/js/reader.go create mode 100644 internal/js/writer_js.go diff --git a/internal/env/conn_js.go b/internal/env/conn_js.go new file mode 100644 index 0000000..53bc16d --- /dev/null +++ b/internal/env/conn_js.go @@ -0,0 +1,12 @@ +package env + +import ( + "context" + "net" + + "github.com/gnolang/gnopls/internal/js" +) + +func GetConnection(ctx context.Context) (net.Conn, error) { + return js.DialHost(ctx) +} diff --git a/internal/env/conn_other.go b/internal/env/conn_other.go new file mode 100644 index 0000000..f67ce50 --- /dev/null +++ b/internal/env/conn_other.go @@ -0,0 +1,15 @@ +//go:build !js + +package env + +import ( + "context" + "net" + "os" + + "go.lsp.dev/pkg/fakenet" +) + +func GetConnection(_ context.Context) (net.Conn, error) { + return fakenet.NewConn("stdio", os.Stdin, os.Stdout), nil +} diff --git a/internal/js/conn_js.go b/internal/js/conn_js.go new file mode 100644 index 0000000..416d50c --- /dev/null +++ b/internal/js/conn_js.go @@ -0,0 +1,22 @@ +// Package js provides primitives to integrate language server with javascript environments such as browsers and Node. +package js + +import ( + "context" + "net" + + "go.lsp.dev/pkg/fakenet" +) + +// DialHost registers LSP message listener in JavaScript host and returns connection to use by LSP server. +// +// This function should be called only once before starting LSP server. +func DialHost(ctx context.Context) (net.Conn, error) { + reader, err := registerRequestListener(ctx) + if err != nil { + return nil, err + } + + conn := fakenet.NewConn("js", reader, messageWriter) + return conn, nil +} diff --git a/internal/js/imports_js.go b/internal/js/imports_js.go new file mode 100644 index 0000000..d49cf1b --- /dev/null +++ b/internal/js/imports_js.go @@ -0,0 +1,12 @@ +package js + +import "unsafe" + +//go:wasmimport lsp writeMessage +func writeMessage(p unsafe.Pointer) + +//go:wasmimport lsp closeWriter +func closeWriter() + +//go:wasmimport lsp registerCallback +func registerCallback(callbackID uint32) diff --git a/internal/js/listener_js.go b/internal/js/listener_js.go new file mode 100644 index 0000000..5915f94 --- /dev/null +++ b/internal/js/listener_js.go @@ -0,0 +1,96 @@ +package js + +import ( + "context" + "fmt" + "io" + "reflect" + "syscall/js" +) + +const ( + // jsFieldCallbackID is field name of js.Func struct that contains callback ID of Go func exposed to JS. + jsFieldCallbackID = "id" + + // msgBufferSize is size of a buffer of incoming messages from LSP client. + msgBufferSize = 10 +) + +// registerRequestListener creates and registers LSP request listener at the host. +// This method should only be called once at start of a server. +// +// Context parameter is used to automatically dispose underlying channel and callback. +// +// Returns a reader to consume incoming JSON-RPC messages from host. +// Closing a returned reader acts the same as cancelling an input context. +func registerRequestListener(ctx context.Context) (io.ReadCloser, error) { + chanCtx, cancelFn := context.WithCancel(ctx) + + inputEvents := make(chan []byte, msgBufferSize) + callback := js.FuncOf(func(this js.Value, args []js.Value) any { + message := args[0].String() + inputEvents <- []byte(message) + return nil + }) + + // As it's impossible to pass JS objects to import funcs. + // Obtain the function callback ID pass it to import. + callbackID, err := getFuncCallbackID(callback) + if err != nil { + cancelFn() + callback.Release() + close(inputEvents) + return nil, fmt.Errorf("failed to prepare callback: %w", err) + } + + registerCallback(callbackID) + + go func() { + select { + case <-chanCtx.Done(): + callback.Release() + close(inputEvents) + } + }() + + reader := NewChannelReader(inputEvents, cancelFn) + return reader, nil +} + +// getFuncCallbackID obtains callback ID from wrapped js.Func value. +// +// Internally, Go stores each js.Func handler inside a special lookup table. +// When host calls and wrapped function (js.Func), it resumes Go program and passes a callback ID. +// Go matches the callback ID with the corresponding js.Func handler in the lookup table and calls it. +// +// This flow didn't change since first Go with WASM support initial release. +// +// See: handleEvent in /syscall/js/js.go +func getFuncCallbackID(fn js.Func) (id uint32, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%s", r) + } + }() + + // Obtain the callback id from the function. + // Use reflection to capture possible struct layout changes. + field := reflect.ValueOf(fn).FieldByName(jsFieldCallbackID) + if !field.IsValid() { + return 0, fmt.Errorf("cannot find field %q in %T", jsFieldCallbackID, fn) + } + + if field.Type().Kind() != reflect.Uint32 { + return 0, fmt.Errorf( + "unexpected %T.%s field type: %s (want: %T)", + fn, jsFieldCallbackID, field.Type(), id, + ) + } + + id = uint32(field.Uint()) + if id == 0 { + return 0, fmt.Errorf("empty callback ID in %T.%s", fn, jsFieldCallbackID) + } + + return id, nil +} diff --git a/internal/js/reader.go b/internal/js/reader.go new file mode 100644 index 0000000..384b361 --- /dev/null +++ b/internal/js/reader.go @@ -0,0 +1,62 @@ +package js + +import ( + "context" + "io" +) + +var _ io.Reader = (*ChannelReader)(nil) + +// ChannelReader is a reader that reads from a channel of bytes. +type ChannelReader struct { + buff []byte + source <-chan []byte + cancelFn context.CancelFunc +} + +// NewChannelReader creates a new ChannelReader from a channel. +// +// Second argument is optional function that will be called when `Close` method is called. +func NewChannelReader(source <-chan []byte, cancelFn context.CancelFunc) *ChannelReader { + return &ChannelReader{ + source: source, + } +} + +func (listener *ChannelReader) Read(b []byte) (n int, err error) { + if len(b) == 0 { + return 0, nil + } + + if len(listener.buff) == 0 { + if err := listener.fetchMore(); err != nil { + return 0, err + } + } + + readCount := min(len(listener.buff), len(b)) + + copy(b, listener.buff[:readCount]) + listener.buff = listener.buff[readCount:] + return readCount, nil +} + +func (listener *ChannelReader) Close() error { + if listener.cancelFn != nil { + listener.cancelFn() + } + + return nil +} + +func (listener *ChannelReader) fetchMore() error { + select { + case message, ok := <-listener.source: + if !ok { + return io.EOF + } + + listener.buff = message + return nil + } +} diff --git a/internal/js/writer_js.go b/internal/js/writer_js.go new file mode 100644 index 0000000..8a96c7d --- /dev/null +++ b/internal/js/writer_js.go @@ -0,0 +1,27 @@ +package js + +import "unsafe" + +// messageWriter passes JSON-RPC messages to WASM host. +var messageWriter = wasmWriter{ + writeFunc: writeMessage, + closeFunc: closeWriter, +} + +type wasmWriter struct { + writeFunc func(p unsafe.Pointer) + closeFunc func() +} + +func (w wasmWriter) Write(p []byte) (n int, err error) { + w.writeFunc(unsafe.Pointer(&p)) + return len(p), nil +} + +func (w wasmWriter) Close() error { + if w.closeFunc != nil { + w.closeFunc() + } + + return nil +} diff --git a/internal/lsp/run.go b/internal/lsp/run.go index 9768be8..74c6620 100644 --- a/internal/lsp/run.go +++ b/internal/lsp/run.go @@ -4,18 +4,21 @@ import ( "context" "errors" "io" - "os" "github.com/gnolang/gnopls/internal/env" "go.lsp.dev/jsonrpc2" - "go.lsp.dev/pkg/fakenet" ) -func RunServer(ctx context.Context, env *env.Env) error { - conn := jsonrpc2.NewConn(jsonrpc2.NewStream(fakenet.NewConn("stdio", os.Stdin, os.Stdout))) - handler := BuildServerHandler(conn, env) +func RunServer(ctx context.Context, e *env.Env) error { + conn, err := env.GetConnection(ctx) + if err != nil { + return err + } + + rpcConn := jsonrpc2.NewConn(jsonrpc2.NewStream(conn)) + handler := BuildServerHandler(rpcConn, e) stream := jsonrpc2.HandlerServer(handler) - err := stream.ServeStream(ctx, conn) + err = stream.ServeStream(ctx, rpcConn) if errors.Is(err, io.EOF) { return nil } From e4143b9d202d6f7964a7c082f68707199c013f7d Mon Sep 17 00:00:00 2001 From: Denys Sedchenko Date: Thu, 18 Jul 2024 11:07:17 -0400 Subject: [PATCH 03/10] fix: Update internal/js/reader.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jerónimo Albi --- internal/js/reader.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/js/reader.go b/internal/js/reader.go index 384b361..f1d14f7 100644 --- a/internal/js/reader.go +++ b/internal/js/reader.go @@ -20,6 +20,7 @@ type ChannelReader struct { func NewChannelReader(source <-chan []byte, cancelFn context.CancelFunc) *ChannelReader { return &ChannelReader{ source: source, + cancelFn: cancelFn, } } From 3eb27bf8c6afda1d1cef0b2307f1bf393e8c835e Mon Sep 17 00:00:00 2001 From: Denys Sedchenko Date: Thu, 18 Jul 2024 11:10:13 -0400 Subject: [PATCH 04/10] fix: Update internal/js/reader.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jerónimo Albi --- internal/js/reader.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/js/reader.go b/internal/js/reader.go index f1d14f7..ac015d0 100644 --- a/internal/js/reader.go +++ b/internal/js/reader.go @@ -51,13 +51,11 @@ func (listener *ChannelReader) Close() error { } func (listener *ChannelReader) fetchMore() error { - select { - case message, ok := <-listener.source: - if !ok { - return io.EOF - } - - listener.buff = message - return nil + message, ok := <-listener.source + if !ok { + return io.EOF } + + listener.buff = message + return nil } From 0b5a094ea90dcb6a5e36f88d50dc24dc45febca3 Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 18 Jul 2024 11:11:18 -0400 Subject: [PATCH 05/10] fix: add bounds check --- internal/js/listener_js.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/js/listener_js.go b/internal/js/listener_js.go index 5915f94..28e1ce6 100644 --- a/internal/js/listener_js.go +++ b/internal/js/listener_js.go @@ -28,6 +28,10 @@ func registerRequestListener(ctx context.Context) (io.ReadCloser, error) { inputEvents := make(chan []byte, msgBufferSize) callback := js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) == 0 { + // this is the only way to throw a JS exception. + panic("missing argument") + } message := args[0].String() inputEvents <- []byte(message) return nil From 56ef7f3e8652ddd6e58f047657fd70f976aec19e Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 18 Jul 2024 13:34:38 -0400 Subject: [PATCH 06/10] feat: add js embedding example --- internal/js/example/.gitignore | 4 + internal/js/example/README.md | 14 ++ internal/js/example/package-lock.json | 251 ++++++++++++++++++++++ internal/js/example/package.json | 18 ++ internal/js/example/src/internal/setup.ts | 120 +++++++++++ internal/js/example/src/script.ts | 62 ++++++ internal/js/example/src/types.ts | 14 ++ internal/js/example/src/worker.ts | 37 ++++ internal/js/example/tsconfig.json | 34 +++ 9 files changed, 554 insertions(+) create mode 100644 internal/js/example/.gitignore create mode 100644 internal/js/example/README.md create mode 100644 internal/js/example/package-lock.json create mode 100644 internal/js/example/package.json create mode 100644 internal/js/example/src/internal/setup.ts create mode 100644 internal/js/example/src/script.ts create mode 100644 internal/js/example/src/types.ts create mode 100644 internal/js/example/src/worker.ts create mode 100644 internal/js/example/tsconfig.json diff --git a/internal/js/example/.gitignore b/internal/js/example/.gitignore new file mode 100644 index 0000000..5247532 --- /dev/null +++ b/internal/js/example/.gitignore @@ -0,0 +1,4 @@ +node_modules +.DS_Store +.vscode +.idea \ No newline at end of file diff --git a/internal/js/example/README.md b/internal/js/example/README.md new file mode 100644 index 0000000..b45ba3b --- /dev/null +++ b/internal/js/example/README.md @@ -0,0 +1,14 @@ +# Gnopls Embedding Example + +This directory contains a bare-minimum code to integrate Gnopls as a WebAssembly module +into browser or Node.js environment. + +This example omits such nuances as editor integration or file system support and focuses just on basics. + +## Prerequisites + +* Copy `wasm_exec.js` file using following command: + * `cp $(go env GOROOT)/misc/wasm/wasm_exec.js .` +* Build gnopls as a WebAssembly file for JavaScript environment: + * `GOOS=js GOARCH=wasm make build` +* Modify paths to `wasm_exec.js` and WASM file in [worker.ts](./worker.ts) file. \ No newline at end of file diff --git a/internal/js/example/package-lock.json b/internal/js/example/package-lock.json new file mode 100644 index 0000000..5fe2d41 --- /dev/null +++ b/internal/js/example/package-lock.json @@ -0,0 +1,251 @@ +{ + "name": "example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "example", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/golang-wasm-exec": "^1.15.2", + "comlink": "^4.4.1", + "monaco-languageclient": "^8.7.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5" + } + }, + "node_modules/@codingame/monaco-vscode-base-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-base-service-override/-/monaco-vscode-base-service-override-7.0.10.tgz", + "integrity": "sha512-9t54KDR+6oBbW4lZSWroN5Tw3pxIfn9OhODHuHJNdEk5tdahoWLYJQcP+LRNmDipEjmTZBxkno4ctkYQhAjchg==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-environment-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-environment-service-override/-/monaco-vscode-environment-service-override-7.0.10.tgz", + "integrity": "sha512-WHbRj1pFK6YcBhaG8xcWi6iH3LO1ZyRmceNfXQFWypu550xwt2XFRoapse7xQZVazSK9CsCtorcV9mbyx+5f6Q==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-extensions-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-extensions-service-override/-/monaco-vscode-extensions-service-override-7.0.10.tgz", + "integrity": "sha512-BH7cy4xyUwkQL+YeYSjEEAW6pPBWejION23gUeB4kZNNrUKJ7+dBZYtkIUfbBJ/kai3KZ7Fro/exV14YKklSgQ==", + "dependencies": { + "@codingame/monaco-vscode-files-service-override": "7.0.10", + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-files-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-files-service-override/-/monaco-vscode-files-service-override-7.0.10.tgz", + "integrity": "sha512-A8QsQ2YDc8Pw7Y6FMSaeHugwQqwntoSg6TpuQmFTwJP/OURiPxzkWeaJZWBLA0qYXRzx5CNtgC7NLTQLQz1DAg==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-host-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-host-service-override/-/monaco-vscode-host-service-override-7.0.10.tgz", + "integrity": "sha512-lKP96MzHLKQG39rqMTjpBKr/eYxqssOpAx0plJo0oT2c1YfWWBM9mEtgnlQ2IQImVZ1Fe+rXAegnRG67cqM8cg==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-languages-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-languages-service-override/-/monaco-vscode-languages-service-override-7.0.10.tgz", + "integrity": "sha512-iIZ7e3YMQGu2wAzMt7LC0MwjmatNe3DbYs1VAOckVuCGAxoEb8ssXjCCIftRN7t9/6BdM47gUnu4M9IAsTk3/Q==", + "dependencies": { + "@codingame/monaco-vscode-files-service-override": "7.0.10", + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-layout-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-layout-service-override/-/monaco-vscode-layout-service-override-7.0.10.tgz", + "integrity": "sha512-hP2k7xAWmBIdc43Eft4H+VWn8DYCdI1ioH+gG6jsCiw+EYdrAO+SWet5qjcy04WMuq3MGy6oWBWPh40sK+YhLA==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-localization-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-localization-service-override/-/monaco-vscode-localization-service-override-7.0.10.tgz", + "integrity": "sha512-7PiTrwWWc3/cSJOxgA2RSwmPr3OqCvEYI0KOZiiBVyJakpK7TVxlzmunbZ25KjXK0SZOmQDfhbfltKEjX2fOXQ==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-model-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-model-service-override/-/monaco-vscode-model-service-override-7.0.10.tgz", + "integrity": "sha512-t+Xuz08xRr0fCmZtYYqTvfKP3IVTDKZRRbk3mDKCWtd6/8KoPFL96oHqnNz+HAoaExSB7/svOkHV4k166m7TFA==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@codingame/monaco-vscode-quickaccess-service-override": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-quickaccess-service-override/-/monaco-vscode-quickaccess-service-override-7.0.10.tgz", + "integrity": "sha512-JflI+IOuD/LD5egzjL//5T+bd5oPe0ObBlexyCNFtzLzBhETeLdagOACjPd1kb98NsQv2pW27kGS+GlsHtxXeA==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/@types/golang-wasm-exec": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/golang-wasm-exec/-/golang-wasm-exec-1.15.2.tgz", + "integrity": "sha512-NA77toY4yOiiV5foDVT/rfxmtoox7ASHqGs4Eek8xTMcKWwAhZLOD3SYfLQKq4P2jtOLQQkeISq3zSuQ1Y+apg==" + }, + "node_modules/@vscode/iconv-lite-umd": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz", + "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/comlink": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz", + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==" + }, + "node_modules/jschardet": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.2.tgz", + "integrity": "sha512-mw3CBZGzW8nUBPYhFU2ztZ/kJ6NClQUQVpyzvFMfznZsoC///ZQ30J2RCUanNsr5yF22LqhgYr/lj807/ZleWA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/monaco-editor": { + "name": "@codingame/monaco-vscode-editor-api", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-api/-/monaco-vscode-editor-api-7.0.10.tgz", + "integrity": "sha512-ocjW3vv7mzC++x+RTDtRj+iDpqqRUnPJqV4rx8EzIYW+cWlMh8vouVmf/MLFFldnOdA5AEvSu4MU8G+a5P87PQ==", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@7.0.10" + } + }, + "node_modules/monaco-languageclient": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/monaco-languageclient/-/monaco-languageclient-8.7.0.tgz", + "integrity": "sha512-zmD+u/qjAGwy92kyDjNqCt6v1fr2qSZUEGicYsTE598cS3w3tqaQjcN2rUymRBD/XUYy0bl66SqQJk5i6I/ufQ==", + "dependencies": { + "@codingame/monaco-vscode-extensions-service-override": "~7.0.7", + "@codingame/monaco-vscode-languages-service-override": "~7.0.7", + "@codingame/monaco-vscode-localization-service-override": "~7.0.7", + "@codingame/monaco-vscode-model-service-override": "~7.0.7", + "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~7.0.7", + "vscode": "npm:@codingame/monaco-vscode-api@~7.0.7", + "vscode-languageclient": "~9.0.1" + }, + "engines": { + "node": ">=16.11.0", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~7.0.7", + "vscode": "npm:@codingame/monaco-vscode-api@~7.0.7" + }, + "peerDependenciesMeta": { + "monaco-editor": { + "optional": false + }, + "vscode": { + "optional": false + } + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode": { + "name": "@codingame/monaco-vscode-api", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-api/-/monaco-vscode-api-7.0.10.tgz", + "integrity": "sha512-D0COBXpT25zlJTzGCbWZZaRgcKkIGFirum4dwXzr5nGe7QUGdotkJbZAqS8w/fepQOOVdaIeHpAAlRBAYR/QIA==", + "dependencies": { + "@codingame/monaco-vscode-base-service-override": "7.0.10", + "@codingame/monaco-vscode-environment-service-override": "7.0.10", + "@codingame/monaco-vscode-extensions-service-override": "7.0.10", + "@codingame/monaco-vscode-files-service-override": "7.0.10", + "@codingame/monaco-vscode-host-service-override": "7.0.10", + "@codingame/monaco-vscode-layout-service-override": "7.0.10", + "@codingame/monaco-vscode-quickaccess-service-override": "7.0.10", + "@vscode/iconv-lite-umd": "0.7.0", + "jschardet": "3.1.2" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + } + } +} diff --git a/internal/js/example/package.json b/internal/js/example/package.json new file mode 100644 index 0000000..2cd0da2 --- /dev/null +++ b/internal/js/example/package.json @@ -0,0 +1,18 @@ +{ + "name": "example", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@types/golang-wasm-exec": "^1.15.2", + "comlink": "^4.4.1", + "monaco-languageclient": "^8.7.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5" + } +} diff --git a/internal/js/example/src/internal/setup.ts b/internal/js/example/src/internal/setup.ts new file mode 100644 index 0000000..0b7b63c --- /dev/null +++ b/internal/js/example/src/internal/setup.ts @@ -0,0 +1,120 @@ +import type { ResponseMessage, RequestMessage } from 'vscode-languageserver-protocol' + +// Set of functions copied from wasm_exec.js to read strings from memory. +const decoder = new TextDecoder() + +const getInt64 = (mem: DataView, addr: number) => { + const low = mem.getUint32(addr, true) + const high = mem.getInt32(addr + 4, true) + return low + high * 4294967296 +} + +const loadString = (mem: DataView, addr: number) => { + const saddr = getInt64(mem, addr) + const len = getInt64(mem, addr + 8) + return decoder.decode(new DataView(mem.buffer, saddr, len)) +} + +/** + * Type exposes a private type necessary to manually invoke Go exported funcs. + */ +interface ExtendedGo extends Go { + /** + * Manually calls Go functions exposed to JS. + * + * This is the only way to call exposed function without adding it to 'globalThis' property. + * Will be replaced when '//go:wasmexport' is available. + * + * @see https://github.com/golang/go/issues/42372 + * + * @param callbackId Go callback ID generated by `syscall/js`. + */ + _makeFuncWrapper: (callbackId: number) => (p: TParam) => TReturn +} + +/** + * Checks if Go instance contains expected private fields and acts as a TypeScript + * type guard. + * + * @param go Go instance created by 'wasm_exec.js' + */ +const supportsGoFuncWrappers = (go: Go): go is ExtendedGo => { + return '_makeFuncWrapper' in go +} + +/** + * Setups Go object for gnopls server to send and receive LSP messages via passed MessagePort. + * + * @param go Go instance. + * @param port MessagePort used for communication. + */ +export const configureGoInstance = (go: Go, port: MessagePort) => { + if (!supportsGoFuncWrappers(go)) { + throw new Error('unsupported version of wasm_exec.js') + } + + let requestListener: ((req: string) => void) | null = null + + // Prepare WebAssembly import object so gnopls would be able + // to communicate via message port with a client. + // + // See: /internal/js/imports_js.go + go.importObject.lsp = { + writeMessage: (slicePtr: number) => { + let rawMsg = '' + + try { + rawMsg = loadString(go.mem, slicePtr) + + // Skip header as it's not used by LSP client. + if (rawMsg.startsWith('Content-Length')) { + return + } + + const msg: ResponseMessage & RequestMessage = JSON.parse(rawMsg) + port.postMessage(msg) + } catch (err) { + console.error('gnopls.lspWriteMessage: failed to handle LSP message: ', err) + } + }, + closeWriter: () => { + // Called on server shut down, feel free to implement cleanup logic. + }, + registerCallback: (callbackId: number) => { + // Register Go callback to listen for incoming LSP messages from client. + // + // See: https://medium.com/towardsdev/go-webassembly-internals-part-1-a7ccdafe6822 + requestListener = go._makeFuncWrapper(callbackId) + }, + } + + // Helper to report errors back to LSP client + const reportFatalError = (req: RequestMessage, msg: string) => { + console.error(`gnopls: ${msg}`) + + const { jsonrpc, id } = req + port.postMessage({ + jsonrpc, + id, + error: { + message: msg, + }, + }) + } + + // Subscribe and pass incoming messages to gnopls server. + port.onmessage = ({ data }: MessageEvent) => { + if (!requestListener) { + reportFatalError(data, 'cannot handle request before server started!') + return + } + + try { + const content = JSON.stringify(data) + const body = `Content-Length: ${content.length}\r\n\r\n${content}` + requestListener(body) + } catch (err) { + reportFatalError(data, `failed to handle message: ${err}`) + } + } +} \ No newline at end of file diff --git a/internal/js/example/src/script.ts b/internal/js/example/src/script.ts new file mode 100644 index 0000000..c71c8f9 --- /dev/null +++ b/internal/js/example/src/script.ts @@ -0,0 +1,62 @@ +/** + * This is a simple demo file that shows how to configure a gopls Go instance + * for communication between LSP client and server. + * + * Implementation details may vary depending on editor library. + * + * For Monaco, please check "@codingame/monaco-editor-treemended" package. + * For CodeMirror - https://github.com/FurqanSoftware/codemirror-languageserver + */ + +import { MonacoLanguageClient } from 'monaco-languageclient'; +import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver-protocol/browser' +import { CloseAction, ErrorAction } from 'vscode-languageclient' + +import * as Comlink from 'comlink'; + +import { LSPWorker } from './types'; + +// See: worker.ts +const lspWorker = new Worker('worker.js') + +// Create a pair of message ports dedicated only for LSP messages. +const { port1: clientPort, port2: serverPort } = new MessageChannel(); + +// Comlink provides a convenient way of calling worker functions. +// Feel free to use plain "postMessage" calls instead. +const proxy = Comlink.wrap(lspWorker); + +// Ask worker to start gnopls and use passed port to json-rpc communication. +// MessagePort should be passed as a transferable object. +await proxy.connect(Comlink.transfer({ port: serverPort }, [ serverPort ])) + +// Setup LSP client using a client port. +const reader = new BrowserMessageReader(clientPort) +const writer = new BrowserMessageWriter(clientPort) + +const lspClient = new MonacoLanguageClient({ + name: 'gnopls-lsp-client', + clientOptions: { + // use a language id as a document selector + documentSelector: [ + { language: 'go', scheme: 'file' }, + { language: 'gno', scheme: 'file' }, + ], + // disable the default error handler + errorHandler: { + error: () => ({ action: ErrorAction.Continue }), + closed: () => ({ action: CloseAction.DoNotRestart }), + }, + }, + // create a language client connection to the server running in the web worker + connectionProvider: { + get: () => { + return Promise.resolve({ reader, writer }) + }, + }, + }) + +// Don't forget to dispose all resources at the end. +await lspClient.dispose() +void proxy[Comlink.releaseProxy]() +lspWorker.terminate() diff --git a/internal/js/example/src/types.ts b/internal/js/example/src/types.ts new file mode 100644 index 0000000..511380e --- /dev/null +++ b/internal/js/example/src/types.ts @@ -0,0 +1,14 @@ +interface ConnectParams { + /** + * MessagePort to use by server to communicate with LSP client. + */ + port: MessagePort +} + + +/** + * LSPWorker defines a Comlink worker interface. + */ +export interface LSPWorker { + connect: (args: ConnectParams) => Promise +} \ No newline at end of file diff --git a/internal/js/example/src/worker.ts b/internal/js/example/src/worker.ts new file mode 100644 index 0000000..2db42d9 --- /dev/null +++ b/internal/js/example/src/worker.ts @@ -0,0 +1,37 @@ +import * as Comlink from 'comlink' + +import { LSPWorker } from './types'; +import { configureGoInstance } from './internal/setup'; + +declare const self: DedicatedWorkerGlobalScope + +// Copy wasm_exec.js from '$GOROOT/misc/wasm' +importScripts('/wasm_exec.js') + +// Don't forget to build gnopls +const GNOPLS_URL = '/gnopls.wasm' + +const worker: LSPWorker = { + connect: async ({ port }) => { + // Don't forget to configure filesystem before using gnopls. + // You might need to override globalThis.fs in order to provide source files for a server. + const go = new Go() + + // Feel free to pass custom environment variables and cmdline args + // go.env.FOO = 'bar' + // go.argv = ['gopls'] + + configureGoInstance(go, port) + + // Fetch the worker and instantiate a wasm instance + const { instance } = await fetch(GNOPLS_URL) + .then((rsp) => WebAssembly.instantiateStreaming(rsp, go.importObject)) + + // Start the server in background + go.run(instance) + .then((code) => console.log('gnopls: server exited with code ', code)) + .catch((err) => console.error('gnopls: cannot start server: ', err)) + }, +} + +Comlink.expose(worker); \ No newline at end of file diff --git a/internal/js/example/tsconfig.json b/internal/js/example/tsconfig.json new file mode 100644 index 0000000..49e6267 --- /dev/null +++ b/internal/js/example/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext", + "webworker" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "noImplicitAny": false, + "experimentalDecorators": true, + "noFallthroughCasesInSwitch": true, + "downlevelIteration": true, + "typeRoots": [ + "node_modules/@types" + ] + }, + "include": [ + "src" + ] +} \ No newline at end of file From 291ff2d4624721392a9645bad0f8df3ef07ad958 Mon Sep 17 00:00:00 2001 From: Denys Sedchenko Date: Thu, 18 Jul 2024 13:36:10 -0400 Subject: [PATCH 07/10] fix: Update internal/js/listener_js.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jerónimo Albi --- internal/js/listener_js.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/js/listener_js.go b/internal/js/listener_js.go index 28e1ce6..c6bfad9 100644 --- a/internal/js/listener_js.go +++ b/internal/js/listener_js.go @@ -50,11 +50,9 @@ func registerRequestListener(ctx context.Context) (io.ReadCloser, error) { registerCallback(callbackID) go func() { - select { - case <-chanCtx.Done(): - callback.Release() - close(inputEvents) - } + <-chanCtx.Done() + callback.Release() + close(inputEvents) }() reader := NewChannelReader(inputEvents, cancelFn) From 5959bfa98c02cf1741a66575df63d45e28a859e9 Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 18 Jul 2024 13:41:56 -0400 Subject: [PATCH 08/10] fix: fix doc typos --- internal/js/example/src/internal/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/js/example/src/internal/setup.ts b/internal/js/example/src/internal/setup.ts index 0b7b63c..1ad4532 100644 --- a/internal/js/example/src/internal/setup.ts +++ b/internal/js/example/src/internal/setup.ts @@ -20,7 +20,7 @@ const loadString = (mem: DataView, addr: number) => { */ interface ExtendedGo extends Go { /** - * Manually calls Go functions exposed to JS. + * Creates a callable JS function from Go's callback ID. * * This is the only way to call exposed function without adding it to 'globalThis' property. * Will be replaced when '//go:wasmexport' is available. From b25173b0449de93a23d223a05ffd0f6a998233f9 Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 18 Jul 2024 13:58:51 -0400 Subject: [PATCH 09/10] chore: update docs --- internal/js/example/.gitignore | 2 +- internal/js/example/README.md | 2 +- internal/js/example/src/internal/setup.ts | 2 +- internal/js/example/src/script.ts | 5 +++-- internal/js/example/src/worker.ts | 12 ++++++++---- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/internal/js/example/.gitignore b/internal/js/example/.gitignore index 5247532..27a9f0c 100644 --- a/internal/js/example/.gitignore +++ b/internal/js/example/.gitignore @@ -1,4 +1,4 @@ node_modules .DS_Store .vscode -.idea \ No newline at end of file +.idea diff --git a/internal/js/example/README.md b/internal/js/example/README.md index b45ba3b..1a5ce8c 100644 --- a/internal/js/example/README.md +++ b/internal/js/example/README.md @@ -11,4 +11,4 @@ This example omits such nuances as editor integration or file system support and * `cp $(go env GOROOT)/misc/wasm/wasm_exec.js .` * Build gnopls as a WebAssembly file for JavaScript environment: * `GOOS=js GOARCH=wasm make build` -* Modify paths to `wasm_exec.js` and WASM file in [worker.ts](./worker.ts) file. \ No newline at end of file +* Modify paths to `wasm_exec.js` and WASM file in [worker.ts](./worker.ts) file. diff --git a/internal/js/example/src/internal/setup.ts b/internal/js/example/src/internal/setup.ts index 1ad4532..03bdd53 100644 --- a/internal/js/example/src/internal/setup.ts +++ b/internal/js/example/src/internal/setup.ts @@ -117,4 +117,4 @@ export const configureGoInstance = (go: Go, port: MessagePort) => { reportFatalError(data, `failed to handle message: ${err}`) } } -} \ No newline at end of file +} diff --git a/internal/js/example/src/script.ts b/internal/js/example/src/script.ts index c71c8f9..913bac3 100644 --- a/internal/js/example/src/script.ts +++ b/internal/js/example/src/script.ts @@ -30,11 +30,12 @@ const proxy = Comlink.wrap(lspWorker); // MessagePort should be passed as a transferable object. await proxy.connect(Comlink.transfer({ port: serverPort }, [ serverPort ])) -// Setup LSP client using a client port. +// Feel free to pick up any LSP client to library as long as it supports way to specify custom message transports. +// This example uses LSP client used by Monaco editor and VSCode. const reader = new BrowserMessageReader(clientPort) const writer = new BrowserMessageWriter(clientPort) -const lspClient = new MonacoLanguageClient({ +const lspClient = new MonacoLanguageClient({ name: 'gnopls-lsp-client', clientOptions: { // use a language id as a document selector diff --git a/internal/js/example/src/worker.ts b/internal/js/example/src/worker.ts index 2db42d9..051f163 100644 --- a/internal/js/example/src/worker.ts +++ b/internal/js/example/src/worker.ts @@ -1,3 +1,7 @@ +/** + * This file contains a web worker used to host and configure LSP server. + */ + import * as Comlink from 'comlink' import { LSPWorker } from './types'; @@ -5,7 +9,7 @@ import { configureGoInstance } from './internal/setup'; declare const self: DedicatedWorkerGlobalScope -// Copy wasm_exec.js from '$GOROOT/misc/wasm' +// Don't forget to copy wasm_exec.js from '$GOROOT/misc/wasm' importScripts('/wasm_exec.js') // Don't forget to build gnopls @@ -13,8 +17,8 @@ const GNOPLS_URL = '/gnopls.wasm' const worker: LSPWorker = { connect: async ({ port }) => { - // Don't forget to configure filesystem before using gnopls. - // You might need to override globalThis.fs in order to provide source files for a server. + // For browsers: don't forget to configure filesystem before using gnopls! + // Override globalThis.fs with your own implementation in order to provide source files for a server. const go = new Go() // Feel free to pass custom environment variables and cmdline args @@ -34,4 +38,4 @@ const worker: LSPWorker = { }, } -Comlink.expose(worker); \ No newline at end of file +Comlink.expose(worker); From ef33fd5ca1d06475f67e19f2b170a73254028cf5 Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 18 Jul 2024 13:59:12 -0400 Subject: [PATCH 10/10] fix: fix typos --- internal/js/example/src/script.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/js/example/src/script.ts b/internal/js/example/src/script.ts index 913bac3..d247f94 100644 --- a/internal/js/example/src/script.ts +++ b/internal/js/example/src/script.ts @@ -30,7 +30,7 @@ const proxy = Comlink.wrap(lspWorker); // MessagePort should be passed as a transferable object. await proxy.connect(Comlink.transfer({ port: serverPort }, [ serverPort ])) -// Feel free to pick up any LSP client to library as long as it supports way to specify custom message transports. +// Feel free to pick up any LSP client library as long as it supports way to specify custom message transports. // This example uses LSP client used by Monaco editor and VSCode. const reader = new BrowserMessageReader(clientPort) const writer = new BrowserMessageWriter(clientPort)