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

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.

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').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.

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

66 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

7 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

For me ensure_installed = "maintained" for treesitter didn’t work. I updated it to ensure_installed = "elixir" and it started working properly.

3 Likes

I can’t seem to get ~H sigil highlighting to work with nvim + tree sitter. I have this installed: GitHub - nvim-treesitter/nvim-treesitter: Nvim Treesitter configurations and abstraction layer

but they still appear as plain strings. Here’s my tree sitter config:

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

anyone know how to debug this?

1 Like

try install tree-sitter parser for heex as well

require'nvim-treesitter.configs'.setup {
  ensure_installed = {"elixir", "heex"},
}
1 Like

yes, thank you!!

I tried formatting elixir in nvim, but heex won’t format.
plugin: GitHub - mhartington/formatter.nvim
I changed .formatter.exs and mix format f in the terminal.

How do you format your heex code? or do you run mix format from terminal?

I just use the ElixirLS formatter via vim.lsp.buf.format.

1 Like

Ty, that one works. Now I’ll have to fix bash not formatting.

Should completion work for .heex?
I got it working for .ex, .html, .css
But I can’t complete html-items inside .heex

1 Like

If you mean that you want to get html tags completion in heex files, you need extra configuration for the html language server. By default, the html language server is only enabled for html files, see nvim-lspconfig/server_configurations.md at master · neovim/nvim-lspconfig · GitHub , the default value for filetypes option only includes html. You can pass customized options when doing require'lspconfig'.html.setup{}. The following configuration should work:

require'lspconfig'.html.setup{
  filetypes = { "html", "heex" }
}
5 Likes

Thanks a lot, this post did help me a LOT to have a functional neovim setting for elixir and phoenix.

If I may add one detail : it would be nice to have actual html syntax hightlight for eex/heex files.

The syntax highlighting should work if you’ve installed the tree-sitter parsers for eex and heex.

require'nvim-treesitter.configs'.setup {
  ensure_installed = {"elixir", "heex", "eex"}
}

I do have elixir highlighting, but not html tag highlighting. Like mentioned in this github issue in the “Expected” section.

Are you using tree-sitter or vim-elixir plugin for syntax highlighting?
If you’re using tree-sitter, then install the three parsers as mentioned above and everything should work. If you’re using vim-elixir plugin for that, I’m currently not using this plugin so I can’t provide any more information about your issue.

I am using treesitter, and I did install the parsers, as you can see in my config here . I only used the issue on vim-elixir as an example of what I’m looking for.

Try opening a .heex file, then run :LspInfo and :TSInstallInfo to check what filetype it is and if all these parsers are actually installed.

Looks like they are

dot               [✗] not installed
eex               [✓] installed
elixir            [✓] installed
elm               [✗] not installed
...
heex              [✓] installed
help              [✗] not installed
hjson             [✗] not installed
hlsl              [✗] not installed
hocon             [✗] not installed
html              [✓] installed 

This is what LspInfo says

 Language client log: /home/xxx/.local/state/nvim/lsp.log
 Detected filetype:   heex

 2 client(s) attached to this buffer:

 Client: elixirls (id: 1, pid: nil, bufnr: [1])
 	filetypes:       elixir, eelixir, heex, surface
 	autostart:       true
 	root directory:  /home/xxx/projects/xxx
 	cmd:             /home/xxx/.local/bin/elixir-ls/language_server.sh

 Client: html (id: 2, pid: nil, bufnr: [1])
 	filetypes:       html, heex, elixir
 	autostart:       true
 	root directory:  /home/xxx/projects/xxx
 	cmd:             vscode-html-language-server --stdio