Neovim for Haskell Development

14 minute read

Here’s how I setup neovim or vim 8 to be a functional working environment.

Updated Feb. 2020: Many things have changed in the Haskell/vim ecosystem, and I am not working with Haskell currently. Here is a summary of changes:

For full IDE features in vim/neovim, the current popular option looks like coc.vim combined with haskell-ide-engine. (I can’t recommend this personally only because I have not tried it myself)

intero is no longer being maintained, but intero-neovim is still useful in plain GHCi mode. My neovim config has an example of this setup.

ghc-mod is no longer maintained so don’t use any plugins that rely on it. Development effort has moved to haskell-ide-engine.

hdevtools was working the last time I tried it, but I do not know its current status.

The focus will be on Haskell, but many of the plugins here are useful for any language in both vim and neovim.

Limits of Vim

My vim config for PureScript had been working nicely for me this year, but my Haskell setup was lacking in features.

I started working through Stephen Diehl’s vim article, adding plugins for autocompletion, snippets, and tab alignment.

The features were now there, but performance in vim had gone down noticeably. Opening a file for the first time would take about 4 seconds (as indicated by vim’s profile feature).

On top of that, vim does not support Intero, which is supposed to be the killer feature of Haskell on Emacs. I thought maybe it was time to finally tackle the Emacs learning curve for the sake of Haskell development.

Then I discovered there is Intero support for neovim. Also, some of the plugins I had just installed were intended first for neovim, and only worked in vim 8 through compatibility plugins.

Surely neovim ought to have better performance for these plugins. Right? :thumbsup:

How I Vim

Before I go over the plugins I have chosen to use, I want to show how I use git to save my vim setup for use on multiple machines.

First of all, I load my plugins using Pathogen. With Pathogen, you only have to download the plugin to ~/.vim/bundle/ and it will be loaded when vim starts.

My .vim folder is a git repo that I push to GitHub, so I can access it when setting up a new machine. Since most vim plugins can also be found on GitHub, I add them to this repo as git submodules.

cd ~/.vim/bundle/
git submodule add https://github.com/neovimhaskell/haskell-vim.git

The primary benefit here is that I can update all my plugins by updating submodules:

git submodule update --recursive --remote

Downloading my .vim repo on a new machine therefore requires the --recursive option to also download submodules:

git clone --recursive https://github.com/johnmendonca/vimrc.git .vim

The downside to this approach is that removing submodules can be a pain.

Installing neovim

Installing neovim depends on your system.

In my case (Linux Mint), there is no system package for neovim. I followed the installation instructions to install using the Ubuntu PPA.

Start neovim by running:

nvim

Now with a fresh working install of neovim, we are ready to start setting it up.

Configuring neovim

The config file for neovim exists at ~/.config/nvim/init.vim, rather than ~/.vimrc. If you want to use your existing vim config in neovim, you can do that.

First I copied over my basic .vimrc commands over to ~/.config/nvim/init.vim:

syntax on
filetype plugin indent on

set nocompatible
set number
set showmode
set smartcase
set smarttab
set smartindent
set autoindent
set expandtab
set shiftwidth=2
set softtabstop=2
set background=dark
set laststatus=0

colo darkblue
hi Keyword ctermfg=darkcyan
hi Constant ctermfg=5*
hi Comment ctermfg=2*
hi Normal ctermbg=none
hi LineNr ctermfg=darkgrey

Some of these may be redundant in neovim.

Pathogen

Pathogen is the foundation for installing all plugins.

mkdir -p ~/.config/nvim/autoload
cd !$
wget https://raw.githubusercontent.com/tpope/vim-pathogen/master/autoload/pathogen.vim

In ~/.config/nvim/init.vim add:

execute pathogen#infect()

Now your plugins in ~/.config/nvim/bundle/ should be loaded whenever neovim starts.

Plugins

I have shown above how to install a plugin as a git submodule, but the minimum that you need to install any plugin is to download it into your bundle folder:

cd ~/.config/nvim/bundle/
git clone https://github.com/neovimhaskell/haskell-vim.git

This is the same for all the plugins listed here.

Let’s start with the most general purpose plugins and move to those specifically for Haskell.

NERDTree

NERDTree will provide a vim split with your project folder structure that you can use to browse, create, delete, copy, or move files without typing out the full commands.

nerd tree window

Install the plugin and modify your init.vim:

