Vim and Haskell in 2016

A couple of years I wrote about Haskell editor tooling and figured this deserved a bit of an update now that the tooling has become mature. So let’s walk through how we install an minimalist Haskell dev environment on Linux.

If you don’t want to build the individual components youself, you can just download the source code.

$ git clone --recursive https://github.com/sdiehl/haskell-vim-proto.git 
$ cd haskell-vim-proto
$ cp -n vimrc ~/.vimrc 
$ cp -rn vim ~/.vim

Dev Environment

We’ll presume Ubuntu 14.04 as the lowest common denominator, although nothing here is specific to any particular Linux. Obviously first we need to install the world’s greatest text editor, we’ll presume no base vim configuration and build everything from a clean slate.

$ sudo apt-get install vim

Times have changed quite a bit, and the new preferred way of install GHC in 2016 is to forgo using the system package manager for installing ghc and use Stack to manage the path to the compiler executable and sandboxes. To do this we pull in the FP Complete key and start the build. This will take a few minutes.

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 575159689BEFB442
$ echo 'deb http://download.fpcomplete.com/ubuntu trusty main'|sudo tee /etc/apt/sources.list.d/fpco.list
$ sudo apt-get update && sudo apt-get install stack -y
$ stack setup

After all is said and done you should have the latest Haskell compiler installed in your home folder. Stack is all self-contained within the ~/.stack and places its executables in ~/.local/bin. For instance:

$ stack path
Run from outside a project, using implicit global project config
Using resolver: lts-3.14
global-stack-root: /home/user/.stack
project-root: /home/user/.stack/global-project
config-location: /home/user/.stack/global-project/stack.yaml
local-bin-path: /home/user/.local/bin

All is well and we can launch the interactive shell with the latest GHC 7.10 compiler.

$ stack ghci
Configuring GHCi with the following packages: 
GHCi, version 7.10.3: http://www.haskell.org/ghc/  :? for help
Ok, modules loaded: none.
Prelude> 

pathogen

Pathogen is a bundling system for vim that will allow us to pull directly from Git repos to manage and update our vim packages off of the bundles directory.

$ mkdir -p ~/.vim/autoload ~/.vim/bundle
$ curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim

Using pathogen we’ll install the following libraries.

  1. ghcmod-vim
  2. neco-ghc
  3. vim-snipmate
  4. syntastic
  5. neocompletee.vim
  6. ctrlp.vim
  7. nerdtree
  8. nerdcommenter
  9. tabular
  10. supertab
  11. neocomplete

To do this we simply mmove to our bundles directory and pull the repos.

$ cd ~/.vim/bundle
$ git clone https://github.com/eagletmt/ghcmod-vim.git
$ git clone https://github.com/eagletmt/neco-ghc
$ git clone https://github.com/ctrlpvim/ctrlp.vim.git
$ git clone https://github.com/scrooloose/syntastic.git
$ git clone https://github.com/tomtom/tlib_vim.git
$ git clone https://github.com/MarcWeber/vim-addon-mw-utils.git
$ git clone https://github.com/garbas/vim-snipmate.git
$ git clone https://github.com/scrooloose/nerdtree.git
$ git clone https://github.com/scrooloose/nerdcommenter.git
$ git clone https://github.com/godlygeek/tabular.git
$ git clone https://github.com/ervandew/supertab.git
$ git clone https://github.com/Shougo/neocomplete.vim.git

The one project that’s slightly special is vimproc which contains a C module which needs to be compiled. It can be installed with:

$ git clone https://github.com/Shougo/vimproc.vim.git
$ cd vimproc.vim
$ make

ghc-mod

The preferred tool for background checking of Haskell syntax is still ghc-mod. Over the years it has become more featureful and more efficient. The hlint tool is also used to supplement and provide helpful hints about ways to refactor common code smells. We will integrate with ghc-mod in several ways, but the first step is to install the command-line tool ghc-mod which vim will send program changes to query information. To install it we use stack.

stack install hlint ghc-mod
Copied executables to /home/user/.local/bin:
- ghc-mod
- ghc-modi
- hlint

vim defaults

Ok, now to the fun stuff. We’ll set up our basic vim configuration with some pretty sensible defaults that should work nicely in either terminal vim or gvim. Fairly standard stuff here to enable syntax highlighting, line numbers, tab completion and two space indentation. The last line enables the pathogen manager which pulls all the bundles into the environment.

syntax on
filetype plugin indent on

set nocompatible
set number
set nowrap
set showmode
set tw=80
set smartcase
set smarttab
set smartindent
set autoindent
set softtabstop=2
set shiftwidth=2
set expandtab
set incsearch
set mouse=a
set history=1000
set clipboard=unnamedplus,autoselect

set completeopt=menuone,menu,longest

set wildignore+=*\\tmp\\*,*.swp,*.swo,*.zip,.git,.cabal-sandbox
set wildmode=longest,list,full
set wildmenu
set completeopt+=longest

