For work I have inherited an OCaml project with some custom build setup. It is my first foray into OCaml, which makes it an… interesting ride. As with any programming language, you want a nice setup within the confines of the editor you are used to. In my case, that editor is Vim. I have always liked the concept of the Language Server Protocol (LSP) and noticed it is getting more and more widespread. As such, I want to use tooling which makes use of LSP.

With LSP, there are two sides to the story. On the one hand you need something in your editor that ensures that warnings and errors are drawn, that you can jump around using LSP information, … On the other hand, you need a background server that handles the code analysis and responds to questions (from the client, your editor) about the code.

I assume you are familiar with Vim and have OCaml and opam set up already.

Setting Up Vim

There are some implementations of LSP clients in Vim. I chose mine several months ago and must admit I do not remember what exactly made me choose this particular client. At least part of the choice was that it was in pure VimScript, ensuring I do not need any special setup.

The LSP client I use is vim-lsp by user prabirshrestha on GitHub. Install that plugin however you would do it in your setup (I use vim-plug). Once installed, you still need to register individual language servers. Here is the code you have to put in your ~/.vimrc file in order for vim-lsp to pick up on the server I detail in the next section.

if executable('ocamlmerlin-lsp')
  au User lsp_setup call lsp#register_server({
        \ 'name': 'ocamlmerlin-lsp',
        \ 'cmd': {server_info->[&shell, &shellcmdflag, 'opam config exec -- ocamlmerlin-lsp']},
        \ 'whitelist': ['ocaml'],
        \ })

Once the language server below is installed correctly, you should see warnings and errors pop up (if you have any). The rest of the functionality you can discover under the :Lsp* commands.

I have some small configuration adjustments to vim-lsp that make it just a bit nicer to use. You can decide for yourself which ones you like. These too are added in your ~/.vimrc.

" Echo warning/error under cursor in normal mode
let g:lsp_diagnostics_echo_cursor = 1
" Prettier gutter signs
let g:lsp_signs_error = {'text': '✗'}
let g:lsp_signs_warning = {'text': '‼'}
" Lets you see the hover information by pressing "K".
au FileType ocaml setlocal keywordprg=:LspHover
" Use tag muscle memory to go to definition, press Ctrl+]
au FileType ocaml nnoremap <buffer> <C-]> :LspDefinition<CR>
" Ensure whatever you use for completion knows about the LSP information.
au FileType ocaml setlocal omnifunc=lsp#complete

These last three are set only in OCaml buffers, but are likely useful for any language where you use the LSP. I do not immediately have a setup to make that happen automatically in just LSP contexts. In other words, if you need something similar in another language, you will need to add similar lines to your ~/.vimrc.

Setting Up a Language Server

Up until very recently I had been using ocaml-language-server to serve as a language server. This project was written in JavaScript (or TypeScript) and thus required you to have node installed. An annoying extra dependency. I noticed however that the user seems to have been deleted from GitHub. Whether it was of their own volition or not, I do not know. While the server itself is still available from npm, it does not bode well for its future. As such I decided to hop ship and look around again. Turns out Merlin is in the process of making their own built-in language server. Since ocaml-language-server was, essentially, a wrapper around Merlin, it is nice to cut out the middle man.

There is currently one major downside here. This language server is not yet bundled with the actual Merlin release. To use it, you need to get it straight from the source. Here is the command line call to make that happen. If you are reading this from the future, perhaps you can skip this step and instead install it cleanly from opam.

opam pin add

You should now have the ocamlmerlin-lsp binary in your $PATH somewhere. Depending on your project, this may be enough to get going. In my case, I was still missing some blocks. Specifically, the hover information was lacking documentation and was a bit wordy.

This project is not using Dune as a build system. Instead, it has a Makefile that calls ocamlbuild. Dune does some extra things without you needing to think about it. I did not know which things exactly, so I had to think about squeezing some extra functionality out of everything.

I was missing the docstrings when asking for hover information. I got a type signature, but the text explaining what exactly the function or type was about was not present. To get it, I had to pass an extra flag to ocamlbuild. This is probably obvious to the experienced OCamler, but for newbies like me, finding this information is hard. I have been enjoying some Rust in my free time and in comparison OCaml really lets you down in their documentation. The specific flag I had to pass along to ocamlbuild is -tag bin_annot. This apparently passes it further on to the actual OCaml compiler, but those details do not really matter here. In essence, without this flag the files that are created do not contain any comment information any more. Passing the flag creates cmt files in your _build directory which do have that information. Merlin should pick up on that information and then pass it on to your LSP client (e.g., when running :LspHover).

Finally, the type signatures were a bit too detailed, ignoring aliases of, say, tuples. This made things harder to understand once you know about the aliases. To make things shorter, you have to add the following in your project’s .merlin file.

FLG -short-paths

If you do not have a .merlin file yet, here is mine with some project details removed. S tells Merlin where the sources are, B where the build artefacts are. PKG lets it know you are using libraries of those names.

S src/**
B _build/**

PKG batteries base

FLG -thread -short-paths