I like to describe lsp-zero as an opinionated configuration wrapped in a plugin. It was made to reduce the boilerplate code necessary to configure nvim-cmp and nvim-lspconfig.
Now if you follow the instruction on the Quickstart section of the documentation you'll end up using this code.
local lsp = require('lsp-zero')
lsp.preset('recommended')
lsp.setup()
This is fantastic if you really don't care how things work, you don't care about lua or what have you. You just want a thing that works.
On the other hand, if you are the kind of user who likes to tweak every single option to your liking then it's a little too restrictive. But don't worry, you don't have to use this .setup() function. You can make lsp-zero
play nice with other plugins.
Configuring LSP servers
For this we are going to use a function called .extend_lspconfig(). This will take care of creating the keybindings and commands when the LSP server gets attached to a buffer. And also send the "client capabilities" to the LSP server.
It is very important that you call .extend_lspconfig()
before you setup an LSP server using lspconfig
. Because what happens is lsp-zero will modify some default values inside lspconfig.
Your LSP setup can be as simple as this.
-- See :help lsp-zero.extend_lspconfig()
require('lsp-zero').extend_lspconfig()
-- Call the language servers you have installed
require('lspconfig').tsserver.setup({})
require('lspconfig').eslint.setup({})
require('lspconfig').rust_analyzer.setup({})
What do we get from this? What's the benefit?
There is far more content online about nvim-lspconfig than lsp-zero. You can use snippets of code others have shared online. You can integrate with other plugins more easily, because there is a good chance the author will show an example using lspconfig
. So you don't need to "translate" any snippet to lsp-zero's style, you can just use it.
Change the keybindings
If you wanted to, you can disable lsp-zero's keybindings to add your own. You can do something like this with .extend_lspconfig()
.
require('lsp-zero').extend_lspconfig({
set_lsp_keymaps = false,
on_attach = function(client, bufnr)
local opts = {buffer = bufnr}
vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
vim.keymap.set('n', '<leader>r', vim.lsp.buf.rename, opts)
---
-- and many more...
---
end
})
And of course if you just want to add your own then omit set_lsp_keymaps
. The default value is true
.
About on_attach
The on_attach
function you add to .extend_lspconfig()
acts like a "global hook". It'll run every time a language server (configured by lspconfig) gets attached to a buffer, even if you add an on_attach
function to a server.
This works.
require('lsp-zero').extend_lspconfig({
on_attach = function(client, bufnr)
print(client.name .. ': Hello there.')
end
})
require('lspconfig').tsserver.setup({
on_attach = function(client, bufnr)
print('tsserver: How are you today?')
end
})
When tsserver gets attached to a buffer Neovim will print both messages. First the one on .extend_lspconfig()
then the one on tsserver.
Adding capabilities
Sometimes a plugin you want to install needs you to add arguments to the "client capabilities" send to the LSP server. I know is a little tedious to add it to every single server so .extend_lspconfig
provides an option for this.
require('lsp-zero').extend_lspconfig({
capabilities = {
textDocument = {
foldingRange = {
dynamicRegistration = false,
lineFoldingOnly = true
}
}
}
})
This capabilities
option will be merged with lspconfig
's defaults.
Adding mason.nvim into the mix
So mason.nvim is like a package manager for neovim. But it doesn't handle plugins. It helps you download external tools like formatters, debuggers and LSP servers.
To add this in your configuration first install the plugins mason.nvim and mason-lspconfig. Then call their setup function.
-- See :help lsp-zero.extend_lspconfig()
require('lsp-zero').extend_lspconfig()
require('mason').setup()
require('mason-lspconfig').setup()
-- Call the language servers you have installed
require('lspconfig').tsserver.setup({})
require('lspconfig').eslint.setup({})
require('lspconfig').rust_analyzer.setup({})
Automatic installs
If you want to make sure you always have your language servers available, you can use the option ensure_installed
of mason-lspconfig
.
require('mason').setup()
require('mason-lspconfig').setup({
ensure_installed = {
-- Replace these with the servers you want to install
'rust_analyzer',
'tsserver',
'eslint',
}
})
Enable automatic setup of LSP servers
Are you familiar with for
loops? If the answer is no
then please just call each LSP server manually.
If you do know about for
loops, here's what you can do: call the function get_installed_servers
that's inside mason-lspconfig
then call lspconfig inside the loop.
-- See :help lsp-zero.extend_lspconfig()
require('lsp-zero').extend_lspconfig()
require('mason').setup()
require('mason-lspconfig').setup()
local get_servers = require('mason-lspconfig').get_installed_servers
for _, server_name in ipairs(get_servers()) do
require('lspconfig')[server_name].setup({})
end
But there is another option that is more "magical", a function called .setup_handlers
.
-- See :help lsp-zero.extend_lspconfig()
require('lsp-zero').extend_lspconfig()
require('mason').setup()
require('mason-lspconfig').setup()
require('mason-lspconfig').setup_handlers({
function(server_name)
require('lspconfig')[server_name].setup({})
end
})
Note: Some lsp-zero users say bugs in their config were fixed after
.setup_handler()
was removed from the setup process. Not trying to scare you. But if you notice something weird, just try to call your LSP servers manually and see if the issue persists.
If you need to have a special configuration for a particular LSP server then add a new property. Like this.
require('mason-lspconfig').setup_handlers({
function(server_name)
require('lspconfig')[server_name].setup({})
end,
['tsserver'] = function()
require('lspconfig').tsserver.setup({
on_attach = function(client, bufnr)
print('hello tsserver')
end,
settings = {
completions = {
completeFunctionCalls = true
}
}
})
end
})
Here we are adding a property for tsserver
allowing us to create a function to pass custom settings to the server.
Diagnostics
If you are not familiar with the term, a diagnostic can be an error message, a warning or a hint.
You can opt-in to lsp-zero's config like this.
local lsp = require('lsp-zero')
lsp.set_sign_icons()
vim.diagnostic.config(lsp.defaults.diagnostics({}))
How to change the icons?
You add a config table to .set_sign_icons().
lsp.set_sign_icons({
error = 'e',
warn = 'w',
hint = 'h',
info = 'i'
})
You can pretend those characters are fancy icons. The point is you can replace the defaults if you want.
Change diagnostic config?
If you pass a config table to .defaults.diagnostics() it will merge it with lsp-zero's config. You can pass anything that is valid inside vim.diagnostic.config().
So here's how you enable virtual text.
vim.diagnostic.config(lsp.defaults.diagnostics({
virtual_text = true
}))
Snippets
Here you don't need lsp-zero at all. Install luasnip and have it load the snippet collection you installed (I recommend friendly-snippets).
require('luasnip').config.set_config({
region_check_events = 'InsertEnter',
delete_check_events = 'InsertLeave'
})
require('luasnip.loaders.from_vscode').lazy_load()
Autocompletion
The final piece of the puzzle. Here you need to expose lsp-zero's configuration table using .defaults.cmp_config(). Then pass the configuration to nvim-cmp.
Provided that you have nvim-cmp
, cmp-nvim-lsp and luasnip installed, this will be the minimal configuration needed.
vim.opt.completeopt = {'menu', 'menuone', 'noselect'}
local cmp = require('cmp')
local cmp_config = require('lsp-zero').defaults.cmp_config({})
cmp.setup(cmp_config)
Override defaults in a safe way
A lot of options in nvim-cmp
use tables inside tables, and so the "safest way" to add your own config is inside .defaults.cmp_config().
Here is an example that disables the autocomplete feature.
local cmp_config = require('lsp-zero').defaults.cmp_config({
completion = {
autocomplete = false
}
})
This way .completion.autocomplete
is merged with lsp-zero's defaults. So anything else that lsp-zero sets inside the completion
property will remain.
If you did this instead?
local cmp_config = require('lsp-zero').defaults.cmp_config({})
cmp_config.completion = {
autocomplete = false
}
Then your new completion
config will delete everything lsp-zero sets in the completion
property.
Setup completion sources
There are a few "completion sources" that lsp-zero recommends. And actually if you install them they will be configured automatically:
- cmp-nvim-lsp: Shows data send by the language server.
- cmp-buffer: It provides suggestions based on the current file.
- cmp-path: Gives suggestions based on the filesystem.
- cmp_luasnip: It shows available snippets in the suggestions.
- friendly-snippets: Is the recommended snippet collection.
If you want full control the configuration for the completion sources, override the sources
option outside of .defaults.cmp_config()
. Like this.
local cmp = require('cmp')
local cmp_config = require('lsp-zero').defaults.cmp_config({})
cmp_config.sources = {
---
-- Here you configure your completion sources
---
}
cmp.setup(cmp_config)
vim-style keybindings
The default keybindings lsp-zero uses in nvim-cmp include tab completion and few other keys that aren't standard in vim. I know not everyone is a fan of that. You can override all the keybindings in lsp-zero, replace them with a preset from cmp
.
local cmp = require('cmp')
local cmp_config = require('lsp-zero').defaults.cmp_config({})
cmp_config.mapping = cmp.mapping.preset.insert({
['<C-Space>'] = cmp.mapping.complete(),
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
})
cmp.setup(cmp_config)
Complete example
If you are curious, let me show you how to implement every feature the recommended
preset enables.
---
-- Configure LSP servers
---
require('lsp-zero').extend_lspconfig()
require('mason').setup()
require('mason-lspconfig').setup()
local get_servers = require('mason-lspconfig').get_installed_servers
for _, server_name in ipairs(get_servers()) do
require('lspconfig')[server_name].setup({})
end
---
-- Diagnostic config
---
require('lsp-zero').set_sign_icons()
vim.diagnostic.config(require('lsp-zero').defaults.diagnostics({}))
---
-- Snippet config
---
require('luasnip').config.set_config({
region_check_events = 'InsertEnter',
delete_check_events = 'InsertLeave'
})
require('luasnip.loaders.from_vscode').lazy_load()
---
-- Autocompletion
---
vim.opt.completeopt = {'menu', 'menuone', 'noselect'}
local cmp = require('cmp')
local cmp_config = require('lsp-zero').defaults.cmp_config({})
cmp.setup(cmp_config)
It looks like a lot but it's just 18 lines of code. Without lsp-zero the equivalent would take 150+ lines of code. Just saying.
Conclusion
Hopefully I've demonstrated lsp-zero can coexists peacefully with other plugins. It doesn't have to be this "super plugin" that needs 11 dependencies to work. You can use what you need and configure it however you want. You can even remove things piece by piece until you don't need it anymore.
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 ☕.
Top comments (0)