LSP Configuration in Neovim 0.11

  • neovim, vim, lsp
  • 4
  • 4
  • finished

Neovim 0.11 adds a new function to configure language servers: vim.lsp.config() and allows splitting configurations in specialised lua/<name>.lua files. This is nice because I can now move a bunch of code from ftplugins to a different place. It doesn’t differ a lot from the old approach with vim.lsp.start(), (sidenote: In fact, vim.lsp.config() is a wrapper for vim.lsp.start() which allows us to share, merge and selectively enable configs) but it feels right. Yay!

vim.lsp.config("name", {...}) lets us to define LSP configuration. Name can be anything: name of language server, file type, your favourite fruit… It’s just a key under which we’ll store a bunch of configuration options, which we’ll later enable with vim.lsp.enable("name").

Neovim first (sidenote: :h lsp-config) reads a global configuration for all language servers which we configure for a special name: '*'. I like to put there on_attach function which changes a configuration of buffers for which LSP server runs.

-- init.lua
vim.lsp.config('*', {
    on_attach = function(client, bufnr)
        -- overwrites omnifunc/tagfunc set by some Python plugins to the
        -- default values for LSP
        vim.api.nvim_set_option_value('omnifunc', 'v:lua.vim.lsp.omnifunc', {buf = bufnr})
        vim.api.nvim_set_option_value('tagfunc', 'v:lua.vim.lsp.tagfunc', {buf = bufnr})

        vim.keymap.set({'n', 'v'}, 'K', vim.lsp.buf.hover, { buffer = bufnr })
        vim.keymap.set({'n', 'v'}, '<F4>', vim.lsp.buf.format, { buffer = bufnr })
        vim.keymap.set('n', 'gu', vim.lsp.buf.references, { buffer = bufnr })
        vim.keymap.set('n', 'gr', vim.lsp.buf.rename, { buffer = bufnr })

        vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
            vim.lsp.diagnostic.on_publish_diagnostics, {
                signs = true,
                underline = true,
                virtual_text = true
            }
        )
    end
})

Then Neovim takes configuration tables returned by <runtimepath>/lsp/<name>.lua and merges them all together and then merges them on top of a global configuration from above. It means that options in <name>.lua overwrite global options.

Sometimes it makes sense to extend defaults instead:

-- lua/pylsp.lua
return {
    name = "pylsp",
    on_attach = function(client, bufnr)
        -- extend global configuration
        vim.lsp.config['*'].on_attach(client, bufnr)

        -- "fix" gq
        vim.api.nvim_buf_set_option(bufnr, "formatexpr", "");
    end
}

Oh, and if you don’t know what’s “runtimepath”, assume it’s ~/.config/nvim. (sidenote: :set runtimepath? shows the full path)

Then Neovim takes configurations set elsewhere and merges them on top of whatever it has so far. Basically, this means that any explicit uses of vim.lsp.config() in init.lua will take precedence.

We may now enable LSP configuration. I like to maintain a mapping of configuration names to executable names an enable them conditionally for servers which exist on the system. This makes my Neovim configuration portable to the systems without installed language servers.

-- init.lua

local lsp_servers = {
    pylsp = "pylsp",
    godot = "godot",
    lua = "lua-language-server",
    cpp = "clangd"
}

for server_name, lsp_executable in pairs(lsp_servers) do
    if vim.fn.executable(lsp_executable) == 1 then
        vim.lsp.enable(server_name)
    end
end