Neovim: Tips and tricks for OCaml's LSP server

When developing OCaml projects in Neovim, the OCaml language server (ocamllsp) can be of great help. It should just work out of the box, however there are few tweaks you can do in order to improve your developer experience even more.

First things first: Setting everything up

I'm assuming you have a working OCaml and opam environment. If not, follow the steps on the official "Getting Started" page first.

After that, let's make sure all the necessary packages are installed:

$ opam install ocaml-lsp-server ocamlformat dune

This will install the following packages globally

In order to simplify the server configuration, we're using nvim-lspconfig. Make sure to follow your package manager's instructions on how to add lspconfig to your Neovim installation. Once this is done, add a basic configuration for ocamllsp somewhere in your Lua config files:

local lsp_config = require('lspconfig')
lsp_config['ocamllsp'].setup({})

Once you've reloaded your configuration, you should have a working LSP integration while editing OCaml source files.

Bonus: LSP keybindings

Last but not least, if you have not configured your LSP keybindings so far, feel free to use those as your inspiration:

vim.api.nvim_create_autocmd('LspAttach', {
    group = vim.api.nvim_create_augroup('UserLspConfig', {}),
    callback = function(event)
        local opts = { buffer = event.buf }
        vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
        vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
        vim.keymap.set('n', 'gt', vim.lsp.buf.type_definition, opts)
        vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
        vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
        vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts)
        vim.keymap.set('n', '<leader>wa', vim.lsp.buf.add_workspace_folder, opts)
        vim.keymap.set('n', '<leader>wr', vim.lsp.buf.remove_workspace_folder, opts)
        vim.keymap.set('n', '<leader>wl', function()
            print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
        end, opts)
        vim.keymap.set('n', '<leader>H', vim.lsp.buf.typehierarchy, opts)
        vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
        vim.keymap.set({ 'n', 'v' }, '<leader>ca', vim.lsp.buf.code_action, opts)
        vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
        vim.keymap.set('n', '<leader>fo', function()
            vim.lsp.buf.format { async = true }
        end, opts)
    end,
})

Dune RPC: keeping LSP information up to date

Once you started using the LSP server, you may notice that from time to time diagnostic information are not up to date. This often happens when new files or modules are added the project. One way to solve those issues is to build the project with dune build followed by :LspRestart in Neovim. This will ensure that the latest build information are available to ocamllsp.

However, there is a much better way by utilizing the RPC mechanism of dune. Newer versions of dune will start an (experimental) RPC server when a "watch" build is started. ocamllsp can utilize this RPC mechanism to get notifications about new build information and update the editor diagnostics accordingly.

There is configuration for ocamllsp needed, since it should recognize the running dune process and automatically establish the RPC connection. The only thing to do is to start a continuous build of your project via the -w flag before the LSP server is started (e.g. dune build -w or dune exec -w).

Running ocamllsp in a separate build directory

While the continuous dune build gives up-to-date diagnostics of the project, there is one annoying downside - it will lock your build directory for other dune processes. I.e. you won't be able to execute dune exec or dune runtest while the build process is running. You could obviously stop the build, run your other commands and then restart the build, but this gets annoying fast.

Another way is to use a different build directory for ocamllsp. dune supports this via the --build-dir=<path> option. E.g. to start the continuous build in the _build_lsp folder, run

$ dune build -w --build-dir=_build_lsp

This will ensure that your main _build folder is not locked and can be used for one-off dune commands. The beauty of it is that ocamllsp will automatically detect the new build directory with the running dune instance and uses it for live diagnostics.

Help! My files are not formatting

If the LSP format command is not doing anything, the most likely reason is a missing .ocamlformat file. Without it, the ocamlformat tool will not change any files. To solve the issue, simply add a .ocamlformat file at the root of your project (even an empty one should do). For more details on the formatting options available via this file, run man ocamlformat or check the online documentation.