POV: LSP kind of sucks sometimes | NeovimConf 2024

Source

Hello, amigos, and welcome to my NoBeam conference spiel on LSB, where I'll rant about some things that I don't really like about it after working on it for a while, both in my day job and my open source activities. First, a little about myself, as I need to talk for a while. I’m a software engineer based in Seattle. In a previous life, I was part of a Microsoft team. I worked on TypeScript, VS Code, and Visual Studio. While I didn’t work directly with the TypeScript compiler, I focused more on TypeScript and web development tooling in general. This experience is what led me to work on VS Code and Visual Studio.

Relevant to this talk, I've contributed several non-trivial features to NowIM core. In addition to what I've included here on the slide, I also enjoy contributing to my favorite plugins. Oh yes, and I'm also known as a prime agent meme. However, today I didn't come here to talk about myself, but rather something you're likely familiar with if you've ever configured LVM.

And so you might know about LSP because you downloaded a bunch of server configurations from NVM LSPConfig, or you copy-pasted the key "start on attach" function into your init.lua file to set up your key bindings when a new language server is loaded. Or you actually know the specification by heart because you wrote an LSP server or an extension that depends on it. Given that we have all of these different ideas of what LSP is, let's agree on what I'm referring to. Here’s the definition from the Microsoft docs: the Language Server Protocol defines the protocol used between an editor or IDE and the language server that provides language features like autocomplete, go to definition, find all references, etc. Before I give you my opinionated take on LSP, let me support my authority on the matter.

And so I wrote the ESLint integration for Visual Studio, which is basically a C# wrapper around the VS Code ESLint extension. If you’ve ever used VS Code, I’m almost certain that you have this extension installed. I added support for LSP features in Visual Studio, such as hints and autocomplete. Yes, these features already existed in the IDE, but they relied on Roslyn APIs that only worked with C#. Therefore, I created services that would work for any LSP-friendly language in Visual Studio. I also wrote the specification for the base protocol, which was a nice idea.

In theory, my goal was to develop a protocol that would share common topics between LSP (Language Server Protocol), DAP (Debug Adapter Protocol), the Debug Adapter Protocol, and the Build Server Protocol. Despite the base protocol being part of the LSP specifications, it did not result in anything tangible. I also want to clarify that this is not the perspective of an ex-Microsoft employee; it's just my viewpoint as an open source contributor who is a bit frustrated with LSB (Language Server Base). There is nothing secret here.

Everything that I'm about to share can be found on GitHub, and I'm not going to be sharing any super secret Microsoft internal information. Also, this is my opinion; I am not speaking on behalf of Microsoft or other NeoBIM core maintainers. Now that the paperwork is out of the way, let's start. The first thing to note is that the protocol is ambiguous.

Despite its length, the specification is quite vague. When working on the native Siemens engine in November, I wrote a parser using the Lua LPeg parsing library, following the grammar defined by LSP, which I have included here. However, as I worked on this, I encountered some scenarios that were not documented by the specification, and the different snippet libraries I was using as references supported these unofficial behaviors in different ways. Here’s an example of such ambiguity. Let me pretend that I'm the Primogen, and I'll read it out loud to you all.

And I hid usernames just to keep people anonymous in this thread. I am unsure what to do with this test since it tests something that LSB doesn't support theoretically. The snippet grammar states that the value of a placeholder node consists of a single node, but the current parser allows it to be multiple nodes. Is that intentional? Should we support this?

We only need to support the grammar from the specification; it is fine to drop it. On the other hand, could there be any problems with supporting it? Since the goal of this whole snippet thing shouldn't, in my opinion, only be to support the LSB specification, that’s a minimal requirement. If we can enhance it further and it makes sense to do so, why not pursue that?

I think it's funny that they say "for free," since supporting this is not trivial. No, that is the goal. The basic user interface may be shared and exposed, but the underlying snippet format will only ever be LSP. And LSP for us means the spec and nothing but the spec. And nothing is free.

