Getting started with lsp-mode for Python
Language Server Protocol is a JSON-RPC protocol that allows text editors and IDEs to delegate language-aware features (such as autocomplete, or jump-to-definition) to a common server process. It means that editors can support smart language features just by implementing a generic LSP client.
I'm migrating my Emacs config to use LSP, starting with Python support. It's more powerful than my old setup, and I expect it will only improve over time, as language servers will benefit from more community attention than the long tail of Emacs packages.
Although there was not much configuration required to get LSP working in Emacs, it wasn't always obvious what I needed to do, and I couldn't find many examples of a full Python configuration to get started.
Here are a few questions and issues I encountered.
1. What do I need to install?
The most popular LSP client for Emacs is lsp-mode (although eglot is also in
lsp-mode tries to integrate with sensible existing tools to
minimise user configuration - it supports popular language servers, and it hooks
into Emacs packages like Flycheck and Company.
You will at least want to install
lsp-mode, and probably also
is focused on UI-altering features like popups and "sideline" information).
pip install python-language-server[all]. This will install
pyls, and also install its various dependencies that provide particular features:
pyflakesfor detecting errors,
mccabefor complexity, etc.
pip install python-language-server, and install the dependencies you want directly.
2. Fixing a Jedi compatibility issue with pyls
At time of writing,
python-language-server is not compatible with
0.16. Make sure you adhere to
3. How do I enable
lsp-mode for Python?
For the base
lsp-mode, the only required config is to call
(lsp) when in
(use-package lsp-mode :hook ((python-mode . lsp)))
(use-package lsp-ui :commands lsp-ui-mode)
4. pyls plugins: mypy, isort and black
Some integrations are not available by default in
pyls, but are supported by
plugins. You can install these with
pip install pyls-black pyls-isort pyls-mypy.
To then enable them in
lsp-mode, you can use
(use-package lsp-mode :config (lsp-register-custom-settings '(("pyls.plugins.pyls_mypy.enabled" t t) ("pyls.plugins.pyls_mypy.live_mode" nil t) ("pyls.plugins.pyls_black.enabled" t t) ("pyls.plugins.pyls_isort.enabled" t t))) :hook ((python-mode . lsp)))
5. Fixing a pyls-mypy issue
At time of writing, the
mypy plugin has an issue due to a missing
import. It can be resolved by
pip install future. See pyls-mypy #37.
6. What about flake8?
flake8 is not mentioned in the pyls README, but it is supported. There are two
options to enable it:
You can use
(lsp-register-custom-settings) as before:
(lsp-register-custom-settings '(("pyls.plugins.flake8.enabled" t t)))
lsp-mode automatically turns some configuration parameters into
custom variables, including the flake8 parameters. So:
(setq lsp-pyls-plugins-flake8-enabled t)
7. What other options does
I'm not sure if there is a standard way to retrieve all supported configuration
options from a language server. There is a pretty long list of
pyls options in
8. How do I inspect what lsp-mode is doing?
There are a few places you can look for info:
*pyls::stderr*buffer. If something isn't working as expected, this may help identify the problem - eg. it will show issues loading particular plugins.
(setq lsp-log-io t)- this will log messages between the lsp client and server to a buffer. You can view the buffer by calling
lsp-client-settingsvariable. This contains all the
lsp-modesettings for different language servers, and seems to be what you eventually modify when you run
(lsp-describe-session)shows the capabilities of the current session. See the troubleshooting section of the lsp-mode README.
9. How do I support multiple projects?
Python projects often use virtual environments to manage project
dependencies. My understanding is that
pyls has to be installed in the project's
virtualenv, in order to access dependencies for project-aware features like
checking symbol references.
pyls seem to provide features to manage multiple projects -
the appropriate virtualenv needs to be managed separately.
In the past I've used
pyvenv for this with some success. For various reasons
pyvenv is only able to set a single global virtualenv at a time, but it does
have a "tracking" mode, which can automatically change the global virtualenv
I use a default "emacs" virtualenv as a fallback for editing things like org-mode Python buffers.
(use-package pyvenv :demand t :config (setq pyvenv-workon "emacs") ; Default venv (pyvenv-tracking-mode 1)) ; Automatically use pyvenv-workon via dir-locals
You can use
(add-dir-local-variable) to set
pyvenv-workon for a particular
10. The final code
My initial config for
lsp-mode also included a few other settings. It looked
roughly like this:
(use-package lsp-mode :config (setq lsp-idle-delay 0.5 lsp-enable-symbol-highlighting t lsp-enable-snippet nil ;; Not supported by company capf, which is the recommended company backend lsp-pyls-plugins-flake8-enabled t) (lsp-register-custom-settings '(("pyls.plugins.pyls_mypy.enabled" t t) ("pyls.plugins.pyls_mypy.live_mode" nil t) ("pyls.plugins.pyls_black.enabled" t t) ("pyls.plugins.pyls_isort.enabled" t t) ;; Disable these as they're duplicated by flake8 ("pyls.plugins.pycodestyle.enabled" nil t) ("pyls.plugins.mccabe.enabled" nil t) ("pyls.plugins.pyflakes.enabled" nil t))) :hook ((python-mode . lsp) (lsp-mode . lsp-enable-which-key-integration)) :bind (:map evil-normal-state-map ("gh" . lsp-describe-thing-at-point) :map md/leader-map ("Ff" . lsp-format-buffer) ("FR" . lsp-rename))) (use-package lsp-ui :config (setq lsp-ui-sideline-show-hover t lsp-ui-sideline-delay 0.5 lsp-ui-doc-delay 5 lsp-ui-sideline-ignore-duplicates t lsp-ui-doc-position 'bottom lsp-ui-doc-alignment 'frame lsp-ui-doc-header nil lsp-ui-doc-include-signature t lsp-ui-doc-use-childframe t) :commands lsp-ui-mode :bind (:map evil-normal-state-map ("gd" . lsp-ui-peek-find-definitions) ("gr" . lsp-ui-peek-find-references) :map md/leader-map ("Ni" . lsp-ui-imenu))) (use-package pyvenv :demand t :config (setq pyvenv-workon "emacs") ; Default venv (pyvenv-tracking-mode 1)) ; Automatically use pyvenv-workon via dir-locals
This is not perfect and will definitely require future work, but it's a useful start. In theory, adding support for a new language should only require installing the language server and adding a couple of lines of elisp to enable the new language.
I'll be updating my config on github.