Neovim - Elixir Setup Configuration from Scratch Guide

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

You’ll need to install the latest version (currently v0.6.1) of neovim, 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 a elixir project or open a 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 it 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.

image

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').update_capabilities(vim.lsp.protocol.make_client_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 enter the selected item.

image

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 all maintained language parser. If you want to disable treesitter for some languages, refer to nvim-treesitter for more config options.

require'nvim-treesitter.configs'.setup {
  ensure_installed = "maintained",
  sync_install = false,
  ignore_install = { },
  highlight = {
    enable = true,
    disable = { },
  },
}

However, the treesitter syntax highlighting for elixir is a little slow on initial rendering, and threre have been issues reported for it. If you find it unbearable, you can choose to use vim-elixir plugin for 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.

41 Likes

This is fantastic, thanks for posting @TunkShif :023:

I followed your instructions and can confirm it works, the only thing I did differently was I added the local capabilities =... and capabilities = capabilities lines from here:

To the ElixirLS part above:

-- 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
}

I have also installed LunarVim :003:

My plan is to see how I get on with them as well as Doom Emacs …and I’ll be interested in hearing how others do too!

5 Likes

The treesitter Elixir issues have mostly been fixed in NeoVim 0.7

6 Likes

@threeaccents - I’m seeing weird behavior with indentations in Elixir’s tree-sitter parser in NeoVim 0.7. Which issues were you talking about?

1 Like