neovim/neovim
LSP client
Purpose
Neovim ships a built-in Language Server Protocol client. It is implemented entirely in Lua under runtime/lua/vim/lsp/, with no editor-core code beyond what was already needed for buffers, autocmds, and extmarks. The client speaks LSP version 3.17, supports stdio/tcp/pipe transports, attaches to per-buffer client objects, and exposes the protocol's results via vim.lsp.*, vim.diagnostic, and a handful of well-known autocmds.
Directory layout
runtime/lua/vim/lsp/
├── _capability.lua Capability negotiation
├── _changetracking.lua didChange tracking per buffer
├── _folding_range.lua Folding via LSP
├── _meta/ LuaCATS type stubs
├── _meta.lua
├── _snippet_grammar.lua LSP snippet parser
├── _tagfunc.lua gd / `tagfunc` integration
├── _transport.lua Stdio/socket transports
├── _watchfiles.lua workspace/didChangeWatchedFiles
├── buf.lua (~52k) User-facing methods (definition, references, format, ...)
├── client.lua (~52k) The Client object: lifecycle, send/receive
├── codelens.lua vim.lsp.codelens
├── completion.lua (~42k) vim.lsp.completion (autotrigger)
├── diagnostic.lua Bridge to vim.diagnostic
├── document_color.lua vim.lsp.document_color
├── handlers.lua (~26k) Default response handlers
├── health.lua :checkhealth lsp
├── inlay_hint.lua Inlay hint extmarks
├── inline_completion.lua vim.lsp.inline_completion
├── linked_editing_range.lua Synchronous edits across mirror ranges
├── log.lua LSP-specific log
├── on_type_formatting.lua Format-on-type
├── protocol.lua (~58k) Protocol constants, types, methods
├── rpc.lua (~22k) JSON-RPC framing over a transport
├── semantic_tokens.lua (~35k) Semantic-tokens client
├── sync.lua TextDocumentSyncKind handling
└── util.lua (~81k) Position/range/uri helpers, location previewsThe top-level entry point is runtime/lua/vim/lsp.lua (~56k bytes), which exposes vim.lsp.* and orchestrates the components above.
Key abstractions
| Type / function | File | Description |
|---|---|---|
Client |
lsp/client.lua |
One language-server connection. Has handlers, capabilities, attached buffers, request queue. |
vim.lsp.start(config) |
lsp.lua |
Start (or reuse) a client and attach it to the current buffer. |
vim.lsp.config() |
lsp.lua |
Per-server configuration store. New in v0.11. |
vim.lsp.enable(name) |
lsp.lua |
Activate a registered server. |
vim.lsp.buf.<method> |
lsp/buf.lua |
User-facing operations (definition, references, rename, format, ...). |
vim.lsp.handlers |
lsp/handlers.lua |
The default response handlers. Override to customize. |
| Transport | lsp/_transport.lua |
Spawned-process or socket-based JSON-RPC transport. |
How it works
sequenceDiagram
participant User
participant Buf as Buffer
participant Client as vim.lsp.Client
participant Tx as Transport (stdio)
participant Srv as Language server
User->>Buf: open file.go
Buf->>Client: BufRead → vim.lsp.start
Client->>Tx: spawn gopls
Client->>Srv: initialize {capabilities}
Srv->>Client: initializeResult {server caps}
Client->>Srv: initialized + textDocument/didOpen
User->>Buf: edit
Buf->>Client: TextChanged → didChange
User->>Buf: gd
Buf->>Client: vim.lsp.buf.definition
Client->>Srv: textDocument/definition
Srv->>Client: Location[]
Client->>Buf: jump or quickfixThe flow has three layers: transport, client, and feature.
Transport
lsp/_transport.lua wraps vim.system/vim.uv to start a child process or open a socket and exposes send(msg) / recv(callback). It speaks LSP's Content-Length: ... framing on top of stdio. Multiple transports are supported (stdio, TCP, named pipe). They all expose the same minimal interface.
lsp/rpc.lua sits above the transport and adds JSON-RPC framing: request/response correlation by id, notification dispatch, error handling. The rpc.notify, rpc.request, and rpc.handle primitives are what the client uses.
Client
lsp/client.lua is the largest LSP file (52 KB). It owns:
- Handshake —
initialize→initialized→ server-capability negotiation. - Per-buffer attachment —
vim.lsp.buf_attach_clientregisters callbacks forTextChanged,BufWritePre, etc., that emit the LSP equivalents (textDocument/didChange,willSave,didSave). - Request lifecycle — sending, tracking by id, applying handlers when the response arrives, cancelling on buffer detach or shutdown.
- Capability resolution —
_capability.luacontains the rules for whichclient.server_capabilities.*flag governs which feature. - Lifecycle — graceful
shutdown+exiton detach, force-kill on timeout.
Feature modules
Each lsp/<feature>.lua is roughly the same shape: register some autocmds when vim.lsp.enable_<feature>(client) is called, fire LSP requests in response to user actions, render results via extmarks/quickfix/floating window. Examples:
inlay_hint.lua— requesttextDocument/inlayHintonBufEnter/TextChanged, render results as virt_text via extmarks in thevim.lsp.inlay_hintnamespace.semantic_tokens.lua— requesttextDocument/semanticTokens/fullon attach, decode the delta-encoded token stream, paint highlights via extmarks. Subsequent updates usesemanticTokens/full/delta.codelens.lua— requesttextDocument/codeLens, render as virtual text above the line, run associated commands on user click.
The features share three patterns:
- A namespace per feature — created via
vim.api.nvim_create_namespace('vim_lsp_<feature>'). - An autocmd group per client —
vim.lsp.client.<id>so autocmds are cleared on detach. - Floating windows for previews —
vim.lsp.util.open_floating_previewis the shared helper.
Diagnostics
When the server sends textDocument/publishDiagnostics, lsp/diagnostic.lua translates it into vim.diagnostic entries with the namespace vim.lsp.diagnostic.<client_id>. From there, vim.diagnostic (in runtime/lua/vim/diagnostic.lua) takes over for display, sorting, and the :[Diagnostic] quickfix integration. See Diagnostics.
Configuration
Recent versions (v0.11+) introduced a config registry:
vim.lsp.config('luals', {
cmd = { 'lua-language-server' },
filetypes = { 'lua' },
root_markers = { '.luarc.json' },
settings = { ... },
})
vim.lsp.enable('luals')vim.lsp.enable registers a FileType autocmd that calls vim.lsp.start({...}) for matching buffers. The older path — call vim.lsp.start({...}) from a FileType autocmd by hand — still works.
Integration points
vim.diagnostic—lsp/diagnostic.luais the LSP → diagnostic adapter. The display layer is shared with diagnostics from any other source.vim.snippet— completion items can carry an LSP snippet, whichlsp/completion.luahands off tovim.snippet.expand.vim.ui— code actions, document symbols, and rename UI all go throughvim.ui.selectso users can swap in their preferred picker.vim.lsp.util— Position-encoding conversion, range formatting, URI ↔ filename, are shared utilities. New code consistently goes through these.workspace/configurationandworkspace/didChangeConfiguration— bridged to the per-server config table.- File watching —
_watchfiles.luauses libuv'sfs_event(viavim.uv) to satisfyworkspace/didChangeWatchedFilescapability.
Entry points for modification
- Add a new LSP feature client. Create
runtime/lua/vim/lsp/<feature>.luamirroring the structure ofinlay_hint.lua. Register a namespace, hook autocmds, send requests, render results. - Tweak default handlers.
lsp/handlers.luais the central table; users can override per-handler. - Improve the protocol bindings.
protocol.luais generated/curated. New LSP version constants go there. - Bug a transport.
_transport.luais small; the common bug is incorrect length framing or premature stream close.
Key source files
| File | Purpose |
|---|---|
runtime/lua/vim/lsp.lua |
Top-level entry point (vim.lsp.start, enable, config) |
runtime/lua/vim/lsp/client.lua |
The Client object |
runtime/lua/vim/lsp/buf.lua |
User-facing operations |
runtime/lua/vim/lsp/handlers.lua |
Default handlers |
runtime/lua/vim/lsp/protocol.lua |
LSP protocol constants and types |
runtime/lua/vim/lsp/util.lua |
Shared helpers (position encoding, URIs, previews) |
runtime/lua/vim/lsp/rpc.lua |
JSON-RPC framing |
runtime/lua/vim/lsp/_transport.lua |
Process / socket transports |
runtime/lua/vim/lsp/diagnostic.lua |
LSP → vim.diagnostic bridge |
runtime/lua/vim/lsp/semantic_tokens.lua |
Semantic tokens client |
runtime/lua/vim/lsp/inlay_hint.lua |
Inlay hints |
runtime/lua/vim/lsp/completion.lua |
Built-in autocompletion |
runtime/lua/vim/lsp/_changetracking.lua |
didChange tracking |
runtime/lua/vim/lsp/_capability.lua |
Capability negotiation |
runtime/lua/vim/lsp/_snippet_grammar.lua |
LSP snippet parser |
runtime/doc/lsp.txt |
User docs (regenerated from doc-comments) |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.