A curmudgeon tries a language server

Aug 26, 2026 07:48 PM - 2 hours ago 2

curmudgeon-tries-language-server.jpg

I constitute codification astir the aforesaid measurement I did 10 years ago1 Well, this was true when I started drafting this article. I person since adopted robots to do much more of the existent typing of code, unless it’s a task wherever (a) I care about codification quality, aliases (b) I effort to study something.: I make edits successful my text editor, I move complete to a terminal model and I tally a bid to compile and execute the code. I look astatine the result, past move backmost to my editor. If I am unsure of what happens, I either adhd tracer prints to the codification and restart the program, aliases I restart the programme successful a debugger. If I deed an objection that crashes my program, I hole the problem and past restart it.

I’m envious of what Lisp programmers do. Lisp improvement typically happens by typing codification straight into the repl to add, remove, and switch parts of the live, moving system.2 In believe a Lisp programmer would astir apt type the code into a scratch pad successful their editor and past nonstop it to the repl, but it is as though they typed it straight into the repl.

  • A Lisp programmer does not request to “switch over” to thing other because they are already wrong their programme process.
  • They ne'er “compile and execute” the codification because the codification is already running and they edit it by hot-swapping code.
  • They ne'er restart successful a debugger because they are already wrong their program process and tin inspect thing they want.
  • They don’t person to restart the programme connected exceptions, because the condition system allows resuming the crashing code from anyplace successful the stack after the strategy has been patched pinch a fix.

A consequence of this measurement of moving is that early successful a Lisp project, location may not moreover beryllium immoderate source code to speak of. Instead, the evolving meaning of the strategy exists only successful the memory image of the moving process and nowhere else. This is truthful dissimilar really we usually do improvement that group have expressed problem knowing what that moreover means. A comparison that might help is to deliberation of really group dainty relational databases successful the opening of a project. Often, group don’t commencement retired pinch a version-controlled schema definition; rather, the evolving schema exists only successful the moving database. Sure, astatine immoderate constituent erstwhile the task matures, the database schema is dumped out to a file, put nether type control, and from that constituent forward, schema changes are made done definitive migrations. Similarly, a Common Lisp project that matures will beryllium dumped from representation image to root codification eventually, and then that root codification will spell nether type control, pinch patches applied more carefully to the unrecorded system.

But successful the early stages? Oh, it’s each evolving live.

I’m not a Lisp developer, truthful I won’t ever get that experience. I’ll stay envious. But I besides realised I haven’t really tried to get person to it. Maybe there are immoderate improvements I tin get moreover successful my moving connection of Haskell.

There are immoderate things we tin norm retired immediately, and others that look like partial wins pinch not excessively overmuch effort.

  • There’s nary measurement to constitute Haskell that puts america wrong the programme process, truthful we will still request to restart successful a debugger.
  • Although I’m judge it’s imaginable to instrumentality a Lisp-like information strategy in Haskell, the types of exceptions (EitherT and async exceptions) utilized in common libraries don’t person one. On the flip side, less things successful Haskell lead to exceptions acknowledgment to the powerful type system. That’s a partial win without lifting a finger.
  • For a mates of years, location has been a Haskell connection server, an lsp implementation for Haskell. These days, Emacs has built-in support for speaking lsp done the built-in Eglot client. This seems for illustration another cheap partial triumph to get higher-quality codification introspection than I’m utilized to.
  • Then location is simply a task called ghcid that watches for changes to Haskell source files and instantly recompiles things that person changed. That alone doesn’t springiness america overmuch beyond what hls already does, but ghcid tin besides run a usability erstwhile the codification has compiled successfully. That is powerful, because it tin beryllium mixed pinch the foreign-store room (or the higher level Rapid library) to support process authorities moreover erstwhile each the codification is replaced!

Thus, wherever a Lisp programmer would incrementally nonstop definitions complete to the repl, we usage the operation of ghcid and foreign-store to automatically recompile and restart our full programme without losing important state. For many classes of software, these 2 approaches should lead to akin workflows.

