Set up LSP-mode or Eglot for <insert language here>

What is LSP

General overview

Language Server Protocol, or LSP for short, is a standardization between “development tools” (emacs, vim, vscode…) and “language servers” (clangd, typescript-language-server…)

The best source for explanation is to read the Overview page on the official protocol website. Keep in mind that:

  • the “user” is you
  • the “development tool” is emacs

The very short, and worse-than-official-doc version is :

  • emacs is a “client” and needs to translate user-intent and emacs-lisp package calls from/into JSON-RPC payloads
  • external language servers are installed on the machine and indexes the project’s code to send relevant JSON-RPC payloads

:pushpin: If the worse-than-official version was too short, please check the overview page, there are a lot more people that spent a lot of time to explain it.

Emacs role as a client (Emacs LSP stack)

As of 2020, Emacs is at least 35 years old; it had coding features way before LSP was a thing, through elisp packages. Keeping only currently popular implementations:

  • completion is brought by company
  • syntax/error checking is brought by flycheck or flymake
  • jumping to definitions is brought by xref
  • displaying symbol documentation on point (under the cursor) is brought by eldoc

All those packages use data sources that are defined on a case by case basis; for example, elpy-company-backend is a source that uses Jedi to bring python completions to company

As emacs is a “development tool”, theoretically emacs’ developers are responsible to handle the client part of the protocol. This means integrating LSP in all the existing packages so that they ask a language server over JSON-RPC for the relevant information as a data source.

Instead of implementing a company-lsp source, and a flycheck-lsp source, and a xref-lsp source etc. that could be out of sync and create a lot of compatibilty issues, complete “LSP client” packages exist to act as an abstraction layer between the LSProtocol and the inner emacs packages that need a source.

There are 2 coexisting LSP clients in emacs, eglot and lsp-mode that provide exactly this :

  • convert user intent (“I want completions” or “What is this symbol ?”) from company, xref… requests into JSON-RPC messages
  • send and receive LSProtocol messages**
  • dispatch the received messages to the relevant “source” so that other packages seamlessly use language servers as a source

:warning: You should not have both lsp-mode and eglot active at the same time.

Doom-emacs provides a few opinionated tweaks and configuration to how all those packages are plugged together (“LSP client” and all of company eldoc flycheck etc.). All those different components playing together means that there are a few important places which when affected, can make the whole integration feel “broken”.

All of this is tentatively summarized in this graphic

:pushpin: This is serialization/deserialization, and the reason emacs 27 with jansson support is much more responsive when using LSP.

How to activate LSP in Doom Emacs

Doom supports LSP, but it is not enabled by default. To enable it, you must:

  1. Uncomment :tools lsp in your $DOOMDIR/init.el file. [1]

    If you want eglot instead, change that line to :tools (lsp +eglot) instead.

  2. In the same file, add the +lsp flag to each language module you want to use LSP with:

      :lang
    - go
    + (go +lsp)
    - python
    + (python +lsp)
    
  3. Run doom sync on the command line,

  4. Restart Emacs.

  5. Install any needed LSP servers (through your OS package manager or directly from the source).

Once done, Doom will automatically start the LSP server and connect to it when you open an eligible buffer.

Language modules without +lsp support

Some of Doom’s language modules may lack LSP support, even though lsp-mode (or another lsp Emacs plugin) already provides support for that language. This is usually because I’m not aware of it yet – let me know!).

To enable LSP for these languages, you only need to add the following line to $DOOMDIR/config.el:

(add-hook 'MAJOR-MODE-local-vars-hook #'lsp!)
;; Where =MAJOR-MODE= is the major mode you're targeting. e.g.
;; lisp-mode-local-vars-hook

Installing a server

  • If you use lsp-mode, then many servers can be automatically installed with M-x lsp-install-server. If it isn’t, or you’d prefer to install servers manually, lsp-mode’s documentation contains links to specific clients with their own documentation.

  • If you use eglot (or lsp-mode lacks an installer for the server you want), you must install the server yourself through your system package manager or directly from the source (and the server must be in your PATH).

:pushpin: A “server” is an external program installed on your machine.

Troubleshooting

How to know which component is affected ?

The connected server does not support…

Those messages tend to look like (error "The connected server(s) does not support method textDocument/codeLens To find out what capabilities support your server use ‘M-x lsp-describe-session’ and expand the capabilities section.") and usually this is a problem with the language server you installed. This can not be fixed on Emacs side.

… has exited

When the server abruptly quits it usually has 2 possible sources:

  • If it quits on start, try to start the server in a terminal in the same project folder and try to see if it works:
    • If it works, it’s either a Doom or a LSP-mode/eglot issue.
    • If it doesn’t work, your language server and your project don’t get along and need some counseling (you are the only counselor available).
  • if it quits after a while, then everything is possible. One likely culprit is the language server trying to hog too much RAM and being killed by Emacs to prevent common memory issues, but at this point those are just wild guesses about hard bugs to reason about. Limiting resources usage might help.

:pushpin: If you cannot access the logs from Emacs, try starting the server in a terminal opened in your project folder to see what happens.

