Configuring Neovim's Built-in LSP Client Without External Plugins
Learn how to use Neovim's built-in LSP client to get IDE features without heavy plugins. This guide covers configuration for Neovim 0.11+, root detection, and avoiding formatting conflicts.
03 Nov 2025, 10:27 UTC

The Problem: Plugin Bloat vs. Core Functionality
For years, adding IDE-like features (autocompletion, go-to-definition, real-time diagnostics) to Neovim required heavy external plugins like coc.nvim. These often introduced their own dependencies, such as Node.js, and operated outside Neovim's native Lua API, making configuration fragmented and performance unpredictable.
The solution is Neovim's built-in LSP (Language Server Protocol) client. Since version 0.5, and significantly refined in 0.11, Neovim includes a native client in the vim.lsp namespace. This allows the editor to communicate directly with language servers via JSON-RPC, handling the server lifecycle and request/response routing natively. The takeaway: you no longer need a "plugin manager for LSP" to get basic IDE functionality; you only need the language server binary installed on your system and a few lines of Lua configuration.
How the Native LSP Client Works
The built-in client follows a client-per-buffer model. When you open a file, Neovim checks if a configured server matches the filetype and if a "root marker" (like .git or pyproject.toml) exists in the directory hierarchy. If both match, Neovim spawns the server as a child process managed by libuv and attaches it to that buffer.
Worked Example: Setting up lua_ls (Neovim 0.11+)
To use the native LSP, you must first install the server binary (e.g., via npm, pip, or a system package manager). For this example, assume lua-language-server is installed and available in your system PATH.
-- Required for Neovim 0.11+
-- Configure the server settings
vim.lsp.config('lua_ls', {
cmd = { 'lua-language-server' },
root_markers = { '.luarc.json', '.git' },
})
-- Enable the server for matching buffers
vim.lsp.enable('lua_ls')
-- Optional: Set up keymaps when a server attaches to a buffer
vim.api.nvim_create_autocmd('LspAttach', {
callback = function(args)
local opts = { buffer = args.buf }r> vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
vim.keymap.set('n', 'rn', vim.lsp.buf.rename, opts)
vim.keymap.set('n', 'ca', vim.lsp.buf.code_action, opts)
end,
})
Diagnostic and Completion Integration
Once attached, the server sends diagnostics (errors/warnings) asynchronously. These are managed by the vim.diagnostic namespace and appear in the sign column (the left margin) and as virtual text. To view a detailed error, you can call vim.diagnostic.open_float().
For completion, Neovim provides vim.lsp.omnifunc. In Insert mode, pressing <C-x><C-o> triggers the LSP completion menu. While Neovim 0.11+ introduces vim.lsp.completion.enable() for a more automated experience, it remains a basic list; it does not include fuzzy matching or snippet expansion.
Operational Limits and Common Pitfalls
Root Directory Failures
A common point of failure is root detection. If root_markers are not defined or found, the LSP client may fail to start or start in the current working directory ($CWD), which can lead to incorrect project-wide analysis. Always ensure your project has a recognized marker (like .git) or explicitly define the root directory in your config.
The Formatting Conflict
When calling vim.lsp.buf.format(), Neovim uses the first client that advertises documentFormattingProvider. If you have multiple servers attached (e.g., a language server and a linter like ESLint), Neovim may pick the "wrong" one. To solve this, you must filter the clients:
-- Example: Format using only the 'lua_ls' client
vim.lsp.buf.format({
filter = function(client)
return client.name == 'lua_ls'
end
})
Log File Growth
The LSP log file (typically found at $HOME/.local/state/nvim/lsp.log) can grow indefinitely, especially if the log level is set to DEBUG. This can consume significant disk space on large projects. It is recommended to keep the log level at WARN unless actively debugging a server crash:
vim.lsp.set_log_level('WARN')
Verification and Diagnostics
To verify your configuration is working as expected, use the following checks:
- Version Check: Run
:lua print(vim.version().minor). If the version is below 11, thevim.lsp.configandvim.lsp.enableAPIs will not be available, and you must usenvim-lspconfigorvim.lsp.start_client. - Attachment Check: Run the built-in
:LspInfocommand. This will show which clients are active, which buffers they are attached to, and the root directory they detected. - Communication Check: Open the log file with
:lua vim.cmd('edit ' .. vim.lsp.get_log_path())to see the raw JSON-RPC traffic between Neovim and the server.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.