Joar von Arndt: Emacs does not need LSP

Wait 5 sec.

Table of ContentsIn-buffer completionJump to definition and referencesSyntax checkingConclusionLanguage-server-protocol (lsp) is standard to communicate information about acurrent programming project to an editor integration, powered by a “languageserver” for the specific programming language. Emacs has two majorimplementations of this editor functionality: eglot (built-in) and lsp-mode.Getting this set up is usually one of the first things that new users will wantto do, but in my experience one can get by for a surprisingly long time usingEmacs without needing to reach for eglot or lsp-mode. That is because many ofthe functions that lsp-servers provide can be provided by Emacs through othermeans.As someone who quite often moves between different programming languages, I findthe need to set up each lsp-server to a bit clunky. Despite being presented as asimple one-stop solution, you often need to research exactly what server to use,how to set it up, and to tailor it to your machine in some ways. For me, thisonly makes sense in a few languages that I use often, and even then it might notbe the first thing I take care of.In-buffer completionThe most obvious functionality is auto-complete (aka in-buffer completion). InEmacs this is provided on multiple levels, which is sometimes confusing for newusers. There are many ways to show in-buffer completions, but they are allpowered by the same back-end functionality: completion-at-point-functions(capf). This is a list of functions that run in order, only returning a value ifthere is something to complete.Some major modes (most notable perhaps lisp-interaction-mode) come with theirown buffer-local values of capf1 that provide buffer-specific completions.completion-at-point will then move on to the global value if the buffer-localvalue of capf includes the value t. It is therefore this global value that weare often interested in modifying.For this we can use the cape-package — standing for “Completion At PointFunctions”. Specifically, we can make liberal use of the cape-dabbrev-function.This capf makes use of Emacs’ built-in dabbrev-functionality. Dabbrev will scanall the words in the buffers of your choice and give you completions for wordsthat you have already used. This means that, while working on your project, youhave quick and easy access to all the keywords and function names in yourcurrently opened buffers.This also works as a form of autocomplete for writing prose, and so we cancombine these two using cape-wrap-super:(defun cape-language (&optional interactive) (interactive (list t)) (if interactive (cape-interactive #'cape-language) (cape-wrap-super #'cape-dabbrev #'cape-dict)))This will combine the output cape-dabbrev and cape-dict2 into one capf thatcan run at the same time (and therefore not block each other). For a simplercontribution we also use the cape-keyword capf that comes with a number ofpre-configured programming language3 keywords. This is a bit of a pre-lspsolution, where each editor provided support for each programming language. Toset up cape, simple add something like this to your init file:(add-hook 'completion-at-point-functions #'cape-history)(add-hook 'completion-at-point-functions #'cape-keyword)(add-hook 'completion-at-point-functions #'cape-file)(add-hook 'completion-at-point-functions #'cape-language)(add-hook 'completion-at-point-functions #'cape-abbrev)This is the order in which cape will try your capfs, with the first matchingblocking the rest. As mentioned earlier, locally bound capfs will supersedethese global values, and so python-completion-at-point will run before any ofthese in python-mode buffers.Another improvement we can do is to change the value ofcape-dabbrev-buffer-function. The value of this variable is the function thatreturns the buffers to scan for dabbrev results. By default this iscape-same-mode-buffers, meaning that only buffers in the same mode (org-mode,python-mode, et cetera) will be used for results. This is a good default, but Iwould rather have always have some relevant completion candidate than nothing.So personally I set this value to cape-text-buffers, which will return allbuffers in text-mode or prog-mode buffers. This means extra niceties likereferring to practically anything you are working on when writing a commitmessage in git.Jump to definition and referencesA second major feature of lsp-servers is “jump to definition” that allows you togo to the code that creates a function (its definition) to inspect its innerworkings. There are a few different ways to do this.The first is the package named dumb-jump that uses ag, rg, or simply grep tofind definition-matching strings. This works similarly to cape-keyword in thatit uses a pre-configured set of language-syntax elements to matchfunction-defining cases.4 I however do not personally use this.Instead I use another of Minad’s wonderful additions to the Emacs’ ecosystem;consult. I must confess that I slept on consult for far too long. I had it inmy config for a long time, but the number of (largely stand-alone) functionsmade it a bit overwhelming to learn.5 What Consult offers is a suite ofimproved versions of preëxisting Emacs utilities (and a few new ones buildingupon said features), largely based on the completing-read.Emacs has a built-in feature called imenu that scans the buffer’s structure fortop-level features (Org-mode and markdown headers, or in this case functiondefinitions) and allows you to quickly move to them. Consult of course expandson this by instantly showing you the place you will move to, but it also offersan extension of this feature: consult-imenu-multi (as opposed to the regularconsult-imenu). This will not only scan the current buffer, but also all othersame-mode buffers. Better yet, it integrates with Emacs’ project.el, and so onlychecks buffers that belong to the same project (exempli gratia git repository).As I usually have the relevant buffers open in a project I am working on, thisworks very well as an alternative to dumb-jump, and the preview and subsequentconfirmation mean that I am not worried about jumping to the wrong place —although of course at the cost of speed.Then we of course have the venerable xref built into Emacs. This works a bitdifferently depending on the programming language backend, and plugs into anlsp-server automatically if it is available. Use xref-find-references6Otherwise we can use etags-regen to automatically create and refresh the TAGSfile that keeps track of the project structure:(use-package etags-regen :config (setq etags-regen-ignores '("*.pyc" ".git" ".venv" "venv" "node_modules")) (etags-regen-mode 1))Consult can also be used to improve upon this as well by replacing the uiinteraction component:(setq xref-show-xrefs-function #'consult-xref xref-show-definitions-function #'consult-xref)The reason why I usually prefer using imenu is because it requires no trip toinspect the state of any files on the system — everything is already loaded andavailable, and perhaps 90% of the time it includes the things that I need (sincethey are things I am working on).Syntax checkingHere we can use the built-in flymake or the standalone flycheck. Both of thesemake use of external syntax-checking tools for each language, and so do requiresome more setup on the host machine than the above solutions. In fact, Flycheckcan even act as its own lsp-server just for syntax checking. But my experiencehas still been that this is easier to deal with than lsp-servers that need to beconfigured, started, reconnected, that crash, or have some other issue that needto be dealt with; A simple unix-style shell command that prints text (and thatis then processed by Flymake or Flycheck) rarely needs much setup or maintenancein my experience. This is a matter that I really just want out of my mind.ConclusionThere are only so many things that can be done merely through these staticsolutions. For example, neither of our in-buffer completion orjump-to-definition solution will complete external libraries; only buffers thathave already been opened. There is no hover information.I am not ideologically opposed to the use of language servers, and I willsometimes intentionally start one if I expect to be working on a larger codebase for a longer amount of time. But tools like the ones above are neverthelessincredibly useful because they work in all buffers and contexts.One of the strengths of Emacs is how it acts as a sort of force-multiplier forany task interacting with text, and how it builds upon improvements in one areathroughout the entire editor. Having some of the most critical lsp-likeabilities available anywhere you want without any extra effort is incrediblypowerful. This is especially noteworthy for things like cape-dabbrev since ithas made me so spoiled for autocompletion for everything I write.I am sure that there are lots of different solutions and tools that I havemissed that would work to replace even more lsp-based ones, and probablysolutions that work better and/or are more elegant than the ones I use — theanswer may even be to just use lsp. But honestly I only think about the matterof language server when I am talking with others. In day to day life I rarelyfeel the need for them at all. ❦Footnotes: 1 For lisp-interaction-mode the value is (elisp-completion-at-pointt).2 The results of cape-dict are decided by the value of thecape-dict-file variable.3 Here is the list of programming languages supported:C++, C, Caml, Crystal, C#, D, Elixir, Erlang, f90, Go, Java,Javascript, Kotlin, Lua, Nim. Objective C, Perl, php, Purescript,Python, Ruby, Rust, Scala, Scheme, Swift, Julia, Thrift, sh.4 This supposedly misidentifies things sometimes; as expected, it is “dumb”after all.5 If you are using the built-in switch-to-buffer command (by default bound toC-x b) I highly recommend switching it to consult-buffer instead. Instant“previews” is super useful, and I often just spam C-n (N being right next to B)to cycle through my open buffers instead of starting to write the exact namethat I want.6 By default bound to M-?. To run xref-go-back press M-,.