Having used Emacs since 2006, I never found a good, reliable, and general enough solution to code completion, symbol cross referencing, go-to definition until I stumbled upon the Language Server Protocol (LSP).
From the specification:
The Language Server protocol is used between a tool (the client) and a language smartness provider (the server) to integrate features like auto complete, go to definition, find all references and alike into the tool.
It tackles the code intelligence problem quite nicely: To support a given programming language with LSP, a language server program must be implemented. When running, all editors and IDEs supporting LSP will be able to employ its features. Adoption is already very good across the field. 1
LSP reduces an \(m \times n\) complexity problem to \(m + n\): the latter refers to only needing a language server for each language and a client for each IDE, whereas the former means that for every language each IDE would require, in most cases, standalone support.
In this article, I will go over the configuration and setup of Emacs for LSP support of C++, Python, and Rust. It is assumed that Emacs version 25.1 or later is already installed and setup for basic usage, and that you’re familiar with editing its config file. Additionally, I use macOS but the installation commands can easily be translated to other solutions and other OSs.
2. Emacs configuration
First install the lsp-mode package, which is the LSP client that we will be using:
M-x package-install ↵ lsp-mode ↵
The initial configuration is very simple: 2
(use-package lsp-mode :config (add-hook 'c++-mode-hook #'lsp) (add-hook 'python-mode-hook #'lsp) (add-hook 'rust-mode-hook #'lsp))
lsp-mode comes prepackaged to use certain available LSP servers, that it will automatically spin up when a mode requests it (by invoking
lsp). If LSP servers were already installed at this point, then you would be all set.
Clangd is currently the best working language server for C++ for the projects I work on. Even though cquery comes as a default in
lsp-mode, and has more features than clangd, there were times that it would fail to find known symbols for large projects, and both the index generation time and size were longer and bigger than clangd’s.
The first working version of clangd is LLVM v7 but I’ve had great results with v9 (which is HEAD at the time of writing), so install LLVM and thus clangd:
% brew install llvm --HEAD % which clangd /usr/local/opt/llvm/bin/clangd
Edit 2019-04-02: Using
--HEAD instead of
Add the following to the
(use-package lsp-mode :config ;; `-background-index' requires clangd v8+! (setq lsp-clients-clangd-args '("-j=4" "-background-index" "-log=error")) ;; .. )
It tells clangd that it can use 4 concurrent jobs and to make a complete background index on disk. Without
-background-index, it will only keep an in-memory index of the files that are active in Emacs buffers, but to be able to find references and symbols in any project file the background index is recommended. It is placed at the project root as the “.clangd” folder.
Clangd tries to locate the “compile_commands.json” file in the root of the project, so it’s useful to make a symlink in the project root and to where it’s located in a build folder. Most build tools can output “compile_commands.json”. In CMake you write:
For Python, the default language server is pyls:
% pip install python-language-server % which pyls /usr/local/bin/pyls
% curl https://sh.rustup.rs -sSf | sh % rustup update % rustup component add rls rust-analysis rust-src % which rls ~/.cargo/bin/rls
Skip the rustup installation if Rust is already installed.
Using LSP is easy: open a source code file with one of the major modes associated the
lsp hook. If no running language server is handling the particular project, a new one will be spun up to communicate with
When LSP does not know which project to associate a file, usually the first time visiting a file, it will ask what to do. In this case I’m testing using my vermin project:
vermin.py is not part of any project. Select action:
The following actions are possible:
- Do not ask more for the current project (add “~/git/vermin/” to lsp-session-folder-blacklist)
- Do not ask more for the current project (select ignore path interactively).
- Do nothing and ask me again when opening other files from the folder.
- Import project by selecting root directory interactively.
- Import project root ~/git/vermin/
Usually the last option is what you want. Simply write Import ↹ root ↹ ↵.
The minibuffer will display (the PID will differ):
LSP :: Connected to [pyls:94242 status:starting].
To try out some LSP functionality, take a look at the vermin’s entry point:
#!/usr/bin/env python from vermin.main import main if __name__ == "__main__": main()
Put the cursor at “main” right after
import on the second line. We want to find the definition of that function: M-x lsp-find-definition ↵. Voila, that jumps to line 12 of “vermin/main.py”:
# .. def main(): # Cursor will be at 'm'. config = Config.get() args = parse_args(sys.argv[1:]) # ..
The minibuffer will show that it connected to the same pyls instance:
LSP :: Connected to [pyls:94242].
Next, let’s try to find references of
parse_args() by placing the cursor on it and
M-x lsp-find-references ↵.
Other useful commands:
lsp-describe-thing-at-point: displays the full documentation of the thing at point
lsp-rename: renames the symbol (and all references to it) under point to a new name
lsp-execute-code-action: executes code action, like “did you mean X instead of Y?”
lsp-describe-session: describes current LSP session and its capabilities
Take a look here for more information about extensions and features.
Edit 2019-04-01: My .emacs.d is freely available for the complete LSP configuration, and lots of other stuff related to Emacs.
This section is dedicated to extra features and niceties that I find makes LSP more usable and powerful.
lsp-ui adds higher level UI modules, like documentation child frame while hovering a symbol, integration with flycheck, and enables inline definition and references peeking - just to name a few things.
My configuration relies on
flycheck being set up:
(use-package lsp-mode ;; .. :config (setq lsp-prefer-flymake nil) ;; Prefer using lsp-ui (flycheck) over flymake. ;; .. ) (use-package lsp-ui :requires lsp-mode flycheck :config (setq lsp-ui-doc-enable t lsp-ui-doc-use-childframe t lsp-ui-doc-position 'top lsp-ui-doc-include-signature t lsp-ui-sideline-enable nil lsp-ui-flycheck-enable t lsp-ui-flycheck-list-position 'right lsp-ui-flycheck-live-reporting t lsp-ui-peek-enable t lsp-ui-peek-list-width 60 lsp-ui-peek-peek-height 25) (add-hook 'lsp-mode-hook 'lsp-ui-mode))
Another thing that’s really useful, not just related to LSP, is the
hydra package, which is described very well by its author:
The Hydra is vanquished once Hercules, any binding that isn’t the Hydra’s head, arrives. Note that Hercules, besides vanquishing the Hydra, will still serve his original purpose, calling his proper command. This makes the Hydra very seamless, it’s like a minor mode that disables itself auto-magically.
In essence, it yields a menu with keybindings in the minibuffer, a hydra, which won’t be shown if the bindings are pressed fast enough.
The following configuration is trimmed down to only show parts related to LSP:
(use-package hydra) (use-package helm) (use-package helm-lsp :config (defun netrom/helm-lsp-workspace-symbol-at-point () (interactive) (let ((current-prefix-arg t)) (call-interactively #'helm-lsp-workspace-symbol))) (defun netrom/helm-lsp-global-workspace-symbol-at-point () (interactive) (let ((current-prefix-arg t)) (call-interactively #'helm-lsp-global-workspace-symbol)))) (use-package lsp-mode :requires hydra helm helm-lsp ;; .. (setq netrom--general-lsp-hydra-heads '(;; Xref ("d" xref-find-definitions "Definitions" :column "Xref") ("D" xref-find-definitions-other-window "-> other win") ("r" xref-find-references "References") ("s" netrom/helm-lsp-workspace-symbol-at-point "Helm search") ("S" netrom/helm-lsp-global-workspace-symbol-at-point "Helm global search") ;; Peek ("C-d" lsp-ui-peek-find-definitions "Definitions" :column "Peek") ("C-r" lsp-ui-peek-find-references "References") ("C-i" lsp-ui-peek-find-implementation "Implementation") ;; LSP ("p" lsp-describe-thing-at-point "Describe at point" :column "LSP") ("C-a" lsp-execute-code-action "Execute code action") ("R" lsp-rename "Rename") ("t" lsp-goto-type-definition "Type definition") ("i" lsp-goto-implementation "Implementation") ("f" helm-imenu "Filter funcs/classes (Helm)") ("C-c" lsp-describe-session "Describe session") ;; Flycheck ("l" lsp-ui-flycheck-list "List errs/warns/notes" :column "Flycheck")) netrom--misc-lsp-hydra-heads '(;; Misc ("q" nil "Cancel" :column "Misc") ("b" pop-tag-mark "Back"))) ;; Create general hydra. (eval `(defhydra netrom/lsp-hydra (:color blue :hint nil) ,@(append netrom--general-lsp-hydra-heads netrom--misc-lsp-hydra-heads))) (add-hook 'lsp-mode-hook (lambda () (local-set-key (kbd "C-c C-l") 'netrom/lsp-hydra/body))))
To activate the hydra, press C-c C-l with the cursor placed somewhere relevant:
Edit 2019-04-01: Using
helm-imenu instead of
lsp-ui-imenu because the former enables easy filtering via Helm.
(use-package company :config (setq company-idle-delay 0.3) (global-company-mode 1) (global-set-key (kbd "C-<tab>") 'company-complete)) (use-package company-lsp :requires company :config (push 'company-lsp company-backends) ;; Disable client-side cache because the LSP server does a better job. (setq company-transformers nil company-lsp-async t company-lsp-cache-candidates nil))
- See lists of server implementations here and here, and client implementations. ↩
- use-package is great for automatically downloading and installing packages. ↩