A guide to setting up the Haskell tooling for Emacs in a Nix environment.
You will need the following packages:
- apply-refact
- hasktags
- hlint
- hoogle
Add the following to your ~/.spacemacs
file:
;; ...
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 shell.nix
or default.nix
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 ~/.spacemacs
:
(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:
flycheck-command-wrapper-function
, flycheck-executable-find
,
haskell-process-wrapper-function
and lsp-haskell-process-wrapper-function
.
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 default-nix-wrapper
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
errors.
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 ~/.spacemacs
:
(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:
Command | Keybinding | Description |
---|---|---|
haskell-navigate-imports | , g i |
Go to the imports at the top of the file |
lsp-ui-imenu | , g m |
See an outline of the current file (q to quit) |
lsp-ui-peek-find-definitions | , g g |
Jump to definition |
lsp-ui-peek-find-references | , g r |
View references to symbol in file |
haskell-process-load-file | , s b |
Load the current buffer into a REPL |
haskell-interactive-switch | , s s |
Switch back and forth between REPL and buffer |
flycheck-next-error | , e n |
Go to next error |
flycheck-prev-error | , e p |
Go to previous error |
flycheck-list-errors | , e l |
List all errors (q to quit) |
flycheck-explain-error-at-point | , e e |
Describe error at point in more detail (q to quit) |
lsp-rename | , r R |
Rename the given symbol and all occurences in project |
lsp-format-buffer | , r f |
Format the buffer |
lsp-ui-sideline-apply-code-actions | , r a |
Bring up suggested code actions and select to apply |
lsp-restart-workspace | , l r |
Restart the lsp-workspace (do this after changing default.nix/shell.nix) |
completion-at-point | , , |
Bring up completion suggestions |
lsp-describe-thing-at-point | , . |
Describe thing at point |
Provided features and how to use them:
Feature | How |
---|---|
HLint warnings and GHC warnings/errors | Should appear in sidebar by default, or enable lsp-ui-sideline-mode |
Error highlighting | Should appear by default, or enable flycheck-mode |
Jump to next error | , e n , or flycheck-next-error |
Jump to prev error | , e p , or flycheck-previous-error |
Code actions | , r a , or lsp-ui-sideline-apply-code-actions |
Type information and doco on hover | Should appear in sidebar by default, or enable lsp-ui-doc-mode |
Jump to definition | , g g , or lsp-ui-peek-find-definitions |
List all top-level definitions | , g m , or lsp-ui-imenu |
Highlight references in document | Should appear on hover |
View references in file | , g r , or lsp-ui-peek-find-references (q to quit) |
Completion | , , , or completion-at-point |
Formatting | , r f , or lsp-format-buffer |
Renaming | , r R , or lsp-rename |
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 | , . or lsp-describe-thing-at-point |
Load buffer in REPL | , s b , or haskell-process-load-file |
Switch to REPL | , s s , or haskell-interactive-switch |
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.
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
haskell-process-restart
and lsp-restart-workspace
.
- "Add missing packages to cabal file" feature does not work/has odd behaviour.
- Links in
lsp-ui-doc-mode
don't work (see this issue). They are the correct links but don't open a web page. The current workaround is to uselsp-describe-thing-at-point
, and use the links in that buffer.
Thank you for this, it was very useful to me!
I enhanced a bit the
default-nix-wrapper
to make it work even for projects withoutnix
:I also made it a function for stylistic purpose but the
setq
withlambda
way works too of course.