Use Neovim's Built-in LSP Without a Plugin Manager
Configure hover, definitions, and diagnostics with vim.lsp and vim.diagnostic before deciding whether you need an LSP plugin.
02 Jul 2026, 20:34 UTC

A clean Neovim opens with no language intelligence
You install Neovim, open a Python file, and press K expecting hover documentation. Nothing happens. There are no underlines under undefined variables, and gd does not jump to a definition. The missing piece is not always a plugin. Neovim 0.10 and later ship with a built-in language server protocol client (vim.lsp) and a diagnostic engine (vim.diagnostic). You can attach a language server directly from your config and get hover, go-to-definition, references, rename, and diagnostics without installing an LSP plugin manager.
This is useful if you want to understand what Neovim provides natively before adding dependencies, or if you maintain a minimal config across machines.
What the built-in pieces actually do
vim.lsp is Neovim's native LSP client. It starts a language server process—such as pyright for Python or clangd for C/C++—and sends protocol requests for hover, definitions, and code actions. vim.diagnostic is the separate engine that renders those server messages as virtual text, gutter signs, underlines, and floating windows. It can also aggregate diagnostics from sources other than LSP.
A language server is a standalone program that speaks the Language Server Protocol. Neovim does not install or manage it; you must provide the binary, typically by putting it on $PATH or listing it in cmd.
Worked example: attach a server manually
Add this to ~/.config/nvim/init.lua (or after/plugin/lsp.lua). It requires no special permissions beyond writing your own config. Replace pyright with the server binary for your language.
local function attach_pyright()
local root = vim.fs.root(0, { '.git', 'pyproject.toml', 'setup.py', 'requirements.txt' })
if not root then
return
end
local config = {
cmd = { 'pyright' },
root_dir = root,
name = 'pyright',
}
local client_id = vim.lsp.start(config)
if not client_id then
vim.notify('pyright: failed to start', vim.log.levels.ERROR)
return
end
vim.lsp.buf_attach_client(0, client_id)
end
vim.api.nvim_create_autocmd('FileType', {
pattern = 'python',
callback = attach_pyright,
})
Restart Neovim or source your config, then open a Python file inside a project containing one of the root markers. The expected check is :LspInfo: it should list pyright as an active client with the current buffer attached. If the client is absent, open :messages for startup errors. A common risk is that pyright is not installed or not executable on $PATH, in which case vim.lsp.start returns nil and the autocmd notifies you.
Once attached, try these built-in functions from a Python buffer:
:lua vim.lsp.buf.hover()— show documentation under the cursor:lua vim.lsp.buf.definition()— jump to the definition:lua vim.lsp.buf.references()— list references:lua vim.lsp.buf.rename()— rename a symbol:lua vim.lsp.buf.code_action()— show available code actions
You can map them to keys in the same autocmd if you prefer not to type Lua commands.
Configure diagnostics before you need them
The LSP server may report diagnostics, but Neovim does not show them until you enable rendering. Add this early in your config:
vim.diagnostic.config({
virtual_text = true,
signs = true,
underline = true,
update_in_insert = false,
severity_sort = true,
float = {
border = 'rounded',
source = 'if_many',
},
})
virtual_text places inline messages next to offending lines. signs adds gutter markers. float controls the popup shown by :lua vim.diagnostic.open_float(). Run that command on a line with a diagnostic to confirm the pipeline is active. If nothing appears, the server may not be publishing diagnostics for that file, or the diagnostic may be filtered by severity.
Trade-offs and where the built-in client stops
The built-in client is a foundation, not a complete IDE. It does not provide a completion menu, snippet engine, or symbol picker. Those still depend on community plugins such as nvim-cmp, blink.cmp, or telescope.nvim. Because capabilities are negotiated per server, calling vim.lsp.buf.format() silently no-ops if the server does not advertise formatting support.
| Feature | Built-in vim.lsp | Typical plugin addition |
|---|---|---|
| Hover, definition, references | Yes | Not required |
| Diagnostics display | Yes | Not required |
| Completion menu | No | nvim-cmp, blink.cmp |
| Symbol or file picker | No | telescope.nvim, fzf-lua |
| Server installation | No | mason.nvim |
You also lose nothing by starting small: if you later add a plugin, it can wrap the same vim.lsp clients you already attach.
Verify the result and decide your next step
- Restart Neovim and open a supported source file.
- Run
:LspInfoand confirm the server is listed and attached to the buffer. - Introduce a deliberate error and run
:lua vim.diagnostic.open_float()to see the diagnostic popup. - Check
:messagesif the server fails to launch.
If hover and diagnostics work, you have a working language intelligence setup with no plugin manager. Add completion or file navigation plugins only when the built-in behavior no longer fits your workflow. To revert, comment out or remove the autocmd and vim.diagnostic.config block; no system state is changed beyond your user config.