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).
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.
Emacs showing language-aware auto-completion powered by the clangd language server. Company and company-box are used to provide the auto-completion UI.
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)
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
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.
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
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")))
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)
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
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))
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!