[282] neovim for Elixir

Setting up neovim to have a nice Elixir editing environment

Subscribe now

neovim for Elixir [12.05.2016]

If you're not a vim user (or interested in being one), today's episode might be really boring. Having gotten that out of the way, let's learn a little bit about neovim and various Elixir plugins for vim.

Project

One of the most common questions I get is where my dotfiles are, what my vim configuration is. I think this is a shame, because I'm not terribly proud of what they were. They've grown up for a long time with me, so they had a lot of features, but I in no way thought I'd done a good job.

I'm in the process of moving to neovim and figured it might be worthwhile to re-evaluate my configuration. For what it's worth, I found myself sent on this path when I read a post by Bodo Tasche about his Elixir/Vim setup. I'd promised myself at the beginning of the year that this was the year that I would update my tool configurations...and promptly got busy with work and forgot to do it. This post reminded me that that was unacceptable.

Basic Setup

Neovim alias, to combat treachorous fingers!

The first thing I've done is replace my vim with neovim. Since I can't undo over a decade of muscle memory, it was necessary to alias the vim command to nvim. Here's how to do it in the fish shell, which I also just switched to, but I've included my bash solution at the end of the script as well.

function vim
  nvim $argv
end

Having gotten that out of the way, I can start setting up a new vim configuration from scratch for ideal elixir editing.

vim-plug

I see people suggesting usage of vim-plug. I'm a long-time Vundle fan, but I can't say I've kept up to date so I might as well take a hint from my betters. You can install it for neovim easily:

curl -fLo ~/.config/nvim/autoload/plug.vim --create-dirs \
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

Next, I'll create a vim-plug section in my ~/.config/nvim/init.vim file:

call plug#begin('~/.config/nvim/plugged')

" Plugins go here.  Example:
" Plug 'foo/bar'

call plug#end()

We'll use this to install the various plugins we bring in.

I can also tell I can't handle the default mouse behaviour in nvim. We can fix that with:

set mouse=""

And we can source the file with :source % and now clicking the mouse doesn't do anything, which is what I want from vim.

Deoplete

Everyone has told me that deoplete is a great autocompleter. It provides an asynchronous keyword completion system in the current buffer.

Requires python integration

It requires python integration, which my brew install neovim/neovim/neovim didn't provide. You can check if you have it by opening up nvim and typing :echo has("python3"). If it returns 1, you're golden. Otherwise, you can install it with:

pip3 install neovim

Now you should have it.

Setup

Next, we can set up Deoplete.

call plug#begin()

Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }

call plug#end()

Then we install it with :PlugInstall. That's it, deoplete's installed. I'd like to enable it at startup, and I tend to put configuration right after the plugin installation in my configuration, so I'll do that:

Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }
  let g:deoplete#enable_at_startup = 1
  " use tab for completion
  inoremap <expr><tab> pumvisible() ? "\<c-n>" : "\<tab>"

Basics

I can already tell I need to tweak my defaults, because it would appear I'm starting out with 8 space hard tabs and that's going to drive me to drinking.

" Sane tabs
" - Two spaces wide
set tabstop=2
set softtabstop=2
" - Expand them all
set expandtab
" - Indent by 2 spaces by default
set shiftwidth=2

I can reload the configuration by sourcing it, and % is the current file, so :source % will take care of that and we can confirm that tabs are acting reasonably now.

I like to use comma as my leader key:

" Use comma for leader
let g:mapleader=','
" Double backslash for local leader - FIXME: not sure I love this
let g:maplocalleader='\\'

I also prefer line numbers all the time, so we can turn those on:

set number " line numbering

And we should set the default encoding to UTF-8:

set encoding=utf-8

It'd be nice to have searching be reasonable. Right now it requires case sensitivity, among other things.

" Highlight search results
set hlsearch
" Incremental search, search as you type
set incsearch
" Ignore case when searching
set ignorecase
" Ignore case when searching lowercase
set smartcase
" Stop highlighting on Enter
map <CR> :noh<CR>

