Posts Building an Intelligent Emacs
Post
Cancel

Building an Intelligent Emacs

This post introduces the combination of Emacs and LSP, and how you can make your own editor “smarter” by using the same idea of communications between an editor client and multiple language servers.

Edit: Thank you for the support, this blog post got featured on the front page of Hacker News (YCombinator).

Background

When compared with modern editors and IDEs (such as IntelliJ IDEA, PyCharm, and Visual Studio Code), old-school editors like Emacs or Vim fail to provide intelligent actions such as “auto-complete (Intellisense)”, “go to definition/references”, and “on-the-fly error checking” out-of-the-box. After all, this requires a deep understanding of both the programming language and the code, which is the main reason why IDEs are created in the first place.

Language Server Protocol (LSP)

That said, everything changed when Microsoft released its development of the Language Server Protocol. In short, LSP decouples the tooling into servers and clients, with the former powering all the intelligent activities, and the latter being integrated into any development tool of your choice. If you’d like to read more about the inner workings, consider this post.

Emacs1 Emacs showing language-aware auto-completion powered by the clangd language server. Company and company-box are used to provide the auto-completion UI.

All of a sudden, as long as you have a client in your editor that can communicate with the language servers, which are separately downloaded, any tool can become as intelligent and feature-rich as the popular IDEs and smart editors. In other words, if my “primitive” editor has a thin layer of a client that can communicate with, say, the Java language server of the Eclipse IDE, my editor can obtain all the smart features that are exposed by the server and instantly be on par with Eclipse. Similarly, if I install the same language server that powers the sweet code actions for TypeScript/JavaScript in VSCode and hooks it to my editor’s client, I wouldn’t lose anything by not using VSCode.

Clients for LSP

In the Vim/Neovim world, the main players are CoC and the native LSP client of Neovim. In the Emacs community, the equivalent would be lsp-mode and eglot. As an Emacs user, I have tried out both lsp-mode and eglot, and found the former more configurable, feature-rich, and easy to wrap my head about. In the rest of this post, I will be using lsp-mode as my client to demonstrate the configurations of making Emacs an intelligent editor.

To install lsp-mode from MELPA for Emacs, you can use the following use-package declaration. If you need help setting up use-package, I also wrote a post on this topic.

1
2
3
(use-package lsp-mode
  :hook ((X-mode Y-mode Z-mode) . lsp-deferred) ; XYZ are to be replaced by python, c++, etc.
  :commands lsp)

If we want to enable LSP for C++, Python, Java, and JavaScript, we would write:

1
2
3
(use-package lsp-mode
  :hook ((c++-mode python-mode java-mode js-mode) . lsp-deferred)
  :commands lsp)

To enable extra information on the sideline (fixes, suggestions, documentation), lsp-mode has an extra companion package called lsp-ui. To enable it, simply do:

1
2
(use-package lsp-ui
  :commands lsp-ui-mode)

I’ve tweaked some settings to enable features that are not set by default, and disabled those that I don’t find helpful:

