Recently, I decided to take my long standing , perfectly good GNU Emacs lsp-mode setup and completely replace it with Eglot, the now built in GNU Emacs LSP solution. At one level I didn't have any particularly strong specific reason to switch; I started by trying out Eglot after switching entirely to Corfu then just kept going to see how far I could get towards a good Eglot environment. The result is perfectly good and some things work better (Eglot will do 'complete to common prefix' in Go and Python modes) but it took more than a little bit of yak shaving to get here.
At another level, lsp-mode with lsp-ui is what I'd call a busy interface, with all sorts of things going on, and these days I've decided that I want a quieter LSP experience. Eglot is famously more minimal and quiet than lsp-mode, although you can and should augment Eglot's interface with additional packages . I could have tamed lsp-ui more with additional settings and fiddling, but switching to Eglot took care of all of that all at once, with other benefits. Overall I'm happy to have switched, although it was more work than I was entirely expecting.
(Should you switch? I don't know, but if you stick with GNU Emacs and use it in the modern way, I think you will sooner or later .)
As I've described in an earlier entry , Eglot's minimalism is because it's a modern GNU Emacs package that expects you to fill in features with other packages that interact with it through standard Emacs Lisp APIs. This means that for a good (but non-busy) LSP experience in Eglot, I needed to hook up a variety of additional things.
- Corfu just worked for completion ; my general Corfu settings were fine.
- To get a good cross reference setup where I could get lsp-ui like previews of references to something, I needed to connect consult to the general Emacs xref system by setting '
xref-show-xrefs-function' to 'consult-xref'. - I went back and forth between Flycheck with flycheck-eglot and Flymake before eventually settling on Flycheck. Flymake is better integrated with Eglot (in a way that I notice a bit) but I can make Flycheck work well enough and I prefer it in general. Eglot normally automatically puts buffers into flymake-mode, so to shut that off I do (in my use-package declaration for Eglot):
:config (add-to-list 'eglot-stay-out-of 'flymake)
And then to automatically activate flycheck-eglot:
:hook (eglot-managed-mode . (lambda () (if (eglot-managed-p) (flycheck-eglot-mode 1))))
(In theory flycheck-eglot has a global mode, in practice it didn't work out reliably for me and the brute force of a hook was the easiest approach.)
Eglot has some configuration settings that you'll want to experiment with. I found that I wanted '
eglot-extend-to-xref
' to be '
t
', partly because that makes M-? find other uses in my own project of whatever external thing I've jumped to.
Eglot doesn't ship with any key bindings and I definitely needed some, partly to make LSP code actions more accessible. Since it's early in my Eglot usage, my key bindings are probably going to change, but my current set are:
("C-c r" . eglot-rename)
("C-c o" . eglot-code-action-organize-imports)
("C-c h" . eldoc)
("C-c a" . eglot-code-actions)
("C-c q" . eglot-code-action-quickfix)
("C-M-<mouse-2>" . eglot-code-actions-at-mouse)
The mouse binding exists because of one way flycheck-eglot isn't as fully hooked into Eglot as I'd wish , but it turns out to be generally convenient for access to LSP 'code actions'.
(I have deliberately not bound
eglot-format
to anything. In Go, the one language where I would trust LSP-driven code formatting, I already go-mode's
gofmt
command that I'm accustomed to using. I also don't expect to use the LSP 'organize imports' often, but maybe in Python.)
This is in addition to key bindings for other packages, such as Flymake, where in order to get nice navigation of Flymake reports, I needed to set up a key binding for
consult-flymake
along with a few others for Flymake functions. This became a somewhat unnecessary side trip when I went back to Flycheck, but since I built a working Flymake setup, I'm keeping it for any time when I want to use Flymake instead.
Looking back, I'd estimate that most of my work in switching from lsp-mode to Eglot wasn't in configuring Eglot, it was in configuring other packages. But to say it that way makes it sound more straightforward than it was. The actual process involved a lot of looking around for additional packages, trying things out, discovering things that didn't work for me, and so on (and some amount of backtracking, like my adventures with Flymake). To be fair, this is more or less what I went through with lsp-mode when I first set it up.
Eglot officially recommends that you start it by hand ( cf ), but I'm too lazy for that. Instead, as I did with lsp-mode, I arranged to start it automatically for local files in the relevant modes.
(use-package eglot
:defer t
:init
(defun eglot-ensure-local-only ()
"Enable Eglot only on local buffers."
(unless (file-remote-p default-directory) (eglot-ensure)))
:hook
(python-mode . eglot-ensure-local-only)
(go-mode . eglot-ensure-local-only)
[...]
One potential limitation of
eglot-ensure
as compared to
eglot
is that if you have multiple LSP servers for a particular language (such as 'pylsp' and 'ruff' for Python),
eglot-ensure
just picks the default one while
eglot
offers you a choice. To change afterward, you need to shut down the current LSP server and invoke '
eglot
'.
(There's a program to multiplex LSP servers ( discussion ) if I ever want to run several at once.)
LSP servers can offer you a profusion of 'code actions'. Sadly Eglot doesn't make these particularly conveniently accessible (but then neither did my lsp-mode setup), although I hacked around that with a mouse binding (mentioned above). At one level this is technically fair and correct, because LSP servers only offer you code actions when you ask (and code actions are specific to a particular spot). Eglot also doesn't give you any way of filtering what specific code actions it will show you out of a potentially long server list that you find mostly irrelevant (and some, not working), which sadly makes them rather 'busy' for both Go and Python.
Once I had a basic Eglot setup working, I had a fun time learning how to disable some checkers in pylsp , the Python LSP server I use, because my tastes are strongly against style-based linters in 'present all the time' diagnostics. Lsp-mode provides convenient controls to turn off, for example, diagnostics from the 'mccabe' complexity linter. With Eglot, I got to learn all about user specified workspace configuration , which is definitely the morally correct approach to this but which is much more complex. Here, let me show you:
(setq-default eglot-workspace-configuration
'(:pylsp (:plugins (:mccabe (:enabled :json-false)
:pylint (:enabled :json-false)
:pylsp_mypy (:enabled :json-false)
:mypy (:enabled :json-false)
:pycodestyle (:enabled :json-false))
)))
Yes, sometimes the mypy stuff is "pylsp_mypy" and sometimes it's just "mypy". This is an internal pylsp detail that Eglot makes you learn. Also, that '
setq-default
' is load bearing; you can't use
setq
.
I find it unfortunate that Eglot doesn't have any convenient way to temporarily set LSP server parameters for a project. If you have specific settings, your life will be much easier if you put them in a correctly formatted
.dir-locals.el
file, which may look like this:
(( nil
. ((eglot-workspace-configuration
. ( :gopls (:analyses
(:unusedresult :json-false
:QF1012 :json-false
:fmtappendf :json-false)))))))
(As you can tell, what you need to set varies from LSP server to LSP server. Gopls for Go is completely different than pylsp. This is a directory local setting for me rather than a global one because they only mis-fire on some of my code.)
If you want to change these settings on the fly, Eglot has documentation on that but it's not fun to deal with. If you sometimes want to turn on mypy for your Python (LSP) code but not always, as I do, you'll get to use '
dir-locals-set-class-variables
' to set up a new class, then use a function that looks like this:
(defun cks/mypy-enable ()
"Set Python eglot workspace configuration to enable mypy."
(interactive)
(let ((server (eglot--current-server-or-lose)))
(dir-locals-set-directory-class
(project-root (eglot--project server))
'cks-mypy-enabled)
(eglot-signal-didChangeConfiguration server)))
That this elaborate process is required is an accurate reflection of reality. Eglot is running one LSP server (per language) across your entire 'project' (directory tree), and settings for that LSP apply to all files you're editing in the project, so it can't have any notion of file or buffer local LSP server settings; they have to be project wide. By extension, setting 'eglot-workspace-configuration' through conventional means is a bad idea; that makes it a buffer local variable, which does nothing useful and will only confuse you.
Sidebar: My journey with Flymake and Flycheck in Eglot
Eglot works better with Flymake than with Flycheck and flycheck-eglot , at least currently. Specifically, with Flymake, Emacs will put a button 2 popup menu on the note itself with any LSP server driven corrections (usually a 'quickfix' LSP code action), but with Flycheck, all you get is the error being marked and you have to look for and trigger LSP code actions in another way. I initially switched to Flymake because of this, but Flymake took me some effort to configure so that I liked it.
However, after switching from Flycheck to Flymake, I found that there were still some things that Flycheck did better and sometimes I wanted Flycheck instead. So I retained my Flycheck setup as well (with flycheck-eglot too), which was convenient when the flycheck-eglot author came up with a nice workaround for my issue .
There's stuff to use Flycheck checkers in Flymake but I haven't done much experimentation with it, although I installed the package and set up some support infrastructure. My impression is that Flycheck has a larger collection of checkers than Flymake does and it's easier to shuffle among them. In theory a LSP server should make all other checkers unimportant, but in practice not so, especially if you want to sometimes invoke 'linter' level checkers.
I do sort of miss Flymake's 'show diagnostics at end of line' option, because it was a good way to make LSP diagnostics glaringly obvious, for times when I want that. There's flycheck-inline , but that only displays the current warning when you're on it, not all of the warnings when you scroll through. Sideline with sideline-flycheck has the same limitation but in my view a better UI experience.