OCaml LSP Support in Vim
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'],
\ })
endif
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 merlin-lsp.dev https://github.com/ocaml/merlin.git
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