How I integrate ghcid with vim/neovim

Published on

ghcid by Neil Mitchell is a simple but robust tool to get instant error messages for your Haskell code.

For the most part, it doesn’t require any integration with your editor or IDE, which is exactly what makes it robust—if you can run ghci, you can run ghcid. There’s one feature though for which the editor and ghcid have to talk to one another: the ability to quickly jump to the location of the error.

The “official” way to integrate ghcid with neovim is the plugin. However, the plugin insists on running ghcid from within nvim, which makes the whole thing less robust. For instance, I often need to run ghci/ghcid in a different environment than my editor, like in a nix shell or a docker container.

Therefore, I use a simpler, plugin-less setup. After all, vim/nvim already have a feature to read the compiler output, called quickfix, and ghcid is able to write ghci’s output to a file. All we need is a few tweaks to make them play well together. This article describes the setup, which I’ve been happily using for 1.5 years now.

ghcid setup

ghcid passes some flags to ghci which makes its output a bit harder to parse.

Therefore, I build a modified version of ghcid, with a different default set of flags.

(There are probably ways to achieve this that do not require recompiling ghcid, but this is what I prefer—so that when I run ghcid, it simply does what I want.)

The patch you need to apply is very simple:

--- src/Ghcid.hs
+++ src/Ghcid.hs
@@ -97,7 +97,7 @@ options = cmdArgsMode $ Options
     ,restart = [] &= typ "PATH" &= help "Restart the command when the given file or directory contents change (defaults to .ghci and any .cabal file, unless when using stack or a custom command)"
     ,reload = [] &= typ "PATH" &= help "Reload when the given file or directory contents change (defaults to none)"
     ,directory = "." &= typDir &= name "C" &= help "Set the current directory"
-    ,outputfile = [] &= typFile &= name "o" &= help "File to write the full output to"
+    ,outputfile = ["quickfix"] &= typFile &= name "o" &= help "File to write the full output to"
     ,ignoreLoaded = False &= explicit &= name "ignore-loaded" &= help "Keep going if no files are loaded. Requires --reload to be set."
     ,poll = Nothing &= typ "SECONDS" &= opt "0.1" &= explicit &= name "poll" &= help "Use polling every N seconds (defaults to using notifiers)"
     ,max_messages = Nothing &= name "n" &= help "Maximum number of messages to print"
--- src/Language/Haskell/Ghcid/Util.hs
+++ src/Language/Haskell/Ghcid/Util.hs
@@ -47,7 +47,8 @@ ghciFlagsRequiredVersioned =
 -- | Flags that make ghcid work better and are supported on all GHC versions
 ghciFlagsUseful :: [String]
 ghciFlagsUseful =
-    ["-ferror-spans" -- see #148
+    ["-fno-error-spans"
+    ,"-fno-diagnostics-show-caret"
     ,"-j" -- see #153, GHC 7.8 and above, but that's all I support anyway
     ]

Alternatively, you can clone my fork of ghcid at https://github.com/feuerbach/ghcid, which already contains the patch.

Apart from changing the default flags passed to ghci, it also tells ghcid to write the ghci output to the file called quickfix by default, so that you don’t have to write -o quickfix on the command line every time.

vim/neovim setup

Here are the vim pieces that you’ll need to put into your .vimrc or init.vim. First, set the errorformat option to tell vim how to parse ghci’s error messages:

set errorformat=%C%*\\s•\ %m,
               \%-C\ %.%#,
               \%A%f:%l:%c:\ %t%.%#

Don’t ask me how it works—it’s been a long time since I wrote it—but it works.

Next, I prefer to define a few keybindings that make quickfix’ing easier:

map <F5> :cfile quickfix<CR>
map <C-j> :cnext<CR>
map <C-k> :cprevious<CR>

When I see any errors in the ghcid window, I press F5 to load them into vim and jump to the first error. Then, if I need to, I use Ctrl-j and Ctrl-k to jump between different errors.