The wrong server starts for my language

You need to set the priority between available servers to tell it which one to choose.

Eglot just takes the first server that matches the correct major mode for the buffer. That means the last call to (add-to-list 'eglot-server-programs '(cc-mode . ("clangd")) or the last call to (set-eglot-client! 'cc-mode '("clangd")) will “win”

LSP mode has its own priority system, for any major mode the server with the highest priority is chosen (default priority is 0, builtin priority is -1). Therefore you can use (set-lsp-priority! 'clangd 10) to use clangd when available.

How to use a custom server?

Server communication is the prerogative of the LSP client package, so the method depends on using eglot or lsp mode.

(set-eglot-client! 'cc-mode '("clangd" "--clang-tidy"))

You need to pass the symbol of the mode and the command line to start the server. For more complex cases, it is best to look at eglot documentation

Registering servers for lsp-mode is a tad more complex:

(lsp-register-client
 (make-lsp-client :new-connection (lsp-stdio-connection '("lua-langserver" "--lang=EN"))
                  :major-modes '(lua-mode)
                  :priority -1
                  :server-id 'lua-langserver))

Again, for more complex cases, it is best to go look at the documentation

Checking lsp-mode//eglot issue tracker/PRs for your language

When you have an issue with LSP, it’s safe to assume you are not the only one. Trying to search the issue trackers of LSP mode or eglot might give pointers to what’s failing and how to fix it.

If the fix includes setting a lisp value, wrap it in (after! lsp-mode ...) or (after! eglot ...) and add it in your config.el

Why are there extra packages like ccls and rustic sometimes ?

LSP is a protocol that allows extensions, and for some languages and language server, there are additional Emacs packages that enable those extensions :

  • ccls allows to query and see all the inheritance tree and overloads for functions/classes
  • rustic used to use the inline hints feature supported by rust-analyzer (the feature has been upstreamed to lsp-mode since then)

So the most likely reason for an additional “LSP” package to be added for a language in Doom is to access LSP extensions.

How to limit cpu / ram usage ?

Sadly, this is a per-language server setting, and is heavily dependent on what you are using. Checking the readme of the Doom module for the language you are using, or the documentation on the language server you are using is your best bet. LSP-mode documentation might help as well.

If you find a setting to limit it that is not supported and have the time to contribute back, please make a PR to expose it as a setting for other users to tweak it more easily

Doom-specific settings

The simplest way to know more about LSP customization options for your workflow is to check :tools lsp's documentation and the sections regarding the programming languages that you use.

For example, you can search the “Learn > Configuration” category for posts tagged with LSP or Python (and even subscribe to those tags)

The crux of it is that you can check for the relevant variables in the documentation of eglot or lsp-mode, and apply those settings using (after! eglot (setq ...)) or (after! lsp-mode (setq ...))


  1. By doing this you are enabling Doom modules. More information about Doom modules can be found in the documentation. ↩︎

2 Likes

Just a tiny addition. The following is no longer true:

LSP Mode can start multiple servers per major mode, not just one.

Not sure I understand. Looking at lsp-mode’s source, it still very much respects client priorities unless a non-nil :add-on? property is specified. Or did you mean that the guide lacks precisely this explanation for :add-on?

Yes, you are right. Sorry for my poor wording.

I meant the fact that add-on servers can start in addition to the highest priority server.

The documentation says (for reference; if anyone else is reading this):

:grey_question: I have multiple language servers registered for language FOO. Which one will be used when opening a project?

The one with highest priority wins. Servers defined in lsp-mode tend to have lower priority than the external packages (priority 0 if unspecified). If a server is registered with :add-on? flag set to t it will be started in parallel to the other servers that are registered for the current mode. If the server that you want to use is not with the highest priority you may use lsp-disabled-clients to disable the server with higher priority or use lsp-enabled-clients to enable only the servers you want to use. In order to find the server ids you may check *lsp-log* buffer.

Emacs LSP supports Ansible (was added recently): Ansible - LSP Mode - LSP support for Emacs

It should be part of the currently pinned emacs-lsp package, though updates to the Ansible client were done since.

I am not sure if Doom supports LSP for Ansible, as the documentation is missing, but from what I can see in Doom’s Ansible module, it does not, yet.

EDIT:

OK, this is too complicated for me to configure in Doom. Since ansible is a minor mode and the major mode is yaml-mode, I don’t think you would configure (ansible +lsp), but rather (yaml +lsp)?

If you install the LSP server for lsp-ansible, it will be chosen over lsp-yaml, as it has a higher priority, if the minor mode ansible is active.

Doom enables the ansible minor mode automatically based on the presence of certain files in the project. And if it does, the ansible LSP server is used correctly.

If the ansible minor mode is not active, the YAML LSP server will start. You can then manually enable the ansible minor mode and lsp-workspace-restart to get the Ansible server over the YAML server.

EDIT2:

I considered updating the ansible documentation in rewrite-docs, but I don’t even know how to document it in a good way. I cannot describe what I wrote above in a concise, readable way for users.