Make lsp-zero.nvim coexists with other plugins instead of controlling them
Tags: neovim
Published: 2023-01-14 02:39:18

---
title: Make lsp-zero.nvim coexists with other plugins instead of controlling them
published: true
description: Learn how to use lsp-zero's functions to configure LSP related features in neovim
tags: #neovim
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-01-14 02:35 +0000
---

I created [lsp-zero](https://github.com/VonHeikemen/lsp-zero.nvim) to help people reduce the boilerplate code needed to use [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) and [nvim-cmp](https://github.com/hrsh7th/nvim-cmp). At first it was a pre-made config wrapped in a plugin, but now is more like a collection of functions. In its current state lsp-zero can help you enhance your config instead of controlling it. Let's see what we can do with it.

## Configuring LSP servers

lsp-zero will integrate nvim-cmp and nvim-lspconfig when you require the module for the first time. This means the call to `require("lsp-zero")` [will take care of a few things for you](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/guides/what-require-lsp-zero-does.md). And after that you are free to use `lspconfig` to setup each language server. This a valid usage.

```lua
require('lsp-zero')

-- Call the language servers you have installed
require('lspconfig').tsserver.setup({})
require('lspconfig').eslint.setup({})
require('lspconfig').rust_analyzer.setup({})
```

In this particular example we don't customize any of these servers, we are just using the defaults lspconfig provides. And if this is what you actually want, you can use the function [.setup_servers()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#setup_serverslist-opts) with a list of language servers.

```lua
require('lsp-zero').setup_servers({'tsserver', 'eslint', 'rust_analyzer'})
```

### Disable automatic integration

If you want to prevent lsp-zero from doing things automatically, you can set the following global variables before using lsp-zero:

```lua
vim.g.lsp_zero_extend_cmp = 0
vim.g.lsp_zero_extend_lspconfig = 0
```

Alternatively, you can require the module `lsp-zero.api`. This will return all the currently supported functions and avoid the side effects.

And then you can control when the integration happens using the functions [.extend_lspconfig()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#extend_lspconfig) and [.extend_cmp()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#extend_cmpopts).

### Adding keybindings

At this point you would want to create the keybindings that use the wonderful features the language servers provide. In nvim-lspconfig's documentation recommend you use the `LspAttach` autocommand, so lsp-zero has a convenience method to help you do that.

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.on_attach(function(client, bufnr)
  local opts = {buffer = bufnr}

  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)

  vim.keymap.set('n', 'gl', '<cmd>lua vim.diagnostic.open_float()<cr>', opts)
  vim.keymap.set('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<cr>', opts)
  vim.keymap.set('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<cr>', opts)
end)


require('lspconfig').tsserver.setup({})
require('lspconfig').eslint.setup({})
require('lspconfig').rust_analyzer.setup({})
```

But what if I told you all those keybindings are already in lsp-zero? You can just create them using the function [.default_keymaps()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#default_keymapsopts).

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.on_attach(function(client, bufnr)
  lsp_zero.default_keymaps({buffer = bufnr})
  local opts = {buffer = bufnr}

  vim.keymap.set('n', 'gd', '<cmd>Telescope lsp_definitions<cr>', opts)
  vim.keymap.set('n', 'gi', '<cmd>Telescope lsp_implementations<cr>', opts)
  vim.keymap.set('n', 'gr', '<cmd>Telescope lsp_references<cr>', opts)
end)

require('lspconfig').tsserver.setup({})
require('lspconfig').eslint.setup({})
require('lspconfig').rust_analyzer.setup({})
```

Not only I can use the keybindings in lsp-zero, I can override them if I want. In this example I replaced a few Neovim functions with [Telescope](https://github.com/nvim-telescope/telescope.nvim) commands that do something similar.

### About on_attach

The `on_attach` function acts like a "global hook". It'll run every time a language server gets attached to a buffer, even if you add a custom `on_attach` function to a particular server.

You can try it.

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.on_attach(function(client, bufnr)
  lsp_zero.default_keymaps({buffer = 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 `.on_attach()` then the one on tsserver.

### Creating your own default config

Sometimes **you** need to add arguments to each language server. Maybe you want to enable a feature, or add arguments required by a plugin. I know that process is a little annoying so lsp-zero provides a function called [.set_server_config()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#set_server_configopts), with it you can add a configuration to each server you configure with lspconfig.

Here is an example that adds the options [nvim-ufo](https://github.com/kevinhwang91/nvim-ufo) needs to pass to the each language server. It will also disable semantic highlights. And finally disable single file support.

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.on_attach(function(client, bufnr)
  lsp_zero.default_keymaps({buffer = bufnr})
end)

lsp_zero.set_server_config({
  single_file_support = false,
  on_init = function(client)
    client.server_capabilities.semanticTokensProvider = nil
  end,
  capabilities = {
    textDocument = {
      foldingRange = {
        dynamicRegistration = false,
        lineFoldingOnly = true
      }
    }
  }
})

require('lspconfig').tsserver.setup({})
require('lspconfig').eslint.setup({})
require('lspconfig').rust_analyzer.setup({})
```

### Diagnostic icons

You can change the default text Neovim shows in the sign column. The default behaviour will show letters like `E` or `W`, but we can change it to whatever we want using [.set_sign_icons()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#set_sign_iconsopts).

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.on_attach(function(client, bufnr)
  lsp_zero.default_keymaps({buffer = bufnr})
end)

lsp_zero.set_sign_icons({
  error = '✘',
  warn = '▲',
  hint = '⚑',
  info = '»',
})

require('lspconfig').tsserver.setup({})
```

### Enable format on save

We can do this in multiple ways but let me just show the one that looks easier.

We can call the function [.async_autoformat()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#async_autoformatclient-bufnr-opts) inside the `on_attach` function and lsp-zero will setup format on save in every file the language server is active.

```lua
local lsp_zero = require('lsp-zero')

require('lspconfig').tsserver.setup({
  on_attach = function(client, bufnr)
    lsp_zero.async_autoformat(client, bufnr)
  end,
})
```

> Note: if you want synchronous autoformatting you can use [.buffer_autoformat()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#buffer_autoformatclient-bufnr-opts).

Now you might be thinking "oh, great I can use it in the global on_attach and enable it for every language server." That would work fine... until you have multiple language servers attached to a single file. Then you would get into the game "which language server is doing this to my code?" and you don't want that.

## Autocompletion

When you call `require('lsp-zero')` then lsp-zero will try to add the "essential" options to have a working autocompletion menu. You will be able to use Neovim's default keybindings to control the completion menu. And you'll also get suggestions from your active language servers.

What if you wanted to customize nvim-cmp? Easy, just call the `cmp` module.

```lua
local cmp = require('cmp')
local cmp_action = require('lsp-zero').cmp_action()

cmp.setup({
  window = {
    completion = cmp.config.window.bordered(),
    documentation = cmp.config.window.bordered(),
  },
  mapping = cmp.mapping.preset.insert({
    -- `Enter` key to confirm completion
    ['<CR>'] = cmp.mapping.confirm({select = false}),

    -- Ctrl+Space to trigger completion menu
    ['<C-Space>'] = cmp.mapping.complete(),

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

    -- Navigate between snippet placeholders
    ['<C-f>'] = cmp_action.luasnip_jump_forward(),
    ['<C-b>'] = cmp_action.luasnip_jump_backward(),
  }),
})
```

In lsp-zero's documentation you can find a whole section about how to customize nvim-cmp: [Autocomplete.md](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/autocomplete.md).

## Lesser known features

### Setup a server without lspconfig

If for some reason you don't have access to `lspconfig` or just don't want to install it, you can setup a language server manually using  the function [.new_client()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#new_clientopts).

To setup a language server you will need to know the command that start the server, the filetypes where it should be active, and a function that detects the "root directory" of your project.

Here is an example using the lua language server.

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.on_attach(function(client, bufnr)
  lsp_zero.default_keymaps({buffer = bufnr})
end)

lsp_zero.new_client({
  name = 'lua_ls',
  cmd = {'lua-language-server'},
  filetypes = {'lua'},
  root_dir = function()
    return lsp_zero.dir.find_first({'.luarc.json'})
  end,
})
```

The function in `root_dir` will be executed everytime Neovim opens a lua file. If it finds a file called `.luarc.json` in the current folder or any parent folder then it will attach the language server to that buffer.

Do note that `.new_client()` is a thin wrapper over a built-in Neovim function called [vim.lsp.start()](https://neovim.io/doc/user/lsp.html#vim.lsp.start()), so it takes the same arguments (plus `filetypes` and `root_dir`, I added those).

### Get some basic completions

If for some reason you don't have access to nvim-cmp or don't want to install it, you can use the function [.omnifunc.setup()](https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/api-reference.md#omnifuncsetupopts) to configure Neovim's built-in completion mechanism.

So `omnifunc` is a vim term, is a function that you can create and then put into a variable. This is how vim lets users create their own custom completions. Neovim has an implementation that lets you get completions from the active language servers. The default keybinding for this is `ctrl-x + ctrl-o`.

Since Neovim's default are not intuitive for a lot of people lsp-zero has a way to modify it. Here are some examples.

* Setup tab completion

You can use the `Tab` key to trigger the completions when the cursor is in the middle of a word. And when the completion menu is visible, you can navigate between the items using `Tab` and `Shift + tab`.

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.omnifunc.setup({
  tabcomplete = true,
  use_fallback = true,
  update_on_delete = true,
})
```

* Toggle completion with a custom key

You can specify a keybinding to toggle the completion menu.

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.omnifunc.setup({
  trigger = '<C-Space>',
  use_fallback = true,
  update_on_delete = true,
})
```

* Autocomplete

There is an option to get autocomplete but I must warn you the implementation is very naive. Is basically like pressing `ctrl-x + ctrl-o` after every single letter in a word.

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.omnifunc.setup({
  autocomplete = true,
  use_fallback = true,
  update_on_delete = true,
})
```

All of this means that you can have a basic setup just using lsp-zero. No extra plugins are needed.

#### An example without dependencies

```lua
local lsp_zero = require('lsp-zero')

lsp_zero.on_attach(function(client, bufnr)
  lsp_zero.default_keymaps({buffer = bufnr})
end)

lsp_zero.omnifunc.setup({
  tabcomplete = true,
  use_fallback = true,
  update_on_delete = true,
})

lsp_zero.new_client({
  name = 'lua_ls',
  cmd = {'lua-language-server'},
  filetypes = {'lua'},
  root_dir = function()
    return lsp.dir.find_first({'.luarc.json'})
  end,
  settings = {
    Lua = {
      -- Neovim's omnifunc doesn't support snippets
      completion = {keywordSnippet = 'Disable'},
    }
  }
})
```

## 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 just ignore the rest.

---

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 ☕](https://www.buymeacoffee.com/vonheikemen).

[![buy me a coffee](https://res.cloudinary.com/vonheikemen/image/upload/v1618466522/buy-me-coffee_ah0uzh.png)](https://www.buymeacoffee.com/vonheikemen)