How to use the NeoVim text editor as your Ocaml IDE

Why OCaml?

I’ve always been interested in learning an ML language. But Haskell, the poster child of functional programming, has a high learning curve.
OCaml and ReasonML (an alternative syntax for OCaml) are much more beginner-friendly.

I started a free MOOC on functional programming with OCaml a few days ago. Thus, it’s the perfect time to set up my editor for OCaml development.

If you want to know more about OCaml, read the short article Why OCaml?.

What does it take to develop OCaml in NeoVim?

What is NeoVim?

NeoVim is a fork of the terminal-based Vim text editor.
It’s fully compatible with Vim, but adds more features with a slimmer code base.

Plugin Manager and Basic Setup

NeoVim comes with very sensible defaults. See :h nvim-defaults for more information.
If you’d like some suggestions for further customization, you can check out neovim-sensible by Jeff Kreeftmeijer.

Install a plugin loader for convenience, for example, minpac.

There are two alternatives for code support in NeoVim: Merlin (standalone) or esy.

You’ll install Merlin with opam, the package manager for OCaml. esy is the package manager for Reason and OCaml and works with the npm ecosystem (Node.js/JavaScript).

You can use Merlin and its Vim integration, or you can use esy’s Vim integration.
esy only works, if you compile your programs to native binaries.

Merlin

Let’s start with Merlin. Skip this section section, if you want to try out esy.

Basic Requirements

Merlin is the de-facto code helper for OCaml. If you’d like to use Merlin with Neovim, you need Python and pynvim.

pip2 install --user pynvim
pip3 install --user pynvim

Make sure that your Python integration for NeoVim works correctly.
If you encounter problems, use :checkhealth provider for trouble-shooting.

For Merlin, you can follow the vim from scratch guide:

opam install merlin

Then add this line to your init.vim:

let g:opamshare = substitute(system('opam config var share'),'\n$','','''')
execute "set rtp+=" . g:opamshare . "/merlin/vim"

That not only adds merlin to your runtime path, but will always pick the version corresponding to your opam switch without you needing to modify your config (assuming you install merlin on each of your switches).

In NeoVim, run the following command:

:execute "helptags " . substitute(system('opam config var share'),'\n$','','''') .  "/merlin/vim/doc"

Merlin comes with default key mappings. Be aware that those might not work.
You might want to set your own key bindings. I find :MerlinTypeOf in normal mode especially useful.
See :h merlin for more info.

(Tab) Completion

Merlin supports several completion plugins out of the box, for example, Deoplete or SuperTab.

I prefer VimCompletesMe, a minimal plugin that does everything I need:

call minpac#add('ajh17/VimCompletesMe')

Merlin will now suggest completions when you press <Tab>.

Language Server

Merlin offers the most important features, like showing type signatures.
If you desire more functionality, for example, hover information about different modules, you can use a language server.

You’ll need a Language Server client. I recommed LanguageClient-neovim, as it works with every language server I’ve tried.

Install with your Vim plugin manager, e.g.:

call minpac#add('autozimu/LanguageClient-neovim', {'branch': 'next', 'do': 'bash install.sh'})

Now you have to configure the plugin. Modify init.vim (or ~/.vimrc or similar):

set hidden

 let g:LanguageClient_serverCommands = {
 \   'ocaml':           ['ocamllsp'],
 \}

nnoremap <F5> :call LanguageClient_contextMenu()<CR>
" Or map each action separately
nnoremap <silent> K :call LanguageClient#textDocument_hover()<CR>
nnoremap <silent> gd :call LanguageClient#textDocument_definition()<CR>
nnoremap <silent> <F2> :call LanguageClient#textDocument_rename()<CR>

Now install the language server. There are several alternatives. ocaml-lsp is a community project written in OCaml that’s in active development.

See the installation instructions for further details.

I use opam:

opam pin add ocaml-lsp-server https://github.com/ocaml/ocaml-lsp.git

Now open an Ocaml file, hit K while the cursor is on a definition. The language server will give you information about the function or file.

If you use a different language server than ocaml-lsp, you have to change the setup for LanguageClient-neovim accordingly.

esy

If you’re willing to work with the Node ecosystem, esy is a good choice that works both with OCaml and ReasonML.

Basic Requirements

Install esy with npm (see Node.js:

npm install -g esy

Now, you’ll need a Vim plugin: vim-reasonml. Install it with your plugin manager. For example, with minpac:

call minpac#add('jordwalke/vim-reasonml')

The plugin supports Merlin out of the box. For example, if you have a .merlin file in your project, vim-reasonml will pull merlin from your dev dependencies.
That means that you don’t have to install Merlin separately.

(Tab) Completion

You can use Vim’s inbuilt completion, but the easier way is to install a completion plugin.

I prefer VimCompletesMe, a minimal plugin that does everything I need:

call minpac#add('ajh17/VimCompletesMe')

vim-reasonml will now suggest completions when you press <Tab>.

ALE And ocamlformat

ALE is a linting and fixing tool for Vim. I heavily rely on it for avoiding syntax errors and tidying up my code.

Install it with your package manager (or find alternative instructions on GitHub):

call minpac#add('dense-analysis/ale')

Example setup in your init.vim (or similar):

let g:ale_sign_error                  = '✘'
let g:ale_sign_warning                = '⚠'
highlight ALEErrorSign ctermbg        =NONE ctermfg=red
highlight ALEWarningSign ctermbg      =NONE ctermfg=yellow
let g:ale_linters_explicit            = 1
let g:ale_lint_on_text_changed        = 'never'
let g:ale_lint_on_enter               = 0
let g:ale_lint_on_save                = 1
let g:ale_fix_on_save                 = 1

let g:ale_linters = {
\   'ocaml':      ['merlin'],
\}

let g:ale_fixers = {
\   'ocaml':      ['ocamlformat'],
\   '*':          ['remove_trailing_lines', 'trim_whitespace'],
\}

Improve ALE’s performance by setting the linters you need and don’t lint every time you type in something.
Fix a file when you save or call :ALEFix manually.
See :h ale for more information.

We use ocamlformat for parsing our code.

opam install ocamlformat