You'll always pay a cost in maintenance and documentation. I think the specification intended for placeholders to have multiple nodes. For example, the "any" node should allow for multiple nodes. The law will parse this "any" node, which will include a text node and a tab subnode. My decision policy was that the implementation should be supported by code behavior as much as possible.

VS Code allows that. However, that is different from Neovim's decision policy, which is that if VS Code can't stick to its own specifications, that's their problem. If the specifications can be interpreted as allowing multiple nodes, supporting a permissive reading is acceptable. That's a different matter. Stick with me; this is the end of the thread.

There are many details that are not specified in the LSPS specification. For example, LSPS does not determine the colors or precision after applying text edits. In such cases, I try to check the behavior of VS Code as much as possible. However, I agree that certain features of VS Code should be implemented. I agree with Fool and Bar; we should follow the LSP specification and not promote a VS Code flavor of it.

In case of ambiguities, we can have a larger discussion. However, in this case, it is very clear that the LSP states that "any" consists of a single node. There's no reason for that rule not to be written as such. If we need to resolve the ambiguity or ensure that VS Code adheres to their specification, we should make the change upstream rather than guessing per editor. As you can see, an ambiguous protocol results in these kinds of heated discussions. Another issue I have with the protocol is how difficult it is to make meaningful contributions to it, especially if you don't work at Microsoft.

Sure, you can fix a trivial typo, but the process for proposing a new feature is quite obscure. Moreover, only a couple of VS Code maintainers have write access to the repository, and there isn't a discussion space for reviewing additions to the protocol. Of course, I'm not asking for something like the Rust Foundation, as we already know how that works, but rather for a group of people, not all of whom work at Microsoft, where we can discuss new proposals. This would make sense for the editors that depend on LSP and would help ensure that it remains, by definition, an open-source project. Here is an example of one of my attempts to make the contribution guidelines for the LSP more straightforward and clear. However, I must point out that this pull request is still open today.

And so, yeah, I don't think that this attempt was successful. Another thing is that LSB, let's be real, is just driven by VS Code extensibility. First, I should explain what the meta model is, which is basically this JSON object that describes the LSP using JSON. The nice thing about the meta model is that it allows editors like Neovim to read this JSON file and generate types for LSB. Neovim does this and generates the Lua types that extensions might use, or you might even use your own LSP configuration.

And so, as you can see here on the slides, the npm scripts that generate and update the meta model live in the VS Code Language Server Node, which is the LSP implementation for VS Code. This basically means that in order to update the public APIs, you first need to implement it in VS Code. Generally, 99% of proposals to the protocol start in VS Code. Yes, I just said 99% because Neovim has managed to break the VS Code monopoly by proposing and implementing a feature that was later officialized in the specs: allowing Markdown to be used for diagnostic messages. Hopefully, now when you have that diagnostic floating window, it will feature beautiful Markdown with three-seater syntax highlighting.

Another thing is that I feel that Language Server Protocol (LSP) kind of limits what an editor or a server can do regarding feature development. Implementing LSP is already a pain, both for servers and editors. You need to think about how to handle the RPC connection, have handlers, and create function handlers for every request. If your server has a different representation of the objects used in the protocol, you have to deal with how to translate those back and forth. It involves a lot of templates and boilerplate code.

There's also the fact that adding a feature that's out of spec is a huge hassle. As you can see, incorporating something into the specifications takes forever. If any of you want to have a custom message that's not part of the protocol, you need to create your own types and design your own message. It might just not be worth the effort. Additionally, simply implementing the Language Server Protocol (LSP) isn't enough for Integrated Development Environments (IDEs) where users expect much more than what's documented. One example of this is the rename feature.

