Skip to content
Go back

Migrating to Neovim >0.10 LSP core

Published:  at  16:59

Table of contents

Open Table of contents

Why Migrate?

Neovim 0.10 (and later versions, including the upcoming 0.11) brings Language Server Protocol (LSP) support into its core. This means:

External Link Globe :help lsp

Coming from Manual Configuration

Previously, you likely had custom on_attach and capabilities functions defined for each LSP server setup.

With the new core LSP, you can define global defaults using vim.lsp.config('*', { ... }). When you later configure or enable a specific language server (e.g., lua_ls), these global settings will be automatically merged with any server-specific settings.

External Link Globe :help vim.lsp.config

Why We Still Need nvim-lspconfig

While Neovim core now handles LSP management, it doesn’t come pre-packaged with configurations for every possible language server. Without nvim-lspconfig, you would need to manually write the specific setup code (like command paths, root detection logic, default settings) for each language server you want to use. nvim-lspconfig provides these community-maintained configurations, saving you significant effort.

How It Works

The Runtime Path (RTP)

The core LSP module expects language server configurations to live in a specific directory structure within your Neovim config’s runtime path (RTP). Specifically, it looks for Lua modules under lsp/ within your config directory.

For example, configuration for lua_ls would be expected at ~/.config/nvim/lsp/lua_ls.lua (assuming ~/.config/nvim is your stdpath('config')). You can see examples of this structure in the nvim-lspconfig plugin’s repository.

With lspconfig

When you initialize nvim-lspconfig (typically via require('lspconfig')), it programmatically adds its internal lsp/ directory (containing all its server configurations) to Neovim’s runtimepath. This makes all its predefined server setups available to the core vim.lsp module.

As a result, you can simply call vim.lsp.enable('lua_ls') (or any other server supported by lspconfig), and Neovim will find the necessary configuration within lspconfig’s runtime path contribution. You don’t need to provide the base configuration yourself.

Why We Still Want Our Own ~/.config/nvim/lsp

While nvim-lspconfig provides excellent defaults, you’ll often want to customize server behavior. This could involve:

By creating your own configuration file for a server within your personal ~/.config/nvim/lsp/ directory (e.g., ~/.config/nvim/lsp/gopls.lua), you can provide these overrides. As explained next, these settings will merge with the defaults from nvim-lspconfig.

Why This Works: Merging via Runtime Path

Because nvim-lspconfig adds its own lsp/ directory to Neovim’s runtimepath, and your personal ~/.config/nvim/lsp/ directory is also part of the runtimepath (usually appearing later), Neovim effectively loads configurations from both locations.

When vim.lsp.enable('gopls') (or similar) is called, it implicitly triggers vim.lsp.config() for that server. Since configurations are found in both lspconfig’s path and your path, vim.lsp.config() is effectively called twice for the same server:

-- Simplified illustration
vim.lsp.config('gopls', gopls_opts_from_lspconfig) -- Loaded first from lspconfig's RTP entry
vim.lsp.config('gopls', gopls_opts_from_our_config) -- Loaded second from our config's RTP entry

Minimum Working Example

Here’s a simplified example demonstrating how these pieces fit together using lazy.nvim as the plugin manager:

-- lua/user/lazy.lua (Plugin setup)
require("lazy").setup({
  spec = {
    -- Ensure lspconfig is loaded; lazy=true defers loading until needed
    { "neovim/nvim-lspconfig", lazy = true },
    -- Add other plugins here...
  },
})
-- lua/lsp.lua (Main LSP setup file)
-- Define global LSP settings using vim.lsp.config('*', ...)
-- These apply to ALL language servers unless overridden.
vim.lsp.config("*", {
  flags = {
    -- Debounce settings can improve performance
    debounce_text_changes = 150,
  },
  -- Example: Define common on_attach or capabilities here if desired
})

-- Initialize lspconfig to add its configurations to the runtime path
require("lspconfig")

-- List the servers you want to enable
local servers = {
  "lua_ls",
  "gopls",
  -- Add other servers like "tsserver", "pyright", etc.
}

-- Enable the listed servers.
-- This triggers Neovim to look for configurations in the runtime path
-- (both from lspconfig and your custom ~/.config/nvim/lsp/)
vim.lsp.enable(servers)

-- Optional: Add global keymaps for LSP functions here,
-- perhaps within a general on_attach function passed to vim.lsp.config('*', ...)
-- ~/.config/nvim/lsp/gopls.lua (Custom overrides for gopls)
-- This file MUST be located at 'lsp/gopls.lua' relative to your config root.
-- It should return a Lua table containing the settings to merge.
return {
  -- Example: Override capabilities (ensure your LSP client supports this)
  -- capabilities = {
  --   workspace = {
  --     didChangeWatchedFiles = { dynamicRegistration = false },
  --   },
  -- },
  settings = {
    gopls = {
      -- Example: Enable all inlay hints for gopls
      hints = {
        assignVariableTypes = true,
        compositeLiteralFields = true,
        compositeLiteralTypes = true,
        constantValues = true, -- Example: Also enable constant value hints
        functionTypeParameters = true,
        parameterNames = true,
        rangeVariableTypes = true,
      },
      -- Example: Add build tags
      buildFlags = { "-tags=e2e,integration" },
      -- Add other gopls specific settings here
    },
  },
  -- Example: Add a custom on_attach specifically for gopls
  -- on_attach = function(client, bufnr)
  --   print("Attaching gopls with custom settings!")
  --   -- Add gopls-specific keymaps or logic here
  -- end,
}
-- init.lua (Or your main entry point)
-- Load plugin manager first
require("user.lazy")
-- Load your LSP setup
require("lsp")
-- Load other configurations...

Notice in the example, we don’t explicitly call vim.lsp.config('gopls', ...) within our lua/lsp.lua setup file. We only define the overrides in lsp/gopls.lua.

This works because when vim.lsp.enable('gopls') runs, Neovim scans the runtimepath for lsp/gopls.lua modules. It finds the one provided by nvim-lspconfig and the one in your personal config directory. It loads both, and the vim.lsp.config mechanism automatically merges the settings, with your configuration taking precedence.



Previous Post
Using Git as S3