7 minutes
Written: 2021-07-20 02:28 +0000
Updated: 2024-08-06 00:53 +0000
Doom Emacs and Language Servers
doom-emacs
as anssh
IDE withTRAMP
usingeglot
and language servers.
Background
For most of my emacs
configuration1, there normally isn’t very much to write about which isn’t immediately evident from my configuration site. However, since my shift to a MacBook, I have needed to fine tune my existing lsp-mode
default setup for TRAMP
and this post will cover a little bit of that. Though most of the post is about doom-emacs
, it is also applicable to vanilla emacs after porting the snippets over to use-package
instead.
There are two primary methods of interfacing emacs
to language servers, eglot
and lsp-mode
. Not for me to do a full comparison, but some thoughts of the top of my head were:
- eglot
- Feels more minimal, maintained by the author of
yassnippet
- lsp-mode
- Much more configuration, better documentation in some aspects, strangely difficult
TRAMP
setup
Note that the comparisons stated are neither fair nor particularly useful. I just need something to work quickly with my main programming languages at the moment. I went with eglot
.
Desiderata
The things I really need, which both provide are:
- Easily look up doc-strings
- Auto-completion
- Renaming symbols
- Figuring out common mistakes
- Indexing and lookup for symbol definitions and usage
Eglot
Getting started with eglot
was surprisingly pleasant, for doom
ships a pretty out-of the box configuration anyway.
1;; init.el
2(lsp +eglot) ;; Activate eglot
3(python +lsp) ;; Python with pyls by default
4(cc +lsp) ;; C++ with clangd by default
Usage was pretty sweet (after getting clang-tools
for clangd
), it can be activated by running eglot
in any supported buffer, and it came with all the standard bells and whistles.
For working with tramp
too, after Emacs 27.1
, in most cases it just works, one simply needs to supply the location of the language server executable and we’re off to the races. In-fact, even adding the full path to the binary to PATH
is good enough for eglot
.
Caveats
- No
projectile
support planned (though there are workarounds)- Uses
project.el
to pick root directories (i.e..git
)- In particular this means it works poorly with
git
sub-modules
- In particular this means it works poorly with
- Uses
Overall the main issue with eglot
seems to be the insistence to be accepted into emacs
core someday. This is great, and a good direction for the project to grow in, but it constrains my workflow unnecessarily. I’m more interested in working with doom-emacs
than vanilla emacs
and so am pretty invested in non-core libraries like projectile
2.
Language Servers
For getting the language server providers themselves, we will mostly leverage direct binaries where possible, but also, depending on the implementation, virtual environments3. Essentially we have the following Rosetta stone.
Language | Server | Installation | Management |
---|---|---|---|
C++ | clangd | Manual (binary) | None |
Python | pylsp | pip | Micromamba |
Bash | bash-language-server | npm | NVM |
R | languageserver | install.packages | N/A |
TeX | digestiff | Manual (wrapper) | None |
Nix | rnix-lsp | Nix | N/A |
Fortran | fortran-language-server | pip | Micromamba |
So we need to setup the following, assuming an existing conda
helper (micromamba
as described here) setup.
1# Get Micromamba
2mkdir -p ~/.local/bin
3export PATH=$HOME/.local/bin:$PATH
4wget -qO- https://micromamba.snakepit.net/api/micromamba/linux-64/latest | tar -xvj bin/micromamba
5mv bin/micromamba ~/.local/bin
6rm -rf bin
7micromamba shell init -s bash -p $HOME/.micromamba
8. ~/.bashrc
Now we can use it.
1micromamba create -n lsp
2micromamba activate lsp
3micromamba install python==3.9 pip -c conda-forge
Note that for this to work out well, we need to activate this environment by default, with something like micromamba activate lsp
added to your .bashrc
. In fact, it is best to also add the full path, since eglot
doesn’t seem to pick it up after micromamba activate
.
1export PATH=$HOME/.micromamba/envs/lsp/bin/:$PATH
Also, we need to setup nvm
.
1wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
2. $HOME/.bashrc
3nvm install node
4nvm use node
C++
For clangd
(details here) and tramp
this amounted to:
1mkdir -p ~/.local/lsp
2cd ~/.local/lsp
3wget https://github.com/clangd/clangd/releases/download/12.0.0/clangd-linux-12.0.0.zip
4unzip clangd-linux-12.0.0.zip
5mv clangd-linux-12.0.0/* .
In combination with the standard doom-emacs
cc module, this is a very good workflow, with the only issue being the ability to set the project root and files to be considered. There is doom-emacs
version of appending and setting the language server used, which will be used here:
1(after! eglot
2 :config
3 (set-eglot-client! 'cc-mode '("clangd" "-j=3" "--clang-tidy"))
4 )
Note that the best way to make a language server behave is to have a compilation database, which can be generated (for a CMake
project) as follows:
1# From src
2mkdir build
3cd build
4# cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .. -G 'Unix Makefiles' # default on *nix
5cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .. -G 'Ninja' # way faster
6ninja # or make
7cp compile_commands.json ..
This will ensure the best experience, since the compile_commands.json
(described here) file contains paths to all the files used, including system and write-only files 4. The setup here works well with enough with the ccls
language server as well, which in turn is a more maintained fork of cquery
.
Python
Palantir’s python-language-server
is EOL, and was weird to begin with, but the Spyder team has taken up the gauntlet and maintains the python-lsp-server
(repo).
1micromamba activate lsp # should be done in the ~/.bashrc
2micromamba install python-lsp-server[all] -c conda-forge
For working with this in an optimal manner the eglot
readme suggests using .dir-local.el
files. For example:
1((python-mode
2 . ((eglot-workspace-configuration
3 . ((:pylsp . (:plugins (:jedi_completion (:include_params t)))))))))
It so happens that this method is flexible enough to allow multi-configuration of servers as well. Additionally, we have options for working with the setup:
1(after! eglot
2 :config
3 (set-eglot-client! 'python-mode '("pylsp"))
4 (set-eglot-client! 'cc-mode '("clangd" "-j=3" "--clang-tidy"))
5)
Or in vanilla emacs
:
1(add-to-list 'eglot-server-programs
2 '(python-mode "pylsp"))
Bash
This is fairly straightforward, and implemented in js
here.
1nvm use node
2npm i -g bash-language-server
Along with:
1(sh +lsp) ;; in the init.el file
Fortran
The installation of fortls
(repo) is simple enough.
1micromamba activate lsp
2pip install fortran-language-server
Unfortunately, doom-emacs
does not actually support the fortran
language server out of the box, so some additional emacs-lisp
needs to go into the configuration.
1(add-hook 'f90-mode-hook 'eglot-ensure)
This works quite well for larger projects.
Nix
rnix-lsp
(repo) has no non nix
installation, and a slightly uncertain future, but it is still fantastic and shouldn’t be left out5.
1nix-env -i -f https://github.com/nix-community/rnix-lsp/archive/master.tar.gz
2nix-env -i nixkpgs-fmt
As this is another unsupported server in doom-emacs
, the setup needs some tweaking.
1(add-hook 'nix-mode-hook 'eglot-ensure)
TeX
The last of the language servers, digestif
(repo) is implemented in lua
, and probably shouldn’t be on a remote machine6, but nevertheless.
1mkdir -p ~/.local/lsp/bin
2cd ~/.local/lsp/bin
3wget https://raw.githubusercontent.com/astoff/digestif/master/scripts/digestif
4chmod +x digestif
5./digestif
This sets up $HOME/.digestif/bin
which is yet another path to be managed. Note that this assumes a standard TexLive
installation with luatex
.
Local Usage
For local use, assuming similar installation instructions, it is easier to manipulate exec-path
.
1(after! eglot
2 :config
3 (add-hook 'nix-mode-hook 'eglot-ensure)
4 (add-hook 'f90-mode-hook 'eglot-ensure)
5 (set-eglot-client! 'cc-mode '("clangd" "-j=3" "--clang-tidy"))
6 (set-eglot-client! 'python-mode '("pylsp"))
7 (when (string= (system-name) "blah")
8 (setq exec-path (append exec-path '(
9 (concat (getenv "HOME") "/.micromamba/envs/lsp/bin/") ;; python, fortran
10 (concat (getenv "HOME") "/.local/lsp/bin/") ;; clangd
11 (concat (getenv "HOME") "/.digestif/bin/") ;; tex
12 (concat (getenv "HOME") "/.nvm/versions/node/v16.1.0/bin/bash-language-server")
13 )))
14 )
15 )
Where you will need to replace "blah"
with the output of (system-name)
.
Conclusions
Embarrassingly, neither setup provided as pleasant an SSH based workflow as VS Code. However, that probably has a lot more to do with TRAMP than any of the language servers, and in any case Magit still makes emacs
far superior. In any case, the setup described here is still far superior to not using anything other than projectile
and ripgrep
so I am satisfied for now. The path setup in this post is deliberately polluting for simplification however, and in practice my Dotfiles are managed a lot better with bombadil
which is something for a later post. Each of these will take some getting used to, and new keybindings will probably be needed, especially for my idiomatic Colemak setup.
More than adequately managed by
hlissner
’s fantastic doom-emacs ↩︎If you need more information, Doug Davis has a more in-depth vanilla emacs write-up on
eglot
and C++ ↩︎Though my university cluster has
nix
, most remote machines do not, so we don’t depend on it here ↩︎This means, libraries like
Eigen
or even the standard library can be looked up viaeglot
↩︎The original author unfortunately passed away earlier this year ↩︎
Visualizing the resultant
pdf
would be a chore ↩︎