"Open NERDTree when nvim starts
autocmd StdinReadPre * let s:std_in=1
autocmd VimEnter * if argc() == 0 && !exists("s:std_in") | NERDTree | endif

"Toggle NERDTree with Ctrl-N
map <C-n> :NERDTreeToggle<CR>

"Show hidden files in NERDTree
let NERDTreeShowHidden=1

ctrlp.vim

ctrlp.vim allows you to perform a fuzzy name search on the files within your project directory. This brings the killer feature of Sublime Text into vim.

control p window

Simply install and restart nvim. Now whenever you press <Ctrl-p> a new file search window should appear.

Grepper

Grepper performs text search throughout the files in your project. I had previously used ack.vim but it requires you to install the Perl tool ack.

I don’t use text search in my work very often, and both of these plugins mess up my splits sometimes. There are times when text search is critical though, and I wouldn’t be without a plugin like this.

Grepper supports a variety of search tools, but the default works fine for me. I bind the key command \ga to search the entire project, and \gb to search only the current buffer:

"Use Grepper
nnoremap <leader>ga :Grepper<cr>
nnoremap <leader>gb :Grepper -buffer<cr>

Now you can search for the text “foo” throughout your project by typing the following in command mode:

\gafoo<Enter>

Vim Tmux Navigator

This one is for tmux users only. tmux is useful when you’re working on a remote server and don’t want to worry about your ssh session becoming disconnected. It also allows you to split one terminal emulator into many terminal windows.

I use both tmux and vim window splits at the same time. For example here is my screen while writing this page (using vim 8):

tmux and vim screen

Vim Tmux Navigator allows you to move between both types of splits seamlessly using one set of key commands. After this is setup you will be able to move between all splits using <Ctrl> + <h, j, k, l>.

These are the same keys for moving around within a vim document. Now just hold <Ctrl> and you will be moving between splits.

Configuration changes are needed for tmux, add this to your ~/.tmux.conf:

# Smart pane switching with awareness of Vim splits.
# See: https://github.com/christoomey/vim-tmux-navigator
is_vim="ps -o state= -o comm= -t '#{pane_tty}' \
    | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|\.?n?vim?x?(-wrapped)?)(diff)?$'"
bind-key -n C-h if-shell "$is_vim" "send-keys C-h"  "select-pane -L"
bind-key -n C-j if-shell "$is_vim" "send-keys C-j"  "select-pane -D"
bind-key -n C-k if-shell "$is_vim" "send-keys C-k"  "select-pane -U"
bind-key -n C-l if-shell "$is_vim" "send-keys C-l"  "select-pane -R"
bind-key -n C-\ if-shell "$is_vim" "send-keys C-\\" "select-pane -l"
bind-key -T copy-mode-vi C-h select-pane -L
bind-key -T copy-mode-vi C-j select-pane -D
bind-key -T copy-mode-vi C-k select-pane -U
bind-key -T copy-mode-vi C-l select-pane -R
bind-key -T copy-mode-vi C-\ select-pane -l

This plugin has worked well for me in vim, and does not require config changes if you use the default key bindings.

Some people have problems getting this to work right with neovim. There are some outstanding issues with neovim that you should check if you have trouble.

I encountered one of these issues, where <Ctrl-h> would not work in neovim inside tmux (the keypress registers as Backspace). The following change to init.vim is a workaround:

"Hack for neovim and vim-tmux-navigator
nnoremap <silent> <BS> :TmuxNavigateLeft<cr>

Previously I had installed neovim through Nix, and had an issue caused by binary names used in Nix. If you installed neovim though Nix, use my tmux config above and not the default from the plugin README. There is a necessary regex change for Nix binary support.

vim-gitgutter

vim-gitgutter will show git diff information in a column along the left edge of your file. You will be able to see which lines are new or modified, and where lines have been removed. It’s a little convenience that saves you a trip to the console.

If you want to run even more git commands within your editor, check out fugitive.vim.

Haskell IDE

Let’s look at the plugins that will allow us to turn neovim into a Haskell IDE. Thanks to Stephen Diehl and Jake Zimmerman for much of this information.

haskell-vim

haskell-vim provides an alternative syntax highlighter, and options for indentation. Here are my settings for init.vim:

let g:haskell_classic_highlighting = 1
let g:haskell_indent_if = 3
let g:haskell_indent_case = 2
let g:haskell_indent_let = 4
let g:haskell_indent_where = 6
let g:haskell_indent_before_where = 2
let g:haskell_indent_after_bare_where = 2
let g:haskell_indent_do = 3
let g:haskell_indent_in = 1
let g:haskell_indent_guard = 2
let g:haskell_indent_case_alternative = 1
let g:cabal_indent_section = 2

