Attach a language server to Neovim using the built-in LSP client
Configure Neovim's built-in LSP client in init.lua, attach a server to buffers, and map buffer-local actions without external plugins.
18 Jul 2025, 00:19 UTC

The problem and what changes
You open a source file in Neovim and gd does nothing. There are no underlines for undefined variables, and renaming a symbol means a manual search-and-replace. Neovim ships with a built-in language server protocol (LSP) client in the vim.lsp module. LSP is a standard that lets an editor communicate with language-specific tools for definitions, references, diagnostics, and formatting. You can attach a server directly from init.lua without installing an LSP plugin.
The useful takeaway: declare one server per language, enable it with vim.lsp.enable, and create buffer-local keymaps only after the server successfully attaches to a buffer.
Desired outcome
- A configured language server starts and attaches to buffers whose filetype it supports.
- Normal-mode keys such as
gd,gr,rn, andcarun LSP actions on the current buffer. - Diagnostics appear in the sign column and as virtual text.
- Buffer formatting uses one chosen formatter per language.
Prerequisites
- Neovim 0.10 or later. This guide uses the declarative
vim.lsp.configandvim.lsp.enableAPI; older releases rely on the lower-levelvim.lsp.start_client. - A language server binary available on
PATH, for examplepyright,gopls, or another server for your language. Neovim does not install servers itself. - Write access to your
init.lua.
Configure and enable a language server
Add the following to init.lua. No elevated permissions are required unless the server binary itself needs them.
vim.lsp.config('pyright', { cmd = { 'pyright-langserver', '--stdio' }, root_markers = { 'pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', '.git' },})vim.lsp.enable('pyright')vim.lsp.config registers a named server configuration. cmd is the command Neovim runs to start the server. root_markers are files Neovim looks for to identify a project root. vim.lsp.enable tells Neovim to start that server for matching buffers.
Map buffer-local actions in on_attach
Define on_attach so keymaps are created only when a server binds to a buffer. Buffer-local mappings apply to that buffer alone.
local function on_attach(client, bufnr) local function map(keys, action, desc) vim.keymap.set('n', keys, action, { buffer = bufnr, desc = desc }) end map('gd', vim.lsp.buf.definition, 'LSP definition') map('gr', vim.lsp.buf.references, 'LSP references') map('rn', vim.lsp.buf.rename, 'LSP rename') map('ca', vim.lsp.buf.code_action, 'LSP code action') map('f', function() vim.lsp.buf.format { async = true } end, 'LSP format buffer') vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'endvim.lsp.config('pyright', { cmd = { 'pyright-langserver', '--stdio' }, root_markers = { 'pyproject.toml', '.git' }, on_attach = on_attach,})vim.lsp.enable('pyright')vim.lsp.buf.definition, references, rename, and code_action run asynchronously and operate on the current buffer. vim.lsp.buf.format { async = true } formats without blocking the UI; synchronous formatting can freeze Neovim while the server responds.
Risk: if the server reattaches to the same buffer, on_attach can run again and duplicate buffer-local mappings. Clear existing mappings first or guard the block with a buffer variable.
Control diagnostics and formatting
Diagnostics are messages from the language server, such as undefined variables or type mismatches. Configure how they display:
vim.diagnostic.config({ virtual_text = true, signs = true, underline = true, update_in_insert = false, severity_sort = true,})virtual_text shows the message at the end of the line. signs places markers in the sign column. Avoid attaching more than one formatting server to the same buffer; multiple formatters can issue conflicting edits to the same file.
Verify the attachment
- Open a file of the matching type, for example
:e example.py. - Run
:LspInfo. Confirm the configured server appears under clients attached to the current buffer. - Place the cursor on a symbol and press
gd. Expect a jump to the definition, not a "no handler" message. - Introduce a syntax error and confirm a diagnostic sign or virtual text appears.
- If attachment fails, run
:checkhealthfor provider and executable errors, then inspect:lsp logfor server stderr and stdout.
Rollback and recovery
Editing init.lua changes state. To roll back, comment out or remove the vim.lsp.config and vim.lsp.enable block, then restart Neovim. To stop a running server without restarting, run :LspStop and check :LspInfo to confirm the client detached.
Limitations
- Neovim does not install language servers. Install the binary separately or through an external manager such as Mason.
- Choose one formatter per language per buffer to prevent conflicting edits.
- Blocking LSP requests freeze the UI. Use async helpers such as
vim.lsp.buf.format { async = true }instead of waiting synchronously. - If a server fails to start, inspect
:lsp logand:checkhealthbefore changing configuration.
Practical check: open a supported filetype, run :LspInfo, and confirm the expected server is listed as attached to the current buffer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.