DEV Community

Heiker
Heiker

Posted on • Edited on

Getting started with neovim's native LSP client: The easy way

A lot has changed since I wrote this post back in 2022. Back then Neovim v0.6 was the stable version and having a setup with "sane defaults" was not easy. Now, Neovim v0.10 is the current stable version and this brings a lot features that make our lives easier.

Quick note: if you don't know how to configure Neovim using lua I recommend reading this Build your first Neovim configuration in lua.

Here I'm going to show a minimal configuration for Neovim's LSP client. This is what we'll do:

  • Install a language server
  • Configure the language server
  • Setup some keymaps
  • Setup an autocompletion plugin

I'm going to show the setup for golang and rust. Why those languages? Because I assume they work well on windows, mac and linux. And I need at least two language servers to make it clear some instructions vary depending on the programming language you want to use.

We need a language server

A language server is an external program. It can analyze the source code of our projects and provide useful information to the editor. You can watch this 5 minute video if you want to know more details.

Where can we find these language servers? In the Neovim ecosystem we have a wonderful plugin called nvim-lspconfig. In the documentation of this plugin you can find a list of language servers.

Let's go to our specific examples:

If you have the toolchain for golang you can install its language server (gopls) using this command.

go install golang.org/x/tools/gopls@latest
Enter fullscreen mode Exit fullscreen mode

For rust, if you have rustup installed, you can download the language server (rust_analyzer) using this command.

rustup component add rust-analyzer
Enter fullscreen mode Exit fullscreen mode

Can we automate this step?

Yes. There is a plugin called mason.nvim. This will offer an interface you can use to download language servers from inside Neovim.

I will not show you how to use mason.nvim here, because it's optional. I don't want people to think mason.nvim is essential to configure Neovim. It works great, but you should understand its benefits and problems before you decide to use it.

Configure a language server

nvim-lspconfig is the plugin we will use to configure our language servers.

In our Neovim configuration we are going to add the setup function for the language servers we have installed.

We are going to follow this syntax.

require('lspconfig').example_server.setup({})
Enter fullscreen mode Exit fullscreen mode

Where example_server is the name of the language server we have installed. For example:

require('lspconfig').gopls.setup({})
require('lspconfig').rust_analyzer.setup({})
Enter fullscreen mode Exit fullscreen mode

This is already enough to get some features working. With gopls and rust_analyzer provide error detection out the box.

Remember to read nvim-lspconfig documentation to know what language servers are supported: doc/configs.md.

Custom keymaps

There are other cool features we could use now, but they are opt-in. You need to create a custom keybinding for them to work.

The common convention here is to enable these features only when a language server is active in the file we are editing. For this we use an autocommand in the event LspAttach. Like this.

-- Create the lsp keymaps only when a 
-- language server is active
vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'LSP actions',
  callback = function(event)
    local opts = {buffer = event.buf}

    vim.keymap.set('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', opts)
    vim.keymap.set('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>', opts)
    vim.keymap.set('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<cr>', opts)
    vim.keymap.set('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<cr>', opts)
    vim.keymap.set('n', 'go', '<cmd>lua vim.lsp.buf.type_definition()<cr>', opts)
    vim.keymap.set('n', 'gr', '<cmd>lua vim.lsp.buf.references()<cr>', opts)
    vim.keymap.set('n', 'gs', '<cmd>lua vim.lsp.buf.signature_help()<cr>', opts)
    vim.keymap.set('n', '<F2>', '<cmd>lua vim.lsp.buf.rename()<cr>', opts)
    vim.keymap.set({'n', 'x'}, '<F3>', '<cmd>lua vim.lsp.buf.format({async = true})<cr>', opts)
    vim.keymap.set('n', '<F4>', '<cmd>lua vim.lsp.buf.code_action()<cr>', opts)
  end,
})
Enter fullscreen mode Exit fullscreen mode

Here's the description of the keymaps:

Setup autocompletion

nvim-cmp is the plugin we will use to get code autocompletion. By default nvim-cmp only handles the interface of the completion menu. It does not gather data from language servers or any other source.

cmp_nvim_lsp is an extension for nvim-cmp. This is the plugin that collects data from the language servers and gives it to nvim-cmp.

So a minimal configuration for nvim-cmp would like this.

local cmp = require('cmp')

cmp.setup({
  sources = {
    {name = 'nvim_lsp'},
  },
  snippet = {
    expand = function(args)
      vim.snippet.expand(args.body)
    end
  },
  mapping = cmp.mapping.preset.insert({}),
})
Enter fullscreen mode Exit fullscreen mode

