Neovim Setup for OCaml

Info

Updates:

  • 2024-09-14. If an LSP contains a Formatter, then we don’t need to install the corresponding Formatter on our own.

In previous post, I elaborated on how to set up Neovim from scratch. However, I didn’t answer the following questions:

  1. What if an LSP does not support formatting, how to configure Neovim to support a third-party code formatting tool?
  2. How to change configuration files when adding a new programming language?

Recently I was learning OCaml, I thought it would be a good chance to answer the questions.

Info
I assume you have already read my previous post, so I will omit some details here.
Info
If you are not using a Mac, you may refer to the official instructions for more details.

It is quite handy to install OCaml on a Mac with Homebrew.

sh

$ brew install opam

According to the output of Homebrew, we need to perform additional configurations and modify ~/.zshrc(for Zsh users) or ~/.bashrc(for Bash users).

sh

$ opam init
# press `y` to confirm

To make the modifications work, run the following command in your terminal

sh

$ source ~/.zshrc
# $ source ~/.bashrc # if you use bash

Finally, we can use this command to check if everything goes right

sh

$ opam switch

The output will be like

text

#   switch   compiler                   description
->  5.0.0    ocaml-base-compiler.5.0.0  5.0.0
    default  ocaml.4.14.0               default

Similar to IPython for Python, the OCaml offers a REPL tool called utop. We can install this to practice programming OCaml

sh

$ opam install utop dune

Just try the utop REPL yourself :)

sh

$ utop

The output would be like

text

─────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────
                                                          Welcome to utop version 2.13.1 (using OCaml version 5.0.0)! 
                                                         └─────────────────────────────────────────────────────────────┘

Type #utop_help for help about using utop.

( 10:33:26 )< command 0 >────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────{ counter: 0 }
utop #
┌──────────────┬──────────────┬───────────────┬─────┬───────────┬────┬───┬──────────┬─────┬───────────┬──────┬────────────┬───────┬───────────┬──────────────┬──────────┬────────┐
Afl_instrumentAlias_analysisAllocated_constAnnotApplicativeArchArgArg_helperArrayArrayLabelsAsmgenAsmlibrarianAsmlinkAsmpackagerAssert_failureAst_helperAst_inva
└──────────────┴──────────────┴───────────────┴─────┴───────────┴────┴───┴──────────┴─────┴───────────┴──────┴────────────┴───────┴───────────┴──────────────┴──────────┴────────┘
Info

If you follow my previous post, then the file hierarch of your setup will be like

text

nvim
├── init.lua
└── lua
    ├── colorscheme.lua
    ├── config
    │   └── nvim-cmp.lua
    ├── keymaps.lua
    ├── lsp.lua
    ├── options.lua
    └── plugins.lua

Previously, we leveraged mason.nvim to set up LSPs. To add a new LSP for a specific programming language, we can also use this. Steps to follow:

  1. Open Neovim and type :Mason, found ocaml-lsp in the (2) LSP section. Press i when the cursor stays on the line.
  2. Open the configurations file for LSP(lsp.lua in my configurations), add the following lua code

lua

... -- rest of the configurations
lspconfig.ocamllsp.setup({
	on_attach = on_attach,
})
Warning
You may have your configuration files and do not follow my previous post. So adding the aforementioned Lua code may not work for you. However, you can check the full configuration here to understand how it works and adapt to your configurations.

After modifying lsp.lua, we can restart Neovim to make the configurations take effect.

Warning
Similar to rust-analyzer, we can only use ocaml-lsp in a project but not a single file.

Now you can create a new dune project to see if LSP works.

The ocaml-lsp LSP does not support formatting OCaml code. If you try to format an OCaml code, you will get an error message:

text

[LSP] Format request failed, no matching language servers.

Now, I will talk about how to set up a formatter for OCaml.

You may have noticed that the Mason tool also offers Linter and Formatter. The formatter for OCaml is ocamlformat. So We can install it just like we install an LSP.

However, ocamlformat is a CLI tool which can not be integrated into Neovim. Luckily, we have none-ls plugin, which can expose linter, formatter, and other CLI tools as LSP for Neovim.

Info
none-ls.nvim is maintained by the community because its predecessor null-ls.nvim has been archived

To make it easier, we can use the mason-null-ls.nvim plugin, developed by the same author of mason.nvim. Open plugins.lua file and append these Lua codes. After saving the changes and restarting Neovim you should see lazy.nvim is installing these plugins.

lua

