4/12/22 Update: I've started working on a dedicated Neovim plugin for Elixir called elixir.nvim. Please follow me on Twitter for more frequent updates!
This article is the spiritual successor to How to use Elixir LS with Vim.
Since then, I've switched from Vim to the nightly release of Neovim as well as how I integrate linters, formatters, and LSPs.
This article will cover:
- Installing Neovim
- Getting started with the builtin LSP client
- Setting up Elixir LS
- Integrating Credo
If you run into any problems with this guide, feel free to shoot me an email.
Let's get started!
Installing Neovim
You can install Neovim with your package manager of choice, or asdf.
Homebrew
$ brew install neovim
asdf
$ asdf plugin add neovim
$ asdf install neovim stable
Nightly
With the release of 0.5, you no longer need to use Neovim nightly for this setup. I will leave these instructions in case you still want to live on the edge.
As of this writing, the builtin LSP client is only available on the nightly build of Neovim. Once 0.5 is released, you should be able to switch to a stable build, but for now, let's get nightly installed.
My preferred method for managing my installation of Neovim is to use asdf. You can install it with Homebrew as well, but I find asdf to be better.
For this article, I am going to assume you already have asdf installed, as it is the most prevalent way to manage Elixir and Erlang installations.
But we still need to install the Neovim asdf plugin.
$ asdf plugin add neovim
Listing the available versions demonstrates that we can install any previously released version of Neovim, as well as the nightly build. These versions are pre-built and downloaded as a GitHub release artifact. This makes installing them very fast.
$ asdf list all neovim
0.1.0
0.1.1
0.1.2
0.1.3
0.1.4
0.1.5
0.1.6
0.1.7
0.2.0
0.2.1
0.2.2
0.3.0
0.3.1
0.3.2
0.3.3
0.3.4
0.3.5
0.3.6
0.3.7
0.3.8
0.4.0
0.4.1
0.4.2
0.4.3
0.4.4
nightly
stable
If for some reason the nightly build cron job is on the fritz (as it sometimes is), you can also build form source with:
$ asdf install neovim ref:master
What we are going to stick with is:
$ asdf install neovim nightly
$ asdf global neovim nightly
Now, if you want to update your nightly installation, all you have to do is uninstall and reinstall. I use the following as a convenient shell alias.
$ alias update-nvim-nightly='asdf uninstall neovim nightly && asdf install neovim nightly'
So, just to tie this all together, these are the steps you will go through to get Neovim nightly installed.
$ asdf plugin add neovim
$ asdf install neovim nightly
$ asdf global neovim nightly
$ echo "alias update-nvim-nightly='asdf uninstall neovim nightly && asdf install neovim nightly'" >> .zshrc # bash/fish/etc
Getting started with the builtin LSP client
To help users get started with the LSP client, the Neovim team provides a plugin called nvim-lspconfig that contains configurations for many common language servers.
There are also a few other plugins revolving around autocomplete that you'll need to install to get the full LSP experience.
With your preferred plugin manager, install the following plugins. I'm using packer.nvim. If you don't use a package manager, I suggest learning more about them by checking out packer.nvim as well as vim-plug.
vim.cmd [[packadd packer.nvim]]
local startup = require("packer").startup
startup(function(use)
-- language server configurations
use "neovim/nvim-lspconfig"
-- autocomplete and snippets
use("hrsh7th/nvim-cmp")
use("hrsh7th/cmp-nvim-lsp")
use("hrsh7th/cmp-vsnip")
use("hrsh7th/vim-vsnip")
use("onsails/lspkind-nvim")
end)
Now that we have the required plugins installed, let's set them up so they get booted when we start Neovim. I am using the init.lua
config file, but you can also add this to your init.vim
if you use a lua heredoc.
local lspconfig = require("lspconfig")
-- Neovim doesn't support snippets out of the box, so we need to mutate the
-- capabilities we send to the language server to let them know we want snippets.
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true
-- Setup our autocompletion. These configuration options are the default ones
-- copied out of the documentation.
local cmp = require("cmp")
cmp.setup({
snippet = {
expand = function(args)
-- For `vsnip` user.
vim.fn["vsnip#anonymous"](args.body)
end,
},
mapping = {
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.close(),
["<C-y>"] = cmp.mapping.confirm({ select = true }),
},
sources = {
{ name = "nvim_lsp" },
{ name = "vsnip" },
},
formatting = {
format = require("lspkind").cmp_format({
with_text = true,
menu = {
nvim_lsp = "[LSP]",
},
}),
},
})
That should be it for the basic LSP client configuration.
Setting up Elixir LS
Elixir LS is a tool that needs to be compiled from source, but it's pretty simple. Pick a directory to install and run the following commands.
$ git clone git@github.com:elixir-lsp/elixir-ls.git
$ cd elixir-ls && mkdir rel
# checkout the latest release if you'd like
$ git checkout tags/v0.7.0
$ mix deps.get && mix compile
$ mix elixir_ls.release -o release
Now that we have Elixir LS installed and compiled, let's get it set up in Neovim.
-- A callback that will get called when a buffer connects to the language server.
-- Here we create any key maps that we want to have on that buffer.
local on_attach = function(_, bufnr)
local function map(...)
vim.api.nvim_buf_set_keymap(bufnr, ...)
end
local map_opts = {noremap = true, silent = true}
map("n", "df", "<cmd>lua vim.lsp.buf.formatting()<cr>", map_opts)
map("n", "gd", "<cmd>lua vim.lsp.diagnostic.show_line_diagnostics()<cr>", map_opts)
map("n", "dt", "<cmd>lua vim.lsp.buf.definition()<cr>", map_opts)
map("n", "K", "<cmd>lua vim.lsp.buf.hover()<cr>", map_opts)
map("n", "gD", "<cmd>lua vim.lsp.buf.implementation()<cr>", map_opts)
map("n", "<c-k>", "<cmd>lua vim.lsp.buf.signature_help()<cr>", map_opts)
map("n", "1gD", "<cmd>lua vim.lsp.buf.type_definition()<cr>", map_opts)
-- These have a different style than above because I was fiddling
-- around and never converted them. Instead of converting them
-- now, I'm leaving them as they are for this article because this is
-- what I actually use, and hey, it works ¯\_(ツ)_/¯.
vim.cmd [[imap <expr> <C-l> vsnip#available(1) ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>']]
vim.cmd [[smap <expr> <C-l> vsnip#available(1) ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>']]
vim.cmd [[imap <expr> <Tab> vsnip#jumpable(1) ? '<Plug>(vsnip-jump-next)' : '<Tab>']]
vim.cmd [[smap <expr> <Tab> vsnip#jumpable(1) ? '<Plug>(vsnip-jump-next)' : '<Tab>']]
vim.cmd [[imap <expr> <S-Tab> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<S-Tab>']]
vim.cmd [[smap <expr> <S-Tab> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<S-Tab>']]
vim.cmd [[inoremap <silent><expr> <C-Space> compe#complete()]]
vim.cmd [[inoremap <silent><expr> <CR> compe#confirm('<CR>')]]
vim.cmd [[inoremap <silent><expr> <C-e> compe#close('<C-e>')]]
vim.cmd [[inoremap <silent><expr> <C-f> compe#scroll({ 'delta': +4 })]]
vim.cmd [[inoremap <silent><expr> <C-d> compe#scroll({ 'delta': -4 })]]
-- tell nvim-cmp about our desired capabilities
require("cmp_nvim_lsp").update_capabilities(capabilities)
end
-- Finally, let's initialize the Elixir language server
-- Replace the following with the path to your installation
local path_to_elixirls = vim.fn.expand("~/.cache/nvim/lspconfig/elixirls/elixir-ls/release/language_server.sh")
lspconfig.elixirls.setup({
cmd = {path_to_elixirls},
capabilities = capabilities,
on_attach = on_attach,
settings = {
elixirLS = {
-- I choose to disable dialyzer for personal reasons, but
-- I would suggest you also disable it unless you are well
-- acquainted with dialzyer and know how to use it.
dialyzerEnabled = false,
-- I also choose to turn off the auto dep fetching feature.
-- It often get's into a weird state that requires deleting
-- the .elixir_ls directory and restarting your editor.
fetchDeps = false
}
}
})
Elixir LS should be all set up now! Let's test it out by seeing if autocompletion, documentation on hover, and go to definition is working.
Integrating Credo
I decided to completely remove ALE, so I was wondering how I might get linters and formatters like credo and prettier hooked back in.
Luckly, there are a few projects that implement a language server for the purpose of running these tools for you. I am currently using efm-langserver.
I install efm with brew.
$ brew install efm-langserver
Once that is installed, let's hook it up to Neovim.
lspconfig.efm.setup({
capabilities = capabilities,
on_attach = on_attach,
filetypes = {"elixir"}
})
And last, we need to teach efm how to speak Credo. Create a new file at ~/.config/efm-langserver/config.yaml
. Please note that we need to run Credo with MIX_ENV=test
or else it's going to mess with Phoenix code reloading.
version: 2
tools:
mix_credo: &mix_credo
lint-command: "MIX_ENV=test mix credo suggest --format=flycheck --read-from-stdin ${INPUT}"
lint-stdin: true
lint-formats:
- '%f:%l:%c: %t: %m'
- '%f:%l: %t: %m'
lint-category-map:
R: N
D: I
F: E
W: W
root-markers:
- mix.lock
- mix.exs
languages:
elixir:
- <<: *mix_credo
Now you should be seeing Credo checks showing up inside Neovim.
My Setup
If you would like to check out my actual dotfiles, feel free to check them out on GitHub.
I've extracted quite a few helper modules and functions that make organizing my plugins a little easier.
Enjoy!