What does this do? sources will be the list of nvim-cmp extensions that gather data.

mapping is where you add your keybindings.

snippet.expand is a function that gives the snippet text to other plugins, so they can parse it and expand it. But in this case we are using Neovim's built-in snippet engine (only available v0.10).

What about this cmp.mapping.preset.insert({}) thing? That is a set of keymaps that are meant to emulate Neovim's defaults. It will set the following keymaps:

  • <Ctrl-y>: Confirms selection.

  • <Ctrl-e>: Cancel the completion.

  • <Down>: Navigate to the next item on the list.

  • <Up>: Navigate to previous item on the list.

  • <Ctrl-n>: Go to the next item in the completion menu, or trigger completion menu.

  • <Ctrl-p>: Go to the previous item in the completion menu, or trigger completion menu.

If you want to add more keymaps, you put them inside the curly braces.

mapping = cmp.mapping.preset.insert({
  -- confirm completion
  ['<Enter>'] = cmp.mapping.confirm({select = true}),

  -- scroll up and down the documentation window
  ['<C-u>'] = cmp.mapping.scroll_docs(-4),
  ['<C-d>'] = cmp.mapping.scroll_docs(4),   
}),
Enter fullscreen mode Exit fullscreen mode

One more thing...

Now that we have cmp_nvim_lsp configured we need to update the options we send to our language servers.

So, we need to get the "default capabilities" provided by cmp_nvim_lsp and add them to the setup function of our language servers.

This is what we do:

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

require('lspconfig').example_server.setup({
  capabilities = lsp_capabilities,
})
Enter fullscreen mode Exit fullscreen mode

For our concrete example with go and rust it'll be like this.

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

require('lspconfig').gopls.setup({
  capabilities = lsp_capabilities,
})
require('lspconfig').rust_analyzer.setup({
  capabilities = lsp_capabilities,
})
Enter fullscreen mode Exit fullscreen mode

Complete code

We are done. We have everything we need to start our journey.

Here's the complete code from the previous sections put together.

---
-- Autocompletion
---

local cmp = require('cmp')

cmp.setup({
  sources = {
    {name = 'nvim_lsp'},
  },
  snippet = {
    expand = function(args)
      vim.snippet.expand(args.body)
    end
  },
  mapping = cmp.mapping.preset.insert({
    -- confirm completion
    ['<Enter>'] = cmp.mapping.confirm({select = true}),

    -- scroll up and down the documentation window
    ['<C-u>'] = cmp.mapping.scroll_docs(-4),
    ['<C-d>'] = cmp.mapping.scroll_docs(4),   
  }),
})

---
-- Language server configuration
---

-- Create the lsp keymaps only when a 
-- language server is active
vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'LSP actions',
  callback = function(event)
    local opts = {buffer = event.buf}

    vim.keymap.set('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', opts)
    vim.keymap.set('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>', opts)
    vim.keymap.set('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<cr>', opts)
    vim.keymap.set('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<cr>', opts)
    vim.keymap.set('n', 'go', '<cmd>lua vim.lsp.buf.type_definition()<cr>', opts)
    vim.keymap.set('n', 'gr', '<cmd>lua vim.lsp.buf.references()<cr>', opts)
    vim.keymap.set('n', 'gs', '<cmd>lua vim.lsp.buf.signature_help()<cr>', opts)
    vim.keymap.set('n', '<F2>', '<cmd>lua vim.lsp.buf.rename()<cr>', opts)
    vim.keymap.set({'n', 'x'}, '<F3>', '<cmd>lua vim.lsp.buf.format({async = true})<cr>', opts)
    vim.keymap.set('n', '<F4>', '<cmd>lua vim.lsp.buf.code_action()<cr>', opts)
  end,
})

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

-- These are just examples. Replace them with the language
-- servers you have installed in your system.
require('lspconfig').gopls.setup({
  capabilities = lsp_capabilities,
})
require('lspconfig').rust_analyzer.setup({
  capabilities = lsp_capabilities,
})
Enter fullscreen mode Exit fullscreen mode

Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.

buy me a coffee

Top comments (3)

Collapse
 
brian_richardson_bd31d65f profile image
Brian Richardson

Talk about saving a massive headache. lsp-zero saves a ton of fiddling and just "works" out of the box!

Collapse
 
georgeanderson profile image
George Guimarães

Thank you so much for this. I have followed your article and have finally switched to init.lua. It was easier than I initially thought. Better yet, I have all the nice features of LSP ans snippets easily available. Thanks again.