Morten's Dev

C++, Python, Rust, Emacs, Clang, CMake, and other technobabble..

Emacs and the Language Server Protocol


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.

1. Prerequisites

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.

2.1. C++

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 --head.

Add the following to the lsp-mode config:

(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:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

2.2. Python

For Python, the default language server is pyls:

% pip install python-language-server
% which pyls
/usr/local/bin/pyls

That’s it.

2.3. Rust

The default language server for Rust is rls which is installed via rustup. Before installing rustup blindly, first download the shell script and look it over, then proceed:

% 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.

3. Usage

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 lsp-mode.

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 ↵.

Results of lsp-find-references.

The results are displayed this way due to the helm and helm-xref packages.

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.

4. Extras

This section is dedicated to extra features and niceties that I find makes LSP more usable and powerful.

4.1. lsp-ui

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))

4.2. hydra

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:

LSP hydra shown for C-c C-l.

Edit 2019-04-01: Using helm-imenu instead of lsp-ui-imenu because the former enables easy filtering via Helm.

4.3. company-lsp

For completions at cursor, I use company-mode with company-lsp for the purposes of LSP:

(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))



  1. See lists of server implementations here and here, and client implementations.
  2. use-package is great for automatically downloading and installing packages.

Related Posts