Here is a guide to setup your neovim for elixir development from scratch, it will include basic stuffs like language server configuration, auto-completion support and syntax highlighting by tree-sitter. All the configuration will be written in lua, and I’ll try to document as much to explain all those configurations.
If you just want an out-of-box experience, I would suggest you use some preconfigured distribution. For me LunarVim is pretty good, is has all the essential stuffs configured, you just need to install the language server you need and then it just works.
Installing Neovim
The minimum required version of neovim is 0.6
. Check the official guide on how to install neovim on your system.
Installing a plugin manager
We’ll use the most popular one, packer.nvim. Just copy and paste the following code in your init.lua
to bootstrap the plugin manager. The init.lua
should be placed under ~/.config/nvim
on Linux, or ~\AppData\Local\nvim
on Windows. If you’re still not sure, you can open your neovim and execute :echo stdpath('config')
to find out where the config folder is.
local execute = vim.api.nvim_command
local fn = vim.fn
local fmt = string.format
local pack_path = fn.stdpath("data") .. "/site/pack"
-- ensure a given plugin from github.com/<user>/<repo> is cloned in the pack/packer/start directory
local function ensure (user, repo)
local install_path = fmt("%s/packer/start/%s", pack_path, repo)
if fn.empty(fn.glob(install_path)) > 0 then
execute(fmt("!git clone https://github.com/%s/%s %s", user, repo, install_path))
execute(fmt("packadd %s", repo))
end
end
-- ensure the plugin manager is installed
ensure("wbthomason", "packer.nvim")
After saving, then launch your neovim, you should see that the repo has been cloned. Then just add the plugins we need.
Installing required plugins
Add the following code in your init.lua
to install all the plugins we need.
- nvim-lspconfig: configurations for Neovim’s built-in language server client
- nvim-cmp: auto-completion framework, and you also need to install completion source for it
- nvim-vsnip: snippet engine for vscode format snippet support
require('packer').startup(function(use)
-- install all the plugins you need here
-- the plugin manager can manage itself
use {'wbthomason/packer.nvim'}
-- lsp config for elixir-ls support
use {'neovim/nvim-lspconfig'}
-- cmp framework for auto-completion support
use {'hrsh7th/nvim-cmp'}
-- install different completion source
use {'hrsh7th/cmp-nvim-lsp'}
use {'hrsh7th/cmp-buffer'}
use {'hrsh7th/cmp-path'}
use {'hrsh7th/cmp-cmdline'}
-- you need a snippet engine for snippet support
-- here I'm using vsnip which can load snippets in vscode format
use {'hrsh7th/vim-vsnip'}
use {'hrsh7th/cmp-vsnip'}
-- treesitter for syntax highlighting and more
use {'nvim-treesitter/nvim-treesitter'}
end)
After that, launch your neovim again, execute :PackerCompile
to compile the plugin list, then :PackerInstall
to install all the missing plugins.
Configure language server
First you’ll need to install the elixir-ls, for installation guide, check out this instructions.
And add the following config to your init.lua
.
-- `on_attach` callback will be called after a language server
-- instance has been attached to an open buffer with matching filetype
-- here we're setting key mappings for hover documentation, goto definitions, goto references, etc
-- you may set those key mappings based on your own preference
local on_attach = function(client, bufnr)
local opts = { noremap=true, silent=true }
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<leader>cr', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<leader>ca', '<cmd>lua vim.lsp.buf.code_action()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<leader>cf', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<leader>cd', '<cmd>lua vim.diagnostic.open_float()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', ']d', '<cmd>lua vim.diagnostic.goto_next()<CR>', opts)
end
-- setting up the elixir language server
-- you have to manually specify the entrypoint cmd for elixir-ls
require('lspconfig').elixirls.setup {
cmd = { "/path/to/elixir-ls/language_server.sh" },
on_attach = on_attach
}
The elixir language server will only be started when you enter an elixir project or open an elixir file, after the language server has been started, an on_attach
callback will be called, so here we’re setting those lsp related key mappings inside the on_attach
callback, so those key bindings will only be available when a language server is running.
After that, try to open an elixir project or elixir file, then run :LspInfo
, you should see the elixirls is running and attached to the current buffer.
For now, features like hover documentation, goto definition and goto references should be working, use the key bindings set inside on_attach
to try it yourself.
Configure auto completing
First we need some addition to our lspconfig to make it support completion.
local capabilities = require('cmp_nvim_lsp').default_capabilities()
require('lspconfig').elixirls.setup {
cmd = { "elixir-ls" },
on_attach = on_attach,
capabilities = capabilities
}
Then add configuration for nvim-cmp.
local cmp = require'cmp'
cmp.setup({
snippet = {
expand = function(args)
-- setting up snippet engine
-- this is for vsnip, if you're using other
-- snippet engine, please refer to the `nvim-cmp` guide
vim.fn["vsnip#anonymous"](args.body)
end,
},
mapping = {
['<CR>'] = cmp.mapping.confirm({ select = true }),
},
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'vsnip' }, -- For vsnip users.
{ name = 'buffer' }
})
})
After that, the auto completion should work now, use <C-n>
and <C-p>
to select completion items, then use <CR>
to confirm the selected item.
You may want to use <Tab>
to cycle through the list, we need some additional settings for that. Add some helper functions, and corresponding key mappings.
-- helper functions
local has_words_before = function()
local line, col = unpack(vim.api.nvim_win_get_cursor(0))
return col ~= 0 and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match("%s") == nil
end
local feedkey = function(key, mode)
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(key, true, true, true), mode, true)
end
cmp.setup({
-- other settings ...
mapping = {
-- other mappings ...
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif vim.fn["vsnip#available"](1) == 1 then
feedkey("<Plug>(vsnip-expand-or-jump)", "")
elseif has_words_before() then
cmp.complete()
else
fallback()
end
end, { "i", "s" }),
["<S-Tab>"] = cmp.mapping(function()
if cmp.visible() then
cmp.select_prev_item()
elseif vim.fn["vsnip#jumpable"](-1) == 1 then
feedkey("<Plug>(vsnip-jump-prev)", "")
end
end, { "i", "s" })
}
})
Please notice that all those settings are for vsnip, if you’re using other snippet engines, please refer to nvim-cmp wiki for help.
Syntax Highlighting
We’re using tree sitter for syntax highlighting, add the following settings and then launch neovim, run :TSUpdate
, it will install and update the needed language parsers. If you want to enable treesitter for other languages, refer to nvim-treesitter for more config options.
require'nvim-treesitter.configs'.setup {
ensure_installed = {"elixir", "heex", "eex"}, -- only install parsers for elixir and heex
-- ensure_installed = "all", -- install parsers for all supported languages
sync_install = false,
ignore_install = { },
highlight = {
enable = true,
disable = { },
},
}
Also, you can choose to use vim-elixir plugin as an alternative for providing syntax highlighting and auto indentation.
The final config file
Now you should have a minimal but fully functional neovim config setup for elixir development, and the full init.lua
file can be found here.
Feel free to add more features to your own customized neovim. And you can find lots of useful plugins in awesome-neovim.