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
ocaml-lsp-server
: the language server itself (the executable is calledocamllsp
)ocamlformat
: the file formatting tool which ocamllsp usesdune
: the newest OCaml build system.
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 Dune in a separate build directory
Correction: A previous version of this article stated that ocamllsp will work out of the box if the continuous dune build is running in a different build folder. This is not correct, as it needs additional configuration as stated below.
See also this GitHub issue for more information.
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 different build directories - one for your continuous dune
builds which provide realtime
diagnostics to ocamllsp - and the default directory to execute tests and programs. Dune supports this via the
--build-dir=<path>
option. E.g. to execute the dune instance for ocamllsp in the _build_lsp
folder, run
$ dune build -w --build-dir=_build_lsp
In order for ocamllsp to recognize non-standard build directories, the DUNE_BUILD_DIR
environment variable needs to be
set accordingly. In lspconfig, this can be achieved by adding the cmd_env
config parameter to the ocamllsp entry
lsp_config['ocamllsp'].setup({
cmd_env = {DUNE_BUILD_DIR = '_build_lsp'},
--- ...
})
This will ensure that one Dune instance is running builds in _build_lsp
and provides up to date diagnostics to
ocamllsp
via RPC, while you can still execute tests and run your OCaml programs simultaneously in the default build
directory.
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.