Intero

Intero for Neovim will run a GHCi console process inside neovim, making it convenient to reload your files and enter expressions.

intero window

This plugin uses stack by default but can be configured otherwise. Another alternative is neovim-ghci.

The first time you open a Haskell file after installing the plugin, it will automatically open a split in vim and build intero for your stack project.

The example configuration on the GitHub page is a good place to start, though I have the following setup for my needs:

" Automatically reload on save
au BufWritePost *.hs InteroReload

" Lookup the type of expression under the cursor
au FileType haskell nmap <silent> <leader>t <Plug>InteroGenericType
au FileType haskell nmap <silent> <leader>T <Plug>InteroType
" Insert type declaration
au FileType haskell nnoremap <silent> <leader>ni :InteroTypeInsert<CR>
" Show info about expression or type under the cursor
au FileType haskell nnoremap <silent> <leader>i :InteroInfo<CR>

" Open/Close the Intero terminal window
au FileType haskell nnoremap <silent> <leader>nn :InteroOpen<CR>
au FileType haskell nnoremap <silent> <leader>nh :InteroHide<CR>

" Reload the current file into REPL
au FileType haskell nnoremap <silent> <leader>nf :InteroLoadCurrentFile<CR>
" Jump to the definition of an identifier
au FileType haskell nnoremap <silent> <leader>ng :InteroGoToDef<CR>
" Evaluate an expression in REPL
au FileType haskell nnoremap <silent> <leader>ne :InteroEval<CR>

" Start/Stop Intero
au FileType haskell nnoremap <silent> <leader>ns :InteroStart<CR>
au FileType haskell nnoremap <silent> <leader>nk :InteroKill<CR>

" Reboot Intero, for when dependencies are added
au FileType haskell nnoremap <silent> <leader>nr :InteroKill<CR> :InteroOpen<CR>

" Managing targets
" Prompts you to enter targets (no silent):
au FileType haskell nnoremap <leader>nt :InteroSetTargets<CR>

With this config, Intero will start automatically and I can open a terminal split using \nn, then hide it again using \nh.

Using the terminal in neovim is a little strange. Move to the split and press i to enter Terminal mode (not insert mode) and you can enter commands into the REPL. Exit terminal mode by pressing <Ctrl-\> <Ctrl-n>.

The author of the plugin has an example keybinding to make exiting terminal mode a little more intuitive. I have adapted his example to use the split movement keys we setup in the tmux section:

" Ctrl-{hjkl} for navigating out of terminal panes
tnoremap <C-h> <C-\><C-n><C-w>h
tnoremap <C-j> <C-\><C-n><C-w>j
tnoremap <C-k> <C-\><C-n><C-w>k
tnoremap <C-l> <C-\><C-n><C-w>l

You still need to hit i to enter terminal mode. Alternatively, you can use :InteroEval to enter an expression in the REPL without moving panes at all.

With my config, you can type \ne2+2<Enter> to execute 2+2 in the REPL without ever leaving your file.

Testing

When you are writing tests for a Stack project, and want to use Intero to load and run your test code, you have to first change the target using :InteroSetTargets. The reason for this is that a Stack project may have different dependencies for a library or executable versus your test code. So, unless you change to the testing target, dependencies like hspec will not be loaded and available to Intero.

When I am working on a spec I want to save my file and run the spec without moving to the console. I can bind the expression hspec spec to the command \nb using :InteroSend:

" Run the spec in the current file
au FileType haskell nnoremap <silent> <leader>nb :InteroSend hspec spec<CR>

Neomake

Neomake allows us to run programs against our files and project. By default for Haskell files, neomake will use ghc-mod, hlint, and hdevtools to check your files

neomake in effect

These programs need to be installed first:

stack install ghc-mod hlint hdevtools

Neomake is an alternative for Syntastic. Add this to your init.vim to have checks ran against your file every time you save it:

call neomake#configure#automake('w')

Neomake will highlight lines of code with issues, and the command :lopen will open a window listing the issues. If you want to have this window open automatically use this config:

let g:neomake_open_list = 2

Intero uses Neomake to do syntax checks on your files. I prefer to use Intero only and none of the others. This config will disable the default checkers:

let g:neomake_haskell_enabled_makers = []