1
2
3
4
5
6
7
8
9
(use-package lsp-ui
  :commands lsp-ui-mode
  :config
  (setq lsp-ui-doc-enable nil)
  (setq lsp-ui-doc-header t)
  (setq lsp-ui-doc-include-signature t)
  (setq lsp-ui-doc-border (face-foreground 'default))
  (setq lsp-ui-sideline-show-code-actions t)
  (setq lsp-ui-sideline-delay 0.05))

Servers for LSP

Now that we have the client installed and initialized, we need to provide Emacs with the respective language servers so lsp-mode can pick up the channels of communication. We’ll continue to use C++, Python, Java, and JavaScript as examples since they seem to be the most popular languages.

Setting up C++

By default, lsp-mode will look for the “clangd” executable on the path. “Clangd” is a language server developed by LLVM (which also develops the clang compilers) and can be downloaded with brew install llvm on macOS, or pacman -S clang on Arch.

Clangd is my choice of language server for both C and C++. If for some reason you don’t like LLVM’s implementation, you can try out ccls, an alternative language server for C/C++/ObjC. For lsp-mode to prioritize ccls over clangd, you need to install and set up this extra client that leverages lsp-mode.

Emacs2 Emacs detecting typos and suggesting fixes, powered by LSP.

Setting up Python

LSP-mode supports 5 different Python language servers, namely Spyder IDE’s python-lsp-server, the Jedi language server, Palantir’s pyls, Microsoft’s Pyright language server, and Microsoft’s Python language server.

Pyright is my choice of language server for Python. I’ve heard good things about Sypder IDE’s server but I have yet to try it.

You can install pyright globally with pip, npm, or your system’s package manager. For instance, pacman -S pyright.

We will need a thin layer of extra client, lsp-pyright, to sit atop lsp-mode and leverage pyright’s features. The following is the snippet to automatically download the client and set it up. Note that I am using the command python3 as the executable command, in case both Python 2 and 3 are installed on the system.

1
2
3
4
(use-package lsp-pyright
  :hook (python-mode . (lambda () (require 'lsp-pyright)))
  :init (when (executable-find "python3")
          (setq lsp-pyright-python-executable-cmd "python3")))

Emacs3 Fuzzy match auto-completion in Emacs, powered by lsp-pyright.

Setting up Java

The go-to language server for Java is Eclipse’s JDT Language Server. We need yet another thin layer of a client called lsp-java to help leverage lsp-mode and the JDT server. The good news is once you have lsp-java installed in Emacs through use-package, the client will “automatically detect whether the server is missing and download Eclipse JDT Language Server before the first startup”!

1
2
(use-package lsp-java
  :after lsp)

Setting up JavaScript

Finally, let’s set up JavaScript for development in Emacs. The recommended language server for JavaScript (and TypeScript) is the conveniently-named typescript-language-server (or ts-ls for short), which is a wrapper around Visual Studio Code’s tsserver.

Since lsp-mode has ts-ls support integrated by default, there’s no need to install an additional thin layer of client on top of lsp-mode. However, we do need to install the language server on our system. We can install it globally with npm:

1
npm install -g typescript-language-server

Extra Configuration for lsp-mode (Optional)

In my opinion, lsp-mode aggressively enables too many features by default, which may result in visual clutter and a slow-down in performance. The following is some of my extra configuration for the lsp-mode client. Whenever you see a pattern of (setq ... nil), it means I’m disabling that feature. Towards the bottom of the list, I am also bumping up the chunk-processing threshold to allow for a smoother Emacs experience.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(use-package lsp-mode
  :hook ((c-mode          ; clangd
          c++-mode        ; clangd
          c-or-c++-mode   ; clangd
          java-mode       ; eclipse-jdtls
          js-mode         ; ts-ls (tsserver wrapper)
          js-jsx-mode     ; ts-ls (tsserver wrapper)
          typescript-mode ; ts-ls (tsserver wrapper)
          python-mode     ; pyright
          web-mode        ; ts-ls/HTML/CSS
          haskell-mode    ; haskell-language-server
          ) . lsp-deferred)
  :commands lsp
  :config
  (setq lsp-auto-guess-root t)
  (setq lsp-log-io nil)
  (setq lsp-restart 'auto-restart)
  (setq lsp-enable-symbol-highlighting nil)
  (setq lsp-enable-on-type-formatting nil)
  (setq lsp-signature-auto-activate nil)
  (setq lsp-signature-render-documentation nil)
  (setq lsp-eldoc-hook nil)
  (setq lsp-modeline-code-actions-enable nil)
  (setq lsp-modeline-diagnostics-enable nil)
  (setq lsp-headerline-breadcrumb-enable nil)
  (setq lsp-semantic-tokens-enable nil)
  (setq lsp-enable-folding nil)
  (setq lsp-enable-imenu nil)
  (setq lsp-enable-snippet nil)
  (setq read-process-output-max (* 1024 1024)) ;; 1MB
  (setq lsp-idle-delay 0.5))

Conclusion

Hopefully, this post gives you a good head-start in making Emacs a modern and intelligent IDE. I enjoy the experience of fine-tuning my development environment, even down to hand-picking my own back-end for editor behaviors. I find it extremely satisfying to be putting these pieces together and eventually see them work, as if I’m developing a unique editor configuration that is different from any tool other people use. Of course, this “hobby” isn’t for everybody and if you prefer an out-of-the-box experience to be able to jump straight into the work, then feel free to use any IDE that suits your liking.

Either way, I’m glad you stumbled upon my post. That’s all for today, cheers!

This post is licensed under CC BY 4.0 by the author.