In Visual Studio, when you rename a variable, you receive AI-generated suggestions. You have options to include comments or strings in the rename, and you're able to preview the rename as you change the value in the input field. On the other hand, if you simply want LSP rename functionality, a text field would be sufficient. However, to achieve the experience expected from an IDE, LSP is simply not enough. Therefore, you will always need to build on top of it and extend its capabilities if you want this level of experience. Another relevant example is something that occurred with Rust Analyzer.

And so, five years ago in 2019, there was a request to allow code actions to specify cursor position. In Rust, in case you have never written Rust code—if so, why?—I do have experience with Rust, by the way.

Anyway, going back to the topic, Rust has this code action that allows you to add a derive macro to a struct, an enum, or some other type. When you execute that "Add Derive" code action, Rust Analyzer intended for the IDE to place the cursor inside the parentheses so that the user could continue typing and smoothly fill in the trait that they wanted to derive from. This feature was requested in an issue opened in 2019, and it wasn't until last year, in 2023—four years later—that I finally closed it. The fact that there was this straightforward request from Maclad, who is an amazing open-source contributor to Rust Analyzer, and it took four years for this feature to be officially implemented shows how difficult and cumbersome it can be to extend the protocol.

In case you're curious, the fix involved snippets. Code actions can indicate that they will apply a workspace edit, but the idea was that instead of applying a workspace edit, they could also indicate that they want to apply a snippet edit. Given that snippets have all of this logic around tab stops and placeholders, you get that cursor control for free. I also have to say that I kind of fell victim to that software engineering bias; after working on the snippet engine, I thought that snippets would fix everything.

And so snippets were, of course, fresh in my mind, and I thought these to be a nice way to fix it. Should we just get rid of LSB? No, but let's be aware of the things that are wrong with it. Right. I think that's a good first step.

And listen, I would be a hypocrite if I ended this presentation by saying that I don't love the protocol because I do. Thanks to the standard language specification, we can achieve the kind of editing experience that makes VS Code useful. Join the dark side! It also makes the work much easier for Language Server Protocol (LSP) servers when they want to support a new feature because they can focus on the language server's core functionalities without worrying about UI variations that differ across every editor. For example, if you have your LSP server and you want to provide completions, imagine if you had to implement similar logic to what Neovim's `nvim-cmp` does, figuring out how to translate a completion item to a format that can be used within the editor, creating the completion window, sorting the items, and managing all of this logic that varies in every editor. It would be a huge pain and would create a significant barrier to achieving feature parity among editors. Finally, I just don't know what we should be doing instead.

Before making any drastic changes, I think we should first consider our alternatives. Since I don't currently have one, let's focus on how we can improve the current situation. When you encounter issues with one of your LSP (Language Server Protocol) servers, such as receiving an angry red error message like "unexpected null" after executing a particular code action, it’s a good idea to look into the implementation of the server. Compare it against the relevant notifications or requests in the LSP specification to determine if the server is behaving inappropriately. If the issue involves returning a null value, the LSP specification is clear about when returning null is an acceptable response.

And so, make sure that the investigation doesn't immediately blame Neovim. In particular, don't just say, "Oh, it works in VS Code," because that isn't helpful. After the investigation, you will probably know enough to explicitly pinpoint where the error is and what the fix should be. At that point, just be a good citizen and contribute the fix. Usually, these solutions are straightforward and greatly appreciated. Definitely avoid replicating hacks. Sometimes things may work in VS Code because it might have a hack for a certain LSP server, but we won't take that approach.

Now, we do not support misbehaving servers. So, don’t just copy and paste a hack that you saw in VS Code, because VS Code is not the standard. We follow the Language Server Protocol (LSP), so please refrain from doing that. I am not revolutionary in saying that I don't speak positively about the LSP.

If you enjoyed this presentation, I would suggest that you take a look at these blog posts. The first one is also from Maclad, a Rust Analyzer legend. LSP could have been better. Then there's a newer one, "LSB: The Good, the Bad, and the Ugly," from a core maintainer of LSB in Haskell. Those would be great reads, and that's all.

I hope you enjoyed it, and thank you for listening.