Rishav Raj
Posted on October 4, 2023
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
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]),
})
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.
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:
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"
}
})
Now import the lspconfig
init.lua
{...}
-- Setup language servers.
local lspconfig = require('lspconfig')
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 {}
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'] = {},
},
}
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,
})
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",
}
})
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
Setting up Nvim-cmp
Import the Nvim-cmp
local cmp = require 'cmp'
cmp.setup {
}
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,
},
}
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"},
},
}
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,
},
}),
}
Finally we got auto-completion while we typing.
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,
})
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,
})
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
}
That's it.
References:
Posted on October 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.