Skip to content

Instantly share code, notes, and snippets.

@sevanspowell
Last active July 6, 2022 01:11
Show Gist options
  • Save sevanspowell/23b0135dae2834e59904a502b8a0eb5d to your computer and use it in GitHub Desktop.
Save sevanspowell/23b0135dae2834e59904a502b8a0eb5d to your computer and use it in GitHub Desktop.
A guide to setting up the Haskell tooling for Emacs in a Nix environment.

Running Emacs Haskell Tooling in a Nix environment

A guide to setting up the Haskell tooling for Emacs in a Nix environment.

Suggestions/Contact

mail@sevanspowell.net

Configuration

Requirements

You will need the following packages:

Packages

Haskell packages

  • apply-refact
  • hasktags
  • hlint
  • hoogle

Basic configuration

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.

Advanced configuration

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.

Keybindings

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

Features

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

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 haskell-process-restart and lsp-restart-workspace.

Outstanding issues

  • "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 use lsp-describe-thing-at-point, and use the links in that buffer.
@ibizaman
Copy link

ibizaman commented Sep 3, 2020

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 without nix:

  (defun default-nix-wrapper (argv)
	(if-let ((sandbox (nix-current-sandbox)))
		(append
		 (append (list "nix-shell" "-I" "." "--command" )
				 (list (mapconcat 'identity argv " ")))
		 (list sandbox))
	  argv))

I also made it a function for stylistic purpose but the setq with lambda way works too of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment