Open-Source Wikis

/

Neovim

/

Features

/

LSP client

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 previews

The 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 quickfix

The 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:

  • Handshakeinitializeinitialized → server-capability negotiation.
  • Per-buffer attachmentvim.lsp.buf_attach_client registers callbacks for TextChanged, 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.lua contains the rules for which client.server_capabilities.* flag governs which feature.
  • Lifecycle — graceful shutdown + exit on 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 — request textDocument/inlayHint on BufEnter/TextChanged, render results as virt_text via extmarks in the vim.lsp.inlay_hint namespace.
  • semantic_tokens.lua — request textDocument/semanticTokens/full on attach, decode the delta-encoded token stream, paint highlights via extmarks. Subsequent updates use semanticTokens/full/delta.
  • codelens.lua — request textDocument/codeLens, render as virtual text above the line, run associated commands on user click.

The features share three patterns:

  1. A namespace per feature — created via vim.api.nvim_create_namespace('vim_lsp_<feature>').
  2. An autocmd group per clientvim.lsp.client.<id> so autocmds are cleared on detach.
  3. Floating windows for previewsvim.lsp.util.open_floating_preview is 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.diagnosticlsp/diagnostic.lua is the LSP → diagnostic adapter. The display layer is shared with diagnostics from any other source.
  • vim.snippet — completion items can carry an LSP snippet, which lsp/completion.lua hands off to vim.snippet.expand.
  • vim.ui — code actions, document symbols, and rename UI all go through vim.ui.select so 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/configuration and workspace/didChangeConfiguration — bridged to the per-server config table.
  • File watching_watchfiles.lua uses libuv's fs_event (via vim.uv) to satisfy workspace/didChangeWatchedFiles capability.

Entry points for modification

  • Add a new LSP feature client. Create runtime/lua/vim/lsp/<feature>.lua mirroring the structure of inlay_hint.lua. Register a namespace, hook autocmds, send requests, render results.
  • Tweak default handlers. lsp/handlers.lua is the central table; users can override per-handler.
  • Improve the protocol bindings. protocol.lua is generated/curated. New LSP version constants go there.
  • Bug a transport. _transport.lua is 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.

LSP client – Neovim wiki | Factory