Language Servers

lsp.lua, the vendored Neovim vim.lsp runtime, and how servers attach.

ctrlvim's LSP support is built on Neovim's own vim.lsp implementation, not a reimplementation of it. This is the one place in the project where ctrlvim runs Neovim's actual code rather than a Rust port of it: runtime/lua/vim/lsp.lua and the modules it depends on, client, rpc, protocol, util, buf, handlers, sync, and completion, along with vim/diagnostic.lua and vim/uri.lua, are vendored unmodified from Neovim, pinned to a specific Neovim tag. ctrlvim supplies the vim.api, vim.uv, and vim.fn primitives those files call into, and a small bootstrap that wires vim.lsp and vim.diagnostic onto the global vim table the same way real Neovim's C side does.

The practical effect: an lsp.lua written for real Neovim, using vim.lsp.config(...) and vim.lsp.enable(...), works on ctrlvim without changes.

# Where lsp.lua lives

~/.config/ctrlvim/lsp.lua, alongside config.toml. This file declares which language servers exist and when to start them; config.toml stays TOML and does not grow an LSP section of its own.

# Declaring a server

luavim.lsp.config('rust_analyzer', {
  cmd = { 'rust-analyzer' },
  filetypes = { 'rust' },
  root_markers = { 'Cargo.toml', '.git' },
})

vim.lsp.enable('rust_analyzer')

vim.lsp.enable also accepts a list, for enabling several servers at once: vim.lsp.enable({'clangd', 'lua_ls'}).

Everything documented for Neovim's vim.lsp.ClientConfig applies here: cmd, filetypes, root_markers, root_dir as either a path or a function, settings, init_options, capabilities, and the lifecycle callbacks (on_init, on_attach, on_exit, on_error).

# Root resolution

Servers are rooted per file, not per editor session. root_markers names the files that mark a project root, and ctrlvim walks upward from whichever file you opened to find one. Two buffers from different checkouts each get their own server instance; a scratch file that belongs to no project gets a server rooted at its own directory. Both can be true at once, so opening a one off file from inside another project never asks that project's server about it.

init_options and single_file_init_options are both supported: server settings are passed through verbatim, with single_file_init_options merged over init_options only when a file has no project root, and ${file} / ${root} substituted in either.

# What is included

The vendored runtime covers the core flow this integration targets first: start, diagnostics, hover, go to definition, references, rename, and formatting.

A few more advanced or optional pieces of vim.lsp are not vendored and are only reached lazily if something actually calls them: semantic tokens, inlay hints, code lens, document color, folding range, linked editing range, on type formatting, inline completion, and the LSP health check. Their absence only matters if a plugin or a config specifically calls into one of them.

# The Settings tab

The dashboard's Settings tab lists every server declared in lsp.lua under Language Servers, shows whether each one's binary is actually installed and on PATH, and lets you enable or disable one for the current session without touching the file. If a declaration includes an install command, the Settings tab can run it and stream the output into the same overlay :!{cmd} uses.

A server declared with no filetypes is treated as a build tool or linker rather than a language server proper, present only for the installed/not installed check, for things like mold or a formatter binary.

# If lsp.lua does not exist yet

The Settings tab reports "No servers declared" and ctrlvim runs with no language servers attached. Nothing else about the editor depends on one being present.

On this page