Skip to main content
Neovim’s LSP diagnostics integration provides real-time feedback about errors, warnings, and other issues in your code through the vim.diagnostic API.

Overview

The LSP diagnostics system:
  • Automatically receives diagnostics from language servers
  • Displays inline signs, virtual text, and underlines
  • Provides floating windows with detailed information
  • Supports multiple severity levels (Error, Warning, Info, Hint)
  • Integrates with quickfix and location lists
Diagnostics are enabled by default when an LSP client attaches to a buffer.

Configuration

Global Configuration

Configure diagnostic display options globally:
vim.diagnostic.config({
  -- Show diagnostics in virtual text
  virtual_text = {
    severity = { min = vim.diagnostic.severity.WARN },
    source = 'if_many', -- Show source if multiple sources
    prefix = '●', -- Could be '■', '▎', 'x', etc.
  },
  
  -- Show signs in the sign column
  signs = {
    severity = { min = vim.diagnostic.severity.HINT },
  },
  
  -- Underline diagnostics
  underline = {
    severity = { min = vim.diagnostic.severity.WARN },
  },
  
  -- Update diagnostics while typing
  update_in_insert = false,
  
  -- Sort diagnostics by severity
  severity_sort = true,
  
  -- Floating window configuration
  float = {
    focusable = false,
    style = 'minimal',
    border = 'rounded',
    source = 'always',
    header = '',
    prefix = '',
  },
})

Buffer-Local Configuration

Override settings for specific buffers:
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    -- Disable virtual text for this buffer
    vim.diagnostic.config({ virtual_text = false }, args.buf)
  end
})

Diagnostic Signs

Customize Signs

Change the appearance of diagnostic signs:
-- Define custom sign text
local signs = {
  Error = '󰅚 ',
  Warn = '󰀪 ',
  Hint = '󰌶 ',
  Info = ' ',
}

for type, icon in pairs(signs) do
  local hl = 'DiagnosticSign' .. type
  vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
end

Highlight Groups

Customize diagnostic colors:
vim.api.nvim_set_hl(0, 'DiagnosticError', { fg = '#db4b4b' })
vim.api.nvim_set_hl(0, 'DiagnosticWarn', { fg = '#e0af68' })
vim.api.nvim_set_hl(0, 'DiagnosticInfo', { fg = '#0db9d7' })
vim.api.nvim_set_hl(0, 'DiagnosticHint', { fg = '#1abc9c' })

vim.api.nvim_set_hl(0, 'DiagnosticUnderlineError', { undercurl = true, sp = '#db4b4b' })
vim.api.nvim_set_hl(0, 'DiagnosticUnderlineWarn', { undercurl = true, sp = '#e0af68' })
vim.api.nvim_set_hl(0, 'DiagnosticUnderlineInfo', { undercurl = true, sp = '#0db9d7' })
vim.api.nvim_set_hl(0, 'DiagnosticUnderlineHint', { undercurl = true, sp = '#1abc9c' })

Keymaps for Navigation

1
Set up diagnostic navigation keymaps
2
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, { desc = 'Go to previous diagnostic' })
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, { desc = 'Go to next diagnostic' })

vim.keymap.set('n', '[e', function()
  vim.diagnostic.goto_prev({ severity = vim.diagnostic.severity.ERROR })
end, { desc = 'Go to previous error' })

vim.keymap.set('n', ']e', function()
  vim.diagnostic.goto_next({ severity = vim.diagnostic.severity.ERROR })
end, { desc = 'Go to next error' })
3
Open diagnostic float
4
vim.keymap.set('n', '<leader>e', vim.diagnostic.open_float, { desc = 'Show diagnostic' })
5
Show diagnostics in location list
6
vim.keymap.set('n', '<leader>q', vim.diagnostic.setloclist, { desc = 'Diagnostics to loclist' })

Advanced Navigation

Jump to specific severity levels:
-- Jump to next warning or error
vim.diagnostic.goto_next({
  severity = { min = vim.diagnostic.severity.WARN }
})

-- Jump to next diagnostic with a custom message filter
vim.diagnostic.goto_next({
  severity = vim.diagnostic.severity.ERROR,
  float = true, -- Show float automatically
})

-- Wrap around when reaching the last diagnostic
vim.diagnostic.goto_next({ wrap = true })

