drshapeless


Emacs lsp clients comparison

Tags: emacs

Create: 2022-07-25, Update: 2023-05-27

This blog post investigate the difference between lsp-mode, eglot, and lsp-bridge in Emacs.

What is lsp

The lsp (Language Server Protocol) was implemented by MicroSoft, originally for the Visual Studio Code. It separates the logic of diagnosing language errors from the IDE. All the extensible editors, including Emacs, benefit from this, making them very competitive with IDE.

The lsp is a good invention, although with some stupid decisions made by MicroSoft in the first place, e.g. using UTF-16 instead of UTF-8. (It is MicroSoft, what do you expect.) With the crazy popularity of VScode, lsp has become the standard.

Background

When I started using Emacs, I use lsp-mode. It is the most popular lsp client back in the days. lsp-mode usually works. But when it doesn't, you are not likely to fix it. Also, the configuration of lsp-mode is extremely complex.

I mainly wrote in C++ in the past. I use the clangd server, and lsp-mode kept restarting for no reasons. Later that year, I was writing an iOS app. I tried the sourcekit-lsp from Apple, which is the lsp for Swift. lsp-mode was completely unusable, and I did not know why.

I made the switch to eglot from lsp-mode. I never look back since then. I appreciate the philosophy of eglot, simple, reuse internal Emacs functions rather than external packages, e.g. flymake over flycheck, project over projectile. Eglot does not have to install a lsp-"whatever" package for each language. It is minimalistic. The speed was obviously faster when I made the switch. Things may have changed. People in Reddit claims that lsp-mode is blazingly fast now, but I don't care, eglot is the best.

Things were great until I started to write some Flutter app recently. Flutter uses the dart language. The dart lsp is slow. I usually have to wait for a few of seconds for the completion candidates to appear. This is unbearable.

I tried to switch back to lsp-mode for dart. The dart language officially supports Emacs via lsp-mode with the lsp-dart package. It has a much richer functionality and visual candies. I don't care about the indent guide, project tree layout. The only important factor to me is speed, especially completion speed. But the fancy visuals come at a cost, even slower in speed to eglot. I kind of expected that, but can help to feel disappointed.

I started to looking for some other solutions. To my surprise, there was a third lsp client for Emacs which just released a few months ago. I am excited because it claims to be the fastest lsp client in Emacs. It is lsp-bridge.

Blazingly fast

lsp-bridge was originated from Emacs China, from manateelazycat. If you can read Chinese, you can read his thoughts on the architecture of the package from here. He uses a completely different approach to implement lsp client in Emacs. He used Python to implement an async RPC, making the lsp truely multi-thread and non-blocking, (this is very hard in Emacs because of the single-threaded nature, both of historical reason and lisp).

It sounds great. But what do we do with the completion? Company and corfu surely will not work well with this async server. The author wrote an other async completion system called acm, packaged with lsp-bridge.

Things start to be getting out of control. Everything lsp-bridge does are not tightly integrated with Emacs, and it never would by the nature.

It depends on a package called posframe. It is a pop-up frame in Emacs, which displays definitions at point. I have heard of this package for a long time, but have been avoiding it because I prefer minibuffer than pop-up, (it is disturbing).

I tried to install it via straight.el. And it was not in the straight index. I manually installed it with straight-use-package by explictly specifying the github repo name. Do NOT do it in this way. Since lsp-bridge bundled its own completion framework, acm, within its repo, but straight won't build acm by default. The correct way should be git clone the repo, load the path according to its readme.

lsp-bridge does not disappoint me. It is fast, really fast. The completion is shown immediately.

Drawbacks

Lack of tramp support

The speed of lsp-bridge does not come with no cost. It cannot be used over tramp. This is not a big issue to me. I work mostly with the local machine.

Posframe conflicts with EXWM

lsp-bridge uses posframe to display completion candidates. It seems to be the only way to display async content. However, posframe does not work well in EXWM. For example, if I open a Firefox window on the left in a vertical split layout, posframe does not know the window is a EXWM window, and will display the frame behind Firefox, making completion absolutely impossible to use.

Posframe also does not do well with pdf-mode. The posframe will display behind the pdf document.

UI

By default, it will display a column of icons on the completion frame. Some like icons, I hate them. I never find icons to be a good way to identify types of things. Icons are distracting. Although icons can be turned off, I don't like unnecessary things built into the package.

I also find the theme color of the completion box in lsp-bridge a bit off with my custom theme.

The most concerning thing is that, I can no longer use C-v or M-v to scroll the completion candidates by page. Instead, I have to use C-n to scroll them one by one.

Does not work well with Go

For whatever reasons, lsp-bridge does not work well with Go. I don't know whether it is because it find the wrong project root. Since the author of lsp-bridge is against using internal project.el to find the project root and insists on using the .git location.

Conclusion

I did not switch to lsp-bridge this time. It is a relatively new package that is not very polished. And it is doing things in its own ways like ivy, helm, lsp-mode. I don't like that.

The speed of lsp-bridge is amazing, but overly relying on posframe is a big pain in the ass for users that like to use Emacs to display all sorts of things.

In the end, instead of switching, I looked for some ways to boost the performance of eglot. And luckily, I found one, sarcastically also from Emacs China. I just discover this site and was surprise by the number and quality of Emacs users in China. You can turn off eglot events buffer.

;; This stops eglot from logging the json events of lsp server.
(setq eglot-events-buffer-size 0)

My suggestion

If you are a Emacs user, the best choice is eglot.

If you were a VScode soydev, and are in the transition to a real hacker, you may find lsp-mode's fancy ui and millions of unnecessary functions useful. You may be a doom Emacs user. Until you tailor made you Emacs configurations from scratch, staying with lsp-mode is a reasonable choice.

If you are a Emacs wizard who are experienced in writing and debugging elisp, you may use lsp-bridge and help its development. If you are a regular user who just want to write some code, unless fine tuning eglot does not have a reasonable performance, (less than 0.3 delay of completion is acceptable to me), you should be using eglot.

Overall, lsp-bridge is a usable, but not perfectly reliable. The speed is fast, but have the risk of bumping into a tree. The approach to use async by spinning the lsp to python is interesting. Although most decent Emacs user should have python installed, but ideally a lsp client of Emacs should not depend on python. I even prefer a C version and distributing a compiled binary.