Running Emacs Haskell Tooling in a Nix environment
A guide to setting up the Haskell tooling for Emacs in a Nix environment.
You will need the following packages:
Add the following to your
;; ... dotspacemacs-configuration-layers '( ;; ... other layers lsp (haskell :variables haskell-process-type 'cabal-new-repl) ;; "cabal-repl" or "cabal-new-repl" will do here, but "cabal-new-repl" ;; is more robust since it works out of the box in projects with ;; multiple targets. You can add extra options using the ;; "haskell-process-wrapper-function" and even override this on a ;; per-project basis. ) ;; ... dotspacemacs-additional-packages '( ;; ... other packages nix-sandbox (lsp-haskell :location (recipe :fetcher github :repo "emacs-lsp/lsp-haskell")) )
lsp: Layer that adds general LSP support to Spacemacs.
haskell: Spacemacs Haskell layer, adds syntax highlighting, formatting, and much more.
nix-sandbox: Package that adds a number of utilities to make working with
nix-shells easier. We'll be using it to find the
file at the root directory of our project.
lsp-haskell: Package for communicating with haskell-ide-engine using the LSP.
Add this configuration to your
(defun dotspacemacs/user-config () ;; ... ;; Haskell configuration (require 'lsp-haskell) ;; Define a wrapper for the haskell-ide-engine process (setq default-nix-wrapper (lambda (args) (append ;; Change this to match your home directory/preferences (append (list "nix-shell" "-I" "ssh-config-file=/home/sam/.ssh/nixbuild.config" "--command" ) (list (mapconcat 'identity args " ")) ) (list (nix-current-sandbox)) ) ) ) (setq haskell-nix-wrapper (lambda (args) (apply default-nix-wrapper (list (append args (list "--ghc-option" "-Wwarn")))) ) ) ;; Flycheck is for error checking (setq flycheck-command-wrapper-function default-nix-wrapper flycheck-executable-find (lambda (cmd) (nix-executable-find (nix-current-sandbox) cmd))) ;; Haskell repl session that runs in the background (setq haskell-process-wrapper-function haskell-nix-wrapper) ;; Haskell-ide-engine process (setq lsp-haskell-process-wrapper-function default-nix-wrapper) ;; Haskell mode is activated whenever we open a .hs file buffer ;; Load flycheck when we activate haskell mode in a buffer (add-hook 'haskell-mode-hook 'flycheck-mode) ;; Load lsp-haskell when we activate haskell mode in a buffer (add-hook 'haskell-mode-hook #'lsp-haskell-enable) ;; Keep our haskell tags up to date (used for jumping to defn. etc.) (custom-set-variables '(haskell-tags-on-save t)) )
The important bit is that we're customizing four variables:
We set each of these functions to be a wrapper that wraps the commands executed
by the package in a nix shell.
I've used a custom
haskell-nix-wrapper that wraps the
and forces some ghc options. It's not helpful having
-Werror in a REPL, the
'haskell-process' will end and you have to manually restart it every time it
This above should serve you well enough for most projects, but if you need to override any of the above for a specific project, you can use directory variables.
Create a file called
.dir-locals.el in the root of your project:
( ;; Begin list, maps Emacs mode names to alists (nil . ;; Apply to all modes ((default-nix-wrapper . ( ;; Set default-nix-wrapper lambda (args) (append (append (list "nix-shell" "--command" ) (list (mapconcat 'identity args " ")) ) (list (nix-current-sandbox)) ) ) ) (haskell-tags-on-save . nil) ;; Set haskell-tags-on-save to off ) ) )
On opening a file in this project, Emacs should ask if you want to apply these variables, hit 'y' for yes.
I've defined some custom bindings here, feel free to change these bindings to match your preference.
Also, you can skip all this and just use the default bindings:
(defun dotspacemacs/user-config () ;; ... (spacemacs/lsp-bind-keys-for-mode 'haskell-mode) ;; ... )
These are the bindings in my
(defun dotspacemacs/user-config () ;; ... (evil-leader/set-key-for-mode 'haskell-mode "gm" 'lsp-ui-imenu) (evil-leader/set-key-for-mode 'haskell-mode "gg" 'lsp-ui-peek-find-definitions) (evil-leader/set-key-for-mode 'haskell-mode "gr" 'lsp-ui-peek-find-references) (evil-leader/set-key-for-mode 'haskell-mode "en" 'flycheck-next-error) (evil-leader/set-key-for-mode 'haskell-mode "ep" 'flycheck-previous-error) (evil-leader/set-key-for-mode 'haskell-mode "el" 'flycheck-list-errors) (evil-leader/set-key-for-mode 'haskell-mode "ee" 'flycheck-explain-error-at-point) (evil-leader/set-key-for-mode 'haskell-mode "rR" 'lsp-rename) (evil-leader/set-key-for-mode 'haskell-mode "rf" 'lsp-format-buffer) (evil-leader/set-key-for-mode 'haskell-mode "ra" 'lsp-ui-sideline-apply-code-actions) (evil-leader/set-key-for-mode 'haskell-mode "lr" 'lsp-restart-workspace) (evil-leader/set-key-for-mode 'haskell-mode "," 'completion-at-point) (evil-leader/set-key-for-mode 'haskell-mode "." 'lsp-describe-thing-at-point) )
This creates the following keybindings:
||Go to the imports at the top of the file|
||See an outline of the current file (q to quit)|
||Jump to definition|
||View references to symbol in file|
||Load the current buffer into a REPL|
||Switch back and forth between REPL and buffer|
||Go to next error|
||Go to previous error|
||List all errors (q to quit)|
||Describe error at point in more detail (q to quit)|
||Rename the given symbol and all occurences in project|
||Format the buffer|
||Bring up suggested code actions and select to apply|
||Restart the lsp-workspace (do this after changing default.nix/shell.nix)|
||Bring up completion suggestions|
||Describe thing at point|
Provided features and how to use them:
|HLint warnings and GHC warnings/errors||Should appear in sidebar by default, or enable
|Error highlighting||Should appear by default, or enable
|Jump to next error||
|Jump to prev error||
|Type information and doco on hover||Should appear in sidebar by default, or enable
|Jump to definition||
|List all top-level definitions||
|Highlight references in document||Should appear on hover|
|View references in file||
|Type quick fixes||Use "Code actions"|
|Add missing imports||Use "Code actions"|
|Add missing packages to cabal file||Not supported at this time|
|Description at point||
|Load buffer in REPL||
|Switch to REPL||
Help it doesn't work
The first thing to try is that you can build this project without Spacemacs.
Navigate to the project directory, open a nix-shell and run
cabal new-repl or
equivalent. The files generated by those commands should help bootstrap the
Spacemacs/HIE process. Just delete all buffers associated with the project and
re-open one of the project files to try again.
Can't find dependencies listed in other targets
Most likely you're in a test file and it can't find dependencies only listed in
other targets. You just need to open a terminal, navigate to the project
directory, open a nix-shell and run
cabal configure --enable-tests or
cabal new-configure --enable-tests.
You can jump into a Haskell REPL (in Spacemacs) and run
haskell-session-change-target to choose another target, which will let your
REPL access that target. You may also need to do the same in your buffer and run
- "Add missing packages to cabal file" feature does not work/has odd behaviour.
- Links in
lsp-ui-doc-modedon't work (see this issue). They are the correct links but don't open a web page. The current workaround is to use
lsp-describe-thing-at-point, and use the links in that buffer.