... -- rest of the configurations
require("lazy").setup({
	-- Add hooks to LSP to support Linter && Formatter
	{
		"jay-babu/mason-null-ls.nvim",
		event = { "BufReadPre", "BufNewFile" },
		dependencies = {
			"williamboman/mason.nvim",
			"nvimtools/none-ls.nvim",
		},
		config = function()
			-- Note:
			--     the default search path for `require` is ~/.config/nvim/lua
			--     use a `.` as a path seperator
			--     the suffix `.lua` is not needed
			require("config.mason-null-ls")
		end,
	},
    ... -- rest of the configurations
})

Then, create a new file called mason-null-ls.lua in config folder and put the following code

lua

local mason_ok, mason = pcall(require, "mason")
if not mason_ok then
	return
end

local null_ls_ok, null_ls = pcall(require, "null-ls")
if not null_ls_ok then
	return
end

local mason_null_ls_ok, mason_null_ls = pcall(require, "mason-null-ls")
if not mason_null_ls_ok then
	return
end

mason.setup()

mason_null_ls.setup({
	-- A list of sources to install if they're not already installed.
	-- This setting has no relation with the `automatic_installation` setting.
	ensure_installed = {},
	-- Run `require("null-ls").setup`.
	-- Will automatically install masons tools based on selected sources in `null-ls`.
	-- Can also be an exclusion list.
	-- Example: `automatic_installation = { exclude = { "rust_analyzer", "solargraph" } }`
	automatic_installation = false,
	-- Sources found installed in mason will automatically be set up for null-ls.
	automatic_setup = true,
	handlers = {
		-- Hint: see https://github.com/nvimtools/none-ls.nvim/blob/main/doc/BUILTIN_CONFIG.md
		--       to see what sources are available
		-- Hint: see https://github.com/jose-elias-alvarez/null-ls.nvim/blob/main/doc/BUILTIN_CONFIG.md
		--       to check what we can configure for each source
		ocamlformat = function(source_name, methods)
			null_ls.register(null_ls.builtins.formatting.ocamlformat.with({
				-- Add more arguments to a source's defaults
				-- Default: { "--enable-outside-detected-project", "--name", "$FILENAME", "-" }
				-- Type `ocamlformat --help` in your terminal to check more args
				extra_args = { "--if-then-else", "vertical" },
			}))
		end,
	},
})

null_ls.setup()
Tip

When you set automatic_setup = true, the mason-null-ls will help you configure the installed formatter automatically. If you want to configure a specific formatter, you should make some changes in handlers.

FAQs

  1. How to find the default configurations of a formatter? See here
  2. How to configure handlers? See here
  3. How to know other configurations of a formatter? See the official formatter page. For example, In the README file of the ocamlformat, it says we can type ocamlformat --help to see more options.

Finally, we also need to change the lsp.lua file. The original liens related to code formmating looks like this:

lua

... -- rest of the configurations
vim.keymap.set("n", "<space>f", function()
    vim.lsp.buf.format({ async = true })
end, bufopts)
... -- rest of the configurations

Now we are using none-ls to do formatting. So we need to set the filter argument as follows

lua

... -- rest of the configurations
vim.keymap.set("n", "<space>f", function()
    vim.lsp.buf.format({
        async = true,
		-- Predicate used to filter clients. Receives a client as
		-- argument and must return a boolean. Clients matching the
		-- predicate are included.
        filter = function(client)
            return client.name == "null-ls"
        end,
    })
end, bufopts)
... -- rest of the configurations

Save the changes and restart Neovim to see if it works.

Tip

What if an LSP contains a Formatter? For example, the Haskell-language-server does contain a Formatter called ormolu. In such circumstances, we do need to use null-ls. All we need to do is change the filter

lua

... -- rest of the configurations
vim.keymap.set("n", "<space>f", function()
    vim.lsp.buf.format({
        async = true,
		-- Predicate used to filter clients. Receives a client as
		-- argument and must return a boolean. Clients matching the
		-- predicate are included.
        filter = function(client)
            -- hls stands for Haskell-language-server
			return client.name == "null-ls" or client.name == "hls"
        end,
    })
end, bufopts)
... -- rest of the configurations

Now let’s summarize how to set up Neovim for a new programming language. If you also use mason.nvim, mason-lspconfig, none-ls.nvim and mason-null-ls.nvim, you can follow the instructions to setup:

  1. Read the official pages and install the programming language’s environment on your machine.
  2. Open Neovim, type :Mason, and install the corresponding LSP. Refer to the official LSP pages and check if the LSP contains a built-in Formatter. If the answer is no, just use :Mason to install one.
  3. Open the lsp.lua file and configure LSP
  4. If you install Formatter by yourself, open the mason-null-ls.lua file to configure handlers

For things that are not clearly stated in this post, you can view my dotfiles configurations repo to learn more information.