Using Neovim as an Erlang IDE

As a less mainstream language, Erlang doesn’t generally have a great out-of-the-box developer experience. As someone new to Erlang (and the BEAM in general), who also uses Neovim as their daily driver, I wanted to see whether I could setup a functional Neovim configuration that would give me IDE-like features for Erlang. I specifically wanted the following:


  • Code formatting on save

  • Hover documentation

  • Goto definition

  • Code actions

  • Quickly jumping to workspace symbols


Luckily, there is an erlang language server which has pretty much everything you need out of the box. Once you install it (and erlang_ls is in your $PATH), configuring it for Neovim is actually pretty simple. I adapted my OCaml configuration a bit, and it resulted in something that worked. The plugin spec is as follows.

erlang.lua
1
return {
2
setup = function()
3
local lsp = require "lspconfig"
4
5
local c = vim.lsp.protocol.make_client_capabilities()
6
c.textDocument.completion.completionItem.snippetSupport = true
7
c.textDocument.completion.completionItem.resolveSupport = {
8
properties = {
9
"documentation",
10
"detail",
11
"additionalTextEdits",
12
},
13
}
14
15
local on_attach = function(client, bufnr)
16
-- enable completion triggered by <C-x><C-o>
17
-- vim.api.nvim_buf_set_option(bufnr, "omnifunc", "v:lua.vim.lsp.omnifunc")
18
19
if client.server_capabilities.documentFormattingProvider then
20
vim.api.nvim_create_autocmd("BufWritePre", {
21
group = vim.api.nvim_create_augroup("Format", { clear = true }),
22
buffer = bufnr,
23
callback = function() vim.lsp.buf.formatting_seq_sync() end,
24
})
25
end
26
--
27
-- code lens
28
if client.resolved_capabilities and client.resolved_capabilities.code_lens then
29
local codelens = vim.api.nvim_create_augroup("LSPCodeLens", { clear = true })
30
vim.api.nvim_create_autocmd({ "BufEnter", "InsertLeave", "CursorHold" }, {
31
group = codelens,
32
callback = function() vim.lsp.codelens.refresh() end,
33
buffer = bufnr,
34
})
35
end
36
37
local original_progress_handler = vim.lsp.handlers["$/progress"]
38
local initialized = false
39
40
-- Silence most of the annoying messages from the LSP outside of initialization messages
41
vim.lsp.handlers["$/progress"] = function(err, result, ctx, config)
42
if result.value and result.value.title and result.value.title:match "Indexing OTP" then
43
return
44
end
45
46
if not initialized then
47
original_progress_handler(err, result, ctx, config)
48
if result.value and result.value.kind == "end" or result.value == "Indexing OTP" then initialized = true end
49
else
50
return
51
end
52
end
53
end
54
55
local capabilities = require("cmp_nvim_lsp").default_capabilities(c)
56
57
return {
58
cmd = { "erlang_ls" },
59
filetypes = { "erlang" },
60
root_dir = lsp.util.root_pattern "*.erl",
61
on_attach = on_attach,
62
capabilities = capabilities,
63
}
64
end,
65
}

You can import it like a normal lua module and call setup where necessary. Personally, I use AstroNvim, so I added it to the config table as follows:

astrolsp.lua
1
return {
2
opts = {
3
-- ...
4
config = {
5
erlang_ls = erlang.setup(),
6
}
7
-- ...
8
}
9
}

Anyway, I know this was short, but I thought this was cool and might help someone else in the future. Also, it’s been two weeks since I’ve written a post and I don’t want the two people who read this blog to think I’ve abandoned it (I haven’t!). See you next time (:


erlang
neovim