You just updated to Neovim 0.11 (Nightly/Pre-release), ran :LspInfo to confirm your language server is attached, and verified that the process is running. Yet, your editor gutter is empty. There are no red 'E' icons, no virtual text, and no underlines, even on code that is intentionally broken.
This regression is not an LSP failure; it is a configuration mismatch caused by breaking changes in how Neovim 0.11 handles diagnostic rendering and legacy sign definitions.
The Root Cause: Deprecation of Legacy Sign Definitions
In versions prior to 0.10/0.11, Neovim relied heavily on a legacy Vimscript bridge (vim.fn.sign_define) to determine which icons to display in the sign column (gutter). Users typically looped through a table of icons and registered DiagnosticSignError, DiagnosticSignWarn, etc.
Neovim 0.11 has shifted the diagnostic rendering subsystem to be fully declarative within the Lua vim.diagnostic API.
- Decoupling: The internal diagnostic handler no longer implicitly checks the global vim sign registry (
sign_define) by default if thesignsconfiguration object is not explicitly structured to use them. - Structure Change: The
vim.diagnostic.configschema forsignshas evolved. It now prefers a direct mapping of severity levels to text/icons within the config table, rather than looking up global highlighting groups defined elsewhere. - Namespace Isolation: Diagnostics are increasingly namespaced. If your config relies on the global (default) namespace but the LSP client attaches to a specific namespace without inherited configuration, nothing renders.
The Fix: declarative Diagnostic Configuration
To resolve this, you must migrate away from procedural vim.fn.sign_define loops and define your diagnostic presentation logic declaratively inside vim.diagnostic.config.
Place the following code in your init.lua (or lua/config/lsp.lua), ensuring it runs after your plugins load but before your LSP servers attach.
-- lua/config/diagnostics.lua
-- 1. Define your icons strictly for the Lua API
local icons = {
Error = " ",
Warn = " ",
Hint = " ",
Info = " ",
}
-- 2. Construct the declarative config
vim.diagnostic.config({
-- Enable virtual text with specific spacing and prefixes
virtual_text = {
source = "if_many", -- Or "always"
prefix = '●', -- Could also be '■', '▎', 'x'
spacing = 4,
},
-- 0.11+ Validated: Enable signs using the direct `text` mapping
-- This bypasses legacy sign_define lookups entirely
signs = {
text = {
[vim.diagnostic.severity.ERROR] = icons.Error,
[vim.diagnostic.severity.WARN] = icons.Warn,
[vim.diagnostic.severity.HINT] = icons.Hint,
[vim.diagnostic.severity.INFO] = icons.Info,
},
-- Ensure highlights act as a fallback if the above text mapping fails
linehl = {
[vim.diagnostic.severity.ERROR] = 'DiagnosticSignError',
[vim.diagnostic.severity.WARN] = 'DiagnosticSignWarn',
[vim.diagnostic.severity.HINT] = 'DiagnosticSignHint',
[vim.diagnostic.severity.INFO] = 'DiagnosticSignInfo',
},
numhl = {
[vim.diagnostic.severity.ERROR] = 'DiagnosticSignError',
[vim.diagnostic.severity.WARN] = 'DiagnosticSignWarn',
[vim.diagnostic.severity.HINT] = 'DiagnosticSignHint',
[vim.diagnostic.severity.INFO] = 'DiagnosticSignInfo',
},
},
-- Enable underlining for errors/warnings
underline = true,
-- UX: Do not update diagnostics while typing (distracting)
update_in_insert = false,
-- Sort diagnostics by severity (High -> Low)
severity_sort = true,
})
-- 3. Force a redraw of current diagnostics to apply changes immediately
-- Useful if you are reloading config at runtime
local current_buf = vim.api.nvim_get_current_buf()
local active_clients = vim.lsp.get_clients({ bufnr = current_buf })
if #active_clients > 0 then
vim.cmd("e") -- Light reload of buffer to trigger LSP re-attach/publish
end
Why This Implementation Works
Direct Severity Mapping
In the signs.text table, we use [vim.diagnostic.severity.ERROR] keys. This binds the icon directly to the internal enum used by the Language Server Protocol handler. Neovim 0.11's render cycle reads this table directly during the decoration provider phase. It removes the ambiguity of "does DiagnosticSignError exist in the Vimscript sign registry?"
Fallback Highlighting
While we define the text in the config, we also map linehl (line highlight) and numhl (number column highlight) within the same object. This ensures that even if a specific theme overrides the text rendering, the color groups (DiagnosticSignError, etc.) are explicitly linked to the severity levels within the new API structure.
Update Behavior
Setting update_in_insert = false is critical for performance and stability in 0.11. The new diagnostic engine is faster but more aggressive. If set to true, the constant stream of textDocument/publishDiagnostics notifications during typing can cause flickering in the gutter or virtual text, leading to a perceived "broken" state where diagnostics appear and disappear rapidly.
Conclusion
Neovim 0.11 moves us closer to a pure Lua ecosystem, reducing reliance on Vimscript legacies. While this breaks older "copy-paste" configurations reliant on sign_define, the new vim.diagnostic.config structure provides a type-safe, declarative, and more performant way to handle LSP feedback. Migrating to the explicit signs = { text = { ... } } format ensures your editor remains future-proof against upcoming stable releases.