The 1 point we still cannot do is measure codification successful the repl of the running program – because ghcid doesn’t support it. However, I mostly do this to test that functions activity arsenic intended, truthful what we tin do alternatively is constitute that codification as an existent portion trial case, and person ghcid besides tally tests erstwhile it reloads the code.

We extremity up pinch a process wherever we still constitute codification successful our editor, pinch the lsp server giving america accusation astir our code. Instead of playing astir successful the repl to effort things out, we constitute our experiments arsenic portion tests. When we save our changes (tests aliases exertion logic), ghcid reloads the code, runs the tests, and restarts the exertion successful a measurement that doesn’t suffer important state. We ne'er time off our editor. It’s neat connected paper, but I wasn’t judge really it’d work in practice, truthful I tried it pinch a artifact project.

At the clip of penning this, I was trying to spot up my deficiency of accomplishment with differential equations and non-linear dynamics. The book I was reading3 Differential Equations; Blanchard, Devaney, Hall; Cengage Learning; 2011 has a pleasant attraction connected computational methods4 Traditional first classes successful differential equations – including the 1 I took galore years ago – had a dense analytic attraction retired of necessity. In believe successful the industry, differential equations are solved computationally because successful galore cases calculus is not capable to lick them immoderate different way., and the book comes with software for performing the exercises, but I want to constitute the codification connected my own.

At slightest successful the early chapters, the package is not precisely rocket surgery. Here’s an illustration of my programme producing a slope field successful ray grey to visualise the wide solution to a differential equation, and past successful achromatic the specific solution that passes done the coordinate nether the cursor.

diffeq-slopefield-euler.png

When exploring ideas from the book, I want to constitute immoderate code, constitute immoderate tests, and past erstwhile I save, the model containing the visualisation should automatically update to bespeak the latest changes. It’s not precisely a Lisp development flow, but contains immoderate of its bully parts; specifically those that are imaginable pinch the technologies mentioned above. It sounded easy connected the surface to get there, but it wasn’t.

