DEV Community

Heiker
Heiker

Posted on • Updated on • Originally published at vonheikemen.github.io

Neovim LSP: Setup nvim-lspconfig + nvim-cmp

Pueden leer la versión en español aquí

Neovim includes a lua framework that allows the editor to communicate with a language server. What does that mean for us? Means we can have some IDE-like features such as rename variables, smart jump to definition, list references, etc. But of course we need to configure all of this first.

Wait, why do we need all these plugins?

Out of the box Neovim offers the tools to start and query a language server but it doesn't have any opinion on how we should use them. Enter nvim-lspconfig, with it Neovim can scan the "root directory" of a project and choose which language server you configured should be initialiazed.

And then we have nvim-cmp, the autocompletion plugin. I think the completions provided by Neovim are good enough but the thing is it requires a fair amount of manual intervention. Modern code editors have all this automated in a way that feels more intuitive, this is what nvim-cmp offers. We can have that smart autocompletion in Neovim.

Now you know the why, let's move on to the how.

Requirements

In nvim-lspconfig documentation you'll find instructions to install the language servers it supports: server_configurations.md

LSP Config

What we need to do here is use the module lspconfig and call the .setup() of the language server we want to configure.

How do we know which language servers are supported? lspconfig's documentation has the answer. You can find the list of valid names using :help lspconfig-server-configurations.

For the language lua we can use a server called lua_ls. Install it and then call it in your config like this.

local lspconfig = require('lspconfig')

lspconfig.lua_ls.setup({})
Enter fullscreen mode Exit fullscreen mode

There is one thing you need to keep in mind. We are going to use nvim-cmp to handle the autocompletion. So, we have to modify an option called capabilities in every language server we use. This option tells the language server the features Neovim supports.

We are going to tell the language server what features nvim-cmp adds to Neovim. For this we call the module cmp_nvim_lsp and get the default capabilities.

local lsp_capabilities = require('cmp_nvim_lsp').default_capabilities()
Enter fullscreen mode Exit fullscreen mode

Once we have this new variable lsp_capabilities we can add it to our setup.

local lspconfig = require('lspconfig')
local lsp_capabilities = require('cmp_nvim_lsp').default_capabilities()

lspconfig.lua_ls.setup({
  capabilities = lsp_capabilities,
})
Enter fullscreen mode Exit fullscreen mode

If want to know the available options in .setup() checkout the help page using :help lspconfig-setup.

At this point to take advantage of some "LSP features" we need to create some keybindings. Neovim will emit the event LspAttach each time a language server is attached to a buffer, this will give us the opportunity to create our keybindings. Here is an example.

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'LSP actions',
  callback = function()
    local bufmap = function(mode, lhs, rhs)
      local opts = {buffer = true}
      vim.keymap.set(mode, lhs, rhs, opts)
    end

    -- Displays hover information about the symbol under the cursor
    bufmap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>')

    -- Jump to the definition
    bufmap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>')

    -- Jump to declaration
    bufmap('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<cr>')

    -- Lists all the implementations for the symbol under the cursor
    bufmap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<cr>')

    -- Jumps to the definition of the type symbol
    bufmap('n', 'go', '<cmd>lua vim.lsp.buf.type_definition()<cr>')

    -- Lists all the references 
    bufmap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<cr>')

    -- Displays a function's signature information
    bufmap('n', 'gs', '<cmd>lua vim.lsp.buf.signature_help()<cr>')

    -- Renames all references to the symbol under the cursor
    bufmap('n', '<F2>', '<cmd>lua vim.lsp.buf.rename()<cr>')

    -- Selects a code action available at the current cursor position
    bufmap('n', '<F4>', '<cmd>lua vim.lsp.buf.code_action()<cr>')

    -- Show diagnostics in a floating window
    bufmap('n', 'gl', '<cmd>lua vim.diagnostic.open_float()<cr>')

    -- Move to the previous diagnostic
    bufmap('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<cr>')

    -- Move to the next diagnostic
    bufmap('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<cr>')
  end
})
Enter fullscreen mode Exit fullscreen mode

