How we built a VS Code Extension with Rust, WebAssembly, and TypeScript
osohq.com
osohq.com
Also friendly heads-up that the second sentence under the Introducing WebAssembly (Wasm) heading might have gotten cut off.
> Build platform-specific binaries from the Rust crate and ... do some host identification magic .... This was also pretty easy to rule out because [of] ... the complexities of multi-platform builds.
This is how I currently do it, but I agree it is not ideal. Wasm is a nicer solution.
> Both tower-lsp and lspower require implementing the language server as an asynchronous service powered by Tower, a framework for async Rust. We didn't think the benefits of asynchrony would be worth the added complexity to the language server.
I definitely agree. Async is way over-used in Rust land. That said, it wasn't that hard to use tower-lsp. It's pretty well designed.
> we initialize the Wasm-ified polar-language-server and manage the server side of the LSP connection from TypeScript.
Does this mean that it's more difficult to use the LSP from anything except VSCode?
I think the solution I would go for is probably to compile my language server with Wasmtime, bundle Wasmtime for every platform I support and then just run it through that. Then I don't have to cross-compile my Rust code, but it's still a fully featured language server on its own.
Could be... we shall see when we try porting it to a second IDE. :-D
I was so focused on VS Code that I haven't spent much time considering the set of changes that'll be required to support a second client.
> I think the solution I would go for is probably to compile my language server with Wasmtime, bundle Wasmtime for every platform I support and then just run it through that. Then I don't have to cross-compile my Rust code, but it's still a fully featured language server on its own.
Bundling in wasmtime is a really interesting idea. Thanks for sharing!
At Wasmer we are planning to do the something similar for the VS Code WASM extension [1]. Right now it uses wabt.js (wabt compiled to Wasm + js bindings with Emscripten), but there are some more interesting ways to approach automatic bindings via Interface Types... here are some prototypes! [2]
[1] https://marketplace.visualstudio.com/items?itemName=dtsvet.v...
That is interesting. Is it that much complexity when using async Rust?
The network and disk IO is a tiny part (after the initial startup scan). Most of the work is CPU bound.
So async really doesn't get you much.
Async is also okay (not perfect) for CPU parallelism, with tokio::spawn etc., which I think is important for fast incremental compilation once everything is in RAM. To pick an example, let’s say you change the type of a widely used function, now you have to typecheck the entire project again.
If I were to design a language server, my hope would be to achieve both good IO interleaving and good CPU parallelism with a single async based design.
Not really —- there’s bowl of spaghetti worth of concurrency in LSP. All of the following things can happen at the same time, and they use the same state:
* several read-only requests from the client (compute completion *and* highlighting)
* write request from the client (the user typed a comment in Cargo.toml)
* write request from file system (user switched branches can Cargo.lock is now different)
* background project update (cargo metadata finished with a new project model)
* background compiler update (cargo check emitted a new diagnostic)
* updates on background “indexing” which were kicked in quiescent state
That’s significantly more logical concurrency than in a typical HTTP server, where each request is conceptually independent from every other and all the hard synchronization bits lie in the database.To manage this complexity, csp-style APIs (async/await and blocking threads) don’t really work, as there’s too much fundamental shared mutable state. What sort-of-works (but still is complex) is an explicit actor model, where you manually code the state machine and explicitly resolve the conflicts of the style “ok, I got results from cargo metadata, so I could spawn cargo check, but I also know that the user changed Cargo.toml in the meantime, so the results of metadata are actually stale and I better re-run that”.
Secondly adding some parallelization in normal "vanilla" Rust with std seems rather easy with channels and Rayon, so I'm not looking to retry async rust if I don't have to.
But I agree that we’re past that point at least for simple async worker threads and simple IO. I don’t follow the progress that closely, but I think work is happening towards standardization there.
I think the same way, and was enthusiastic of async Rust at the beginning, but right now I think it has caused a mess in libraries. Maybe they did the standardization way too early? Maybe they could have waited until the async libraries can be separated from runtimes, which I think they are now planning to do.
If you start to dig through otherwise useful libraries you see a divide to those who async and those who don't, and top of that which runtime they have chosen. Mixing and matching between those is painful.
This state totally sucks for the authors of async-std who have an uphill battle getting traction in the tokio-dominated ecosystem, but from user perspective you can pretend the problem doesn't exist. Treat tokio as the standard library, as if it was hardcoded and you had no choice on the matter.
They are mentioning that LSP will open a door to port the functionality to IntelliJ. There's no native support for LSP in IntelliJ as far as I am aware. Wondering how did they plan the port to IntelliJ when the time comes?