Neovim - Elixir Setup Configuration from Scratch Guide

I will give that a try this weekend. Thank you for pointing it out. @kevinschweikert @adamu

Assuming you have already figured out how to install plugins, the instructions for configuring elixirls can be found here. The only required config flag is cmd, which is the path where you installed elixirls. If you have elixirls in $PATH, you can just use "elixir-ls" as the command - the brew package installs it this way.

It’s hard to give specific help because the nature of such an open ecosystem is that there are like 30 different valid ways to do something - for example, which of the several popular plugin managers are you using? Are you using lua or vimscript? What is your completions plugin? And so on.

I agree with Dimi - using a distro might make life a lot easier for you. Have a look at what people are using here:

I started writing instructions by updating my blog post on setting up an Elixir (and Ruby) dev environment, but I haven’t got around to finishing it yet. If you want to have a look it’s here (if anyone can spot any mistakes please let me know!)

1 Like

Just to provide another perspective, I am personally not a fan of the number of deps that would be pulled in by a distro and prefer to manage my (very short) plugin list myself. Really this is the whole reason I use nvim - otherwise I would just be using Zed or whatever :slight_smile:

3 Likes

11 posts were split to a new topic: Pros/cons of using a code editor distro vs managing your own plugins (split thread)

I am looking for config help to avoid unnecessary dependencies from distros. My focus is solely on working with Elixir and Phoenix code.

Personalization and other settings can come later. Right now, I just want to set up ElixirLS with auto-completion and Dialyzer working. That’s my main priority.

For a minimal setup you need a plugin manager, something to configure the lsp, and a completions plugin for autocomplete. If you’re not sure what to go with, a simple setup would be Plug for plugins, nvim-lspconfig to run the LSP, and nvim-cmp for completions. You need cmp-nvim-lsp from the same guy as nvim-cmp to get completions from the LSP, and you probably also want cmp-buffer to get completions from the current file (“buffer” in vim terms).

If you follow the setup guides from each of these you should be able to piece together a basic init.vim. Again there are many different ways to do things. If you get stuck on something just ask on here.

vim-plug, nvim-lspconfig, nvim-cmp

In addition to vim stuff you also need to have a working elixir-ls installation. If you’re on a Mac the easiest way is brew, otherwise follow the elixir-ls installation instructions.

2 Likes

I use the lazy-vim package manager.
You can get inspiration from my Neovim config.
ElixirLS autocompletions runs via the elixir-tools.nvim plugin.
I have not tried the Dialyzer config in ElixirLS.

1 Like

I recently switched from nvim-cmp to blink. GitHub - Saghen/blink.cmp: Performant, batteries-included completion plugin for Neovim

I find the performance to be better and things like the included snippet support let me drop a handful of packages in the migration.

4 Likes

Neovim version 0.11 just released.

One of the highlights is “Simpler LSP setup and configuration”.

Another involves upstreaming LSP virtual lines.

11 Likes

FWIW, native support for packages is in neovim master branch since last week, in case anyone is brave enough to try that out :slight_smile:

A tracking issue is available here: `vim.pack` improvements · Issue #34763 · neovim/neovim · GitHub

2 Likes

I think I finally have a working setup using the built-in LSP integration.

Things seem to have changed quite a lot since this guide was written, most notably that nvim-lspconfig and nvim-cmp no longer seem to be required. I still had to install nvim-treesitter, though. Here are a few notes from my setup, in case it helps anyone else.

  1. Install nvim-treesitter. Also don’t forget to install the prerequisite CLI via for example brew install tree-sitter-cli. At the time of writing, they are in the process of a complete rewrite on the main branch, and the master one will become unsupported. Also, the docs state that we should run :TSUpdate when updating the plugin, so I set that up too. Using vimplug:
    Plug 'nvim-treesitter/nvim-treesitter', { 'branch': 'main', 'build': ':TSUpdate' }
    require'nvim-treesitter'.install { 'elixir', 'eex', 'heex' }
    
  2. Set up ElixirLS. We need to supply the cmd to tell it where it’s installed, and root_markers so neovim can find the project root.
     vim.lsp.config('elixirls', {
         cmd = { "/path/to/language_server.sh" },
         -- required to make language server aware of other files
         root_markers = { 'mix.exs', '.git' },
     })
     vim.lsp.enable('elixirls')
    
  3. Set up an autocmd to start tree-sitter when an elixir file is opened. I also set up a sub-autocmd to configure some LSP-related buffer settings when the LSP attaches to the buffer: enabling completion and auto-format on save. I was stuck here for a while, because while in most places the pattern for an autocmd is something like *.ex, for the FileType autocmd it’s the detected filetype name, e.g. elixir :exploding_head:
    vim.api.nvim_create_autocmd('FileType', {
      pattern = { 'elixir', 'eex', 'heex' },
      callback = function(ft_args)
        vim.treesitter.start()
        -- https://github.com/nvim-treesitter/nvim-treesitter/tree/main?tab=readme-ov-file#indentation
        vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
    
        vim.api.nvim_create_autocmd('LspAttach', {
            buffer = ft_args.buf,
            callback = function(args)
              local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
              -- Enable auto-completion
              vim.lsp.completion.enable(true, client.id, args.buf, {autotrigger = true})
              -- autoformat on save
              -- Apparently not needed if server supports "textDocument/willSaveWaitUntil".
              vim.api.nvim_create_autocmd('BufWritePre', {
                buffer = args.buf,
                callback = function()
                  vim.lsp.buf.format({ bufnr = args.buf, id = client.id, timeout_ms = 1000 })
                end,
              })
          end,
        })
      end,
    })
    
  4. Turn on “virtual lines” for diagnostics, so warnings and error messages show up below the line (otherwise it’s necessary to press CTRL+W d to view the warning - which you can still do if the warning scrolls off the edge of the screen and you want to view it in a popup).
    vim.diagnostic.config({ virtual_lines = true })
    
  5. Set up any custom keymappings in addition to the defaults. For me I set gd to go to definition, grr to find references, and Ctrl+m to show the diagnostic in a popup.
    vim.keymap.set('n', 'gd', function() vim.lsp.buf.definition() end)
    vim.keymap.set('n', 'grr', function() vim.lsp.buf.references() end)
    vim.keymap.set('n', '<C-m>', function() vim.diagnostic.open_float() end)
    
  6. Finally I added some vimscript to customise the autocompletion popup. The key bindings shouldn’t be necessary because neovim apparently already has these tab/shift+tab bindings, but for some reason it wasn’t working for me (also they are in vimscript, not lua, because I edited some existing code rather than figuring out how to do a conditional binding in lua
)
    set completeopt+=menuone,noselect,popup
    imap <expr> <enter> pumvisible() ? "<C-y>" : "<enter>"
    imap <expr> <tab> pumvisible() ? "<C-n>" : "<tab>"
    imap <expr> <S-tab> pumvisible() ? "<C-p>" : "<S-tab>"
    