I also like my current row and column to be highlighted:

" highlight cursor position
set cursorline
set cursorcolumn

You can also make iTerm's title reflect the edited file:

" Set the title of the iterm tab
set title

Add language support

There's a great package I recently learned about called vim-polyglot

" Polyglot loads language support on demand!
Plug 'sheerun/vim-polyglot'

It loads in the language support you need when you need it, so it doesn't cause slowdowns. That means I don't need to pull in my litany of language-specific plugins, unless I feel like something's not quite right. So that's a nice win, as I had a ton of them.

Theme

Obviously this is a mixed bag and people's feelings vary. For now, I'll be sticking with molokai because I've become quite fond of it over the years and it feels like home:

Plug 'tomasr/molokai'
" ...
call plug#end()

" You have to have this after you end the plug section
set background=dark
syntax enable
colorscheme molokai

Elixir Bits

I'm starting to feel good about my setup at this point. It's far enough along that I feel good about trying to edit some Elixir code with it. Let's make a new elixir project and start fiddling about:

mix new dumping_ground
cd dumping_ground
vim mix.exs

I'd like to get syntax checking and linting working. Let's make the mix.exs have some invalid Elixir code and note how nice it would be to know before we tried to run our app or our tests:

  def project do
    [app: :dumping_ground,
     version: "0.1.0",
     elixir: "~> 1.3" # <--
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

When I save this file - nothing tells me I've made a mistake. We all know I make enough typos that this should be something I fix. Let's install Neomake.

Neomake

Neomake is a plugin that is intended to run code linters and compilers from within vim, asynchronously. Let's install it:

" Execute code checks, find mistakes, in the background
Plug 'neomake/neomake'
  " Run Neomake when I save any buffer
  augroup localneomake
    autocmd! BufWritePost * Neomake
  augroup END
  " Don't tell me to use smartquotes in markdown ok?
  let g:neomake_markdown_enabled_makers = []

Now let's install the plugin, reload vim, and open and save that mix file again. We get some nice syntax error checking out of the box.

Neomake Elixir syntax checking

We can do a bit better though. Looking through the code, it appears to support credo and dogma as well. Let's install credo in our project:

  defp deps do
    [
      {:credo, "~> 0.5", only: [:dev, :test]}
    ]
  end
mix deps.get
mix credo

We can see that there's a notice that a module should have a @moduledoc declaration. We'll ignore that for the moment. We can enable credo in neomake with the following setting:

  let g:neomake_elixir_enabled_makers = ['mix', 'credo']

However, it doesn't do remotely what I want. I found an issue on the neomake github issues list that had a nice setup though, so here's what I'm using:

Plug 'neomake/neomake'
  " Run Neomake when I save any buffer
  augroup neomake
    autocmd! BufWritePost * Neomake
  augroup END
  " Don't tell me to use smartquotes in markdown ok?
  let g:neomake_markdown_enabled_makers = []

  " Configure a nice credo setup, courtesy https://github.com/neomake/neomake/pull/300
  let g:neomake_elixir_enabled_makers = ['mycredo']
  function! NeomakeCredoErrorType(entry)
    if a:entry.type ==# 'F'      " Refactoring opportunities
      let l:type = 'W'
    elseif a:entry.type ==# 'D'  " Software design suggestions
      let l:type = 'I'
    elseif a:entry.type ==# 'W'  " Warnings
      let l:type = 'W'
    elseif a:entry.type ==# 'R'  " Readability suggestions
      let l:type = 'I'
    elseif a:entry.type ==# 'C'  " Convention violation
      let l:type = 'W'
    else
      let l:type = 'M'           " Everything else is a message
    endif
    let a:entry.type = l:type
  endfunction

  let g:neomake_elixir_mycredo_maker = {
        \ 'exe': 'mix',
        \ 'args': ['credo', 'list', '%:p', '--format=oneline'],
        \ 'errorformat': '[%t] %. %f:%l:%c %m,[%t] %. %f:%l %m',
        \ 'postprocess': function('NeomakeCredoErrorType')
        \ }

Now if we open up the empty module we'll see the error reported by neomake. We can fix it by adding a @moduledoc declaration:

defmodule DumpingGround do
  @moduledoc false
end

Neat! This is bound to help me write better Elixir code over time.

phoenix.vim

If you like jumping between related files, then you'll enjoy phoenix.vim.

Plug 'c-brenn/phoenix.vim'
Plug 'tpope/vim-projectionist' " required for some navigation features

There are a lot of features, but for a fun example I'll open up the time-tracker router and go to the file for a given route. Just navigate over the controller name and type gf to go to the file. For more details, type :h phoenix.

alchemist.vim

alchemist.vim is a way to get information from alchemist-server in vim. This allows you to do things like:

  • Documentation lookup
  • Jump to definition
  • Completion for modules and functions
  • Mix integration
  • IEx integration

This is really the crowning achievement of this whole episode, but I thought the other stuff was useful as well if someone was interested in getting started with neovim.

First, we'll install it:

Plug 'slashmili/alchemist.vim'

Let's look at all the features, in the time-tracker app.

Documentation Lookup

To get the documentation for the item under your cursor, just hit K.

This looks kind of awful because we don't support ansi escape codes presently. There's a vim plugin for this though!

Plug 'powerman/vim-plugin-AnsiEsc'

If we install it and restart, documentation will look nice!

NOTE: This was the case in bash. In fish, it made things worse! So yeah...if you're using bash I recommend it, but if it makes things worse, kill it with fire!

Jump to Definition

If you built Elixir or Erlang from source, then pressing C-] on something from the standard library will jump to the source file for the item in question. I haven't, presently, so I can use it on something more pedestrian like a dependency:

vim web/router.ex
# Press C-] on `Guardian.Plug.VerifyHeader` for example

Completion for modules and functions

This is easy to show off. I can just type Agent. and hit my completion key, which is C-X C-O by default, and it will autocomplete for me with deoplete.

Mix integration

You can use :Mix to run a given mix command from vim. For instance, to run the tests, you can just do :Mix test.

IEx integration

To run IEx, you can just use the command :IEx.

Summary

So that's it. There's still a lot I want to do before I'm comfortable saying my neovim setup is complete, but this is a good start and from here I'm confident that I can use it to great effect. Already, the syntax checking is dramatically more usable since it's done in the background.

If you have no interest in vim - well, I am assuming you aren't still around at the end of the video. If you do, I hope I've either shown you something useful or piqued your interest. See you soon!

If you want to check up on where my configuration is at presently, you can always check my dotfiles.

Resources

Left out of the episode for various reasons

ctags

ctags are a great way to be able to jump around between portions of your codebase. We can set them up fairly easily for Elixir. We'll start by installing vim-gutentags:

" Easily manage tags files
Plug 'ludovicchabant/vim-gutentags'
  let g:gutentags_cache_dir = '~/.tags_cache'

Then you'll want to copy this elixir-ctags file into your ~/.ctags file.

If you're on a mac and you haven't done this next part yet, it won't be working just yet. You need to install a new ctags and replace the existing one with it:

brew install ctags
# I put this in my aliases file that always gets loaded
if command_exists brew ; then
  alias ctags="`brew --prefix`/bin/ctags"
fi

Even still, this wasn't working for me. It should be, and I'll figure it out later. For now, you can manually run ctags to generate the file:

ctags -R --exclude=.git --exclude=log *

Then jumping between tags works with C-] and friends...but this isn't using this plugin at all :(

Aliasing vim to neovim in bash

I share my bash configuration across a few machines, and some don't have nvim, so I don't want to replace it unless it's available. Here's how I go about that:

# Helper to see if a command exists
command_exists () {
  type "$1" &> /dev/null ;
}

# Use neovim if it's installed
if command_exists nvim ; then
  alias vim='nvim'
fi