I’ve been using Emacs for almost 15 years now. Somewhat surprisingly, I hadn’t touched my config in three years! It’s been working that well. But now that Emacs 29 has been released I’ve decided to take a look at what’s new and there have been some big changes, particularly for Python.
Goodbye Elpy, Goodbye Projectile
Elpy has been the primary mode for Python development for me for years now. But sadly, it looks like the project is no more. The good news is there are better ways to do what it did. It’s bittersweet to say goodbye to it and I will be eternally grateful to the authors, but progress is progress.
Similarly, Projectile was what I used to manage projects. But now Emacs has project.el
built in and I’ve opted to use that instead. One nice thing about project.el is it uses
other standard stuff underneath like xref. I configured xref to use Ripgrep and now the
Project commands like C-x p g
use Ripgrep:
1(use-package xref
2 :config
3 (setq xref-search-program 'ripgrep))
Native builds and tree-sitter
I always build Emacs myself from source if I can. I run Gentoo on my personal computer so that goes without saying, but I do it on Ubuntu too, if only to get the latest versions. This does mean I can easily enable a couple of new features: native builds and tree-sitter.
On Gentoo this was a simple as adding a couple of USE flags to portage. My USE flags for emacs now look like:
app-editors/emacs athena cairo gui gtk harfbuzz json libxml2 source tree-sitter jit -X
The gtk -X
also implies a pgtk
build which is nice because I use wayland (sway).
On Ubuntu (20.04, yeah, old, this is one reason I prefer rolling distros) it was more
difficult. I first pulled the source code (emacs-29.1.tar.gz
) from a nearby GNU
mirror per the GNU website. Then a few packages are required (I use i3/X11 on
Ubuntu):
1sudo apt install autoconf make gcc texinfo libgtk-3-dev libxpm-dev libjpeg-dev \
2 libgif-dev libtiff5-dev libgnutls28-dev libncurses5-dev libjansson-dev \
3 libharfbuzz-dev libharfbuzz-bin imagemagick libmagickwand-dev libgccjit-10-dev \
4 libgccjit0 gcc-10 libjansson4 libjansson-dev xaw3dg-dev texinfo libx11-dev
Now, because libgccjit
(required for native builds) is only for GCC 10, the build
process has to be configured for GCC 10 specifically, in addition to enabling all the
wanted features:
1CC="gcc-10" ./configure --prefix=$HOME --with-json --with-native-compilation=aot \
2 --with-modules --with-compress-install --with-threads --with-included-regex \
3 --with-x-toolkit=lucid --with-zlib --with-jpeg --with-png --with-imagemagick \
4 --with-tiff --with-xpm --with-gnutls --with-xft --with-xml2 --with-mailutils \
5 --with-tree-sitter
Note that I keep my own builds in $HOME
by setting --prefix
. By default the
installation would put it in the system directories which I prefer not to do as those
are controlled by my system package manager. Also note that I set
--with-native-compilation=aot
which makes native builds ahead of time instead of JIT
compiling them. Run ./configure --help
to see all of the build options.
Then I just compiled it:
1make -j 8 # 8 threads
The build can be tested with src/emacs -Q
then, if it works:
1make install
Eglot
Elpy provided a proper IDE experience for Python but it did it in a completely custom,
albeit very clever, way via a special RPC process which used jedi
. Now with LSP we
can get essentially the same sort of thing but in a more standard way that works with
all languages.
I have tried LSP (in particular, lsp-mode
) in emacs before, but I wasn’t impressed. I
cannot stand latency and the moment I detect latency when merely typing in a text
editor, I walk away. But I’m pleased to say that with Emacs 29, native builds, Eglot
and python-lsp-server
it is now fast enough for me. lsp-mode
might very well be
fast enough now too. I’ll probably try it eventually.
I installed python-lsp-server
(with pipx
on Ubuntu). This is my preferred way of
installing Python apps if they’re not available in the base distro. Notice how there
will be only one LSP server installed for my whole system (not one per virtualenv).
Enabling Eglot is easy. To make it work for Python it just needs the following:
1(use-package eglot
2 :hook (python-mode . eglot-ensure))
Now just open a Python file and it should work. It does everything Elpy did (or, at least, what I used it for) and more. Just like that.
By default, Eglot uses Flymake. I had previously been using Flycheck. I can’t really remember why, to be honest, so I’ll try using Flymake instead and say goodbye to Flycheck for now too.
Virtualenvs
OK, so, it doesn’t completely just work. One of the most important things for me is being able to jump to the definition of a symbol in the source. This does just work for first party stuff and (kinda) for standard library stuff, but it won’t work for third party stuff. That’s because the LSP server doesn’t know where to find those libraries.
Usually when developing on a Python project one would create a virtual environment for
it. I make everything a package such that doing a pip install -e .
installs the
package and all of its dependencies into the virtualenv. Then you just need to make the
LSP server aware of this environment.
I used to use virtualenvwrapper
to create virtualenvs for each project, but I’ve found
a better way: direnv
. This allows you to create .envrc
files in directories with
anything you want in it then automatically loads it into your environment when you
change to that directory. What’s even neater is it has built-in support for Python (and
other languages).
To install direnv
on Gentoo I used the Guru overlay:
1eselect repository enable guru
After installing and setting up direnv
, make a file called .envrc
at the top of your
project and put the following:
1layout python
That’s it! After enabling your project for direnv
support it will automatically
create a virtualenv and activate it. When you change directory, it will deactivate it.
Amazing!
In Emacs you can install the direnv
package and enable it:
1(use-package direnv
2 :config
3 (direnv-mode))
Now when you browse to a project with a .envrc
file it will just work.
Tree-sitter
Finally, to enable tree-sitter I needed to first install the grammar for Python, I added the following to my emacs config:
1(setq treesit-language-source-alist
2 '((python "https://github.com/tree-sitter/tree-sitter-python")))
And then (after evaling the above) you can run: M-x treesit-install-language-grammar
.
This builds the grammar for you and puts it in your emacs config.
Now you can use the mode python-ts-mode
instead of python-mode
:
1(use-package python
2 :mode ("\\.py\\'" . python-ts-mode)
3 :init
4 (add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode)))
The major-mode-remap-list
entry means python-ts-mode
will be used whenever
python-mode
would have been used, like when opening a script with no file extension
but a Python shebang.
Completion
One thing I cannot do without is some kind of completion capability. In bash I use tab-completion extensively and I consider any keyboard-driven software that doesn’t support at least tab-completion to be defective.
Basic completion is supported in Emacs out of the box but it can be extended to be quite sophisticated. But I’ve always found it a bit overwhelming. My life was changed when I first enabled ido. The combination of completion and narrowing is amazing. Later I switched to other packages like ivy, Helm and Selectrum and enabled in-buffer completion with Company. Selectrum is now defunct and replaced with Vertico.
For the first time, I have a completion set up that I understand and that I’m very happy with.
What I really wanted was fuzzy-style completion in minibuffer contexts but dead basic prefix-style completion within buffers. I also want the completion within-buffer to be driven by the tab key like in a bash shell. I’ve settled on Company within-buffer and Vertico in the minibuffer.
I like the setting (setq tab-always-indent 'complete)
which causes TAB to indent
first, then complete, but I was getting weird behaviour where that completion would not
launch company. So instead:
1(global-set-key (kbd "TAB") #'company-indent-or-complete-common)
This now does the right thing but launches Company instead of the default completion function.
The other major part is completion styles. I like the Orderless style for the fuzzy
minibuffer style, but it doesn’t work for basic completion. Emacs supports setting a
list of completion styles by setting completion-styles
and further refining that for
specific categories by setting completion-category-overrides
. The trouble is, the
category names for the latter are quite hard to find. But eventually I settled on the
following configuration:
1(use-package orderless
2 :init
3 (setq completion-styles '(basic partial-completion orderless)
4 completion-category-defaults nil
5 completion-category-overrides '((project-file (styles orderless))
6 (buffer (styles orderless))
7 (command (styles orderless)))))
This sets basic
and partial-completion
styles first by default everywhere. Company
doesn’t really support the Orderless style, which is fine by me as I don’t want it
in-buffer anyway. I then override it for particular categories to add orderless
to
the front. project-file
is for finding files in projects with C-x p f
, buffer
is
for switching buffers and command
is for running commands with M-x
.
Conclusion
To sum up, I’ve switched from Projectile to project.el, from Elpy to Eglot/LSP and from virtualenvwrapper to direnv as well as including the latest improvements like native builds and tree-sitter. This has really simplified my config and I seem to have a renewed love for Emacs.
I’ve been using this configuration for a few days now for real work and I really love it so far. Things like the eldoc and xref jump to definition features are working perfectly now and I’ve had real trouble with consistent behaviour before.
My actual emacs config does include a number of extra tweaks to all of this stuff. I love reading other people’s .emacs files, so maybe you’ll enjoy reading mine too: https://github.com/georgek/dot-emacs
Happy hacking!