set t_Co=256

set cmdheight=1

execute pathogen#infect()

syntax highlighting

The best syntax highlighting that I know of is maintaind under the vim-scripts project on Github. We’ll download it and place them into ~/.vim/syntax/.


syntastic

Syntactic provides background syntax checking with line-by-line error reporting. It integrates with ghc-mod and hlint to provide semantic hinting about type errors and possible code corrections. To enable it we add the following lines to our .vimrc.

map <Leader>s :SyntasticToggleMode<CR>

set statusline+=%#warningmsg#
set statusline+=%{SyntasticStatuslineFlag()}
set statusline+=%*

let g:syntastic_always_populate_loc_list = 1
let g:syntastic_auto_loc_list = 0
let g:syntastic_check_on_open = 0
let g:syntastic_check_on_wq = 0

Now when a syntax error, type error, or code smell is introduced the left gutter will highlight the line and show the error message from GHC in the status line.

For example a syntax error.

Or a hlint error.


ghc-mod

To hook into GHC’s code competion capabilities we map several keyboard commands to ghc-mod functions.

map <silent> tw :GhcModTypeInsert<CR>
map <silent> ts :GhcModSplitFunCase<CR>
map <silent> tq :GhcModType<CR>
map <silent> te :GhcModTypeClear<CR>

Now to use ghc-mod’s case expand feature we simply highlight the scrutinized variable of case expression and type t + s to expand out the cases. For example:

To autofill in the type of an expression we simply highlight the name of toplevel expression and type t + w which will insert the signature on the line above. For example:

To query the type of a subexpression we highlight any term and type t + q to get the type of value under the cursor.


supertab

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

if has("gui_running")
  imap <c-space> <c-r>=SuperTabAlternateCompletion("\<lt>c-x>\<lt>c-o>")<cr>
else " no gui
  if has("unix")
    inoremap <Nul> <c-r>=SuperTabAlternateCompletion("\<lt>c-x>\<lt>c-o>")<cr>
  endif
endif

To enable familiar tab completion we configure supertab to dispatch to neco-ghc’s tab completion routines instead of the usual local variable completion. After that we configure necoghc to be the default tab completion method.

let g:haskellmode_completion_ghc = 1
autocmd FileType haskell setlocal omnifunc=necoghc#omnifunc

So now pressing tab while in a LANGUAGE pragma, import statement, or anywhere in a subexpression will use neco-ghc’s tab completion routines to find the context appropriate statement that matches the partial expression under the cursor.

For example completing language extensions:

Or module import declarations:

Or import name management:

nerdtree

Nerdtree is the standard file management plugin which replaces vim’s default left-hand file pile. It allows recursive directory traversal with folds and slew of other convenient features. It’s usually one of the first things installed in any respectable vim installation.

map <Leader>n :NERDTreeToggle<CR>

tabularize

Tabularize allows uniform aligned code formatting based on any textual regex pagttern. For Haskell there are several common identifiers that we typically align on; and we can map specific keys to these common patterns.

let g:haskell_tabular = 1

vmap a= :Tabularize /=<CR>
vmap a; :Tabularize /::<CR>
vmap a- :Tabularize /-><CR>

For example typing a + - on the case arm branches will align on the right arrow.

vim-snipmate

The best Haskell snippet collection that I know of is one I’ve curated over the years, that automates the insertion of many common insert statements, language extensions and instance declarations. This is placed in the ~/.vim/snippets folder.

The usage is simple, simply define a keyword in the haskell.snippets file of the form:

snippet derive
	{-# LANGUAGE DeriveDataTypeable #-}
	{-# LANGUAGE DeriveGeneric #-}
	{-# LANGUAGE DeriveFunctor #-}
	{-# LANGUAGE DeriveTraversable #-}
	{-# LANGUAGE DeriveFoldable #-}

Then tab completing on the this phrase in Insert Mode will expand out the code block.


ctrl-p

Ctrl-p is a fuzzy file search plugin which allows quick browsing of a project based on a fuzzy text search of the filename or it’s contents. We’ll bind the ctrl-p panel launch to \ + t.

map <silent> <Leader>t :CtrlP()<CR>
noremap <leader>b<space> :CtrlPBuffer<cr>
let g:ctrlp_custom_ignore = '\v[\/]dist$'


Summary

In summary adding these plugins and lines to the .vimrc will introduce several Haskell specific commands which are bound to these keyboard shortcuts:

Command
t + w Insert type for toplevel declaration
t + q Query type of expression under cursor
t + s Case split expression under cursor
t + e Erase type query
a + = Align on equal sign
a + - Align on case match
Ctrl + x + o Tab complete under cursor
\ + t Open fuzzy file finder
\ + n Open file explorer
\ + c + Space Toggle comment of text under cursor
\ + c + s Toggle “sexy” comment of text