And overall overhead for running a language server in a separate process isn't that big. LSP is designed in such a way that only minimal amount of information is needed to be passed, like edits or short responses. Packing/unpacking JSONs isn't a bottleneck, the heaviest job like program analysis is done in the language server itself without interprocess communication overhead involved.
IPC is the natural solution to that - run a process in its own address space and communicate with it via I/O. TCP is not most optimal, but it is uniquous. Same thing with JSON.
In LSP, most of the processing happens in the LSP server anyway - communication overhead between client and server is negligible in comparison to it.
Windows COM does have a way to run the server out-of-process with a performance cost, and this fact is transparent to the client (if it only uses COM interfaces and not global variables or something). I assume you can even switch between the modes at runtime (with restarting the plugin).
My current Zed editor instance has 5 different language servers running, implemented in at least 3 different languages, some of which require a full VM runtime.
Could Zed (written in Rust) host a full .NET runtime to run the Roslyn language server for C#? Could an Electron-based editor? It feels a bit daunting. Plugins could be native shared libraries, but what happens when multiple language plugins all try to initialize huge process-wide runtimes like .NET or the JVM? Even multiple instances of the Python runtime are going to potentially be stepping on each others' toes.
IPC and separate processes is probably the pragmatic solution for now, even though I would love to live in a world where in-process was more of an option.
WASM could be the solution, but would lock language server authors into the subset of programming languages that can be compiled to WASM, and WASM still looks a lot like IPC in practice. (Zed already uses WASM components for its plugins.)
If you have freedom then you have the the freedom to make mistakes. In Windows globals are tightly scoped to a DLL and cannot accidentally cross over, so multiple python interpreters are no problem if the python interpreter is statically linked in each python plugin.
Also, I doubt that you would have a very great time running both .NET and JVM in the same process. For one, both of them install signal handlers, and both of them do funky things with threads.
However, to come back to your point, there are much better high performance OS IPC mechanisms for inter process communication than sending JSON down through a TCP wire.
As others point out, Electron.
There are some arguments that do carry water in favor of using a client/server protocol transmitting JSON, in particular, the ability for a nearly complete decoupling of analysis of code and the displaying and editing of that code. Also, LSP was (to my knowledge) the first language/platform/usecase agnostic protocol intended for use in code editors.
I get why this is where a big chunk of developers have ended up, but I do bemoan the lost potential for a language to grab mindshare and popularity on the usability and performance of its tooling and developer experience via-a-vis a custom designed and hyper specific code editor. I mean, Rust and Elm received endless praise for their error messages as a massive boon to developer experience, so it is a facet of language design and implementation that can act as great advertisement. I just hate that the prevalence of LSP at this point precludes custom editors as a first choice in the current zeitgeist.
This isn’t for Electron app only, but is also helpful for Emacs, vim or helix (especially when they lack said language plugins).
Now, some IDEs provide capabilities for a language far exceeding what can even be implemented in a LSP.