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

Heiker @vonheikemen

About: Web developer from Venezuela. I like solving problems. Currently trying to improve my communication skills

Joined:
Apr 3, 2018

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

Publish Date: Feb 25 '22
33 2

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. Here I will show you a simple setup you can use in Neovim v0.9 and greater.

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

We are going to create 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

Because I like practical examples I'll 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.

At this point in time (May 2025) we are in a transition period. Neovim v0.11 just came out and added a new configuration method. But on many linux systems v0.9 or v0.10 is what we have available. So I'll show the new and the old method.

On Neovim v0.11 and greater, to use a language server we must execute the function vim.lsp.enable().

vim.lsp.enable('example_server')
Enter fullscreen mode Exit fullscreen mode

On Neovim v0.10 or lower, we must use the "legacy setup" that depends on the module lspconfig.

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

Important: Newer versions of nvim-lspconfig require Neovim v0.10 or greater. If you need support for Neovim v0.9 use the tag v1.8.0.

So if you have Neovim v0.11 you can enable the language servers like this.

vim.lsp.enable({'gopls', 'rust_analyzer'})
Enter fullscreen mode Exit fullscreen mode

And on older versions you'll have to do it like this.

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.

Now if you want your configuration to remain backwards compatible you may have to write a custom function that checks Neovim's version and chooses the right configuration method. Something like this.

-- This function will use the "legacy setup" on older Neovim version.
-- The new api is only available on Neovim v0.11 or greater.
local function lsp_setup(server, opts)
  if vim.fn.has('nvim-0.11') == 0 then
    require('lspconfig')[server].setup(opts)
    return
  end

  if not vim.tbl_isempty(opts) then
    vim.lsp.config(server, opts)
  end

  vim.lsp.enable(server)
end
Enter fullscreen mode Exit fullscreen mode

Then you can enable your language servers like this.

lsp_setup('gopls', {})
lsp_setup('rust_analyzer', {})
Enter fullscreen mode Exit fullscreen mode

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

LSP keymaps

On Neovim v0.11 we have keymaps for almost everything, but on older versions we have to create it ourselves. So here I'll show how to create the default keymaps that are not available on older versions.

-- These keymaps are the defaults in Neovim v0.10
vim.keymap.set('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<cr>')
vim.keymap.set('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<cr>')
vim.keymap.set('n', '<C-w>d', '<cmd>lua vim.diagnostic.open_float()<cr>')
vim.keymap.set('n', '<C-w><C-d>', '<cmd>lua vim.diagnostic.open_float()<cr>')

vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(event)
    local bufmap = function(mode, rhs, lhs)
      vim.keymap.set(mode, rhs, lhs, {buffer = event.buf})
    end

    -- These keymaps are the defaults in Neovim v0.11
    bufmap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>')
    bufmap('n', 'grr', '<cmd>lua vim.lsp.buf.references()<cr>')
    bufmap('n', 'gri', '<cmd>lua vim.lsp.buf.implementation()<cr>')
    bufmap('n', 'grn', '<cmd>lua vim.lsp.buf.rename()<cr>')
    bufmap('n', 'gra', '<cmd>lua vim.lsp.buf.code_action()<cr>')
    bufmap('n', 'gO', '<cmd>lua vim.lsp.buf.document_symbol()<cr>')
    bufmap({'i', 's'}, '<C-s>', '<cmd>lua vim.lsp.buf.signature_help()<cr>')
  end,
})
Enter fullscreen mode Exit fullscreen mode

Here's the description of the keymaps I show on the snippet above, and also the ones Neovim already has:

  • <Ctrl-]>: Jumps to the definition of the symbol under the cursor.

  • gq: This is the format operator. Whenever possible it'll request the language server to perform the formatting.

  • [d: Jump to the previous diagnostic in the current file.

  • ]d: Jump to the next diagnostic in the current file.

  • <Ctrl-w>d: Open a floating window showing the diagnostics in the line under the cursor.

  • K: Displays hover information about the symbol under the cursor in a floating window. See :help vim.lsp.buf.hover().

  • grr: Lists all the references to the symbol under the cursor in the quickfix window. See :help vim.lsp.buf.references().

  • gri: Lists all the implementations for the symbol under the cursor in the quickfix window. See :help vim.lsp.buf.implementation().

  • grn: Renames all references to the symbol under the cursor. See :help vim.lsp.buf.rename().

  • gra: Selects a code action available at the current cursor position. See :help vim.lsp.buf.code_action().

  • gO: Lists all symbols in the current buffer. See :help vim.lsp.buf.document_symbol().

  • <Ctrl-s>: Displays signature information about the symbol under the cursor in a floating window. See :help vim.lsp.buf.signature_help(). If a mapping already exists for this key this function is not bound.

