DEV Community

Cover image for How to setup lsp in neovim.
Rishav Raj
Rishav Raj

Posted on

How to setup lsp in neovim.

How to setup lsp in neovim

In this tutorial we will learn how to setup lsp for auto-completion and understand role of each package.

We will also setup auto-formating.

Neovim support lsp, meaning it acts as a client for lsp.

Neovim have inbuilt lsp support. But neovim does't have any opinion on how to use it. Neovim left this for users.





Understanding what is all of this

Big picture of lsp in neovim

When you open a file in neovim a lsp client will attach to the buffer.

The client which attach dependents on the filetype and configration in lsp.

Then the client will start talk to the language server which is installed on your machine.

The lsp respond with code completiation, error diagnostics and many more features.





Lets start neovim and attach a lsp client to buffer

We will try to start a lsp client and attach the client to a buffer.

init.lua

vim.lsp.start_client({
  name = 'my-server-name',
  cmd = {'lua-language-server'},
  root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]),
})

Enter fullscreen mode Exit fullscreen mode

Parameters:

  • name is server name given by user, must be unique
  • cmd to start the language server on your machine
  • root_dir check for pattern
    • upward = true is to start searching from upward to downward

Place setup.py(or any type of file which is mentioned in root_dir) beside init.lua, then lsp client will recognize the pattern and attached to the buffer (init.lua in this case).

Running :lua =vim.lsp.get_active_clients() will give you a lua table.

If the table is empty the lsp does't attach and running :lua =vim.lsp.get_active_clients() will gives empty table {}.

If everything goes well you can see this.

Lsp diagnostics in neovim

The lsp attached and talking to language server.

You can see warnings pop up in init.lua.

Or you can open any lua file , just place setup.py or pyproject.toml and lsp will get attached to the buffer.

You can also get code completions just type <C-x><C-o>, For some reason this does't work for me so i use <C-o><C-n><C-p> (next and prev completions item).

Type vim. place the cursor after the . and in the insert mode press <C-o><C-n><C-p>.

If everything is alright, you will see a pop-up open.

Like this:

Lsp code completion popup

Doing all of this for each file requires a lot of effort, which is why we have many plugins available to automate these tasks.

I will provide an introduction to each plugin and its respective role in the LSP setup.

This will give you a comprehensive understanding of the function and purpose of each plugin in the LSP setup.





neovim/nvim-lspconfig

This is a official plugins from neovim which contains configration for servers and many more functions to help you setup lsp client and start a particular language server.

Delete all of the previous code .

Install lspconfig, I used lazy.vim here you can use your fav package manager

init.lua

local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", -- latest stable release
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup({
-- All of the packages goes here
    "neovim/nvim-lspconfig"
}
})
Enter fullscreen mode Exit fullscreen mode

Now import the lspconfig

init.lua

{...}

-- Setup language servers.
local lspconfig = require('lspconfig')
Enter fullscreen mode Exit fullscreen mode

Now lspconfig directly give the configration of all the available lsps with default settings. see configrations available for your programming language.

init.lua

local lspconfig = require('lspconfig')

-- Python
lspconfig.pyright.setup {}

-- Typescript , Javascript
lspconfig.tsserver.setup {}

-- Lua
lspconfig.lua_ls.setup {}
Enter fullscreen mode Exit fullscreen mode

Now you can supply custom options to the each lsp setup. see configrations for your programming language.

init.lua

lspconfig.rust_analyzer.setup {
  -- Server-specific settings. See `:help lspconfig-setup`
  settings = {
    ['rust-analyzer'] = {},
  },
}
Enter fullscreen mode Exit fullscreen mode

Now we want to setup some keymaps when a lsp client attach to buffer.

Here a autocmd is created

-- Use LspAttach autocommand to only map the following keys
-- after the language server attaches to the current buffer
vim.api.nvim_create_autocmd('LspAttach', {
  group = vim.api.nvim_create_augroup('UserLspConfig', {}),
  callback = function(ev)
    -- Enable completion triggered by <c-x><c-o>
    vim.bo[ev.buf].omnifunc = 'v:lua.vim.lsp.omnifunc'

    -- Buffer local mappings.
    -- See `:help vim.lsp.*` for documentation on any of the below functions
    local opts = { buffer = ev.buf }
    vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
    vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
    vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
    vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts)
    vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, opts)
    vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, opts)
    vim.keymap.set('n', '<space>wl', function()
      print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
    end, opts)
    vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, opts)
    vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, opts)
    vim.keymap.set({ 'n', 'v' }, '<space>ca', vim.lsp.buf.code_action, opts)
    vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
    vim.keymap.set('n', '<space>f', function()
      vim.lsp.buf.format { async = true }
    end, opts)
  end,
})
Enter fullscreen mode Exit fullscreen mode

Here some useful keymaps is defined like gd for going to defination.

Again if we press <C-o><C-n><C-p> we will get completion popup like previously.

We want to automatic triggering of a completion popup, similar to that of IDEs.

So we have to use another package called nvim_cmp

Delete the autocmd





hrsh7th/nvim-cmp

Install nvim_cmp and few sources.

require("lazy").setup({

   'hrsh7th/nvim-cmp', -- Autocompletion plugin,
   'hrsh7th/cmp-nvim-lsp', -- LSP source for nvim-cmp,
   'saadparwaiz1/cmp_luasnip', -- Snippets source for nvim-cmp
   'L3MON4D3/LuaSnip', -- Snippets plugin

    "neovim/nvim-lspconfig",
}
})

