An overview of OCaml editor tooling

OCaml

For a small community, OCaml ecosystem is quite rich, maybe too rich, in editor tooling.

I have gone through some options over time. Here is an overview for my future self.

Relevant background§

Package and build tooling§

Over time, the OCaml community has converged on:

  • opam as a package manager and a project environment handler
  • dune as a build system
  • ocamlformat as a code formatter

Other tools in these spaces exist, but the above two are officially recommended, and relatively more popular.

Editor service - Merlin§

Merlin is a "headless" tool which provides modern IDE features for OCaml/ReasonML code.

Supporting OCaml in an editor/IDE is essentially a matter of talking to Merlin.

Editor service - LSP§

With the advent of VS Code and Language Server Protocol, ocaml-lsp was built as an LSP wrapper on top of Merlin.

How everything comes together§

A glue package integrates the editor service (merlin or ocaml-lsp) with an editor.

The glue package assumes that the editor service is available in the user environment. The recommended approach is to have opam create a project-specific environment (called a opam local switch), and install merlin or ocaml-lsp in that env.

merlin v/s ocaml-lsp§

Feature parity

In terms of features, both ocaml-lsp and merlin are pretty much on par with each other.

ocaml-lsp lacks one feature though: determining the type of a selected block of code. Its a pretty nifty feature thanks to OCaml's strong typing system and merlin.

Quoting Marek Kubica from OCaml forums:

The only thing I’ve used with Merlin directly that I don’t have at the moment is determining the type of a selection.

Dependency on dune

ocaml-lsp depends on dune to generate type information. Merlin does not need it. Legacy projects predating dune have found this to be a hurdle in transitioning to ocaml-lsp.

Speed

ocaml-lsp, despite being an abstraction on top of merlin, feel faster than the direct merlin because ocaml-lsp operates asynchronously.

Quoting Rudi Grinberg from OCaml forums:

In my experience the editor frontends that come with merlin perform poorly compared to their LSP counterparts. In Emacs for example, I recall that switching to eglot to ocamllsp gave me a very noticeable boost in completion performance and error checking performance.

In a (significantly) large codebase, the editing experience in vim/emacs+merlin has reported to become less responsive and more laggy.

Editing setups§

I will list a few editing setups I have seen in use.

These are official in the sense that setup instructions are available on the official website.

  • merlin + emacs
  • merlin + vim
  • ocaml-lsp + vscode

Thanks to ocaml-lsp, alternatives have emerged:

  • ocaml-lsp + neovim
  • ocaml-lsp + emacs

Worthy outliers§

  • IntelliJ with ReasonML plugin by Herve Giraud. The only one that does not use merlin underneath, rather takes advantage of IntelliJ's IDE infrastructure.
  • ocaml-lsp + helix
  • Anything else that can work with LSP protocol, like KDE's Kate editor
  • Thanks to a strong static typing with a very fast compiler and a built tool with watch mode, some ditch tooling altogether and go with plain vim or emacs or even nano

My excursions§

I care a lot about vim-like modal editing.

The other thing I care about: less hassle to set up.

Now, let's see how some of these have fared for me.

merlin + emacs§

The glue packages in this case are tuareg and user-setup.

Unfortunately, official instructions didn't work for me. Instructions from Real World OCaml did:

$ opam install user-setup tuareg ocamlformat merlin
$ opam user-setup install

There is another alternative to tuareg - caml-mode, but I haven't tried it.

Tuareg also provides a Send to REPL for evaluation action, which provides a quick way to test and design code on-the-fly.

This setup works (type inference, error detection, etc.), but is very barebones. I realised I had to do a lot more on Emacs side - install and configure packages like flycheck, eldoc, merlin-eldoc, and what not - to modernise the editing experience. I stopped here.

merlin + vim§

The glue package is user-setup.

However, this setup did not work due to the following error:

"src/soup.ml" 1326L, 39757B
Error detected while processing BufRead Autocommands for "*.ml"..FileType Autocommands for "*"..function <SNR>4_LoadFTPlugin[18]..script /Users/jb/projects/lambdasoup/_opam
/share/merlin/vim/ftplugin/ocaml.vim[2]../Users/jb/projects/lambdasoup/_opam/share/merlin/vim/autoload/merlin.vim:
line    9:
Error: Required vim compiled with +python or +python3
Error detected while processing BufRead Autocommands for "*.ml"..FileType Autocommands for "*"..function <SNR>4_LoadFTPlugin[18]..script /Users/jb/projects/lambdasoup/_opam
/share/merlin/vim/ftplugin/ocaml.vim:
line    2:
E117: Unknown function: merlin#Register
Press ENTER or type command to continue

I get the issue of course: my vim is not compiled with python. But it also means that now I have to look for a vim that is compiled with python. And I tried out two vims: macOS's default vim, and the vim in nixpkgs.

So I moved on.

ocaml-lsp + vscode§

The glue package is VSCode OCaml Platform, which, besides integrating ocaml-lsp, also brings together ocamlformat, opam environment sandbox selection tool, etc., under a single VS Code extension.

Despite that fact that there is one more abstraction to deal with - ocaml-lsp built on top of merlin - this setup has been one of the most seamless.

It is also the top official recommendation now, thanks to a lot of effort put in by OCaml tooling developers.

The extension also provides a "Send to REPL for evaluation" action, much popular with the Emacs crowd.

The experience hasn't been 100% flawless though. Once, when I opened VS Code with an OCaml project, the editor didn't show any type hints at all. No errors were thrown either. After a long debugging session, it turned out that the OCaml LSP did throw an error saying that VS Code needs to be updated, but it was buried in Output Pane's OCaml LSP tab. I wish that the Output window had popped up on its own to highlight the error, instead of having to look for the problem myself. I have seen this happen in some other language project.

This is VS Code specific, but I also miss a good modal editing experience. I also keep getting bugged by a weird problem caused due to my MacBook's virtual Fn bar and while executing a specific keychord sequence, in which a slight touch from one of my raised fingers triggers the find window in VS Code. Very annoying!

ocaml-lsp + neovim§

The glue package is nvim-lspconfig.

This setup has often broken down, though the fault lied with the constant churn in neovim and neovim's LSP ecosystem more than with OCaml tooling.

There are a lot of other helper neovim packages to reckon with, like, the configurations of which I copy-paste and then pray for them to keep working.

When everything works, this is the most elegant setup for me. However, once in a while, when I load an OCaml project in neovim, some setup-related error would pop up.

Debugging a broken neovim configuration is a nightmare I wouldn't wish upon my enemy.

ocaml-lsp + helix§

There is no glue package. This has been the only zero-config setup for me.

Only Helix has worked immediately and flawlessly for me.

Conclusion§

At this point, I would consider the merlin-based setups appropriate only for veterans and advanced users. They are worth it only if you care about having every fringe feature available under merlin and don't mind going through some pain of setting things up.

merlin-based setup might also be the only sane option if a project does not use dune as its build tool. But don't quote me on this.

A beginner should absolutely choose VS Code OCaml Platform. When there is a whole new ecosystem to reckon with, you don't want to be scared away by the editing experience.

Only Helix provides a similar seamless experience. So VS Code dodgers can choose helix (at the cost of the baggage of a non-vim modal editing).

To vim lovers, I recommend the ocaml-lsp + neovim route.

To emacs lovers, I recommend the ocaml-lsp + emacs route.