Now only Intero will perform syntax checks when a file is saved, and we can still manually run the other checks if desired:

:Neomake ghcmod
:Neomake hlint
:Neomake hdevtools

ghcmod-vim

ghcmod-vim allows you to use ghc-mod commands in vim. You can display the type of the item under the cursor, or expand a function definition for all the cases of a data type.

You have to also install vimproc. This plugin requires an extra installation step depending on your system. On linux:

cd bundle/vimproc.vim/
make

Since there is overlap between this plugin and Intero, I only use a couple commands:

au FileType haskell nmap <leader>mc :GhcModSplitFunCase<CR>
au FileType haskell nmap <leader>ms :GhcModSigCodegen<CR>

vim-hindent / vim-stylishask

vim-hindent and vim-stylishask are pretty-printers that will reformat your source code to match standard style guides. The programs need to be installed first:

stack install hindent stylish-haskell

The resulting code looks very nice! :bowtie:

hindent in effect

I have set these tasks to only be run manually:

let g:hindent_on_save = 0
au FileType haskell nnoremap <silent> <leader>ph :Hindent<CR>

let g:stylishask_on_save = 0
au FileType haskell nnoremap <silent> <leader>ps :Stylishask<CR>

These plugins can also process your file every time you save. Just replace a 0 with a 1 above on the printer you want to use.

Tabular

Tabular allows you to align characters in multiple rows. For example, this can align the = in a series of assignments, or the -> in a set of pattern matches.

nnoremap <leader>= :Tabularize /=<CR>
nnoremap <leader>- :Tabularize /-><CR>
nnoremap <leader>, :Tabularize /,<CR>
nnoremap <leader># :Tabularize /#-}<CR>

Pressing \= inside the following code block:

area Point = 0
area (Square x) = x * x
area (Rectangle x y) = x * y

will reformat it into:

area Point           = 0
area (Square x)      = x * x
area (Rectangle x y) = x * y

vim-hsimport

vim-hsimport will insert import statements for the identifier under the cursor. It is based on hsimport and hdevtools which must be installed first:

stack install hsimport hdevtools

It also requires a globally accessible ghc command. Using this plugin with Stack resulted in an error for me, until I added the location of ghc to my system path. It is a bit of a hack, but I added the following to my .bashrc file:

# Put GHC on the path globally
GHC_PATH=`stack path | grep compiler-bin | sed -e 's/compiler-bin: //'`
export PATH="$PATH:$GHC_PATH"

Also kill any hdevtools processes, so it will restart :disappointed_relieved::

pkill hdevtools

Once installed, you can add insert statements for entire modules or individual symbols:

au FileType haskell nnoremap <silent> <leader>ims :HsimportSymbol<CR>
au FileType haskell nnoremap <silent> <leader>imm :HsimportModule<CR>

deoplete.nvim

deoplete is a general framework for completions in neovim. It requires installation of a Python 3 module:

pip3 install neovim

Vim 8 requires a couple extra plugins for compatibility.

Activate deoplete when nvim starts:

" Use deoplete.
let g:deoplete#enable_at_startup = 1

Now when you are typing, you should see a completion menu recommending other words from your file. You can select them with the arrow keys and hit <Enter> to insert your choice.

We’ll add a couple more plugins to make this more useful.

supertab

supertab will allow us to choose a completion with the Tab key instead of the arrow keys.

Setup the Tab key to call vim’s omnifunc:

let g:SuperTabDefaultCompletionType = '<c-x><c-o>'

neco-ghc

neco-ghc uses ghc-mod to fill the autocomplete menus with useful Haskell options.

neco-ghc completions screen

" Disable haskell-vim omnifunc
let g:haskellmode_completion_ghc = 0

" neco-ghc
autocmd FileType haskell setlocal omnifunc=necoghc#omnifunc 
let g:necoghc_enable_detailed_browse = 1

SnipMate / UltiSnips

SnipMate and UltiSnips will allow you to paste snippets into your documents. If you have any sort of boilerplate code that you use often, these plugins can save you some time.

Which plugin to choose depends on what snippets file you want to use. I have not used these enough to have a preference.

Here are some haskell snippets to pick from:

Conclusion

Thank you for joining me on this tour of great open source software.

If you like what you’ve seen here, you can download and use my neovim configuration:

I am happy with this setup, as it delivers the performance and usability improvements I had hoped for.

If you find that this article has missed something, or if you have any other suggestions, please contact me on Twitter at @johnmendonca.