Floating Windows

Show Diagnostics on Hover

Automatically show diagnostics in a floating window:
vim.api.nvim_create_autocmd('CursorHold', {
  callback = function()
    local opts = {
      focusable = false,
      close_events = { 'BufLeave', 'CursorMoved', 'InsertEnter', 'FocusLost' },
      border = 'rounded',
      source = 'always',
      prefix = ' ',
      scope = 'cursor',
    }
    vim.diagnostic.open_float(nil, opts)
  end
})

-- Adjust the hover delay
vim.opt.updatetime = 250

Manual Float

Open a diagnostic float for the current line:
vim.diagnostic.open_float({
  scope = 'line',
  border = 'rounded',
  source = 'always',
  header = 'Diagnostics:',
  format = function(diagnostic)
    return string.format('[%s] %s', diagnostic.source, diagnostic.message)
  end
})

Filtering Diagnostics

By Severity

-- Get only errors
local errors = vim.diagnostic.get(0, {
  severity = vim.diagnostic.severity.ERROR
})

-- Get errors and warnings
local errors_and_warnings = vim.diagnostic.get(0, {
  severity = {
    min = vim.diagnostic.severity.WARN,
    max = vim.diagnostic.severity.ERROR
  }
})

By Source

Filter diagnostics from specific language servers:
-- Get diagnostics from a specific namespace
local client = vim.lsp.get_clients({ name = 'lua_ls' })[1]
if client then
  local ns = vim.lsp.diagnostic.get_namespace(client.id)
  local diagnostics = vim.diagnostic.get(0, { namespace = ns })
end

Pull Diagnostics

Some servers support “pull” diagnostics where the client requests diagnostics instead of the server pushing them.

Enable Pull Diagnostics

The LSP client automatically handles pull diagnostics if the server supports it. Manually refresh:
-- Force refresh diagnostics for current buffer
vim.lsp.diagnostic._refresh(0)

-- Refresh for all buffers attached to a client
vim.api.nvim_create_autocmd('LspNotify', {
  callback = function(args)
    if args.data.method == 'textDocument/didSave' then
      vim.lsp.diagnostic._refresh(args.buf)
    end
  end
})

Custom Diagnostic Handlers

Override Default Handler

Customize how diagnostics are processed:
vim.lsp.handlers['textDocument/publishDiagnostics'] = function(err, result, ctx, config)
  -- Filter out certain diagnostics
  if result and result.diagnostics then
    result.diagnostics = vim.tbl_filter(function(d)
      -- Ignore "unused variable" warnings
      return not (d.message:match('unused') or d.message:match('never read'))
    end, result.diagnostics)
  end
  
  -- Call default handler
  vim.lsp.diagnostic.on_publish_diagnostics(err, result, ctx, config)
end

Per-Client Configuration

vim.lsp.start({
  name = 'my-server',
  cmd = { 'my-language-server' },
  handlers = {
    ['textDocument/publishDiagnostics'] = function(err, result, ctx)
      -- Custom handling for this client
      vim.lsp.diagnostic.on_publish_diagnostics(err, result, ctx, {
        virtual_text = false,
        signs = true,
      })
    end
  }
})

Diagnostic Quickfix

Populate quickfix or location list with diagnostics:
-- All diagnostics to quickfix
vim.diagnostic.setqflist()

-- Only errors to quickfix
vim.diagnostic.setqflist({ severity = vim.diagnostic.severity.ERROR })

-- Diagnostics for current buffer to location list
vim.diagnostic.setloclist()

-- Custom title
vim.diagnostic.setqflist({ open = true, title = 'LSP Diagnostics' })

API Reference

vim.diagnostic.get()

Get diagnostics for a buffer.
local diagnostics = vim.diagnostic.get(bufnr, opts)
Parameters:
  • bufnr (integer|nil): Buffer number (nil for all buffers)
  • opts (table|nil):
    • namespace (integer): Filter by namespace
    • lnum (integer): Filter by line number
    • severity (table|integer): Filter by severity
Returns: Array of diagnostic objects

vim.diagnostic.config()

Configure diagnostic display.
vim.diagnostic.config(config, namespace)
See Configuration for available options.

Build docs developers (and LLMs) love