Enter fullscreen mode Exit fullscreen mode

What is mean by sources here

  • Nvim_cmp gets the completions from many sources like
    • cmp-nvim-lsp gets data from language server
    • luasnip gets data from snippets and expand snippets for nvim_cmp
    • buffers gets data from buffer in neovim

Here we extend the capabilities of lsp with capabilities of cmp to leverage features of cmp.

And created a lua table which contains all the lsp server which i want to setup.

local capabilities = require("cmp_nvim_lsp").default_capabilities()
local luasnip = require 'luasnip'

local servers = {
    "lua_ls"
    "tsserver"
}

for _, lsp in ipairs(servers) do
  lspconfig[lsp].setup {
    -- on_attach = my_custom_on_attach,
    capabilities = capabilities,
  }
end
Enter fullscreen mode Exit fullscreen mode




Setting up Nvim-cmp

Import the Nvim-cmp

local cmp = require 'cmp'

cmp.setup {

}
Enter fullscreen mode Exit fullscreen mode

Snippet

Nvim-cmp doesn't "know" how to expand a snippet, that's why we need luasnip.

This callback function gets data from snippet and here cmp need to expand it.

So we use luasnip to expand the snippet.

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

Sources

Nvim-cmp needs sources to show suggestions to us

Sources include the response from language server , buffer and many more.


cmp.setup {
    snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      sources = {
        { name = 'nvim_lsp' },
        {name = "buffer"},
      },
}
Enter fullscreen mode Exit fullscreen mode

Define keymaps for Nvim-cmp

cmp.setup {
    snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      sources = {
        { name = 'nvim_lsp' },
        {name = "buffer"},
      },

      mapping = cmp.mapping.preset.insert({
        ['<C-u>'] = cmp.mapping.scroll_docs(-4), -- Up
        ['<C-d>'] = cmp.mapping.scroll_docs(4), -- Down
        ['<C-j>'] = cmp.mapping.select_next_item(),
        ['<C-k>'] = cmp.mapping.select_prev_item(),
        ['<C-Space>'] = cmp.mapping.complete(),
        ['<CR>'] = cmp.mapping.confirm {
          behavior = cmp.ConfirmBehavior.Replace,
          select = true,
        },
      }),
}

Enter fullscreen mode Exit fullscreen mode

Finally we got auto-completion while we typing.

nvim-cmp code completion popup

You can see donald which is not a keyword just present in buffer which then suggestion by cmp.





Format on save

Now we have auto-completion. We should setup a way to format our code.

One way to format a buffer using lsp is to call vim.lsp.buf.format()

Calling like this :lua vim.lsp.buf.format() should format the buffer.

For this we have to define a autocmd which will run vim.lsp.buf.format() before buffer is saved.

async options tells how to format, if true the format is non-blocking(when vim is ideal then the formatting of buffer will happen).

I make async = false because i want to format the buffer before the buffer is saved no matter what.

    vim.api.nvim_create_autocmd('BufWritePre', {
      callback = function()
        vim.lsp.buf.format {
          async = false,
        }
      end,
    })
Enter fullscreen mode Exit fullscreen mode

This will run when any buffer is save, which we don't want because if lsp_client is not attached to the buffer then it will throw error.

So we have to define the above autocommand only when a lsp is attached to buffer.

We have to create another autocommand which will run only when a lsp attached.

vim.api.nvim_create_autocmd('LspAttach', {
  group = vim.api.nvim_create_augroup('lsp-attach-format', { clear = true }),
  -- This is where we attach the autoformatting for reasonable clients
  callback = function(args)
    local client_id = args.data.client_id
    local client = vim.lsp.get_client_by_id(client_id)
    local bufnr = args.buf

    if not client.server_capabilities.documentFormattingProvider then
      return
    end


    vim.api.nvim_create_autocmd('BufWritePre', {
      group = get_augroup(client),
      buffer = bufnr,
      callback = function()
        if not format_is_enabled then
          return
        end
        vim.lsp.buf.format {
          async = false,
          filter = function(c)
            return c.id == client.id
          end,
        }
      end,
    })
  end,
})

Enter fullscreen mode Exit fullscreen mode

Filter in lsp.buf.format receives client and must return boolean. false will stop from request to format the buffer.

For Example:

This will not format buffers which have tsserver attached.

-- Never request typescript-language-server for formatting
vim.lsp.buf.format {
  filter = function(client) return client.name ~= "tsserver" end
}
Enter fullscreen mode Exit fullscreen mode

That's it.

My dot files

References:

Top comments (3)

Collapse
 
lucasm4sco profile image
Lucas

its a good tutorial, i have a question about: what are the advantages using neovim?

It seems interesting to me but I don't know if the learning time would be worth it for me

Collapse
 
rishavmngo profile image
Rishav Raj

It is worth for me bcz

  • It will not eat up your ram like vs code does.
  • The editing of files is very fast.
  • More control over each pieces.
  • You will get majority of features what ide has to offer but with configration.

You should give it a try. I am exploring neovim and learning many things.

Collapse
 
lucasm4sco profile image
Lucas

thanks for the answer, these are really good points, maybe I'll try not to get angry with him in a while 🤣