Setup autocompletion

Neovim does offer code completion out the box, they call it insert mode completion. This isn't automatic and the support for snippets varies depending on the version of Neovim we are using.

To get a simple autocompletion setup that is backwards compatible with Neovim v0.9 I would recommend using mini.nvim. mini.nvim is a collection of lua modules, it's meant to enhance Neovim's builtin features. Now, everything in mini.nvim is opt-in so we have to enable the modules we want to use. Right now is just these two.

require('mini.snippets').setup({})
require('mini.completion').setup({})
Enter fullscreen mode Exit fullscreen mode

Note that mini.completion is using Neovim's builtin completion menu. To control it we use Neovim's default keybindings.

  • <Down>: Select the next item on the list.

  • <Up>: Select previous item on the list.

  • <Ctrl-n>: Select and insert text of the next item on the list.

  • <Ctrl-p>: Select and insert text of the previous item on the list.

  • <Ctrl-y>: Confirm selected item.

  • <Ctrl-e>: Cancel the completion.

  • <Enter>: If item was selected using <Up> or <Down> it confirms selection. If no item is selected, hides completion menu. Else, inserts a newline character.

Complete code

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

If you are using Neovim v0.11 or greater, this is all you need.

-- NOTE: this is meant for Neovim v0.11 or greater

require('mini.snippets').setup({})
require('mini.completion').setup({})

vim.lsp.enable({'gopls', 'rust_analyzer'})
Enter fullscreen mode Exit fullscreen mode

If you want something that still works on older Neovim versions, this is what you have to do.

-- NOTE: this is meant to be backwards compatible with Neovim v0.9

---
-- Autocompletion
---

require('mini.snippets').setup({})
require('mini.completion').setup({})

---
-- Language server configuration
---

-- These keymaps are the defaults in Neovim v0.10
if vim.fn.has('nvim-0.11') == 0 then
  -- NOTE: vim.diagnostic.goto_* methods are deprecated in v0.11
  -- that's why we put these under a conditional block
  vim.keymap.set('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<cr>')
  vim.keymap.set('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<cr>')
  vim.keymap.set('n', '<C-w>d', '<cmd>lua vim.diagnostic.open_float()<cr>')
  vim.keymap.set('n', '<C-w><C-d>', '<cmd>lua vim.diagnostic.open_float()<cr>')
end

vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(event)
    local bufmap = function(mode, rhs, lhs)
      vim.keymap.set(mode, rhs, lhs, {buffer = event.buf})
    end

    -- These keymaps are the defaults in Neovim v0.11
    bufmap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>')
    bufmap('n', 'grr', '<cmd>lua vim.lsp.buf.references()<cr>')
    bufmap('n', 'gri', '<cmd>lua vim.lsp.buf.implementation()<cr>')
    bufmap('n', 'grn', '<cmd>lua vim.lsp.buf.rename()<cr>')
    bufmap('n', 'gra', '<cmd>lua vim.lsp.buf.code_action()<cr>')
    bufmap('n', 'gO', '<cmd>lua vim.lsp.buf.document_symbol()<cr>')
    bufmap({'i', 's'}, '<C-s>', '<cmd>lua vim.lsp.buf.signature_help()<cr>')
  end,
})

-- This function will use the "legacy setup" on older Neovim version.
-- The new api is only available on Neovim v0.11 or greater.
local function lsp_setup(server, opts)
  if vim.fn.has('nvim-0.11') == 0 then
    require('lspconfig')[server].setup(opts)
    return
  end

  if not vim.tbl_isempty(opts) then
    vim.lsp.config(server, opts)
  end

  vim.lsp.enable(server)
end

lsp_setup('gopls', {})
lsp_setup('rust_analyzer', {})
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

Comments 2 total

  • George Guimarães
    George GuimarãesDec 16, 2022

    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.

  • Brian Richardson
    Brian RichardsonAug 2, 2023

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

Add comment