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.