I somehow managed to get Haskell Language Server running and play nicely together with Emacs. I’ll briefly explain here what the bits and pieces where I had to put together.
Setup Overview
This will sound more complicated than it actually is. :)
So the basic idea is define your package as a Nix Flake. I’m using the haskell.nix infrastructure by IOHK for this. The flake defines the whole development environment including GHC, all libraries and the Haskell language server. We’ll use direnv to load that environment automatically when entering the project. Finally, we will integrate direnv into Emacs so that it can pick up the right tools, especially Haskell language server.
System setup
This is stuff that needs to go into your configuration.nix
:
{ config, pkgs, ... }:
let
# Define your own Emacs…
emacsBuild =
with (pkgs.emacsPackagesNgGen pkgs.emacs);
(epkgs: (with epkgs.melpaStablePackages;
emacsWithPackages [ # Needed to integrate with direnv
direnv# Editing Haskell
haskell-mode
structured-haskell-mode]) ++ (with epkgs.melpaPackages; [
# Stuff which lsp wants
lsp-mode
lsp-ui
flycheck
company
lsp-treemacs
dap-mode
lsp-haskell
iedit]));
in
{ # Start up an Emacs service. You can connect to it with emacsclient.
services.emacs = {
enable = true;
package = emacsBuild;
};
environment.systemPackages = [
# Add your own emacs to the system packages
emacsBuild
pkgs.direnv# Add nix capabilities to direnv. This makes direnv play nicely
# with flake definitons.
pkgs.nix-direnv
# Haskell dev tooling, you could also define these in your flake
# definition.
pkgs.haskellPackages.structured-haskell-mode
pkgs.haskellPackages.stylish-haskell
pkgs.hpack
pkgs.cabal-install
pkgs.cabal2nix
pkgs.haskellPackages.hlint
pkgs.ghc];
# This is needed to integrate `nix-direnv` into `direnv`
environment.pathsToLink = [
"/share/nix-direnv"
];
# See https://input-output-hk.github.io/haskell.nix/tutorials/getting-started-flakes/
nix = {
package = pkgs.nixFlakes;
extraOptions =
''
# [...]
experimental-features = nix-command flakes
keep-outputs = true
keep-derivations = true
'';
# You definitely want these. Otherwise you will end up compiling a lot.
binaryCaches = [ "https://hydra.iohk.io" ];
binaryCachePublicKeys = [ "hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" ];
};
}
Init your first Project
Fine, we basically have everything we need on our system
now. Create new project by creating a directory and calling cabal
init --interactive
in it. (I usually work with cabal, but haskell.nix
also plays nicely with stack.)
After that create a flake.nix
alongside your .cabal
file
basically using this template. Now call nix shell
inside this
directory. This will take some time while nix is pulling GHC and
compiling tools. Make yourself a coffee, maybe two. The first time
you are using this ensure that nix doesn’t start compiling GHC. If
that’s the case you did not set up the binaryCaches
correctly.
If this works you should be able to call haskell-language-server
tailored to your projects GHC version. Nice!
Integrate Direnv
This is as easy as generating a file .envrc
alongside your
flake.nix
containing:
use flake;
After creating this file direnv will ask you to whitelist this
project using direnv allow
. Do it and nix
will start again to
produce heat on your CPU. Note that this will only take some time
on the first run. Nix caches the results and all subsequent
environment loads will be finished in less than a second.
Set up Emacs
This is the snippet defining my Emacs setup:
(defun tbd-cfg-haskell ()
(add-hook 'haskell-literate-mode-hook #'lsp-deferred)
(add-hook 'haskell-mode-hook #'lsp-deferred)
(add-hook 'haskell-mode-hook
(lambda ()
(define-key haskell-mode-map (kbd "C-c C-d") 'haskell-compile)
(when (and (bound-and-true-p haskell-indentation-mode)
(fboundp 'haskell-indentation-mode))
(haskell-indentation-mode 0))
(setq haskell-compiler-type 'cabal)
(setq haskell-process-type 'cabal)
(setq haskell-stylish-on-save 't)
(direnv-mode)
(structured-haskell-mode)
(interactive-haskell-mode)
(haskell-decl-scan-mode)
(setq lsp-haskell-server-path "haskell-language-server"
lsp-haskell-formatting-provider "stylish-haskell")
(lsp-treemacs-sync-mode 1))))
;; […]
(tbd-cfg-haskell)
You probably don’t want all of that. But things work smoothly for
me with these settings and I’m too lazy to tear them apart to see
which ones will provide the bare minimum. If you have enabled
services.emacs.enable
as me above, then don’t forget to execute
systemctl --user restart emacs
after altering the config file.
Spin up your Emacs and visit one of the Haskell source files in
your project. If lsp
doesn’t start automatically, do M-x lsp
and import the project for the first time.
Eventually Emacs/direnv won’t notice changes you did to the cabal
file. Use M-x direnv-update-environment
and eventually M-x
lsp-workspace-restart
to get back on track then.