I’ll first attraction connected getting Eglot pinch hls up and running, because that’s the less absorbing part. The database beneath contains a batch of steps, because getting it installed from scratch pinch a caller task takes a batch of steps. After this initial installation, only steps 2 and 4 are required to alteration hls successful any project.

  1. At first I ran cabal init to create a modular quiet Haskell project, split into executable, library, and trial code.

    I discovered later that ghci (and truthful besides ghcid) don’t behave well when it comes to reloading multi-part projects for illustration that. Thus, I ended up co-locating each codification successful 1 executable project. I wouldn’t want to do that for production codification because it makes different types of improvement much difficult, but it useful okay for this benignant of artifact project.

  2. I created a flake.nix to wrap the cabal task and supply dependency management, including libraries and build tools. This way, I wouldn’t person to globally instal ghcid and hls, but could sandbox them to this project. Nix is awesome for fearlessly trying caller things.
  3. I installed direnv pinch my strategy package manager, and hooked it into my shell’s personification profile.5 Actually, I had already done this earlier for a different project. I should usage direnv more. It seems for illustration a bully idea. For immoderate reason, I still had to manually telephone up the Nix improvement ammunition in my terminal earlier I ran the project. I’m not judge why, but it mightiness mean hooking direnv into the ammunition is an optional step. No idea.
  4. I created an .envrc record containing conscionable the instruction usage flake successful the project directory. This tells direnv that erstwhile clients petition the environment for immoderate way nether this task directory, it should automatically put the Nix flake improvement ammunition limitations successful the path.
  5. I cloned nix-direnv and imported it successful my direnv configuration. This is an optimisation that makes direnv load Nix-based environments overmuch faster. For terminal-only use, it’s optional, but it’s useful erstwhile we thatch Emacs to direnv, because Emacs loads that situation often.
  6. I installed the envrc plug-in for Emacs and group up its hook. This means (some) Emacs commands will prime up the direnv. This is basal successful order for Eglot to beryllium capable to link to hls, since we didn’t instal it globally.
  7. Manually connecting to hls should beryllium a matter of moving M-x eglot, and then functionality tin beryllium tested by e.g. hovering complete a usability telephone and running xref-find-definition. Eglot besides creates arena buffers successful Emacs containing each postulation betwixt server and client, which tin beryllium read for further troubleshooting information.
  8. But astatine this point, I struggled for a while to get Eglot to work. It grounded to find the hls server. I manually enabled envrc-global-mode and past I could run M-x eglot successfully. That was astir apt a one-off problem, because the envrc hook hadn’t fired for the codification buffer I already had open.
  9. I still find I person to manually tally M-x eglot-reconnect to restart the lsp server sometimes erstwhile thing gets stuck. The drawback of this is that the server takes a small while to restart, moreover for a mini project. Restarting the server has been needed astatine slightest erstwhile aft a wide build failure, and often aft I person added third-party libraries to the Cabal file. Maybe this is the strategy moving arsenic intended, but it’s annoying early on successful a task erstwhile the dependency group is successful flux.
  10. Once I could reliably unfastened files successful the task and link to the lsp server, I added an eglot-ensure hook to haskell-mode truthful it automatically connects erstwhile I unfastened Haskell code. Unfortunately, this causes a mini delay any clip I unfastened a Haskell file, and it triggers an disfigured correction if that project’s situation does not person hls installed. I group up a information in the Emacs codification truthful it only tries to commencement Eglot if it detects there’s a good chance it mightiness work.
  11. I abnormal Flycheck successful Haskell buffers because it ne'er worked well anyway, and if I instrumentality pinch hls, it would switch what I usage Flycheck for anyway.
  12. I person configured Emacs to keep my cursor centred, pinch adjacent discourse above and beneath my edit. This interacts severely pinch the default archiving “pop-up” that Eglot uses, which changes the size of the editing model whenever it displays documentation. Fortunately, this was easy to hole by setting eldoc-echo-area-use-multiline-p to nil. With that setting, it only shows one statement of archiving while I’m editing: the type signature of the function nether the cursor. That’s perfect for my needs.

Overall, I for illustration the idea of the Eglot-and-hls experience. The additional formatting it adds to the buffer is not intrusive. Some of the hints and code actions are convenient. The 1 drawback I person problem adjusting to is that it does adhd latency to editing successful a measurement that sometimes really messes with me. I americium utilized to Vim commands that zoom maine astir the editing buffer, but I can’t get them to occurrence arsenic reliably erstwhile Eglot is enabled. I’m not judge why, but it’s bad capable to forestall maine from moving Eglot/hls successful different projects.

If I could return the clip to spell on, I’d play astir pinch it a small much to get a amended knowing of its crisp corners. I’d besides publication the manual alternatively of improvising. The manual tends to beryllium a bully measurement to study astir crisp corners and how to soil them.

One of the drawbacks of utilizing ghci (and result ghcid) for unrecorded reload is that ghci tin only way 1 group of root files astatine a time. This intends that if the task is decently group up pinch chopped components for executable, library, and trial code, ghci tin only reload 1 of those components. To apply changes to immoderate of the different components, the full ghci process must be restarted.

Since portion of the intended workflow was being capable to edit some tests and program codification and reload both, we are forced to co-locate each the codification into one component. This is simply a non-starter for superior projects, but acceptable for the kind of artifact task I was aiming for – and possibly moreover for the early evolutionary stages of experiments wherever this workflow is astir powerful.

The codification for the portion that live-reloads ends up looking thing for illustration this, with comments explaining the mechanism.

In[1]:

module Main (main) where import Control.Concurrent import Control.Concurrent.Async import Control.Exception (bracketOnError) import Data.IORef import Foreign.Store import Test.Hspec (describe, hspec, it) import Test.Hspec.QuickCheck (prop) -- The main usability runs connected reload, and has the -- effect of first moving the tests. If they -- succeed, it runs the update usability which makes -- sure to restart the processing thread while -- reusing resources. main :: IO () main = do spec update -- Unit tests successful a operation of example-based and property -- tests. These are written alternatively of poking about -- in the REPL. spec :: IO () spec = hspec $ do picture "forward euler solution" $ do prop "ever increases pinch illustration study" $ \x y -> -- ... -- A usability that must commencement our process if it is -- not running, aliases restart it (and reuse its -- resources) if it is running. update :: IO () update = let -- This contains an MVar pinch the resources -- needed to tally SDL. It besides serves arsenic a lock -- that prevents aggregate processes from running -- simultaneously. resourceStore = Store 0 -- This contains the Async of the process thread, -- for cancelling during an update. asyncStore = Store 1 -- Wait for resources to beryllium available, reserve -- them, and commencement a caller thread that uses them. withResources action = do withStore resourceStore $ \resources -> async $ -- Catch async exceptions during execution, -- such arsenic erstwhile this thread is cancelled by -- the update procedure. The exception -- already causes the action usability to stop -- running, truthful past we merchandise its resources. bracketOnError (putStrLn "Running action." >> takeMVar resources) (\res -> putStrLn "Action interrupted!" >> putMVar resources res) action commencement = do -- Waits for resources and past runs the render -- loop pinch them. withResources $ \(window, renderer, texture) -> do renderLoop renderer texture (State study Nothing) putStrLn "Render loop exited naturally." -- Getting present successful the power travel intends the -- render loop terminated but not done an -- async exception. That implies the user -- requested an exit, e.g. by closing the -- window. Thus we should destruct resources -- rather than merchandise them backmost for reuse. destroySdl model renderer texture -- We besides request to delete each stores truthful the -- next clip the update usability is called, -- it sees a blank slate and recreates -- everything each over. deleteStore asyncStore deleteStore resourceStore in do lookupStore (case asyncStore of Store one -> i) >>= \case Nothing -> do -- If the thread id shop does not exist, it -- means we're starting from a blank slate. -- We should initialise resources caller and -- start a caller thread to usage them. initialiseSdl >>= void . storeAction (Store 0) . newMVar commencement >>= void . storeAction (Store 1) . newIORef Just tidStore -> -- If the thread id shop exists, we cancel -- the thread its referencing, which will -- return the resources it used, and past we -- start a caller thread. The caller thread picks -- whatever resources were successful store. withStore tidStore $ \ref -> do readIORef ref >>= cancel commencement >>= writeIORef ref

I initially tried utilizing the higher-level Rapid room for this, but I couldn’t get it to activity properly. I recovered it overmuch easier to get stability, resource clean-up, and support for stdout successful each sub-processes erstwhile I implemented the plumbing myself pinch IORefs and Async.

The pursuing invocation launches ghcid successful a measurement that automatically reloads and runs the improvement main function:

In[2]:

ghcid --command "cabal repl --repl-options='-fobject-code -ferror-spans -fdiagnostics-color=always'" \ --reverse-errors \ --reload src \ --restart diffeq.cabal \ --test Main

This is the consequence of accretion-by-confusion. I couldn’t get thing to work, so I tried different bid statement argument, and that happened a fewer times successful a row. This is astir apt not the perfect measurement to constitute this command, but it seems to work and for now I want to play astir pinch it and spot really convenient it is, before I walk much clip connected it.

However, astatine first I couldn’t get this to activity astatine all. The ghcid process started each right, but it didn’t reload erstwhile codification changed. It didn’t show maine why either. I really gave up connected utilizing ghcid because I couldn’t get it to work, and past by mishap I ran this bid successful a Nix improvement ammunition and it worked. I person nary thought what’s going on. I thought direnv would make it unnecessary for maine to first commencement a Nix improvement shell, but successful this lawsuit it seems not.

So acold I really for illustration the ghcid-and-foreign-store acquisition for this type of programming. Instead of creating a analyzable gui for defining differential equations and first conditions and whatnot, I tin alteration constants successful the code and the visualisation updates. When I study caller things successful the book I’m reading, I tin adhd codification for them and the visualisation updates.

More