Neovim's Built-in LSP Client Architecture Note
Neovim's vim.lsp handles JSON‑RPC and diagnostics but ships no server configs. Minimal design: core vim.lsp plus nvim‑lspconfig recipes, completion only if needed. Servers run as your user, so pin versions and gate per‑project enablement.
14 Jun 2026, 12:34 UTC

The Problem: Choosing an LSP Client Layer
Neovim 0.5 introduced vim.lsp, a JSON‑RPC client baked into core. It handles capability negotiation, request lifecycle, diagnostics and buffer attachment. It deliberately ships no per‑language server configuration, leaving that to user config or community plugins such as nvim‑lspconfig. The useful takeaway is that a minimal, low‑overhead design can be achieved by using the built‑in client, pulling in nvim‑lspconfig for server recipes, and adding a completion plugin only when in‑editor completion is required.
Requirements Driving the Design
- Zero external runtime dependency for basic editing
- Per‑buffer server attachment with correct project root
- Capability‑driven UI gating on what the server advertises
- Observable health for operators
Smallest Suitable Design
Enable the built‑in client, use nvim‑lspconfig for launch recipes, attach per buffer via root detection, and add a completion plugin only if required.
Minimal Init Snippet
-- lua/config/lsp.lua
local lspconfig = require('lspconfig')
local capabilities = require('cmp_nvim_lsp').default_capabilities()
lspconfig.ts_ls.setup({
capabilities = capabilities,
root_dir = lspconfig.util.root_pattern('package.json','tsconfig.json','.git'),
single_file_support = true,
})
lspconfig.rust_analyzer.setup({
capabilities = capabilities,
root_dir = lspconfig.util.root_pattern('Cargo.toml','.git'),
})
vim.api.nvim_create_autocmd('LspAttach',{
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
if not client then return end
local buf = args.buf
if client.server_capabilities.hoverProvider then
vim.keymap.set('n','K',vim.lsp.buf.hover,{buffer=buf,desc='LSP hover'})
end
if client.server_capabilities.renameProvider then
vim.keymap.set('n','rn',vim.lsp.buf.rename,{buffer=buf,desc='LSP rename'})
end
end,
})
Run in ~/.config/nvim/. Adjust root patterns per project. Expected check: open a .ts file and run :checkhealth vim.lsp.
Trust and Data Boundaries
Each language server is a separate local process launched with the user’s privileges, typically communicating over stdio. It can read any file the user can read and receives editor configuration. Opening an untrusted repository therefore executes third‑party code selected by that project’s tooling assumptions, so server provenance, version pinning and per‑project enablement belong in the design.
Capability Negotiation and Degradation
At initialize the client and server exchange capabilities. A server that lacks hover or rename simply yields no behavior, so keymaps must gate on resolved client capabilities rather than assume them.
Operational Checks
Run :checkhealth vim.lsp to see attachment status, root dir and capabilities. :LspInfo comes from nvim‑lspconfig and reports the same information. A scripted check can launch Neovim headless and wait for client attachment within a timeout.
Failure Modes
- A crashed or hung server leaves buffers attached but silent; the editor stays responsive because requests are asynchronous.
- Wrong root‑dir detection silently attaches the wrong project scope.
- Server upgrades can change capabilities or config schema.
- Neovim core API renames across releases can break setup scripts.
Conditions That Would Change the Design
- If richer UI conventions, marketplace‑style server management, or cross‑editor standardization are required, a standalone client such as
coc.nvimmay fit. - If only diagnostics and hover are needed,
nvim‑lspconfigcan be skipped and a few lines ofvim.lsp.startorvim.lsp.configsuffice.
Verification Checklist
- Run
:checkhealth vim.lspand confirm the LSP section reports no errors. - Open a file of a language whose server you configured, then run
:LspInfoto confirm a client is attached with the expected root directory. - Exercise a capability end‑to‑end (hover or rename) and a negative case (a server lacking that capability) to confirm capability gating.
- Kill the server process mid‑session and confirm the editor stays responsive and diagnostics simply stop.
- Diff your setup against the Neovim news for your pinned version to catch renamed
vim.lspAPIs.
Limitations
vim.lsp APIs are version‑sensitive across 0.8–0.11; pin a Neovim version and verify against its news file. Feature coverage differs per server, so evaluate multiple servers. Re‑check config API and health command names before publication.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.