This autocommand can live anywhere in our configuration. And if we wanted to, we could write it in vimscript (I'm not doing that here).

Note: if you are using Neovim v0.7.2 or lower you'll need to create a function and add it to the on_attach option of each language server. See :help lspconfig-keybindings for an example.

So, here is the complete code to configure one language server using lspconfig.

local lspconfig = require('lspconfig')
local lsp_capabilities = require('cmp_nvim_lsp').default_capabilities()

lspconfig.lua_ls.setup({
  capabilities = lsp_capabilities,
})
Enter fullscreen mode Exit fullscreen mode

Snippets

This is a good time to configure our snippet engine. The next bit is not related to language servers, LSP or whatever. We are just going to load the snippets we have installed.

require('luasnip.loaders.from_vscode').lazy_load()
Enter fullscreen mode Exit fullscreen mode

The .lazy_load() function will load any snippet we have in our runtimepath. And by that I mean the ones available in friendly-snippets.

Autocompletion

Before we start, nvim-cmp's documentation says we should set completeopt with the following values:

vim.opt.completeopt = {'menu', 'menuone', 'noselect'}
Enter fullscreen mode Exit fullscreen mode

To configure nvim-cmp we will use two modules cmp and luasnip.

local cmp = require('cmp')
local luasnip = require('luasnip')
Enter fullscreen mode Exit fullscreen mode

cmp is the one we will use to configure nvim-cmp. And this luansnip? Well, nvim-cmp doesn't "know" how to expand a snippet, that's why we need it.

We are going to spend some time exploring nvim-cmp's options. I want to explain all the things I do in my personal configuration. For now let's just add the setup function.

local select_opts = {behavior = cmp.SelectBehavior.Select}

cmp.setup({

})
Enter fullscreen mode Exit fullscreen mode

snippet.expand

Callback function, it receives data from a snippet. This is where nvim-cmp expect us to provide a thing that can expand snippets, and we do that with luasnip.lsp_expand.

snippet = {
  expand = function(args)
    luasnip.lsp_expand(args.body)
  end
},
Enter fullscreen mode Exit fullscreen mode

sources

Here we can list all the data sources nvim-cmp will use to populate the completion list.

Each "source" is a lua table that must have a name property. That name is not the name of the plugin, it's the "id" the plugin used when creating the source. Each source should tell what name they have in their documentation.

Other properties you should be aware of are priority and keyword_length. priority allows nvim-cmp to sort the completion list. If you do not set a priority then the order of the sources will determine the priority. Now with keyword_length you can control how many characters are necesary to begin querying the source.

In my personal configuration I have the sources setup this way.

sources = {
  {name = 'path'},
  {name = 'nvim_lsp', keyword_length = 1},
  {name = 'buffer', keyword_length = 3},
  {name = 'luasnip', keyword_length = 2},
},
Enter fullscreen mode Exit fullscreen mode
  • path: Autocomplete file paths.
  • nvim_lsp: Shows suggestions based on the response of a language server.
  • buffer: Suggests words found in the current buffer.
  • luasnip: Shows available snippets and expands them if they are chosen.

window.documentation

Controls the appearance and settings for the documentation window. To configure this quickly nvim-cmp offers a preset we can use to add some borders.

window = {
  documentation = cmp.config.window.bordered()
},
Enter fullscreen mode Exit fullscreen mode

formatting.fields

List of strings that determines the order of the elements in an item.

formatting = {
  fields = {'menu', 'abbr', 'kind'}
},
Enter fullscreen mode Exit fullscreen mode

abbr is the content of the suggestion. kind is the type of data, this can be text, class, function, etc. Finally, menu which apparently is empty by default.

formatting.format

Callback function that allows us to customize the appearance of the completion menu. A simple example I can show: assign an icon to a field based on the source name.

formatting = {
  fields = {'menu', 'abbr', 'kind'},
  format = function(entry, item)
    local menu_icon = {
      nvim_lsp = 'λ',
      luasnip = '⋗',
      buffer = 'Ω',
      path = '🖫',
    }

    item.menu = menu_icon[entry.source.name]
    return item
  end,
},
Enter fullscreen mode Exit fullscreen mode

mapping

List of keybindings. For this we need to declare a list of key/value pairs. Where the value is a function of the cmp module. Like this.

mapping = {
  ['<CR>'] = cmp.mapping.confirm({select = false}),
}
Enter fullscreen mode Exit fullscreen mode

In this example ['<CR>'] is the key/shortcut we want to bind. The function on the other side of the assignment is the action we want to execute.

Here is a list of common keybindings:

  • Move between completion items.
['<Up>'] = cmp.mapping.select_prev_item(select_opts),
['<Down>'] = cmp.mapping.select_next_item(select_opts),

['<C-p>'] = cmp.mapping.select_prev_item(select_opts),
['<C-n>'] = cmp.mapping.select_next_item(select_opts),
Enter fullscreen mode Exit fullscreen mode
  • Scroll text in the documentation window.
['<C-u>'] = cmp.mapping.scroll_docs(-4),
['<C-d>'] = cmp.mapping.scroll_docs(4),
Enter fullscreen mode Exit fullscreen mode
  • Cancel completion.
['<C-e>'] = cmp.mapping.abort(),
Enter fullscreen mode Exit fullscreen mode
  • Confirm selection.
['<C-y>'] = cmp.mapping.confirm({select = true}),
['<CR>'] = cmp.mapping.confirm({select = false}),
Enter fullscreen mode Exit fullscreen mode
  • Jump to the next placeholder in the snippet.
['<C-f>'] = cmp.mapping(function(fallback)
  if luasnip.jumpable(1) then
    luasnip.jump(1)
  else
    fallback()
  end
end, {'i', 's'}),
Enter fullscreen mode Exit fullscreen mode
  • Jump to the previous placeholder in the snippet.
['<C-b>'] = cmp.mapping(function(fallback)
  if luasnip.jumpable(-1) then
    luasnip.jump(-1)
  else
    fallback()
  end
end, {'i', 's'}),
Enter fullscreen mode Exit fullscreen mode
  • Autocomplete with tab.

If the completion menu is visible, move to the next item. If the line is "empty", insert a Tab character. If the cursor is inside a word, trigger the completion menu.

['<Tab>'] = cmp.mapping(function(fallback)
  local col = vim.fn.col('.') - 1

  if cmp.visible() then
    cmp.select_next_item(select_opts)
  elseif col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') then
    fallback()
  else
    cmp.complete()
  end
end, {'i', 's'}),
Enter fullscreen mode Exit fullscreen mode
  • If the completion menu is visible, move to the previous item.
['<S-Tab>'] = cmp.mapping(function(fallback)
  if cmp.visible() then
    cmp.select_prev_item(select_opts)
  else
    fallback()
  end
end, {'i', 's'}),
Enter fullscreen mode Exit fullscreen mode

Complete cmp config

Here is the entire configuration for nvim-cmp and luansnip.

vim.opt.completeopt = {'menu', 'menuone', 'noselect'}

require('luasnip.loaders.from_vscode').lazy_load()

local cmp = require('cmp')
local luasnip = require('luasnip')

local select_opts = {behavior = cmp.SelectBehavior.Select}

cmp.setup({
  snippet = {
    expand = function(args)
      luasnip.lsp_expand(args.body)
    end
  },
  sources = {
    {name = 'path'},
    {name = 'nvim_lsp', keyword_length = 1},
    {name = 'buffer', keyword_length = 3},
    {name = 'luasnip', keyword_length = 2},
  },
  window = {
    documentation = cmp.config.window.bordered()
  },
  formatting = {
    fields = {'menu', 'abbr', 'kind'},
    format = function(entry, item)
      local menu_icon = {
        nvim_lsp = 'λ',
        luasnip = '⋗',
        buffer = 'Ω',
        path = '🖫',
      }

      item.menu = menu_icon[entry.source.name]
      return item
    end,
  },
  mapping = {
    ['<Up>'] = cmp.mapping.select_prev_item(select_opts),
    ['<Down>'] = cmp.mapping.select_next_item(select_opts),

    ['<C-p>'] = cmp.mapping.select_prev_item(select_opts),
    ['<C-n>'] = cmp.mapping.select_next_item(select_opts),

    ['<C-u>'] = cmp.mapping.scroll_docs(-4),
    ['<C-d>'] = cmp.mapping.scroll_docs(4),

    ['<C-e>'] = cmp.mapping.abort(),
    ['<C-y>'] = cmp.mapping.confirm({select = true}),
    ['<CR>'] = cmp.mapping.confirm({select = false}),

    ['<C-f>'] = cmp.mapping(function(fallback)
      if luasnip.jumpable(1) then
        luasnip.jump(1)
      else
        fallback()
      end
    end, {'i', 's'}),

    ['<C-b>'] = cmp.mapping(function(fallback)
      if luasnip.jumpable(-1) then
        luasnip.jump(-1)
      else
        fallback()
      end
    end, {'i', 's'}),

    ['<Tab>'] = cmp.mapping(function(fallback)
      local col = vim.fn.col('.') - 1

      if cmp.visible() then
        cmp.select_next_item(select_opts)
      elseif col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') then
        fallback()
      else
        cmp.complete()
      end
    end, {'i', 's'}),

    ['<S-Tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_prev_item(select_opts)
      else
        fallback()
      end
    end, {'i', 's'}),
  },
})
Enter fullscreen mode Exit fullscreen mode

Bonus content

Technically we are ready. All the code shown so far should get us a functional setup. We can now use our language servers and autocompletion. But there are still a couple of customizations we can make.

Change diagnostic icons

We do this using a function called sign_define. Keep in mind this is not a lua function, it's vimscript. We access it using the prefix vim.fn. To know the details of this function check the documentation: :help sign_define.

Anyway, this is the code you'll need to change the sign icons.

local sign = function(opts)
  vim.fn.sign_define(opts.name, {
    texthl = opts.name,
    text = opts.text,
    numhl = ''
  })
end

sign({name = 'DiagnosticSignError', text = '✘'})
sign({name = 'DiagnosticSignWarn', text = '▲'})
sign({name = 'DiagnosticSignHint', text = '⚑'})
sign({name = 'DiagnosticSignInfo', text = '»'})
Enter fullscreen mode Exit fullscreen mode

Diagnostics config

This time we must use the vim.diagnostic module, specifically the .config() function. It takes a lua table as an argument, and these are the defaults.

{
  virtual_text = true,
  signs = true,
  update_in_insert = false,
  underline = true,
  severity_sort = false,
  float = true,
}
Enter fullscreen mode Exit fullscreen mode
  • virtual_text: Show diagnostic message using virtual text.

  • signs: Show a sign next to the line with a diagnostic.

  • update_in_insert: Update diagnostics while editing in insert mode.

  • underline: Use an underline to show a diagnostic location.

  • severity_sort: Order diagnostics by severity.

  • float: Show diagnostic messages in floating windows.

Each one of these option can be either a boolean or a lua table. You can find more details about them in the documentation: :help vim.diagnostic.config().

I prefer less distracting diagnostics. This is the setup I use.

vim.diagnostic.config({
  virtual_text = false,
  severity_sort = true,
  float = {
    border = 'rounded',
    source = 'always',
  },
})
Enter fullscreen mode Exit fullscreen mode

Help windows with borders

There are two LSP methods that use floating windows: vim.lsp.buf.hover() and vim.lsp.buf.signature_help(). By default these windows don't have any style, but we can change that by modifying the associated "handler" of each method. To achieve this we need to use vim.lsp.with().

I don't want to bore you with the details so let me just show you the code.

vim.lsp.handlers['textDocument/hover'] = vim.lsp.with(
  vim.lsp.handlers.hover,
  {border = 'rounded'}
)

vim.lsp.handlers['textDocument/signatureHelp'] = vim.lsp.with(
  vim.lsp.handlers.signature_help,
  {border = 'rounded'}
)
Enter fullscreen mode Exit fullscreen mode

Install language servers "locally"

Sooner or later you are going to find out about this plugin: mason.nvim. With it you can manage your language servers using Neovim. You'll be able to install, update and remove language server.

But the servers you install using this method will not be available globally. They are installed in the "data folder" of Neovim. Now, some servers work fine in this setup others require extra configurations so it's recommended that you add mason-lspconfig.

Once you have both plugins you should setup mason.nvim using these functions.

require('mason').setup()
require('mason-lspconfig').setup()
Enter fullscreen mode Exit fullscreen mode

After doing that you should use lspconfig like you usually do. Pretend mason.nvim doesn't even exists.

Here is an example usage.

require('mason').setup()
require('mason-lspconfig').setup()

local lspconfig = require('lspconfig')
local lsp_capabilities = require('cmp_nvim_lsp').default_capabilities()

lspconfig.lua_ls.setup({
  capabilities = lsp_capabilities,
})
Enter fullscreen mode Exit fullscreen mode

Right now this is all I have to say about mason.nvim. If you want to know more about it you'll have read their documentation.

Conclusion

We've learned the things nvim-lspconfig can do for us. We figure out how to make a global config for our servers. We setup a handful of LSP functions in our keybindings. Basically, we know everything we need to take advantage of the cool features a language server can provide.

We had the chance to assemble a configuration for nvim-cmp from scratch. We explored some common options step by step.

Finally, we made some additional customizations to diagnostics. Learned a little something about configuring LSP handlers, specifically the ones using floating windows. And we took a brief look at a method to install language servers locally.

You can find all the configuration code here: nvim-lspconfig + nvim-cmp.

And here you can find a fully functional Neovim setup: nvim-starter - branch: 03-lsp.

Sources


Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in buy me a coffee ☕.

buy me a coffee

Top comments (1)

Collapse
 
eckelon profile image
JA Samitier

so helpful, huge thanks for this piece of content!!