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
active development). 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 lsp-ui (which
is focused on UI-altering features like popups and "sideline" information).
For Python support, there are two main language servers - pyls and Microsoft
Python Language Server. I have used pyls. You have two options for installing
pyls and its dependencies:
pip install python-language-server[all]. This will installpyls, and also install its various dependencies that provide particular features:ropefor renaming,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 jedi version
0.16. Make sure you adhere to jedi<0.16,>=0.14.1.
3. How do I enable lsp-mode for Python?
For the base lsp-mode, the only required config is to call (lsp) when in
python-mode:
(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 (lsp-register-custom-settings):
(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 future
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)))
Alternatively, 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 pyls support?
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
the vscode-client.
8. How do I inspect what lsp-mode is doing?
There are a few places you can look for info:
- The
*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-workspace-show-log).- The
lsp-client-settingsvariable. This contains all thelsp-modesettings for different language servers, and seems to be what you eventually modify when you run(lsp-register-custom-settings). (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.
Neither lsp-mode nor 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
using dir-locals.
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
project.
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.