Assembled config:

" Install tree-sitter with plugin manager
Plug 'nvim-treesitter/nvim-treesitter', { 'branch': 'main', 'build': ':TSUpdate' }

lua << LUA
vim.lsp.config('elixirls', {
  cmd = { "/path/to/language_server.sh" },
  -- required to make language server aware of other files
  root_markers = { 'mix.exs', '.git' },
})
vim.lsp.enable('elixirls')

require'nvim-treesitter'.install { 'elixir', 'eex', 'heex' }

-- start tree-sitter and lsp for elixir files
vim.api.nvim_create_autocmd('FileType', {
  pattern = { 'elixir', 'eex', 'heex' },
  callback = function(ft_args)
    vim.treesitter.start()
    -- https://github.com/nvim-treesitter/nvim-treesitter/tree/main?tab=readme-ov-file#indentation
    vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"

    vim.api.nvim_create_autocmd('LspAttach', {
        buffer = ft_args.buf,
        callback = function(args)
          local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
          -- Enable auto-completion
          vim.lsp.completion.enable(true, client.id, args.buf, {autotrigger = true})
          -- autoformat on save
          -- Apparently not needed if server supports "textDocument/willSaveWaitUntil".
          vim.api.nvim_create_autocmd('BufWritePre', {
            buffer = args.buf,
            callback = function()
              vim.lsp.buf.format({ bufnr = args.buf, id = client.id, timeout_ms = 1000 })
            end,
          })
      end,
    })
  end,
})

-- Enable warning/error messsages in virtual lines
vim.diagnostic.config({ virtual_lines = true })

vim.keymap.set('n', 'gd', function() vim.lsp.buf.definition() end)
vim.keymap.set('n', 'grr', function() vim.lsp.buf.references() end)
vim.keymap.set('n', '<C-m>', function() vim.diagnostic.open_float() end)
LUA

set completeopt+=menuone,noselect,popup

" Set up tab/enter navigation for autocomplete popup
" apparently this is already the default for snippets, no idea why it's not working without this
" https://github.com/neovim/neovim/pull/27339/files#diff-e682fc63c5b105e3eab956c380e8abf951a1b0d14fea03ae0f079bd490686e77
" have no idea what <expr> does but think it allows the ? operator
imap <expr> <enter> pumvisible() ? "<C-y>" : "<enter>"
imap <expr> <tab> pumvisible() ? "<C-n>" : "<tab>"
imap <expr> <S-tab> pumvisible() ? "<C-p>" : "<S-tab>"
5 Likes

Hmm, indeed, nvim-lspconfig doesn’t seem necessary at all. The built-in Neovim configuration you showed is sufficient. However, for me personally, it’s still better to keep using nvim-cmp rather than the built-in omnifunc. nvim-cmp feels faster, but I might be subjective here, I only made a quick comparison. Still, its menu, with colored and grouped items, looks nicer than omnifunc’s.

1 Like

I’ve finally been experimenting with Neovim. I’m trying to build my own minimal config for what would make me comfortable. This thread has been a big help to me in getting past the initial steps and figuring out which plugins were key. Here’s my attempt to give back a little.

The one thing I didn’t see mentioned in here that I think is really important is the mason.nvim plugin. This is a plugin that automates the installation of the various LSP servers. I think this is far superior to doing something like brew install elixir-ls. If we do that install with Homebrew, we also get a copy of Erlang and a copy of Elixir. However, most of us are probably managing the languages we want with a tool like asdf. Given that, you need to take care to ensure that you are always using the executable you think you’re using. mason.nvim removes that concern because it is going to install the language server using whatever version of Erlang and Elixir that Neovim can see and execute. You can see my config using mason.nvim and another plugin to tie it into the LSP plugin. Note that I am not yet using Neovim’s built-in LSP functionality.

8 Likes

I found it helpful to keep my own version of ElixirLS and git-pull-recompile it once in a while. You’d need to tell your plugin to use it

          cmd = "/home/am/Proyectos/Other/elixir-ls/releases/language_server.sh",
          settings = elixirls.settings {
            enableTestLenses = true,
            dialyzerEnabled = true,
            fetchDeps = false,
            suggestSpecs = false,
            autoInsertRequiredAlias = false,
            languageServerOverridePath = "/home/am/Proyectos/Other/elixir-ls/releases",
          }, 

Gosh, the code markdown here does not allow lua. It’s I’m time-travelled to 1992.

1 Like

Switched to lazyvim distribution about a year ago from chad - very happy with it so far, no complaints

1 Like