neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

diagnostic.lua (98813B)


      1 local api, if_nil = vim.api, vim.F.if_nil
      2 
      3 local M = {}
      4 
      5 --- @param title string
      6 --- @return integer?
      7 local function get_qf_id_for_title(title)
      8  local lastqflist = vim.fn.getqflist({ nr = '$' })
      9  for i = 1, lastqflist.nr do
     10    local qflist = vim.fn.getqflist({ nr = i, id = 0, title = 0 })
     11    if qflist.title == title then
     12      return qflist.id
     13    end
     14  end
     15 
     16  return nil
     17 end
     18 
     19 --- Diagnostics use the same indexing as the rest of the Nvim API (i.e. 0-based
     20 --- rows and columns). |api-indexing|
     21 --- @class vim.Diagnostic.Set
     22 ---
     23 --- The starting line of the diagnostic (0-indexed)
     24 --- @field lnum integer
     25 ---
     26 --- The starting column of the diagnostic (0-indexed)
     27 --- (default: `0`)
     28 --- @field col? integer
     29 ---
     30 --- The final line of the diagnostic (0-indexed)
     31 --- (default: `lnum`)
     32 --- @field end_lnum? integer
     33 ---
     34 --- The final column of the diagnostic (0-indexed)
     35 --- (default: `col`)
     36 --- @field end_col? integer
     37 ---
     38 --- The severity of the diagnostic |vim.diagnostic.severity|
     39 --- (default: `vim.diagnostic.severity.ERROR`)
     40 --- @field severity? vim.diagnostic.Severity
     41 ---
     42 --- The diagnostic text
     43 --- @field message string
     44 ---
     45 --- The source of the diagnostic
     46 --- @field source? string
     47 ---
     48 --- The diagnostic code
     49 --- @field code? string|integer
     50 ---
     51 --- @field _tags? { deprecated: boolean, unnecessary: boolean}
     52 ---
     53 --- Arbitrary data plugins or users can add
     54 --- @field user_data? any arbitrary data plugins can add
     55 
     56 --- [diagnostic-structure]()
     57 ---
     58 --- Diagnostics use the same indexing as the rest of the Nvim API (i.e. 0-based
     59 --- rows and columns). |api-indexing|
     60 --- @class vim.Diagnostic : vim.Diagnostic.Set
     61 --- @field bufnr integer Buffer number
     62 --- @field end_lnum integer The final line of the diagnostic (0-indexed)
     63 --- @field col integer The starting column of the diagnostic (0-indexed)
     64 --- @field end_col integer The final column of the diagnostic (0-indexed)
     65 --- @field severity vim.diagnostic.Severity The severity of the diagnostic |vim.diagnostic.severity|
     66 --- @field namespace? integer
     67 --- @field _extmark_id? integer
     68 
     69 --- Many of the configuration options below accept one of the following:
     70 --- - `false`: Disable this feature
     71 --- - `true`: Enable this feature, use default settings.
     72 --- - `table`: Enable this feature with overrides. Use an empty table to use default values.
     73 --- - `function`: Function with signature (namespace, bufnr) that returns any of the above.
     74 --- @class vim.diagnostic.Opts
     75 ---
     76 --- Use underline for diagnostics.
     77 --- (default: `true`)
     78 --- @field underline? boolean|vim.diagnostic.Opts.Underline|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Underline
     79 ---
     80 --- Use virtual text for diagnostics. If multiple diagnostics are set for a
     81 --- namespace, one prefix per diagnostic + the last diagnostic message are
     82 --- shown.
     83 --- (default: `false`)
     84 --- @field virtual_text? boolean|vim.diagnostic.Opts.VirtualText|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.VirtualText
     85 ---
     86 --- Use virtual lines for diagnostics.
     87 --- (default: `false`)
     88 --- @field virtual_lines? boolean|vim.diagnostic.Opts.VirtualLines|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.VirtualLines
     89 ---
     90 --- Use signs for diagnostics |diagnostic-signs|.
     91 --- (default: `true`)
     92 --- @field signs? boolean|vim.diagnostic.Opts.Signs|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Signs
     93 ---
     94 --- Options for floating windows. See |vim.diagnostic.Opts.Float|.
     95 --- @field float? boolean|vim.diagnostic.Opts.Float|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Float
     96 ---
     97 --- Options for the statusline component.
     98 --- @field status? vim.diagnostic.Opts.Status
     99 ---
    100 --- Update diagnostics in Insert mode
    101 --- (if `false`, diagnostics are updated on |InsertLeave|)
    102 --- (default: `false`)
    103 --- @field update_in_insert? boolean
    104 ---
    105 --- Sort diagnostics by severity. This affects the order in which signs,
    106 --- virtual text, and highlights are displayed. When true, higher severities are
    107 --- displayed before lower severities (e.g. ERROR is displayed before WARN).
    108 --- Options:
    109 ---   - {reverse}? (boolean) Reverse sort order
    110 --- (default: `false`)
    111 --- @field severity_sort? boolean|{reverse?:boolean}
    112 ---
    113 --- Default values for |vim.diagnostic.jump()|. See |vim.diagnostic.Opts.Jump|.
    114 --- @field jump? vim.diagnostic.Opts.Jump
    115 
    116 --- @class (private) vim.diagnostic.OptsResolved
    117 --- @field float vim.diagnostic.Opts.Float
    118 --- @field update_in_insert boolean
    119 --- @field underline vim.diagnostic.Opts.Underline
    120 --- @field virtual_text vim.diagnostic.Opts.VirtualText
    121 --- @field virtual_lines vim.diagnostic.Opts.VirtualLines
    122 --- @field signs vim.diagnostic.Opts.Signs
    123 --- @field severity_sort {reverse?:boolean}
    124 
    125 --- @class vim.diagnostic.Opts.Float : vim.lsp.util.open_floating_preview.Opts
    126 ---
    127 --- Buffer number to show diagnostics from.
    128 --- (default: current buffer)
    129 --- @field bufnr? integer
    130 ---
    131 --- Limit diagnostics to the given namespace(s).
    132 --- @field namespace? integer|integer[]
    133 ---
    134 --- Show diagnostics from the whole buffer (`buffer`), the current cursor line
    135 --- (`line`), or the current cursor position (`cursor`). Shorthand versions
    136 --- are also accepted (`c` for `cursor`, `l` for `line`, `b` for `buffer`).
    137 --- (default: `line`)
    138 --- @field scope? 'line'|'buffer'|'cursor'|'c'|'l'|'b'
    139 ---
    140 --- If {scope} is "line" or "cursor", use this position rather than the cursor
    141 --- position. If a number, interpreted as a line number; otherwise, a
    142 --- (row, col) tuple.
    143 --- @field pos? integer|[integer,integer]
    144 ---
    145 --- Sort diagnostics by severity.
    146 --- Overrides the setting from |vim.diagnostic.config()|.
    147 --- (default: `false`)
    148 --- @field severity_sort? boolean|{reverse?:boolean}
    149 ---
    150 --- See |diagnostic-severity|.
    151 --- Overrides the setting from |vim.diagnostic.config()|.
    152 --- @field severity? vim.diagnostic.SeverityFilter
    153 ---
    154 --- String to use as the header for the floating window. If a table, it is
    155 --- interpreted as a `[text, hl_group]` tuple.
    156 --- Overrides the setting from |vim.diagnostic.config()|.
    157 --- @field header? string|[string,any]
    158 ---
    159 --- Include the diagnostic source in the message.
    160 --- Use "if_many" to only show sources if there is more than one source of
    161 --- diagnostics in the buffer. Otherwise, any truthy value means to always show
    162 --- the diagnostic source.
    163 --- Overrides the setting from |vim.diagnostic.config()|.
    164 --- @field source? boolean|'if_many'
    165 ---
    166 --- A function that takes a diagnostic as input and returns a string or nil.
    167 --- If the return value is nil, the diagnostic is not displayed by the handler.
    168 --- Else the output text is used to display the diagnostic.
    169 --- Overrides the setting from |vim.diagnostic.config()|.
    170 --- @field format? fun(diagnostic:vim.Diagnostic): string?
    171 ---
    172 --- Prefix each diagnostic in the floating window:
    173 --- - If a `function`, {i} is the index of the diagnostic being evaluated and
    174 ---   {total} is the total number of diagnostics displayed in the window. The
    175 ---   function should return a `string` which is prepended to each diagnostic
    176 ---   in the window as well as an (optional) highlight group which will be
    177 ---   used to highlight the prefix.
    178 --- - If a `table`, it is interpreted as a `[text, hl_group]` tuple as
    179 ---   in |nvim_echo()|
    180 --- - If a `string`, it is prepended to each diagnostic in the window with no
    181 ---   highlight.
    182 --- Overrides the setting from |vim.diagnostic.config()|.
    183 --- @field prefix? string|table|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string, string)
    184 ---
    185 --- Same as {prefix}, but appends the text to the diagnostic instead of
    186 --- prepending it.
    187 --- Overrides the setting from |vim.diagnostic.config()|.
    188 --- @field suffix? string|table|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string, string)
    189 
    190 --- @class vim.diagnostic.Opts.Status
    191 ---
    192 --- A table mapping |diagnostic-severity| to the text to use for each severity section.
    193 --- @field text? table<vim.diagnostic.Severity,string>
    194 
    195 --- @class vim.diagnostic.Opts.Underline
    196 ---
    197 --- Only underline diagnostics matching the given
    198 --- severity |diagnostic-severity|.
    199 --- @field severity? vim.diagnostic.SeverityFilter
    200 
    201 --- @class vim.diagnostic.Opts.VirtualText
    202 ---
    203 --- Only show virtual text for diagnostics matching the given
    204 --- severity |diagnostic-severity|
    205 --- @field severity? vim.diagnostic.SeverityFilter
    206 ---
    207 --- Show or hide diagnostics based on the current cursor line.  If `true`, only diagnostics on the
    208 --- current cursor line are shown.  If `false`, all diagnostics are shown except on the current
    209 --- cursor line.  If `nil`, all diagnostics are shown.
    210 --- (default `nil`)
    211 --- @field current_line? boolean
    212 ---
    213 --- Include the diagnostic source in virtual text. Use `'if_many'` to only
    214 --- show sources if there is more than one diagnostic source in the buffer.
    215 --- Otherwise, any truthy value means to always show the diagnostic source.
    216 --- @field source? boolean|"if_many"
    217 ---
    218 --- Amount of empty spaces inserted at the beginning of the virtual text.
    219 --- @field spacing? integer
    220 ---
    221 --- Prepend diagnostic message with prefix. If a `function`, {i} is the index
    222 --- of the diagnostic being evaluated, and {total} is the total number of
    223 --- diagnostics for the line. This can be used to render diagnostic symbols
    224 --- or error codes.
    225 --- @field prefix? string|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string)
    226 ---
    227 --- Append diagnostic message with suffix.
    228 --- This can be used to render an LSP diagnostic error code.
    229 --- @field suffix? string|(fun(diagnostic:vim.Diagnostic): string)
    230 ---
    231 --- If not nil, the return value is the text used to display the diagnostic. Example:
    232 --- ```lua
    233 --- function(diagnostic)
    234 ---   if diagnostic.severity == vim.diagnostic.severity.ERROR then
    235 ---     return string.format("E: %s", diagnostic.message)
    236 ---   end
    237 ---   return diagnostic.message
    238 --- end
    239 --- ```
    240 --- If the return value is nil, the diagnostic is not displayed by the handler.
    241 --- @field format? fun(diagnostic:vim.Diagnostic): string?
    242 ---
    243 --- See |nvim_buf_set_extmark()|.
    244 --- @field hl_mode? 'replace'|'combine'|'blend'
    245 ---
    246 --- See |nvim_buf_set_extmark()|.
    247 --- @field virt_text? [string,any][]
    248 ---
    249 --- See |nvim_buf_set_extmark()|.
    250 --- @field virt_text_pos? 'eol'|'eol_right_align'|'inline'|'overlay'|'right_align'
    251 ---
    252 --- See |nvim_buf_set_extmark()|.
    253 --- @field virt_text_win_col? integer
    254 ---
    255 --- See |nvim_buf_set_extmark()|.
    256 --- @field virt_text_hide? boolean
    257 
    258 --- @class vim.diagnostic.Opts.VirtualLines
    259 ---
    260 --- Only show virtual lines for diagnostics matching the given
    261 --- severity |diagnostic-severity|
    262 --- @field severity? vim.diagnostic.SeverityFilter
    263 ---
    264 --- Only show diagnostics for the current line.
    265 --- (default: `false`)
    266 --- @field current_line? boolean
    267 ---
    268 --- A function that takes a diagnostic as input and returns a string or nil.
    269 --- If the return value is nil, the diagnostic is not displayed by the handler.
    270 --- Else the output text is used to display the diagnostic.
    271 --- @field format? fun(diagnostic:vim.Diagnostic): string?
    272 
    273 --- @class vim.diagnostic.Opts.Signs
    274 ---
    275 --- Only show signs for diagnostics matching the given
    276 --- severity |diagnostic-severity|
    277 --- @field severity? vim.diagnostic.SeverityFilter
    278 ---
    279 --- Base priority to use for signs. When {severity_sort} is used, the priority
    280 --- of a sign is adjusted based on its severity.
    281 --- Otherwise, all signs use the same priority.
    282 --- (default: `10`)
    283 --- @field priority? integer
    284 ---
    285 --- A table mapping |diagnostic-severity| to the sign text to display in the
    286 --- sign column. The default is to use `"E"`, `"W"`, `"I"`, and `"H"` for errors,
    287 --- warnings, information, and hints, respectively. Example:
    288 --- ```lua
    289 --- vim.diagnostic.config({
    290 ---   signs = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } }
    291 --- })
    292 --- ```
    293 --- @field text? table<vim.diagnostic.Severity,string>
    294 ---
    295 --- A table mapping |diagnostic-severity| to the highlight group used for the
    296 --- line number where the sign is placed.
    297 --- @field numhl? table<vim.diagnostic.Severity,string>
    298 ---
    299 --- A table mapping |diagnostic-severity| to the highlight group used for the
    300 --- whole line the sign is placed in.
    301 --- @field linehl? table<vim.diagnostic.Severity,string>
    302 
    303 --- @class vim.diagnostic.Opts.Jump
    304 ---
    305 --- Default value of the {on_jump} parameter of |vim.diagnostic.jump()|.
    306 --- @field on_jump? fun(diagnostic:vim.Diagnostic?, bufnr:integer)
    307 ---
    308 --- Default value of the {wrap} parameter of |vim.diagnostic.jump()|.
    309 --- (default: true)
    310 --- @field wrap? boolean
    311 ---
    312 --- Default value of the {severity} parameter of |vim.diagnostic.jump()|.
    313 --- @field severity? vim.diagnostic.SeverityFilter
    314 ---
    315 --- Default value of the {_highest} parameter of |vim.diagnostic.jump()|.
    316 --- @field package _highest? boolean
    317 
    318 -- TODO: inherit from `vim.diagnostic.Opts`, implement its fields.
    319 --- Optional filters |kwargs|, or `nil` for all.
    320 --- @class vim.diagnostic.Filter
    321 --- @inlinedoc
    322 ---
    323 --- Diagnostic namespace, or `nil` for all.
    324 --- @field ns_id? integer
    325 ---
    326 --- Buffer number, or 0 for current buffer, or `nil` for all buffers.
    327 --- @field bufnr? integer
    328 
    329 --- @nodoc
    330 --- @enum vim.diagnostic.Severity
    331 M.severity = {
    332  ERROR = 1,
    333  WARN = 2,
    334  INFO = 3,
    335  HINT = 4,
    336 }
    337 
    338 --- @enum vim.diagnostic.SeverityName
    339 local severity_invert = {
    340  [1] = 'ERROR',
    341  [2] = 'WARN',
    342  [3] = 'INFO',
    343  [4] = 'HINT',
    344 }
    345 
    346 do
    347  --- Set extra fields through table alias to hide from analysis tools
    348  local s = M.severity --- @type table<any,any>
    349 
    350  for i, name in ipairs(severity_invert) do
    351    s[i] = name
    352  end
    353 
    354  --- Mappings from qflist/loclist error types to severities
    355  s.E = 1
    356  s.W = 2
    357  s.I = 3
    358  s.N = 4
    359 end
    360 
    361 --- See |diagnostic-severity| and |vim.diagnostic.get()|
    362 --- @alias vim.diagnostic.SeverityFilter
    363 --- | vim.diagnostic.Severity
    364 --- | vim.diagnostic.Severity[]
    365 --- | {min:vim.diagnostic.Severity,max:vim.diagnostic.Severity}
    366 
    367 --- @type vim.diagnostic.Opts
    368 local global_diagnostic_options = {
    369  signs = true,
    370  underline = true,
    371  virtual_text = false,
    372  virtual_lines = false,
    373  float = true,
    374  update_in_insert = false,
    375  severity_sort = false,
    376  jump = {
    377    -- Wrap around buffer
    378    wrap = true,
    379  },
    380 }
    381 
    382 --- @class (private) vim.diagnostic.Handler
    383 --- @field show? fun(namespace: integer, bufnr: integer, diagnostics: vim.Diagnostic[], opts?: vim.diagnostic.OptsResolved)
    384 --- @field hide? fun(namespace:integer, bufnr:integer)
    385 
    386 --- @nodoc
    387 --- @type table<string,vim.diagnostic.Handler>
    388 M.handlers = setmetatable({}, {
    389  __newindex = function(t, name, handler)
    390    vim.validate('handler', handler, 'table')
    391    rawset(t, name, handler)
    392    if global_diagnostic_options[name] == nil then
    393      global_diagnostic_options[name] = true
    394    end
    395  end,
    396 })
    397 
    398 -- Metatable that automatically creates an empty table when assigning to a missing key
    399 local bufnr_and_namespace_cacher_mt = {
    400  --- @param t table<integer,table>
    401  --- @param bufnr integer
    402  --- @return table
    403  __index = function(t, bufnr)
    404    assert(bufnr > 0, 'Invalid buffer number')
    405    t[bufnr] = {}
    406    return t[bufnr]
    407  end,
    408 }
    409 
    410 -- bufnr -> ns -> Diagnostic[]
    411 local diagnostic_cache = {} --- @type table<integer,table<integer,vim.Diagnostic[]?>>
    412 do
    413  local group = api.nvim_create_augroup('nvim.diagnostic.buf_wipeout', {})
    414  setmetatable(diagnostic_cache, {
    415    --- @param t table<integer,vim.Diagnostic[]>
    416    --- @param bufnr integer
    417    __index = function(t, bufnr)
    418      assert(bufnr > 0, 'Invalid buffer number')
    419      api.nvim_create_autocmd('BufWipeout', {
    420        group = group,
    421        buffer = bufnr,
    422        callback = function()
    423          rawset(t, bufnr, nil)
    424        end,
    425      })
    426      t[bufnr] = {}
    427      return t[bufnr]
    428    end,
    429  })
    430 end
    431 
    432 --- @class (private) vim.diagnostic._extmark : vim.api.keyset.get_extmark_item
    433 --- @field [1] integer extmark_id
    434 --- @field [2] integer row
    435 --- @field [3] integer col
    436 --- @field [4] vim.api.keyset.extmark_details
    437 
    438 --- @type table<integer,table<integer,vim.diagnostic._extmark[]>>
    439 local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt)
    440 
    441 --- @type table<integer,true>
    442 local diagnostic_attached_buffers = {}
    443 
    444 --- @type table<integer,true|table<integer,true>>
    445 local diagnostic_disabled = {}
    446 
    447 --- @type table<integer,table<integer,table>>
    448 local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt)
    449 
    450 --- @class vim.diagnostic.NS
    451 --- @field name string
    452 --- @field opts vim.diagnostic.Opts
    453 --- @field user_data table
    454 --- @field disabled? boolean
    455 
    456 --- @type table<integer,vim.diagnostic.NS>
    457 local all_namespaces = {}
    458 
    459 ---@param severity string|vim.diagnostic.Severity?
    460 ---@return vim.diagnostic.Severity?
    461 local function to_severity(severity)
    462  if type(severity) == 'string' then
    463    local ret = M.severity[severity:upper()] --[[@as vim.diagnostic.Severity?]]
    464    if not ret then
    465      error(('Invalid severity: %s'):format(severity))
    466    end
    467    return ret
    468  end
    469  return severity --[[@as vim.diagnostic.Severity?]]
    470 end
    471 
    472 --- @param severity vim.diagnostic.SeverityFilter
    473 --- @return fun(d: vim.Diagnostic):boolean
    474 local function severity_predicate(severity)
    475  if type(severity) ~= 'table' then
    476    local severity0 = to_severity(severity)
    477    ---@param d vim.Diagnostic
    478    return function(d)
    479      return d.severity == severity0
    480    end
    481  end
    482  --- @diagnostic disable-next-line: undefined-field
    483  if severity.min or severity.max then
    484    --- @cast severity {min:vim.diagnostic.Severity,max:vim.diagnostic.Severity}
    485    local min_severity = to_severity(severity.min) or M.severity.HINT
    486    local max_severity = to_severity(severity.max) or M.severity.ERROR
    487 
    488    --- @param d vim.Diagnostic
    489    return function(d)
    490      return d.severity <= min_severity and d.severity >= max_severity
    491    end
    492  end
    493 
    494  --- @cast severity vim.diagnostic.Severity[]
    495  local severities = {} --- @type table<vim.diagnostic.Severity,true>
    496  for _, s in ipairs(severity) do
    497    severities[assert(to_severity(s))] = true
    498  end
    499 
    500  --- @param d vim.Diagnostic
    501  return function(d)
    502    return severities[d.severity]
    503  end
    504 end
    505 
    506 --- @param severity vim.diagnostic.SeverityFilter
    507 --- @param diagnostics vim.Diagnostic[]
    508 --- @return vim.Diagnostic[]
    509 local function filter_by_severity(severity, diagnostics)
    510  if not severity then
    511    return diagnostics
    512  end
    513  return vim.tbl_filter(severity_predicate(severity), diagnostics)
    514 end
    515 
    516 --- @param bufnr integer
    517 --- @return integer
    518 local function count_sources(bufnr)
    519  local seen = {} --- @type table<string,true>
    520  local count = 0
    521  for _, namespace_diagnostics in pairs(diagnostic_cache[bufnr]) do
    522    for _, diagnostic in ipairs(namespace_diagnostics) do
    523      local source = diagnostic.source
    524      if source and not seen[source] then
    525        seen[source] = true
    526        count = count + 1
    527      end
    528    end
    529  end
    530  return count
    531 end
    532 
    533 --- @param diagnostics vim.Diagnostic[]
    534 --- @return vim.Diagnostic[]
    535 local function prefix_source(diagnostics)
    536  --- @param d vim.Diagnostic
    537  return vim.tbl_map(function(d)
    538    if not d.source then
    539      return d
    540    end
    541 
    542    local t = vim.deepcopy(d, true)
    543    t.message = string.format('%s: %s', d.source, d.message)
    544    return t
    545  end, diagnostics)
    546 end
    547 
    548 --- @param format fun(diagnostic: vim.Diagnostic): string?
    549 --- @param diagnostics vim.Diagnostic[]
    550 --- @return vim.Diagnostic[]
    551 local function reformat_diagnostics(format, diagnostics)
    552  vim.validate('format', format, 'function')
    553  vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
    554 
    555  local formatted = {}
    556  for _, diagnostic in ipairs(diagnostics) do
    557    local message = format(diagnostic)
    558    if message ~= nil then
    559      local formatted_diagnostic = vim.deepcopy(diagnostic, true)
    560      formatted_diagnostic.message = message
    561      table.insert(formatted, formatted_diagnostic)
    562    end
    563  end
    564  return formatted
    565 end
    566 
    567 --- @param option string
    568 --- @param namespace integer?
    569 --- @return table
    570 local function enabled_value(option, namespace)
    571  local ns = namespace and M.get_namespace(namespace) or {}
    572  if ns.opts and type(ns.opts[option]) == 'table' then
    573    return ns.opts[option]
    574  end
    575 
    576  local global_opt = global_diagnostic_options[option]
    577  if type(global_opt) == 'table' then
    578    return global_opt
    579  end
    580 
    581  return {}
    582 end
    583 
    584 --- @param option string
    585 --- @param value any?
    586 --- @param namespace integer?
    587 --- @param bufnr integer
    588 --- @return any
    589 local function resolve_optional_value(option, value, namespace, bufnr)
    590  if not value then
    591    return false
    592  elseif value == true then
    593    return enabled_value(option, namespace)
    594  elseif type(value) == 'function' then
    595    local val = value(namespace, bufnr) --- @type any
    596    if val == true then
    597      return enabled_value(option, namespace)
    598    else
    599      return val
    600    end
    601  elseif type(value) == 'table' then
    602    return value
    603  end
    604  error('Unexpected option type: ' .. vim.inspect(value))
    605 end
    606 
    607 --- @param opts vim.diagnostic.Opts?
    608 --- @param namespace integer?
    609 --- @param bufnr integer
    610 --- @return vim.diagnostic.OptsResolved
    611 local function get_resolved_options(opts, namespace, bufnr)
    612  local ns = namespace and M.get_namespace(namespace) or {}
    613  -- Do not use tbl_deep_extend so that an empty table can be used to reset to default values
    614  local resolved = vim.tbl_extend('keep', opts or {}, ns.opts or {}, global_diagnostic_options) --- @type table<string,any>
    615  for k in pairs(global_diagnostic_options) do
    616    if resolved[k] ~= nil then
    617      resolved[k] = resolve_optional_value(k, resolved[k], namespace, bufnr)
    618    end
    619  end
    620  return resolved --[[@as vim.diagnostic.OptsResolved]]
    621 end
    622 
    623 -- Default diagnostic highlights
    624 local diagnostic_severities = {
    625  [M.severity.ERROR] = { ctermfg = 1, guifg = 'Red' },
    626  [M.severity.WARN] = { ctermfg = 3, guifg = 'Orange' },
    627  [M.severity.INFO] = { ctermfg = 4, guifg = 'LightBlue' },
    628  [M.severity.HINT] = { ctermfg = 7, guifg = 'LightGrey' },
    629 }
    630 
    631 --- Make a map from vim.diagnostic.Severity -> Highlight Name
    632 --- @param base_name string
    633 --- @return table<vim.diagnostic.Severity,string>
    634 local function make_highlight_map(base_name)
    635  local result = {} --- @type table<vim.diagnostic.Severity,string>
    636  for k in pairs(diagnostic_severities) do
    637    local name = severity_invert[k]
    638    result[k] = ('Diagnostic%s%s%s'):format(base_name, name:sub(1, 1), name:sub(2):lower())
    639  end
    640 
    641  return result
    642 end
    643 
    644 local virtual_text_highlight_map = make_highlight_map('VirtualText')
    645 local virtual_lines_highlight_map = make_highlight_map('VirtualLines')
    646 local underline_highlight_map = make_highlight_map('Underline')
    647 local floating_highlight_map = make_highlight_map('Floating')
    648 local sign_highlight_map = make_highlight_map('Sign')
    649 
    650 --- Get a position based on an extmark referenced by `_extmark_id` field
    651 --- @param diagnostic vim.Diagnostic
    652 --- @return integer lnum
    653 --- @return integer col
    654 --- @return integer end_lnum
    655 --- @return integer end_col
    656 --- @return boolean valid
    657 local function get_logical_pos(diagnostic)
    658  if not diagnostic._extmark_id then
    659    return diagnostic.lnum, diagnostic.col, diagnostic.end_lnum, diagnostic.end_col, true
    660  end
    661 
    662  local ns = M.get_namespace(diagnostic.namespace)
    663  local extmark = api.nvim_buf_get_extmark_by_id(
    664    diagnostic.bufnr,
    665    ns.user_data.location_ns,
    666    diagnostic._extmark_id,
    667    { details = true }
    668  )
    669  if next(extmark) == nil then
    670    return diagnostic.lnum, diagnostic.col, diagnostic.end_lnum, diagnostic.end_col, true
    671  end
    672  return extmark[1], extmark[2], extmark[3].end_row, extmark[3].end_col, not extmark[3].invalid
    673 end
    674 
    675 --- @param diagnostics vim.Diagnostic[]
    676 --- @param use_logical_pos boolean
    677 --- @return table<integer,vim.Diagnostic[]>
    678 local function diagnostic_lines(diagnostics, use_logical_pos)
    679  if not diagnostics then
    680    return {}
    681  end
    682 
    683  local diagnostics_by_line = {} --- @type table<integer,vim.Diagnostic[]>
    684  for _, diagnostic in ipairs(diagnostics) do
    685    local lnum ---@type integer
    686    local valid ---@type boolean
    687 
    688    if use_logical_pos then
    689      lnum, _, _, _, valid = get_logical_pos(diagnostic)
    690    else
    691      lnum, valid = diagnostic.lnum, true
    692    end
    693 
    694    if valid then
    695      local line_diagnostics = diagnostics_by_line[lnum]
    696      if not line_diagnostics then
    697        line_diagnostics = {}
    698        diagnostics_by_line[lnum] = line_diagnostics
    699      end
    700      table.insert(line_diagnostics, diagnostic)
    701    end
    702  end
    703  return diagnostics_by_line
    704 end
    705 
    706 --- @param diagnostics table<integer, vim.Diagnostic[]>
    707 --- @return vim.Diagnostic[]
    708 local function diagnostics_at_cursor(diagnostics)
    709  local lnum = api.nvim_win_get_cursor(0)[1] - 1
    710 
    711  if diagnostics[lnum] ~= nil then
    712    return diagnostics[lnum]
    713  end
    714 
    715  local cursor_diagnostics = {}
    716  for _, line_diags in pairs(diagnostics) do
    717    for _, diag in ipairs(line_diags) do
    718      if diag.end_lnum and lnum >= diag.lnum and lnum <= diag.end_lnum then
    719        table.insert(cursor_diagnostics, diag)
    720      end
    721    end
    722  end
    723  return cursor_diagnostics
    724 end
    725 
    726 --- @param namespace integer
    727 --- @param bufnr integer
    728 --- @param d vim.Diagnostic.Set
    729 local function norm_diag(bufnr, namespace, d)
    730  vim.validate('diagnostic.lnum', d.lnum, 'number')
    731  local d1 = d --[[@as vim.Diagnostic]]
    732  d1.severity = d.severity and to_severity(d.severity) or M.severity.ERROR
    733  d1.end_lnum = d.end_lnum or d.lnum
    734  d1.col = d.col or 0
    735  d1.end_col = d.end_col or d.col or 0
    736  d1.namespace = namespace
    737  d1.bufnr = bufnr
    738 end
    739 
    740 --- @param bufnr integer
    741 --- @param last integer
    742 local function restore_extmarks(bufnr, last)
    743  for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
    744    local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
    745    local found = {} --- @type table<integer,true>
    746    for _, extmark in ipairs(extmarks_current) do
    747      -- nvim_buf_set_lines will move any extmark to the line after the last
    748      -- nvim_buf_set_text will move any extmark to the last line
    749      if extmark[2] ~= last + 1 then
    750        found[extmark[1]] = true
    751      end
    752    end
    753    for _, extmark in ipairs(extmarks) do
    754      if not found[extmark[1]] then
    755        local opts = extmark[4]
    756        --- @diagnostic disable-next-line: inject-field
    757        opts.id = extmark[1]
    758        pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
    759      end
    760    end
    761  end
    762 end
    763 
    764 --- @param namespace integer
    765 --- @param bufnr? integer
    766 local function save_extmarks(namespace, bufnr)
    767  bufnr = vim._resolve_bufnr(bufnr)
    768  if not diagnostic_attached_buffers[bufnr] then
    769    api.nvim_buf_attach(bufnr, false, {
    770      on_lines = function(_, _, _, _, _, last)
    771        restore_extmarks(bufnr, last - 1)
    772      end,
    773      on_detach = function()
    774        diagnostic_cache_extmarks[bufnr] = nil
    775      end,
    776    })
    777    diagnostic_attached_buffers[bufnr] = true
    778  end
    779  diagnostic_cache_extmarks[bufnr][namespace] =
    780    api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true })
    781 end
    782 
    783 --- Create a function that converts a diagnostic severity to an extmark priority.
    784 --- @param priority integer Base priority
    785 --- @param opts vim.diagnostic.OptsResolved
    786 --- @return fun(severity: vim.diagnostic.Severity): integer
    787 local function severity_to_extmark_priority(priority, opts)
    788  if opts.severity_sort then
    789    if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then
    790      return function(severity)
    791        return priority + (severity - vim.diagnostic.severity.ERROR)
    792      end
    793    end
    794 
    795    return function(severity)
    796      return priority + (vim.diagnostic.severity.HINT - severity)
    797    end
    798  end
    799 
    800  return function()
    801    return priority
    802  end
    803 end
    804 
    805 --- @type table<string,true>
    806 local registered_autocmds = {}
    807 
    808 local function make_augroup_key(namespace, bufnr)
    809  local ns = M.get_namespace(namespace)
    810  return string.format('nvim.diagnostic.insertleave.%s.%s', bufnr, ns.name)
    811 end
    812 
    813 --- @param namespace integer
    814 --- @param bufnr integer
    815 local function execute_scheduled_display(namespace, bufnr)
    816  local args = bufs_waiting_to_update[bufnr][namespace]
    817  if not args then
    818    return
    819  end
    820 
    821  -- Clear the args so we don't display unnecessarily.
    822  bufs_waiting_to_update[bufnr][namespace] = nil
    823 
    824  M.show(namespace, bufnr, nil, args)
    825 end
    826 
    827 --- Table of autocmd events to fire the update for displaying new diagnostic information
    828 local insert_leave_auto_cmds = { 'InsertLeave', 'CursorHoldI' }
    829 
    830 --- @param namespace integer
    831 --- @param bufnr integer
    832 --- @param args vim.diagnostic.OptsResolved
    833 local function schedule_display(namespace, bufnr, args)
    834  bufs_waiting_to_update[bufnr][namespace] = args
    835 
    836  local key = make_augroup_key(namespace, bufnr)
    837  if not registered_autocmds[key] then
    838    local group = api.nvim_create_augroup(key, { clear = true })
    839    api.nvim_create_autocmd(insert_leave_auto_cmds, {
    840      group = group,
    841      buffer = bufnr,
    842      callback = function()
    843        execute_scheduled_display(namespace, bufnr)
    844      end,
    845      desc = 'vim.diagnostic: display diagnostics',
    846    })
    847    registered_autocmds[key] = true
    848  end
    849 end
    850 
    851 --- @param namespace integer
    852 --- @param bufnr integer
    853 local function clear_scheduled_display(namespace, bufnr)
    854  local key = make_augroup_key(namespace, bufnr)
    855 
    856  if registered_autocmds[key] then
    857    api.nvim_del_augroup_by_name(key)
    858    registered_autocmds[key] = nil
    859  end
    860 end
    861 
    862 --- @param bufnr integer?
    863 --- @param opts vim.diagnostic.GetOpts?
    864 --- @param clamp boolean
    865 --- @return vim.Diagnostic[]
    866 local function get_diagnostics(bufnr, opts, clamp)
    867  opts = opts or {}
    868 
    869  local namespace = opts.namespace
    870 
    871  if type(namespace) == 'number' then
    872    namespace = { namespace }
    873  end
    874 
    875  ---@cast namespace integer[]
    876 
    877  --- @type vim.Diagnostic[]
    878  local diagnostics = {}
    879 
    880  -- Memoized results of buf_line_count per bufnr
    881  --- @type table<integer,integer>
    882  local buf_line_count = setmetatable({}, {
    883    --- @param t table<integer,integer>
    884    --- @param k integer
    885    --- @return integer
    886    __index = function(t, k)
    887      t[k] = api.nvim_buf_line_count(k)
    888      return rawget(t, k)
    889    end,
    890  })
    891 
    892  local match_severity = opts.severity and severity_predicate(opts.severity)
    893    or function(_)
    894      return true
    895    end
    896 
    897  ---@param b integer
    898  ---@param d vim.Diagnostic
    899  local match_enablement = function(d, b)
    900    if opts.enabled == nil then
    901      return true
    902    end
    903 
    904    local enabled = M.is_enabled({ bufnr = b, ns_id = d.namespace })
    905 
    906    return (enabled and opts.enabled) or (not enabled and not opts.enabled)
    907  end
    908 
    909  ---@param b integer
    910  ---@param d vim.Diagnostic
    911  local function add(b, d)
    912    if
    913      match_severity(d)
    914      and match_enablement(d, b)
    915      and (not opts.lnum or (opts.lnum >= d.lnum and opts.lnum <= (d.end_lnum or d.lnum)))
    916    then
    917      if clamp and api.nvim_buf_is_loaded(b) then
    918        local line_count = buf_line_count[b] - 1
    919        if
    920          d.lnum > line_count
    921          or d.end_lnum > line_count
    922          or d.lnum < 0
    923          or d.end_lnum < 0
    924          or d.col < 0
    925          or d.end_col < 0
    926        then
    927          d = vim.deepcopy(d, true)
    928          d.lnum = math.max(math.min(d.lnum, line_count), 0)
    929          d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0)
    930          d.col = math.max(d.col, 0)
    931          d.end_col = math.max(d.end_col, 0)
    932        end
    933      end
    934      table.insert(diagnostics, d)
    935    end
    936  end
    937 
    938  --- @param buf integer
    939  --- @param diags vim.Diagnostic[]
    940  local function add_all_diags(buf, diags)
    941    for _, diagnostic in pairs(diags) do
    942      add(buf, diagnostic)
    943    end
    944  end
    945 
    946  if not namespace and not bufnr then
    947    for buf, ns_diags in pairs(diagnostic_cache) do
    948      for _, diags in pairs(ns_diags) do
    949        add_all_diags(buf, diags)
    950      end
    951    end
    952  elseif not namespace then
    953    bufnr = vim._resolve_bufnr(bufnr)
    954    for iter_namespace in pairs(diagnostic_cache[bufnr]) do
    955      add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace])
    956    end
    957  elseif bufnr == nil then
    958    for b, t in pairs(diagnostic_cache) do
    959      for _, iter_namespace in ipairs(namespace) do
    960        add_all_diags(b, t[iter_namespace] or {})
    961      end
    962    end
    963  else
    964    bufnr = vim._resolve_bufnr(bufnr)
    965    for _, iter_namespace in ipairs(namespace) do
    966      add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace] or {})
    967    end
    968  end
    969 
    970  return diagnostics
    971 end
    972 
    973 --- @param loclist boolean
    974 --- @param opts vim.diagnostic.setqflist.Opts|vim.diagnostic.setloclist.Opts?
    975 local function set_list(loclist, opts)
    976  opts = opts or {}
    977  local open = if_nil(opts.open, true)
    978  local title = opts.title or 'Diagnostics'
    979  local winnr = opts.winnr or 0
    980  local bufnr --- @type integer?
    981  if loclist then
    982    bufnr = api.nvim_win_get_buf(winnr)
    983  end
    984  -- Don't clamp line numbers since the quickfix list can already handle line
    985  -- numbers beyond the end of the buffer
    986  local diagnostics = get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], false)
    987  if opts.format then
    988    diagnostics = reformat_diagnostics(opts.format, diagnostics)
    989  end
    990  local items = M.toqflist(diagnostics)
    991  local qf_id = nil
    992  if loclist then
    993    vim.fn.setloclist(winnr, {}, 'u', { title = title, items = items })
    994  else
    995    qf_id = get_qf_id_for_title(title)
    996 
    997    -- If we already have a diagnostics quickfix, update it rather than creating a new one.
    998    -- This avoids polluting the finite set of quickfix lists, and preserves the currently selected
    999    -- entry.
   1000    vim.fn.setqflist({}, qf_id and 'u' or ' ', {
   1001      title = title,
   1002      items = items,
   1003      id = qf_id,
   1004    })
   1005  end
   1006 
   1007  if open then
   1008    if not loclist then
   1009      -- First navigate to the diagnostics quickfix list.
   1010      --- @type integer
   1011      local nr = vim.fn.getqflist({ id = qf_id, nr = 0 }).nr
   1012      api.nvim_command(('silent %dchistory'):format(nr))
   1013 
   1014      -- Now open the quickfix list.
   1015      api.nvim_command('botright cwindow')
   1016    else
   1017      api.nvim_command('lwindow')
   1018    end
   1019  end
   1020 end
   1021 
   1022 --- @param a vim.Diagnostic
   1023 --- @param b vim.Diagnostic
   1024 --- @param primary_key string Primary sort key ('severity', 'col', etc)
   1025 --- @param reverse boolean Whether to reverse primary comparison
   1026 --- @param col_fn (fun(diagnostic: vim.Diagnostic): integer)? Optional function to get column value
   1027 --- @return boolean
   1028 local function diagnostic_cmp(a, b, primary_key, reverse, col_fn)
   1029  local a_val, b_val --- @type integer, integer
   1030  if col_fn then
   1031    a_val, b_val = col_fn(a), col_fn(b)
   1032  else
   1033    a_val = a[primary_key] --[[@as integer]]
   1034    b_val = b[primary_key] --[[@as integer]]
   1035  end
   1036 
   1037  local cmp = function(x, y)
   1038    if reverse then
   1039      return x > y
   1040    else
   1041      return x < y
   1042    end
   1043  end
   1044 
   1045  if a_val ~= b_val then
   1046    return cmp(a_val, b_val)
   1047  end
   1048  if a.lnum ~= b.lnum then
   1049    return cmp(a.lnum, b.lnum)
   1050  end
   1051  if a.col ~= b.col then
   1052    return cmp(a.col, b.col)
   1053  end
   1054  if a.end_lnum ~= b.end_lnum then
   1055    return cmp(a.end_lnum, b.end_lnum)
   1056  end
   1057  if a.end_col ~= b.end_col then
   1058    return cmp(a.end_col, b.end_col)
   1059  end
   1060 
   1061  return cmp(a._extmark_id or 0, b._extmark_id or 0)
   1062 end
   1063 
   1064 --- Jump to the diagnostic with the highest severity. First sort the
   1065 --- diagnostics by severity. The first diagnostic then contains the highest severity, and we can
   1066 --- discard all diagnostics with a lower severity.
   1067 --- @param diagnostics vim.Diagnostic[]
   1068 local function filter_highest(diagnostics)
   1069  table.sort(diagnostics, function(a, b)
   1070    return diagnostic_cmp(a, b, 'severity', false)
   1071  end)
   1072 
   1073  -- Find the first diagnostic where the severity does not match the highest severity, and remove
   1074  -- that element and all subsequent elements from the array
   1075  local worst = (diagnostics[1] or {}).severity
   1076  local len = #diagnostics
   1077  for i = 2, len do
   1078    if diagnostics[i].severity ~= worst then
   1079      for j = i, len do
   1080        diagnostics[j] = nil
   1081      end
   1082      break
   1083    end
   1084  end
   1085 end
   1086 
   1087 --- @param search_forward boolean
   1088 --- @param opts vim.diagnostic.JumpOpts?
   1089 --- @param use_logical_pos boolean
   1090 --- @return vim.Diagnostic?
   1091 local function next_diagnostic(search_forward, opts, use_logical_pos)
   1092  opts = opts or {}
   1093  --- @cast opts vim.diagnostic.JumpOpts1
   1094 
   1095  -- Support deprecated win_id alias
   1096  if opts.win_id then
   1097    vim.deprecate('opts.win_id', 'opts.winid', '0.13')
   1098    opts.winid = opts.win_id
   1099    opts.win_id = nil --- @diagnostic disable-line
   1100  end
   1101 
   1102  -- Support deprecated cursor_position alias
   1103  if opts.cursor_position then
   1104    vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
   1105    opts.pos = opts.cursor_position
   1106    opts.cursor_position = nil --- @diagnostic disable-line
   1107  end
   1108 
   1109  local winid = opts.winid or api.nvim_get_current_win()
   1110  local bufnr = api.nvim_win_get_buf(winid)
   1111  local position = opts.pos or api.nvim_win_get_cursor(winid)
   1112 
   1113  -- Adjust row to be 0-indexed
   1114  position[1] = position[1] - 1
   1115 
   1116  local wrap = if_nil(opts.wrap, true)
   1117 
   1118  local diagnostics = get_diagnostics(bufnr, opts, true)
   1119 
   1120  if opts._highest then
   1121    filter_highest(diagnostics)
   1122  end
   1123 
   1124  local line_diagnostics = diagnostic_lines(diagnostics, use_logical_pos)
   1125 
   1126  --- @param diagnostic vim.Diagnostic
   1127  --- @return integer
   1128  local function col_fn(diagnostic)
   1129    return use_logical_pos and select(2, get_logical_pos(diagnostic)) or diagnostic.col
   1130  end
   1131 
   1132  local line_count = api.nvim_buf_line_count(bufnr)
   1133  for i = 0, line_count do
   1134    local offset = i * (search_forward and 1 or -1)
   1135    local lnum = position[1] + offset
   1136    if lnum < 0 or lnum >= line_count then
   1137      if not wrap then
   1138        return
   1139      end
   1140      lnum = (lnum + line_count) % line_count
   1141    end
   1142    if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then
   1143      local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
   1144      --- @type function, function
   1145      local sort_diagnostics, is_next
   1146      if search_forward then
   1147        sort_diagnostics = function(a, b)
   1148          return diagnostic_cmp(a, b, 'col', false, col_fn)
   1149        end
   1150        is_next = function(d)
   1151          return math.min(col_fn(d), math.max(line_length - 1, 0)) > position[2]
   1152        end
   1153      else
   1154        sort_diagnostics = function(a, b)
   1155          return diagnostic_cmp(a, b, 'col', true, col_fn)
   1156        end
   1157        is_next = function(d)
   1158          return math.min(col_fn(d), math.max(line_length - 1, 0)) < position[2]
   1159        end
   1160      end
   1161      table.sort(line_diagnostics[lnum], sort_diagnostics)
   1162      if i == 0 then
   1163        for _, v in
   1164          pairs(line_diagnostics[lnum] --[[@as table<string,any>]])
   1165        do
   1166          if is_next(v) then
   1167            return v
   1168          end
   1169        end
   1170      else
   1171        return line_diagnostics[lnum][1]
   1172      end
   1173    end
   1174  end
   1175 end
   1176 
   1177 --- Move the cursor to the given diagnostic.
   1178 ---
   1179 --- @param diagnostic vim.Diagnostic?
   1180 --- @param opts vim.diagnostic.JumpOpts?
   1181 local function goto_diagnostic(diagnostic, opts)
   1182  if not diagnostic then
   1183    api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {})
   1184    return
   1185  end
   1186 
   1187  opts = opts or {}
   1188  --- @cast opts vim.diagnostic.JumpOpts1
   1189 
   1190  -- Support deprecated win_id alias
   1191  if opts.win_id then
   1192    vim.deprecate('opts.win_id', 'opts.winid', '0.13')
   1193    opts.winid = opts.win_id
   1194    opts.win_id = nil --- @diagnostic disable-line
   1195  end
   1196 
   1197  local winid = opts.winid or api.nvim_get_current_win()
   1198 
   1199  local lnum, col = get_logical_pos(diagnostic)
   1200 
   1201  vim._with({ win = winid }, function()
   1202    -- Save position in the window's jumplist
   1203    vim.cmd("normal! m'")
   1204    api.nvim_win_set_cursor(winid, { lnum + 1, col })
   1205    -- Open folds under the cursor
   1206    vim.cmd('normal! zv')
   1207  end)
   1208 
   1209  if opts.float then
   1210    vim.deprecate('opts.float', 'opts.on_jump', '0.14')
   1211    local float_opts = opts.float
   1212    float_opts = type(float_opts) == 'table' and float_opts or {}
   1213 
   1214    opts.on_jump = function(_, bufnr)
   1215      M.open_float(vim.tbl_extend('keep', float_opts, {
   1216        bufnr = bufnr,
   1217        scope = 'cursor',
   1218        focus = false,
   1219      }))
   1220    end
   1221 
   1222    opts.float = nil ---@diagnostic disable-line
   1223  end
   1224 
   1225  if opts.on_jump then
   1226    vim.schedule(function()
   1227      opts.on_jump(diagnostic, api.nvim_win_get_buf(winid))
   1228    end)
   1229  end
   1230 end
   1231 
   1232 --- Configure diagnostic options globally or for a specific diagnostic
   1233 --- namespace.
   1234 ---
   1235 --- Configuration can be specified globally, per-namespace, or ephemerally
   1236 --- (i.e. only for a single call to |vim.diagnostic.set()| or
   1237 --- |vim.diagnostic.show()|). Ephemeral configuration has highest priority,
   1238 --- followed by namespace configuration, and finally global configuration.
   1239 ---
   1240 --- For example, if a user enables virtual text globally with
   1241 ---
   1242 --- ```lua
   1243 --- vim.diagnostic.config({ virtual_text = true })
   1244 --- ```
   1245 ---
   1246 --- and a diagnostic producer sets diagnostics with
   1247 ---
   1248 --- ```lua
   1249 --- vim.diagnostic.set(ns, 0, diagnostics, { virtual_text = false })
   1250 --- ```
   1251 ---
   1252 --- then virtual text will not be enabled for those diagnostics.
   1253 ---
   1254 ---@param opts vim.diagnostic.Opts? When omitted or `nil`, retrieve the current
   1255 ---       configuration. Otherwise, a configuration table (see |vim.diagnostic.Opts|).
   1256 ---@param namespace integer? Update the options for the given namespace.
   1257 ---                          When omitted, update the global diagnostic options.
   1258 ---@return vim.diagnostic.Opts? : Current diagnostic config if {opts} is omitted.
   1259 function M.config(opts, namespace)
   1260  vim.validate('opts', opts, 'table', true)
   1261  vim.validate('namespace', namespace, 'number', true)
   1262 
   1263  local t --- @type vim.diagnostic.Opts
   1264  if namespace then
   1265    local ns = M.get_namespace(namespace)
   1266    t = ns.opts
   1267  else
   1268    t = global_diagnostic_options
   1269  end
   1270 
   1271  if not opts then
   1272    -- Return current config
   1273    return vim.deepcopy(t, true)
   1274  end
   1275 
   1276  local jump_opts = opts.jump --[[@as vim.diagnostic.JumpOpts1]]
   1277  if jump_opts and jump_opts.float ~= nil then ---@diagnostic disable-line
   1278    vim.deprecate('opts.jump.float', 'opts.jump.on_jump', '0.14')
   1279 
   1280    local float_opts = jump_opts.float
   1281    if float_opts then
   1282      float_opts = type(float_opts) == 'table' and float_opts or {}
   1283 
   1284      jump_opts.on_jump = function(_, bufnr)
   1285        M.open_float(vim.tbl_extend('keep', float_opts, {
   1286          bufnr = bufnr,
   1287          scope = 'cursor',
   1288          focus = false,
   1289        }))
   1290      end
   1291    end
   1292 
   1293    opts.jump.float = nil ---@diagnostic disable-line
   1294  end
   1295 
   1296  for k, v in
   1297    pairs(opts --[[@as table<any,any>]])
   1298  do
   1299    t[k] = v
   1300  end
   1301 
   1302  if namespace then
   1303    for bufnr, v in pairs(diagnostic_cache) do
   1304      if v[namespace] then
   1305        M.show(namespace, bufnr)
   1306      end
   1307    end
   1308  else
   1309    for bufnr, v in pairs(diagnostic_cache) do
   1310      for ns in pairs(v) do
   1311        M.show(ns, bufnr)
   1312      end
   1313    end
   1314  end
   1315 end
   1316 
   1317 --- Execute a given function now if the given buffer is already loaded or once it is loaded later.
   1318 ---
   1319 ---@param bufnr integer Buffer number
   1320 ---@param fn fun()
   1321 ---@return integer?
   1322 local function once_buf_loaded(bufnr, fn)
   1323  if api.nvim_buf_is_loaded(bufnr) then
   1324    fn()
   1325  else
   1326    return api.nvim_create_autocmd('BufRead', {
   1327      buffer = bufnr,
   1328      once = true,
   1329      callback = function()
   1330        fn()
   1331      end,
   1332    })
   1333  end
   1334 end
   1335 
   1336 --- Set diagnostics for the given namespace and buffer.
   1337 ---
   1338 ---@param namespace integer The diagnostic namespace
   1339 ---@param bufnr integer Buffer number
   1340 ---@param diagnostics vim.Diagnostic.Set[]
   1341 ---@param opts? vim.diagnostic.Opts Display options to pass to |vim.diagnostic.show()|
   1342 function M.set(namespace, bufnr, diagnostics, opts)
   1343  vim.validate('namespace', namespace, 'number')
   1344  vim.validate('bufnr', bufnr, 'number')
   1345  vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
   1346  vim.validate('opts', opts, 'table', true)
   1347 
   1348  bufnr = vim._resolve_bufnr(bufnr)
   1349 
   1350  for _, diagnostic in ipairs(diagnostics) do
   1351    norm_diag(bufnr, namespace, diagnostic)
   1352  end
   1353 
   1354  --- @cast diagnostics vim.Diagnostic[]
   1355 
   1356  if vim.tbl_isempty(diagnostics) then
   1357    diagnostic_cache[bufnr][namespace] = nil
   1358  else
   1359    diagnostic_cache[bufnr][namespace] = diagnostics
   1360  end
   1361 
   1362  -- Compute positions, set them as extmarks, and store in diagnostic._extmark_id
   1363  -- (used by get_logical_pos to adjust positions).
   1364  once_buf_loaded(bufnr, function()
   1365    local ns = M.get_namespace(namespace)
   1366 
   1367    if not ns.user_data.location_ns then
   1368      ns.user_data.location_ns =
   1369        api.nvim_create_namespace(string.format('nvim.%s.diagnostic', ns.name))
   1370    end
   1371 
   1372    api.nvim_buf_clear_namespace(bufnr, ns.user_data.location_ns, 0, -1)
   1373 
   1374    local lines = api.nvim_buf_get_lines(bufnr, 0, -1, true)
   1375    -- set extmarks at diagnostic locations to preserve logical positions despite text changes
   1376    for _, diagnostic in ipairs(diagnostics) do
   1377      local last_row = #lines - 1
   1378      local row = math.max(0, math.min(diagnostic.lnum, last_row))
   1379      local row_len = #lines[row + 1]
   1380      local col = math.max(0, math.min(diagnostic.col, row_len - 1))
   1381 
   1382      local end_row = math.max(0, math.min(diagnostic.end_lnum or row, last_row))
   1383      local end_row_len = #lines[end_row + 1]
   1384      local end_col = math.max(0, math.min(diagnostic.end_col or col, end_row_len))
   1385 
   1386      if end_row == row then
   1387        -- avoid starting an extmark beyond end of the line
   1388        if end_col == col then
   1389          end_col = math.min(end_col + 1, end_row_len)
   1390        end
   1391      else
   1392        -- avoid ending an extmark before start of the line
   1393        if end_col == 0 then
   1394          end_row = end_row - 1
   1395 
   1396          local end_line = lines[end_row + 1]
   1397 
   1398          if not end_line then
   1399            error(
   1400              'Failed to adjust diagnostic position to the end of a previous line. #lines in a buffer: '
   1401                .. #lines
   1402                .. ', lnum: '
   1403                .. diagnostic.lnum
   1404                .. ', col: '
   1405                .. diagnostic.col
   1406                .. ', end_lnum: '
   1407                .. diagnostic.end_lnum
   1408                .. ', end_col: '
   1409                .. diagnostic.end_col
   1410            )
   1411          end
   1412 
   1413          end_col = #end_line
   1414        end
   1415      end
   1416 
   1417      diagnostic._extmark_id = api.nvim_buf_set_extmark(bufnr, ns.user_data.location_ns, row, col, {
   1418        end_row = end_row,
   1419        end_col = end_col,
   1420        invalidate = true,
   1421      })
   1422    end
   1423  end)
   1424 
   1425  M.show(namespace, bufnr, nil, opts)
   1426 
   1427  api.nvim_exec_autocmds('DiagnosticChanged', {
   1428    modeline = false,
   1429    buffer = bufnr,
   1430    -- TODO(lewis6991): should this be deepcopy()'d like they are in vim.diagnostic.get()
   1431    data = { diagnostics = diagnostics },
   1432  })
   1433 end
   1434 
   1435 --- Get namespace metadata.
   1436 ---
   1437 ---@param namespace integer Diagnostic namespace
   1438 ---@return vim.diagnostic.NS : Namespace metadata
   1439 function M.get_namespace(namespace)
   1440  vim.validate('namespace', namespace, 'number')
   1441  if not all_namespaces[namespace] then
   1442    local name --- @type string?
   1443    for k, v in pairs(api.nvim_get_namespaces()) do
   1444      if namespace == v then
   1445        name = k
   1446        break
   1447      end
   1448    end
   1449 
   1450    assert(name, 'namespace does not exist or is anonymous')
   1451 
   1452    all_namespaces[namespace] = {
   1453      name = name,
   1454      opts = {},
   1455      user_data = {},
   1456    }
   1457  end
   1458  return all_namespaces[namespace]
   1459 end
   1460 
   1461 --- Get current diagnostic namespaces.
   1462 ---
   1463 ---@return table<integer,vim.diagnostic.NS> : List of active diagnostic namespaces |vim.diagnostic|.
   1464 function M.get_namespaces()
   1465  return vim.deepcopy(all_namespaces, true)
   1466 end
   1467 
   1468 --- Get current diagnostics.
   1469 ---
   1470 --- Modifying diagnostics in the returned table has no effect.
   1471 --- To set diagnostics in a buffer, use |vim.diagnostic.set()|.
   1472 ---
   1473 ---@param bufnr integer? Buffer number to get diagnostics from. Use 0 for
   1474 ---                      current buffer or nil for all buffers.
   1475 ---@param opts? vim.diagnostic.GetOpts
   1476 ---@return vim.Diagnostic[] : Fields `bufnr`, `end_lnum`, `end_col`, and `severity`
   1477 ---                           are guaranteed to be present.
   1478 function M.get(bufnr, opts)
   1479  vim.validate('bufnr', bufnr, 'number', true)
   1480  vim.validate('opts', opts, 'table', true)
   1481 
   1482  return vim.deepcopy(get_diagnostics(bufnr, opts, false), true)
   1483 end
   1484 
   1485 --- Get current diagnostics count.
   1486 ---
   1487 ---@param bufnr? integer Buffer number to get diagnostics from. Use 0 for
   1488 ---                      current buffer or nil for all buffers.
   1489 ---@param opts? vim.diagnostic.GetOpts
   1490 ---@return table<integer, integer> : Table with actually present severity values as keys
   1491 ---                (see |diagnostic-severity|) and integer counts as values.
   1492 function M.count(bufnr, opts)
   1493  vim.validate('bufnr', bufnr, 'number', true)
   1494  vim.validate('opts', opts, 'table', true)
   1495 
   1496  local diagnostics = get_diagnostics(bufnr, opts, false)
   1497  local count = {} --- @type table<integer,integer>
   1498  for _, d in ipairs(diagnostics) do
   1499    count[d.severity] = (count[d.severity] or 0) + 1
   1500  end
   1501  return count
   1502 end
   1503 
   1504 --- Get the previous diagnostic closest to the cursor position.
   1505 ---
   1506 ---@param opts? vim.diagnostic.JumpOpts
   1507 ---@return vim.Diagnostic? : Previous diagnostic
   1508 function M.get_prev(opts)
   1509  return next_diagnostic(false, opts, false)
   1510 end
   1511 
   1512 --- Return the position of the previous diagnostic in the current buffer.
   1513 ---
   1514 ---@param opts? vim.diagnostic.JumpOpts
   1515 ---@return table|false: Previous diagnostic position as a `(row, col)` tuple
   1516 ---                     or `false` if there is no prior diagnostic.
   1517 ---@deprecated
   1518 function M.get_prev_pos(opts)
   1519  vim.deprecate(
   1520    'vim.diagnostic.get_prev_pos()',
   1521    'access the lnum and col fields from get_prev() instead',
   1522    '0.13'
   1523  )
   1524  local prev = M.get_prev(opts)
   1525  if not prev then
   1526    return false
   1527  end
   1528 
   1529  return { prev.lnum, prev.col }
   1530 end
   1531 
   1532 --- Move to the previous diagnostic in the current buffer.
   1533 ---@param opts? vim.diagnostic.JumpOpts
   1534 ---@deprecated
   1535 function M.goto_prev(opts)
   1536  vim.deprecate('vim.diagnostic.goto_prev()', 'vim.diagnostic.jump()', '0.13')
   1537  opts = opts or {}
   1538 
   1539  opts.float = if_nil(opts.float, true) ---@diagnostic disable-line
   1540 
   1541  goto_diagnostic(M.get_prev(opts), opts)
   1542 end
   1543 
   1544 --- Get the next diagnostic closest to the cursor position.
   1545 ---
   1546 ---@param opts? vim.diagnostic.JumpOpts
   1547 ---@return vim.Diagnostic? : Next diagnostic
   1548 function M.get_next(opts)
   1549  return next_diagnostic(true, opts, false)
   1550 end
   1551 
   1552 --- Return the position of the next diagnostic in the current buffer.
   1553 ---
   1554 ---@param opts? vim.diagnostic.JumpOpts
   1555 ---@return table|false : Next diagnostic position as a `(row, col)` tuple or false if no next
   1556 ---                      diagnostic.
   1557 ---@deprecated
   1558 function M.get_next_pos(opts)
   1559  vim.deprecate(
   1560    'vim.diagnostic.get_next_pos()',
   1561    'access the lnum and col fields from get_next() instead',
   1562    '0.13'
   1563  )
   1564  local next = M.get_next(opts)
   1565  if not next then
   1566    return false
   1567  end
   1568 
   1569  return { next.lnum, next.col }
   1570 end
   1571 
   1572 --- A table with the following keys:
   1573 --- @class vim.diagnostic.GetOpts
   1574 ---
   1575 --- Limit diagnostics to one or more namespaces.
   1576 --- @field namespace? integer[]|integer
   1577 ---
   1578 --- Limit diagnostics to those spanning the specified line number.
   1579 --- @field lnum? integer
   1580 ---
   1581 --- See |diagnostic-severity|.
   1582 --- @field severity? vim.diagnostic.SeverityFilter
   1583 ---
   1584 --- Limit diagnostics to only enabled or disabled. If nil, enablement is ignored.
   1585 --- See |vim.diagnostic.enable()|
   1586 --- (default: `nil`)
   1587 --- @field enabled? boolean
   1588 
   1589 --- Configuration table with the keys listed below. Some parameters can have their default values
   1590 --- changed with |vim.diagnostic.config()|.
   1591 --- @class vim.diagnostic.JumpOpts : vim.diagnostic.GetOpts
   1592 ---
   1593 --- The diagnostic to jump to. Mutually exclusive with {count}, {namespace},
   1594 --- and {severity}.
   1595 --- @field diagnostic? vim.Diagnostic
   1596 ---
   1597 --- The number of diagnostics to move by, starting from {pos}. A positive
   1598 --- integer moves forward by {count} diagnostics, while a negative integer moves
   1599 --- backward by {count} diagnostics. Mutually exclusive with {diagnostic}.
   1600 --- @field count? integer
   1601 ---
   1602 --- Cursor position as a `(row, col)` tuple. See |nvim_win_get_cursor()|. Used
   1603 --- to find the nearest diagnostic when {count} is used. Only used when {count}
   1604 --- is non-nil. Default is the current cursor position.
   1605 --- @field pos? [integer,integer]
   1606 ---
   1607 --- Whether to loop around file or not. Similar to 'wrapscan'.
   1608 --- (default: `true`)
   1609 --- @field wrap? boolean
   1610 ---
   1611 --- See |diagnostic-severity|.
   1612 --- @field severity? vim.diagnostic.SeverityFilter
   1613 ---
   1614 --- Go to the diagnostic with the highest severity.
   1615 --- (default: `false`)
   1616 --- @field package _highest? boolean
   1617 ---
   1618 --- Optional callback invoked with the diagnostic that was jumped to.
   1619 --- @field on_jump? fun(diagnostic:vim.Diagnostic?, bufnr:integer)
   1620 ---
   1621 --- Window ID
   1622 --- (default: `0`)
   1623 --- @field winid? integer
   1624 
   1625 --- @nodoc
   1626 --- @class vim.diagnostic.JumpOpts1 : vim.diagnostic.JumpOpts
   1627 --- @field win_id? integer (deprecated) use winid
   1628 --- @field cursor_position? [integer, integer] (deprecated) use pos
   1629 --- @field float? table|boolean (deprecated) use on_jump
   1630 
   1631 --- Move to a diagnostic.
   1632 ---
   1633 --- @param opts vim.diagnostic.JumpOpts
   1634 --- @return vim.Diagnostic? # The diagnostic that was moved to.
   1635 function M.jump(opts)
   1636  vim.validate('opts', opts, 'table')
   1637 
   1638  -- One of "diagnostic" or "count" must be provided
   1639  assert(
   1640    opts.diagnostic or opts.count,
   1641    'One of "diagnostic" or "count" must be specified in the options to vim.diagnostic.jump()'
   1642  )
   1643 
   1644  -- Apply configuration options from vim.diagnostic.config()
   1645  opts = vim.tbl_deep_extend('keep', opts, global_diagnostic_options.jump)
   1646  --- @cast opts vim.diagnostic.JumpOpts1
   1647 
   1648  if opts.diagnostic then
   1649    goto_diagnostic(opts.diagnostic, opts)
   1650    return opts.diagnostic
   1651  end
   1652 
   1653  local count = opts.count
   1654  if count == 0 then
   1655    return nil
   1656  end
   1657 
   1658  -- Support deprecated cursor_position alias
   1659  if opts.cursor_position then
   1660    vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
   1661    opts.pos = opts.cursor_position
   1662    opts.cursor_position = nil --- @diagnostic disable-line
   1663  end
   1664 
   1665  local diag = nil
   1666  while count ~= 0 do
   1667    local next = next_diagnostic(count > 0, opts, true)
   1668    if not next then
   1669      break
   1670    end
   1671 
   1672    -- Update cursor position
   1673    opts.pos = { next.lnum + 1, next.col }
   1674 
   1675    if count > 0 then
   1676      count = count - 1
   1677    else
   1678      count = count + 1
   1679    end
   1680    diag = next
   1681  end
   1682 
   1683  goto_diagnostic(diag, opts)
   1684 
   1685  return diag
   1686 end
   1687 
   1688 --- Move to the next diagnostic.
   1689 ---
   1690 ---@param opts? vim.diagnostic.JumpOpts
   1691 ---@deprecated
   1692 function M.goto_next(opts)
   1693  vim.deprecate('vim.diagnostic.goto_next()', 'vim.diagnostic.jump()', '0.13')
   1694  opts = opts or {}
   1695  opts.float = if_nil(opts.float, true) ---@diagnostic disable-line
   1696  goto_diagnostic(M.get_next(opts), opts)
   1697 end
   1698 
   1699 ---@param autocmd_key string
   1700 ---@param ns vim.diagnostic.NS
   1701 local function cleanup_show_autocmd(autocmd_key, ns)
   1702  if ns.user_data[autocmd_key] then
   1703    api.nvim_del_autocmd(ns.user_data[autocmd_key])
   1704 
   1705    ---@type integer?
   1706    ns.user_data[autocmd_key] = nil
   1707  end
   1708 end
   1709 
   1710 ---@param autocmd_key string
   1711 ---@param ns vim.diagnostic.NS
   1712 ---@param bufnr integer
   1713 ---@param fn fun()
   1714 local function show_once_loaded(autocmd_key, ns, bufnr, fn)
   1715  cleanup_show_autocmd(autocmd_key, ns)
   1716 
   1717  ---@type integer?
   1718  ns.user_data[autocmd_key] = once_buf_loaded(bufnr, function()
   1719    ---@type integer?
   1720    ns.user_data[autocmd_key] = nil
   1721    fn()
   1722  end)
   1723 end
   1724 
   1725 M.handlers.signs = {
   1726  show = function(namespace, bufnr, diagnostics, opts)
   1727    vim.validate('namespace', namespace, 'number')
   1728    vim.validate('bufnr', bufnr, 'number')
   1729    vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
   1730    vim.validate('opts', opts, 'table', true)
   1731    vim.validate('opts.signs', (opts and opts or {}).signs, 'table', true)
   1732 
   1733    bufnr = vim._resolve_bufnr(bufnr)
   1734    opts = opts or {}
   1735 
   1736    local ns = M.get_namespace(namespace)
   1737    show_once_loaded('sign_show_autocmd', ns, bufnr, function()
   1738      -- 10 is the default sign priority when none is explicitly specified
   1739      local priority = opts.signs and opts.signs.priority or 10
   1740      local get_priority = severity_to_extmark_priority(priority, opts)
   1741 
   1742      if not ns.user_data.sign_ns then
   1743        ns.user_data.sign_ns =
   1744          api.nvim_create_namespace(string.format('nvim.%s.diagnostic.signs', ns.name))
   1745      end
   1746 
   1747      local text = {} ---@type table<vim.diagnostic.Severity|string, string>
   1748      for k in pairs(M.severity) do
   1749        if opts.signs.text and opts.signs.text[k] then
   1750          text[k] = opts.signs.text[k]
   1751        elseif type(k) == 'string' and not text[k] then
   1752          text[k] = k:sub(1, 1):upper()
   1753        end
   1754      end
   1755 
   1756      local numhl = opts.signs.numhl or {}
   1757      local linehl = opts.signs.linehl or {}
   1758 
   1759      local line_count = api.nvim_buf_line_count(bufnr)
   1760 
   1761      for _, diagnostic in ipairs(diagnostics) do
   1762        if diagnostic.lnum <= line_count then
   1763          api.nvim_buf_set_extmark(bufnr, ns.user_data.sign_ns, diagnostic.lnum, 0, {
   1764            sign_text = text[diagnostic.severity] or text[M.severity[diagnostic.severity]] or 'U',
   1765            sign_hl_group = sign_highlight_map[diagnostic.severity],
   1766            number_hl_group = numhl[diagnostic.severity],
   1767            line_hl_group = linehl[diagnostic.severity],
   1768            priority = get_priority(diagnostic.severity),
   1769          })
   1770        end
   1771      end
   1772    end)
   1773  end,
   1774 
   1775  --- @param namespace integer
   1776  --- @param bufnr integer
   1777  hide = function(namespace, bufnr)
   1778    local ns = M.get_namespace(namespace)
   1779    cleanup_show_autocmd('sign_show_autocmd', ns)
   1780    if ns.user_data.sign_ns and api.nvim_buf_is_valid(bufnr) then
   1781      api.nvim_buf_clear_namespace(bufnr, ns.user_data.sign_ns, 0, -1)
   1782    end
   1783  end,
   1784 }
   1785 
   1786 M.handlers.underline = {
   1787  show = function(namespace, bufnr, diagnostics, opts)
   1788    vim.validate('namespace', namespace, 'number')
   1789    vim.validate('bufnr', bufnr, 'number')
   1790    vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
   1791    vim.validate('opts', opts, 'table', true)
   1792 
   1793    bufnr = vim._resolve_bufnr(bufnr)
   1794    opts = opts or {}
   1795 
   1796    local ns = M.get_namespace(namespace)
   1797    show_once_loaded('underline_show_autocmd', ns, bufnr, function()
   1798      if not ns.user_data.underline_ns then
   1799        ns.user_data.underline_ns =
   1800          api.nvim_create_namespace(string.format('nvim.%s.diagnostic.underline', ns.name))
   1801      end
   1802 
   1803      local underline_ns = ns.user_data.underline_ns
   1804      local get_priority = severity_to_extmark_priority(vim.hl.priorities.diagnostics, opts)
   1805 
   1806      for _, diagnostic in ipairs(diagnostics) do
   1807        local higroups = { underline_highlight_map[diagnostic.severity] }
   1808 
   1809        if diagnostic._tags then
   1810          if diagnostic._tags.unnecessary then
   1811            table.insert(higroups, 'DiagnosticUnnecessary')
   1812          end
   1813          if diagnostic._tags.deprecated then
   1814            table.insert(higroups, 'DiagnosticDeprecated')
   1815          end
   1816        end
   1817 
   1818        local lines =
   1819          api.nvim_buf_get_lines(diagnostic.bufnr, diagnostic.lnum, diagnostic.lnum + 1, true)
   1820 
   1821        for _, higroup in ipairs(higroups) do
   1822          vim.hl.range(
   1823            bufnr,
   1824            underline_ns,
   1825            higroup,
   1826            { diagnostic.lnum, math.min(diagnostic.col, #lines[1] - 1) },
   1827            { diagnostic.end_lnum, diagnostic.end_col },
   1828            { priority = get_priority(diagnostic.severity) }
   1829          )
   1830        end
   1831      end
   1832      save_extmarks(underline_ns, bufnr)
   1833    end)
   1834  end,
   1835  hide = function(namespace, bufnr)
   1836    local ns = M.get_namespace(namespace)
   1837    cleanup_show_autocmd('underline_show_autocmd', ns)
   1838    if ns.user_data.underline_ns then
   1839      diagnostic_cache_extmarks[bufnr][ns.user_data.underline_ns] = {}
   1840      if api.nvim_buf_is_valid(bufnr) then
   1841        api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1)
   1842      end
   1843    end
   1844  end,
   1845 }
   1846 
   1847 --- @param namespace integer
   1848 --- @param bufnr integer
   1849 --- @param diagnostics table<integer, vim.Diagnostic[]>
   1850 --- @param opts vim.diagnostic.Opts.VirtualText
   1851 local function render_virtual_text(namespace, bufnr, diagnostics, opts)
   1852  local lnum = api.nvim_win_get_cursor(0)[1] - 1
   1853  local buf_len = api.nvim_buf_line_count(bufnr)
   1854  api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
   1855 
   1856  local function should_render(line)
   1857    if
   1858      (line >= buf_len)
   1859      or (opts.current_line == true and line ~= lnum)
   1860      or (opts.current_line == false and line == lnum)
   1861    then
   1862      return false
   1863    end
   1864 
   1865    return true
   1866  end
   1867 
   1868  for line, line_diagnostics in pairs(diagnostics) do
   1869    if should_render(line) then
   1870      local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts)
   1871      if virt_texts then
   1872        api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
   1873          hl_mode = opts.hl_mode or 'combine',
   1874          virt_text = virt_texts,
   1875          virt_text_pos = opts.virt_text_pos,
   1876          virt_text_hide = opts.virt_text_hide,
   1877          virt_text_win_col = opts.virt_text_win_col,
   1878        })
   1879      end
   1880    end
   1881  end
   1882 end
   1883 
   1884 M.handlers.virtual_text = {
   1885  show = function(namespace, bufnr, diagnostics, opts)
   1886    vim.validate('namespace', namespace, 'number')
   1887    vim.validate('bufnr', bufnr, 'number')
   1888    vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
   1889    vim.validate('opts', opts, 'table', true)
   1890 
   1891    bufnr = vim._resolve_bufnr(bufnr)
   1892    opts = opts or {}
   1893 
   1894    local ns = M.get_namespace(namespace)
   1895    show_once_loaded('virtual_text_show_autocmd', ns, bufnr, function()
   1896      if opts.virtual_text then
   1897        if opts.virtual_text.format then
   1898          diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics)
   1899        end
   1900        if
   1901          opts.virtual_text.source
   1902          and (opts.virtual_text.source ~= 'if_many' or count_sources(bufnr) > 1)
   1903        then
   1904          diagnostics = prefix_source(diagnostics)
   1905        end
   1906      end
   1907 
   1908      if not ns.user_data.virt_text_ns then
   1909        ns.user_data.virt_text_ns =
   1910          api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_text', ns.name))
   1911      end
   1912      if not ns.user_data.virt_text_augroup then
   1913        ns.user_data.virt_text_augroup = api.nvim_create_augroup(
   1914          string.format('nvim.%s.diagnostic.virt_text', ns.name),
   1915          { clear = true }
   1916        )
   1917      end
   1918 
   1919      api.nvim_clear_autocmds({ group = ns.user_data.virt_text_augroup, buffer = bufnr })
   1920 
   1921      local line_diagnostics = diagnostic_lines(diagnostics, true)
   1922 
   1923      if opts.virtual_text.current_line ~= nil then
   1924        api.nvim_create_autocmd('CursorMoved', {
   1925          buffer = bufnr,
   1926          group = ns.user_data.virt_text_augroup,
   1927          callback = function()
   1928            render_virtual_text(
   1929              ns.user_data.virt_text_ns,
   1930              bufnr,
   1931              line_diagnostics,
   1932              opts.virtual_text
   1933            )
   1934          end,
   1935        })
   1936      end
   1937 
   1938      render_virtual_text(ns.user_data.virt_text_ns, bufnr, line_diagnostics, opts.virtual_text)
   1939 
   1940      save_extmarks(ns.user_data.virt_text_ns, bufnr)
   1941    end)
   1942  end,
   1943  hide = function(namespace, bufnr)
   1944    local ns = M.get_namespace(namespace)
   1945    cleanup_show_autocmd('virtual_text_show_autocmd', ns)
   1946    if ns.user_data.virt_text_ns then
   1947      diagnostic_cache_extmarks[bufnr][ns.user_data.virt_text_ns] = {}
   1948      if api.nvim_buf_is_valid(bufnr) then
   1949        api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1)
   1950        api.nvim_clear_autocmds({ group = ns.user_data.virt_text_augroup, buffer = bufnr })
   1951      end
   1952    end
   1953  end,
   1954 }
   1955 
   1956 --- Some characters (like tabs) take up more than one cell. Additionally, inline
   1957 --- virtual text can make the distance between 2 columns larger.
   1958 --- A diagnostic aligned under such characters needs to account for that and that
   1959 --- many spaces to its left.
   1960 --- @param bufnr integer
   1961 --- @param lnum integer
   1962 --- @param start_col integer
   1963 --- @param end_col integer
   1964 --- @return integer
   1965 local function distance_between_cols(bufnr, lnum, start_col, end_col)
   1966  return api.nvim_buf_call(bufnr, function()
   1967    local s = vim.fn.virtcol({ lnum + 1, start_col })
   1968    local e = vim.fn.virtcol({ lnum + 1, end_col + 1 })
   1969    return e - 1 - s
   1970  end)
   1971 end
   1972 
   1973 --- @param namespace integer
   1974 --- @param bufnr integer
   1975 --- @param diagnostics vim.Diagnostic[]
   1976 local function render_virtual_lines(namespace, bufnr, diagnostics)
   1977  table.sort(diagnostics, function(d1, d2)
   1978    return diagnostic_cmp(d1, d2, 'lnum', false)
   1979  end)
   1980 
   1981  api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
   1982 
   1983  if not next(diagnostics) then
   1984    return
   1985  end
   1986 
   1987  -- This loop reads each line, putting them into stacks with some extra data since
   1988  -- rendering each line requires understanding what is beneath it.
   1989  local ElementType = { Space = 1, Diagnostic = 2, Overlap = 3, Blank = 4 } ---@enum ElementType
   1990  ---@type table<integer, [ElementType, string|vim.diagnostic.Severity|vim.Diagnostic][]>
   1991  local line_stacks = {}
   1992  local prev_lnum = -1
   1993  local prev_col = 0
   1994  for _, diag in ipairs(diagnostics) do
   1995    if not line_stacks[diag.lnum] then
   1996      line_stacks[diag.lnum] = {}
   1997    end
   1998 
   1999    local stack = line_stacks[diag.lnum]
   2000 
   2001    if diag.lnum ~= prev_lnum then
   2002      table.insert(stack, {
   2003        ElementType.Space,
   2004        string.rep(' ', distance_between_cols(bufnr, diag.lnum, 0, diag.col)),
   2005      })
   2006    elseif diag.col ~= prev_col then
   2007      table.insert(stack, {
   2008        ElementType.Space,
   2009        string.rep(
   2010          ' ',
   2011          -- +1 because indexing starts at 0 in one API but at 1 in the other.
   2012          distance_between_cols(bufnr, diag.lnum, prev_col + 1, diag.col)
   2013        ),
   2014      })
   2015    else
   2016      table.insert(stack, { ElementType.Overlap, diag.severity })
   2017    end
   2018 
   2019    if diag.message:find('^%s*$') then
   2020      table.insert(stack, { ElementType.Blank, diag })
   2021    else
   2022      table.insert(stack, { ElementType.Diagnostic, diag })
   2023    end
   2024 
   2025    prev_lnum, prev_col = diag.lnum, diag.col
   2026  end
   2027 
   2028  local chars = {
   2029    cross = '┼',
   2030    horizontal = '─',
   2031    horizontal_up = 'â”´',
   2032    up_right = 'â””',
   2033    vertical = '│',
   2034    vertical_right = '├',
   2035  }
   2036 
   2037  for lnum, stack in pairs(line_stacks) do
   2038    local virt_lines = {}
   2039 
   2040    -- Note that we read in the order opposite to insertion.
   2041    for i = #stack, 1, -1 do
   2042      if stack[i][1] == ElementType.Diagnostic then
   2043        local diagnostic = stack[i][2]
   2044        local left = {} ---@type [string, string]
   2045        local overlap = false
   2046        local multi = false
   2047 
   2048        -- Iterate the stack for this line to find elements on the left.
   2049        for j = 1, i - 1 do
   2050          local type = stack[j][1]
   2051          local data = stack[j][2]
   2052          if type == ElementType.Space then
   2053            if multi then
   2054              ---@cast data string
   2055              table.insert(left, {
   2056                string.rep(chars.horizontal, data:len()),
   2057                virtual_lines_highlight_map[diagnostic.severity],
   2058              })
   2059            else
   2060              table.insert(left, { data, '' })
   2061            end
   2062          elseif type == ElementType.Diagnostic then
   2063            -- If an overlap follows this line, don't add an extra column.
   2064            if stack[j + 1][1] ~= ElementType.Overlap then
   2065              table.insert(left, { chars.vertical, virtual_lines_highlight_map[data.severity] })
   2066            end
   2067            overlap = false
   2068          elseif type == ElementType.Blank then
   2069            if multi then
   2070              table.insert(
   2071                left,
   2072                { chars.horizontal_up, virtual_lines_highlight_map[data.severity] }
   2073              )
   2074            else
   2075              table.insert(left, { chars.up_right, virtual_lines_highlight_map[data.severity] })
   2076            end
   2077            multi = true
   2078          elseif type == ElementType.Overlap then
   2079            overlap = true
   2080          end
   2081        end
   2082 
   2083        local center_char ---@type string
   2084        if overlap and multi then
   2085          center_char = chars.cross
   2086        elseif overlap then
   2087          center_char = chars.vertical_right
   2088        elseif multi then
   2089          center_char = chars.horizontal_up
   2090        else
   2091          center_char = chars.up_right
   2092        end
   2093        local center = {
   2094          {
   2095            string.format('%s%s', center_char, string.rep(chars.horizontal, 4) .. ' '),
   2096            virtual_lines_highlight_map[diagnostic.severity],
   2097          },
   2098        }
   2099 
   2100        -- We can draw on the left side if and only if:
   2101        -- a. Is the last one stacked this line.
   2102        -- b. Has enough space on the left.
   2103        -- c. Is just one line.
   2104        -- d. Is not an overlap.
   2105        for msg_line in diagnostic.message:gmatch('([^\n]+)') do
   2106          local vline = {}
   2107          vim.list_extend(vline, left)
   2108          vim.list_extend(vline, center)
   2109          vim.list_extend(vline, { { msg_line, virtual_lines_highlight_map[diagnostic.severity] } })
   2110 
   2111          table.insert(virt_lines, vline)
   2112 
   2113          -- Special-case for continuation lines:
   2114          if overlap then
   2115            center = {
   2116              { chars.vertical, virtual_lines_highlight_map[diagnostic.severity] },
   2117              { '     ', '' },
   2118            }
   2119          else
   2120            center = { { '      ', '' } }
   2121          end
   2122        end
   2123      end
   2124    end
   2125 
   2126    api.nvim_buf_set_extmark(bufnr, namespace, lnum, 0, {
   2127      virt_lines_overflow = 'scroll',
   2128      virt_lines = virt_lines,
   2129    })
   2130  end
   2131 end
   2132 
   2133 --- Default formatter for the virtual_lines handler.
   2134 --- @param diagnostic vim.Diagnostic
   2135 local function format_virtual_lines(diagnostic)
   2136  if diagnostic.code then
   2137    return string.format('%s: %s', diagnostic.code, diagnostic.message)
   2138  else
   2139    return diagnostic.message
   2140  end
   2141 end
   2142 
   2143 M.handlers.virtual_lines = {
   2144  show = function(namespace, bufnr, diagnostics, opts)
   2145    vim.validate('namespace', namespace, 'number')
   2146    vim.validate('bufnr', bufnr, 'number')
   2147    vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
   2148    vim.validate('opts', opts, 'table', true)
   2149 
   2150    bufnr = vim._resolve_bufnr(bufnr)
   2151    opts = opts or {}
   2152 
   2153    local ns = M.get_namespace(namespace)
   2154    show_once_loaded('virtual_lines_show_autocmd', ns, bufnr, function()
   2155      if not ns.user_data.virt_lines_ns then
   2156        ns.user_data.virt_lines_ns =
   2157          api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_lines', ns.name))
   2158      end
   2159      if not ns.user_data.virt_lines_augroup then
   2160        ns.user_data.virt_lines_augroup = api.nvim_create_augroup(
   2161          string.format('nvim.%s.diagnostic.virt_lines', ns.name),
   2162          { clear = true }
   2163        )
   2164      end
   2165 
   2166      api.nvim_clear_autocmds({ group = ns.user_data.virt_lines_augroup, buffer = bufnr })
   2167 
   2168      diagnostics =
   2169        reformat_diagnostics(opts.virtual_lines.format or format_virtual_lines, diagnostics)
   2170 
   2171      if opts.virtual_lines.current_line == true then
   2172        -- Create a mapping from line -> diagnostics so that we can quickly get the
   2173        -- diagnostics we need when the cursor line doesn't change.
   2174        local line_diagnostics = diagnostic_lines(diagnostics, true)
   2175        api.nvim_create_autocmd('CursorMoved', {
   2176          buffer = bufnr,
   2177          group = ns.user_data.virt_lines_augroup,
   2178          callback = function()
   2179            render_virtual_lines(
   2180              ns.user_data.virt_lines_ns,
   2181              bufnr,
   2182              diagnostics_at_cursor(line_diagnostics)
   2183            )
   2184          end,
   2185        })
   2186        -- Also show diagnostics for the current line before the first CursorMoved event.
   2187        render_virtual_lines(
   2188          ns.user_data.virt_lines_ns,
   2189          bufnr,
   2190          diagnostics_at_cursor(line_diagnostics)
   2191        )
   2192      else
   2193        render_virtual_lines(ns.user_data.virt_lines_ns, bufnr, diagnostics)
   2194      end
   2195 
   2196      save_extmarks(ns.user_data.virt_lines_ns, bufnr)
   2197    end)
   2198  end,
   2199  hide = function(namespace, bufnr)
   2200    local ns = M.get_namespace(namespace)
   2201    cleanup_show_autocmd('virtual_lines_show_autocmd', ns)
   2202    if ns.user_data.virt_lines_ns then
   2203      diagnostic_cache_extmarks[bufnr][ns.user_data.virt_lines_ns] = {}
   2204      if api.nvim_buf_is_valid(bufnr) then
   2205        api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_lines_ns, 0, -1)
   2206        api.nvim_clear_autocmds({ group = ns.user_data.virt_lines_augroup, buffer = bufnr })
   2207      end
   2208    end
   2209  end,
   2210 }
   2211 
   2212 --- Get virtual text chunks to display using |nvim_buf_set_extmark()|.
   2213 ---
   2214 --- Exported for backward compatibility with
   2215 --- vim.lsp.diagnostic.get_virtual_text_chunks_for_line(). When that function is eventually removed,
   2216 --- this can be made local.
   2217 --- @private
   2218 --- @param line_diags table<integer,vim.Diagnostic>
   2219 --- @param opts vim.diagnostic.Opts.VirtualText
   2220 function M._get_virt_text_chunks(line_diags, opts)
   2221  if #line_diags == 0 then
   2222    return nil
   2223  end
   2224 
   2225  opts = opts or {}
   2226  local prefix = opts.prefix or 'â– '
   2227  local suffix = opts.suffix or ''
   2228  local spacing = opts.spacing or 4
   2229 
   2230  -- Create a little more space between virtual text and contents
   2231  local virt_texts = { { string.rep(' ', spacing) } }
   2232 
   2233  for i = 1, #line_diags do
   2234    local resolved_prefix = prefix
   2235    if type(prefix) == 'function' then
   2236      resolved_prefix = prefix(line_diags[i], i, #line_diags) or ''
   2237    end
   2238    table.insert(
   2239      virt_texts,
   2240      { resolved_prefix, virtual_text_highlight_map[line_diags[i].severity] }
   2241    )
   2242  end
   2243  local last = line_diags[#line_diags]
   2244 
   2245  -- TODO(tjdevries): Allow different servers to be shown first somehow?
   2246  -- TODO(tjdevries): Display server name associated with these?
   2247  if last.message then
   2248    if type(suffix) == 'function' then
   2249      suffix = suffix(last) or ''
   2250    end
   2251    table.insert(virt_texts, {
   2252      string.format(' %s%s', last.message:gsub('\r', ''):gsub('\n', '  '), suffix),
   2253      virtual_text_highlight_map[last.severity],
   2254    })
   2255 
   2256    return virt_texts
   2257  end
   2258 end
   2259 
   2260 --- Hide currently displayed diagnostics.
   2261 ---
   2262 --- This only clears the decorations displayed in the buffer. Diagnostics can
   2263 --- be redisplayed with |vim.diagnostic.show()|. To completely remove
   2264 --- diagnostics, use |vim.diagnostic.reset()|.
   2265 ---
   2266 --- To hide diagnostics and prevent them from re-displaying, use
   2267 --- |vim.diagnostic.enable()|.
   2268 ---
   2269 ---@param namespace integer? Diagnostic namespace. When omitted, hide
   2270 ---                          diagnostics from all namespaces.
   2271 ---@param bufnr integer? Buffer number, or 0 for current buffer. When
   2272 ---                      omitted, hide diagnostics in all buffers.
   2273 function M.hide(namespace, bufnr)
   2274  vim.validate('namespace', namespace, 'number', true)
   2275  vim.validate('bufnr', bufnr, 'number', true)
   2276 
   2277  local buffers = bufnr and { vim._resolve_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
   2278  for _, iter_bufnr in ipairs(buffers) do
   2279    local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr])
   2280    for _, iter_namespace in ipairs(namespaces) do
   2281      for _, handler in pairs(M.handlers) do
   2282        if handler.hide then
   2283          handler.hide(iter_namespace, iter_bufnr)
   2284        end
   2285      end
   2286    end
   2287  end
   2288 end
   2289 
   2290 --- Check whether diagnostics are enabled.
   2291 ---
   2292 --- @param filter vim.diagnostic.Filter?
   2293 --- @return boolean
   2294 --- @since 12
   2295 function M.is_enabled(filter)
   2296  filter = filter or {}
   2297  if filter.ns_id and M.get_namespace(filter.ns_id).disabled then
   2298    return false
   2299  elseif filter.bufnr == nil then
   2300    -- See enable() logic.
   2301    return vim.tbl_isempty(diagnostic_disabled) and not diagnostic_disabled[1]
   2302  end
   2303 
   2304  local bufnr = vim._resolve_bufnr(filter.bufnr)
   2305  if type(diagnostic_disabled[bufnr]) == 'table' then
   2306    return not diagnostic_disabled[bufnr][filter.ns_id]
   2307  end
   2308 
   2309  return diagnostic_disabled[bufnr] == nil
   2310 end
   2311 
   2312 --- Display diagnostics for the given namespace and buffer.
   2313 ---
   2314 ---@param namespace integer? Diagnostic namespace. When omitted, show
   2315 ---                          diagnostics from all namespaces.
   2316 ---@param bufnr integer? Buffer number, or 0 for current buffer. When omitted, show
   2317 ---                      diagnostics in all buffers.
   2318 ---@param diagnostics vim.Diagnostic[]? The diagnostics to display. When omitted, use the
   2319 ---                             saved diagnostics for the given namespace and
   2320 ---                             buffer. This can be used to display a list of diagnostics
   2321 ---                             without saving them or to display only a subset of
   2322 ---                             diagnostics. May not be used when {namespace}
   2323 ---                             or {bufnr} is nil.
   2324 ---@param opts? vim.diagnostic.Opts Display options.
   2325 function M.show(namespace, bufnr, diagnostics, opts)
   2326  vim.validate('namespace', namespace, 'number', true)
   2327  vim.validate('bufnr', bufnr, 'number', true)
   2328  vim.validate('diagnostics', diagnostics, vim.islist, true, 'a list of diagnostics')
   2329  vim.validate('opts', opts, 'table', true)
   2330 
   2331  if not bufnr or not namespace then
   2332    assert(not diagnostics, 'Cannot show diagnostics without a buffer and namespace')
   2333    if not bufnr then
   2334      for iter_bufnr in pairs(diagnostic_cache) do
   2335        M.show(namespace, iter_bufnr, nil, opts)
   2336      end
   2337    else
   2338      -- namespace is nil
   2339      bufnr = vim._resolve_bufnr(bufnr)
   2340      for iter_namespace in pairs(diagnostic_cache[bufnr]) do
   2341        M.show(iter_namespace, bufnr, nil, opts)
   2342      end
   2343    end
   2344    return
   2345  end
   2346 
   2347  if not M.is_enabled { bufnr = bufnr or 0, ns_id = namespace } then
   2348    return
   2349  end
   2350 
   2351  M.hide(namespace, bufnr)
   2352 
   2353  diagnostics = diagnostics or get_diagnostics(bufnr, { namespace = namespace }, true)
   2354 
   2355  if vim.tbl_isempty(diagnostics) then
   2356    return
   2357  end
   2358 
   2359  local opts_res = get_resolved_options(opts, namespace, bufnr)
   2360 
   2361  if opts_res.update_in_insert then
   2362    clear_scheduled_display(namespace, bufnr)
   2363  else
   2364    local mode = api.nvim_get_mode()
   2365    if mode.mode:sub(1, 1) == 'i' then
   2366      schedule_display(namespace, bufnr, opts_res)
   2367      return
   2368    end
   2369  end
   2370 
   2371  if opts_res.severity_sort then
   2372    if type(opts_res.severity_sort) == 'table' and opts_res.severity_sort.reverse then
   2373      table.sort(diagnostics, function(a, b)
   2374        return diagnostic_cmp(a, b, 'severity', false)
   2375      end)
   2376    else
   2377      table.sort(diagnostics, function(a, b)
   2378        return diagnostic_cmp(a, b, 'severity', true)
   2379      end)
   2380    end
   2381  end
   2382 
   2383  for handler_name, handler in pairs(M.handlers) do
   2384    if handler.show and opts_res[handler_name] then
   2385      local filtered = filter_by_severity(opts_res[handler_name].severity, diagnostics)
   2386      handler.show(namespace, bufnr, filtered, opts_res)
   2387    end
   2388  end
   2389 end
   2390 
   2391 --- Show diagnostics in a floating window.
   2392 ---
   2393 ---@param opts vim.diagnostic.Opts.Float?
   2394 ---@return integer? float_bufnr
   2395 ---@return integer? winid
   2396 function M.open_float(opts, ...)
   2397  -- Support old (bufnr, opts) signature
   2398  local bufnr --- @type integer?
   2399  if opts == nil or type(opts) == 'number' then
   2400    bufnr = opts
   2401    opts = ... --- @type vim.diagnostic.Opts.Float
   2402  else
   2403    vim.validate('opts', opts, 'table', true)
   2404  end
   2405 
   2406  opts = opts or {}
   2407  bufnr = vim._resolve_bufnr(bufnr or opts.bufnr)
   2408 
   2409  do
   2410    -- Resolve options with user settings from vim.diagnostic.config
   2411    -- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
   2412    -- does not have a dedicated table for configuration options; instead, the options are mixed in
   2413    -- with its `opts` table which also includes "keyword" parameters. So we create a dedicated
   2414    -- options table that inherits missing keys from the global configuration before resolving.
   2415    local t = global_diagnostic_options.float
   2416    local float_opts = vim.tbl_extend('keep', opts, type(t) == 'table' and t or {})
   2417    opts = get_resolved_options({ float = float_opts }, nil, bufnr).float
   2418  end
   2419 
   2420  local scope = ({ l = 'line', c = 'cursor', b = 'buffer' })[opts.scope] or opts.scope or 'line'
   2421  local lnum, col --- @type integer, integer
   2422  local opts_pos = opts.pos
   2423  if scope == 'line' or scope == 'cursor' then
   2424    if not opts_pos then
   2425      local pos = api.nvim_win_get_cursor(0)
   2426      lnum = pos[1] - 1
   2427      col = pos[2]
   2428    elseif type(opts_pos) == 'number' then
   2429      lnum = opts_pos
   2430    elseif type(opts_pos) == 'table' then
   2431      lnum, col = opts_pos[1], opts_pos[2]
   2432    else
   2433      error("Invalid value for option 'pos'")
   2434    end
   2435  elseif scope ~= 'buffer' then
   2436    error("Invalid value for option 'scope'")
   2437  end
   2438 
   2439  local diagnostics = get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], true)
   2440 
   2441  if scope == 'line' then
   2442    --- @param d vim.Diagnostic
   2443    diagnostics = vim.tbl_filter(function(d)
   2444      local d_lnum, _, d_end_lnum, d_end_col = get_logical_pos(d)
   2445 
   2446      return lnum >= d_lnum
   2447        and lnum <= d_end_lnum
   2448        and (d_lnum == d_end_lnum or lnum ~= d_end_lnum or d_end_col ~= 0)
   2449    end, diagnostics)
   2450  elseif scope == 'cursor' then
   2451    -- If `col` is past the end of the line, show if the cursor is on the last char in the line
   2452    local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
   2453    --- @param d vim.Diagnostic
   2454    diagnostics = vim.tbl_filter(function(d)
   2455      local d_lnum, d_col, d_end_lnum, d_end_col = get_logical_pos(d)
   2456 
   2457      return lnum >= d_lnum
   2458        and lnum <= d_end_lnum
   2459        and (lnum ~= d_lnum or col >= math.min(d_col, line_length - 1))
   2460        and ((d_lnum == d_end_lnum and d_col == d_end_col) or lnum ~= d_end_lnum or col < d_end_col)
   2461    end, diagnostics)
   2462  end
   2463 
   2464  if vim.tbl_isempty(diagnostics) then
   2465    return
   2466  end
   2467 
   2468  local severity_sort = if_nil(opts.severity_sort, global_diagnostic_options.severity_sort)
   2469  if severity_sort then
   2470    if type(severity_sort) == 'table' and severity_sort.reverse then
   2471      table.sort(diagnostics, function(a, b)
   2472        return diagnostic_cmp(a, b, 'severity', true)
   2473      end)
   2474    else
   2475      table.sort(diagnostics, function(a, b)
   2476        return diagnostic_cmp(a, b, 'severity', false)
   2477      end)
   2478    end
   2479  end
   2480 
   2481  local lines = {} --- @type string[]
   2482  local highlights = {} --- @type { hlname: string, prefix?: { length: integer, hlname: string? }, suffix?: { length: integer, hlname: string? } }[]
   2483  local header = if_nil(opts.header, 'Diagnostics:')
   2484  if header then
   2485    vim.validate('header', header, { 'string', 'table' }, "'string' or 'table'")
   2486    if type(header) == 'table' then
   2487      -- Don't insert any lines for an empty string
   2488      if #(header[1] or '') > 0 then
   2489        lines[#lines + 1] = header[1]
   2490        highlights[#highlights + 1] = { hlname = header[2] or 'Bold' }
   2491      end
   2492    elseif #header > 0 then
   2493      lines[#lines + 1] = header
   2494      highlights[#highlights + 1] = { hlname = 'Bold' }
   2495    end
   2496  end
   2497 
   2498  if opts.format then
   2499    diagnostics = reformat_diagnostics(opts.format, diagnostics)
   2500  end
   2501 
   2502  if opts.source and (opts.source ~= 'if_many' or count_sources(bufnr) > 1) then
   2503    diagnostics = prefix_source(diagnostics)
   2504  end
   2505 
   2506  local prefix_opt = opts.prefix
   2507    or (scope == 'cursor' and #diagnostics <= 1) and ''
   2508    or function(_, i)
   2509      return string.format('%d. ', i)
   2510    end
   2511 
   2512  local prefix, prefix_hl_group --- @type string?, string?
   2513  if prefix_opt then
   2514    vim.validate(
   2515      'prefix',
   2516      prefix_opt,
   2517      { 'string', 'table', 'function' },
   2518      "'string' or 'table' or 'function'"
   2519    )
   2520    if type(prefix_opt) == 'string' then
   2521      prefix, prefix_hl_group = prefix_opt, 'NormalFloat'
   2522    elseif type(prefix_opt) == 'table' then
   2523      prefix, prefix_hl_group = prefix_opt[1] or '', prefix_opt[2] or 'NormalFloat'
   2524    end
   2525  end
   2526 
   2527  local suffix_opt = opts.suffix
   2528    or function(diagnostic)
   2529      return diagnostic.code and string.format(' [%s]', diagnostic.code) or ''
   2530    end
   2531 
   2532  local suffix, suffix_hl_group --- @type string?, string?
   2533  if suffix_opt then
   2534    vim.validate(
   2535      'suffix',
   2536      suffix_opt,
   2537      { 'string', 'table', 'function' },
   2538      "'string' or 'table' or 'function'"
   2539    )
   2540    if type(suffix_opt) == 'string' then
   2541      suffix, suffix_hl_group = suffix_opt, 'NormalFloat'
   2542    elseif type(suffix_opt) == 'table' then
   2543      suffix, suffix_hl_group = suffix_opt[1] or '', suffix_opt[2] or 'NormalFloat'
   2544    end
   2545  end
   2546 
   2547  ---@type table<integer, lsp.Location>
   2548  local related_info_locations = {}
   2549  for i, diagnostic in ipairs(diagnostics) do
   2550    if type(prefix_opt) == 'function' then
   2551      --- @cast prefix_opt fun(...): string?, string?
   2552      local prefix0, prefix_hl_group0 = prefix_opt(diagnostic, i, #diagnostics)
   2553      prefix, prefix_hl_group = prefix0 or '', prefix_hl_group0 or 'NormalFloat'
   2554    end
   2555    if type(suffix_opt) == 'function' then
   2556      --- @cast suffix_opt fun(...): string?, string?
   2557      local suffix0, suffix_hl_group0 = suffix_opt(diagnostic, i, #diagnostics)
   2558      suffix, suffix_hl_group = suffix0 or '', suffix_hl_group0 or 'NormalFloat'
   2559    end
   2560    local hiname = floating_highlight_map[diagnostic.severity]
   2561    local message_lines = vim.split(diagnostic.message, '\n')
   2562    local default_pre = string.rep(' ', #prefix)
   2563    for j = 1, #message_lines do
   2564      local pre = j == 1 and prefix or default_pre
   2565      local suf = j == #message_lines and suffix or ''
   2566      lines[#lines + 1] = pre .. message_lines[j] .. suf
   2567      highlights[#highlights + 1] = {
   2568        hlname = hiname,
   2569        prefix = {
   2570          length = j == 1 and #prefix or 0,
   2571          hlname = prefix_hl_group,
   2572        },
   2573        suffix = {
   2574          length = #suf,
   2575          hlname = suffix_hl_group,
   2576        },
   2577      }
   2578    end
   2579 
   2580    ---@type lsp.DiagnosticRelatedInformation[]
   2581    local related_info = vim.tbl_get(diagnostic, 'user_data', 'lsp', 'relatedInformation') or {}
   2582 
   2583    -- Below the diagnostic, show its LSP related information (if any) in the form of file name and
   2584    -- range, plus description.
   2585    for _, info in ipairs(related_info) do
   2586      local location = info.location
   2587      local file_name = vim.fs.basename(vim.uri_to_fname(location.uri))
   2588      local info_suffix = ': ' .. info.message
   2589      related_info_locations[#lines + 1] = location
   2590      lines[#lines + 1] = string.format(
   2591        '%s%s:%s:%s%s',
   2592        default_pre,
   2593        file_name,
   2594        location.range.start.line + 1,
   2595        location.range.start.character + 1,
   2596        info_suffix
   2597      )
   2598      highlights[#highlights + 1] = {
   2599        hlname = '@string.special.path',
   2600        prefix = {
   2601          length = #default_pre,
   2602          hlname = prefix_hl_group,
   2603        },
   2604        suffix = {
   2605          length = #info_suffix,
   2606          hlname = 'NormalFloat',
   2607        },
   2608      }
   2609    end
   2610  end
   2611 
   2612  -- Used by open_floating_preview to allow the float to be focused
   2613  if not opts.focus_id then
   2614    opts.focus_id = scope
   2615  end
   2616 
   2617  --- @diagnostic disable-next-line: param-type-mismatch
   2618  local float_bufnr, winnr = vim.lsp.util.open_floating_preview(lines, 'plaintext', opts)
   2619  vim.bo[float_bufnr].path = vim.bo[bufnr].path
   2620 
   2621  -- TODO: Handle this generally (like vim.ui.open()), rather than overriding gf.
   2622  vim.keymap.set('n', 'gf', function()
   2623    local cursor_row = api.nvim_win_get_cursor(0)[1]
   2624    local location = related_info_locations[cursor_row]
   2625    if location then
   2626      -- Split the window before calling `show_document` so the window doesn't disappear.
   2627      vim.cmd.split()
   2628      vim.lsp.util.show_document(location, 'utf-16', { focus = true })
   2629    else
   2630      vim.cmd.normal({ 'gf', bang = true })
   2631    end
   2632  end, { buffer = float_bufnr, remap = false })
   2633 
   2634  --- @diagnostic disable-next-line: deprecated
   2635  local add_highlight = api.nvim_buf_add_highlight
   2636 
   2637  for i, hl in ipairs(highlights) do
   2638    local line = lines[i]
   2639    local prefix_len = hl.prefix and hl.prefix.length or 0
   2640    local suffix_len = hl.suffix and hl.suffix.length or 0
   2641    if prefix_len > 0 then
   2642      add_highlight(float_bufnr, -1, hl.prefix.hlname, i - 1, 0, prefix_len)
   2643    end
   2644    add_highlight(float_bufnr, -1, hl.hlname, i - 1, prefix_len, #line - suffix_len)
   2645    if suffix_len > 0 then
   2646      add_highlight(float_bufnr, -1, hl.suffix.hlname, i - 1, #line - suffix_len, -1)
   2647    end
   2648  end
   2649 
   2650  return float_bufnr, winnr
   2651 end
   2652 
   2653 --- Remove all diagnostics from the given namespace.
   2654 ---
   2655 --- Unlike |vim.diagnostic.hide()|, this function removes all saved
   2656 --- diagnostics. They cannot be redisplayed using |vim.diagnostic.show()|. To
   2657 --- simply remove diagnostic decorations in a way that they can be
   2658 --- re-displayed, use |vim.diagnostic.hide()|.
   2659 ---
   2660 ---@param namespace integer? Diagnostic namespace. When omitted, remove
   2661 ---                          diagnostics from all namespaces.
   2662 ---@param bufnr integer? Remove diagnostics for the given buffer. When omitted,
   2663 ---                     diagnostics are removed for all buffers.
   2664 function M.reset(namespace, bufnr)
   2665  vim.validate('namespace', namespace, 'number', true)
   2666  vim.validate('bufnr', bufnr, 'number', true)
   2667 
   2668  local buffers = bufnr and { vim._resolve_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
   2669  for _, iter_bufnr in ipairs(buffers) do
   2670    local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr])
   2671    for _, iter_namespace in ipairs(namespaces) do
   2672      diagnostic_cache[iter_bufnr][iter_namespace] = nil
   2673      M.hide(iter_namespace, iter_bufnr)
   2674    end
   2675 
   2676    if api.nvim_buf_is_valid(iter_bufnr) then
   2677      api.nvim_exec_autocmds('DiagnosticChanged', {
   2678        modeline = false,
   2679        buffer = iter_bufnr,
   2680        data = { diagnostics = {} },
   2681      })
   2682    else
   2683      diagnostic_cache[iter_bufnr] = nil
   2684    end
   2685  end
   2686 end
   2687 
   2688 --- Configuration table with the following keys:
   2689 --- @class vim.diagnostic.setqflist.Opts
   2690 --- @inlinedoc
   2691 ---
   2692 --- Only add diagnostics from the given namespace(s).
   2693 --- @field namespace? integer[]|integer
   2694 ---
   2695 --- Open quickfix list after setting.
   2696 --- (default: `true`)
   2697 --- @field open? boolean
   2698 ---
   2699 --- Title of quickfix list. Defaults to "Diagnostics". If there's already a quickfix list with this
   2700 --- title, it's updated. If not, a new quickfix list is created.
   2701 --- @field title? string
   2702 ---
   2703 --- See |diagnostic-severity|.
   2704 --- @field severity? vim.diagnostic.SeverityFilter
   2705 ---
   2706 --- A function that takes a diagnostic as input and returns a string or nil.
   2707 --- If the return value is nil, the diagnostic is not displayed in the quickfix list.
   2708 --- Else the output text is used to display the diagnostic.
   2709 --- @field format? fun(diagnostic:vim.Diagnostic): string?
   2710 
   2711 --- Add all diagnostics to the quickfix list.
   2712 ---
   2713 ---@param opts? vim.diagnostic.setqflist.Opts
   2714 function M.setqflist(opts)
   2715  set_list(false, opts)
   2716 end
   2717 
   2718 ---Configuration table with the following keys:
   2719 --- @class vim.diagnostic.setloclist.Opts
   2720 --- @inlinedoc
   2721 ---
   2722 --- Only add diagnostics from the given namespace(s).
   2723 --- @field namespace? integer[]|integer
   2724 ---
   2725 --- Window number to set location list for.
   2726 --- (default: `0`)
   2727 --- @field winnr? integer
   2728 ---
   2729 --- Open the location list after setting.
   2730 --- (default: `true`)
   2731 --- @field open? boolean
   2732 ---
   2733 --- Title of the location list. Defaults to "Diagnostics".
   2734 --- @field title? string
   2735 ---
   2736 --- See |diagnostic-severity|.
   2737 --- @field severity? vim.diagnostic.SeverityFilter
   2738 ---
   2739 --- A function that takes a diagnostic as input and returns a string or nil.
   2740 --- If the return value is nil, the diagnostic is not displayed in the location list.
   2741 --- Else the output text is used to display the diagnostic.
   2742 --- @field format? fun(diagnostic:vim.Diagnostic): string?
   2743 
   2744 --- Add buffer diagnostics to the location list.
   2745 ---
   2746 ---@param opts? vim.diagnostic.setloclist.Opts
   2747 function M.setloclist(opts)
   2748  set_list(true, opts)
   2749 end
   2750 
   2751 --- Enables or disables diagnostics.
   2752 ---
   2753 --- To "toggle", pass the inverse of `is_enabled()`:
   2754 ---
   2755 --- ```lua
   2756 --- vim.diagnostic.enable(not vim.diagnostic.is_enabled())
   2757 --- ```
   2758 ---
   2759 --- @param enable (boolean|nil) true/nil to enable, false to disable
   2760 --- @param filter vim.diagnostic.Filter?
   2761 function M.enable(enable, filter)
   2762  filter = filter or {}
   2763  vim.validate('enable', enable, 'boolean', true)
   2764  vim.validate('filter', filter, 'table', true)
   2765 
   2766  enable = enable == nil and true or enable
   2767  local bufnr = filter.bufnr
   2768  local ns_id = filter.ns_id
   2769 
   2770  if not bufnr then
   2771    if not ns_id then
   2772      --- @type table<integer,true|table<integer,true>>
   2773      diagnostic_disabled = (
   2774        enable
   2775          -- Enable everything by setting diagnostic_disabled to an empty table.
   2776          and {}
   2777        -- Disable everything (including as yet non-existing buffers and namespaces) by setting
   2778        -- diagnostic_disabled to an empty table and set its metatable to always return true.
   2779        or setmetatable({}, {
   2780          __index = function()
   2781            return true
   2782          end,
   2783        })
   2784      )
   2785    else
   2786      local ns = M.get_namespace(ns_id)
   2787      ns.disabled = not enable
   2788    end
   2789  else
   2790    bufnr = vim._resolve_bufnr(bufnr)
   2791    if not ns_id then
   2792      diagnostic_disabled[bufnr] = (not enable) and true or nil
   2793    else
   2794      if type(diagnostic_disabled[bufnr]) ~= 'table' then
   2795        if enable then
   2796          return
   2797        end
   2798        diagnostic_disabled[bufnr] = {}
   2799      end
   2800      diagnostic_disabled[bufnr][ns_id] = (not enable) and true or nil
   2801    end
   2802  end
   2803 
   2804  if enable then
   2805    M.show(ns_id, bufnr)
   2806  else
   2807    M.hide(ns_id, bufnr)
   2808  end
   2809 end
   2810 
   2811 --- Parse a diagnostic from a string.
   2812 ---
   2813 --- For example, consider a line of output from a linter:
   2814 ---
   2815 --- ```
   2816 --- WARNING filename:27:3: Variable 'foo' does not exist
   2817 --- ```
   2818 ---
   2819 --- This can be parsed into |vim.Diagnostic| structure with:
   2820 ---
   2821 --- ```lua
   2822 --- local s = "WARNING filename:27:3: Variable 'foo' does not exist"
   2823 --- local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
   2824 --- local groups = { "severity", "lnum", "col", "message" }
   2825 --- vim.diagnostic.match(s, pattern, groups, { WARNING = vim.diagnostic.WARN })
   2826 --- ```
   2827 ---
   2828 ---@param str string String to parse diagnostics from.
   2829 ---@param pat string Lua pattern with capture groups.
   2830 ---@param groups string[] List of fields in a |vim.Diagnostic| structure to
   2831 ---                    associate with captures from {pat}.
   2832 ---@param severity_map table A table mapping the severity field from {groups}
   2833 ---                          with an item from |vim.diagnostic.severity|.
   2834 ---@param defaults table? Table of default values for any fields not listed in {groups}.
   2835 ---                       When omitted, numeric values default to 0 and "severity" defaults to
   2836 ---                       ERROR.
   2837 ---@return vim.Diagnostic?: |vim.Diagnostic| structure or `nil` if {pat} fails to match {str}.
   2838 function M.match(str, pat, groups, severity_map, defaults)
   2839  vim.validate('str', str, 'string')
   2840  vim.validate('pat', pat, 'string')
   2841  vim.validate('groups', groups, 'table')
   2842  vim.validate('severity_map', severity_map, 'table', true)
   2843  vim.validate('defaults', defaults, 'table', true)
   2844 
   2845  --- @type table<string,vim.diagnostic.Severity>
   2846  severity_map = severity_map or M.severity
   2847 
   2848  local matches = { str:match(pat) } --- @type any[]
   2849  if vim.tbl_isempty(matches) then
   2850    return
   2851  end
   2852 
   2853  local diagnostic = {} --- @type table<string,any>
   2854 
   2855  for i, match in ipairs(matches) do
   2856    local field = groups[i]
   2857    if field == 'severity' then
   2858      diagnostic[field] = severity_map[match]
   2859    elseif field == 'lnum' or field == 'end_lnum' or field == 'col' or field == 'end_col' then
   2860      diagnostic[field] = assert(tonumber(match)) - 1
   2861    elseif field then
   2862      diagnostic[field] = match
   2863    end
   2864  end
   2865 
   2866  diagnostic = vim.tbl_extend('keep', diagnostic, defaults or {}) --- @type vim.Diagnostic
   2867  diagnostic.severity = diagnostic.severity or M.severity.ERROR
   2868  diagnostic.col = diagnostic.col or 0
   2869  diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
   2870  diagnostic.end_col = diagnostic.end_col or diagnostic.col
   2871  return diagnostic
   2872 end
   2873 
   2874 local errlist_type_map = {
   2875  [M.severity.ERROR] = 'E',
   2876  [M.severity.WARN] = 'W',
   2877  [M.severity.INFO] = 'I',
   2878  [M.severity.HINT] = 'N',
   2879 }
   2880 
   2881 --- Convert a list of diagnostics to a list of quickfix items that can be
   2882 --- passed to |setqflist()| or |setloclist()|.
   2883 ---
   2884 ---@param diagnostics vim.Diagnostic[]
   2885 ---@return table[] : Quickfix list items |setqflist-what|
   2886 function M.toqflist(diagnostics)
   2887  vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
   2888 
   2889  local list = {} --- @type table[]
   2890  for _, v in ipairs(diagnostics) do
   2891    local item = {
   2892      bufnr = v.bufnr,
   2893      lnum = v.lnum + 1,
   2894      col = v.col and (v.col + 1) or nil,
   2895      end_lnum = v.end_lnum and (v.end_lnum + 1) or nil,
   2896      end_col = v.end_col and (v.end_col + 1) or nil,
   2897      text = v.message,
   2898      nr = tonumber(v.code),
   2899      type = errlist_type_map[v.severity] or 'E',
   2900      valid = 1,
   2901    }
   2902    table.insert(list, item)
   2903  end
   2904  table.sort(list, function(a, b)
   2905    if a.bufnr == b.bufnr then
   2906      if a.lnum == b.lnum then
   2907        return a.col < b.col
   2908      else
   2909        return a.lnum < b.lnum
   2910      end
   2911    else
   2912      return a.bufnr < b.bufnr
   2913    end
   2914  end)
   2915  return list
   2916 end
   2917 
   2918 --- Configuration table with the following keys:
   2919 --- @class vim.diagnostic.fromqflist.Opts
   2920 --- @inlinedoc
   2921 ---
   2922 --- When true, items with valid=0 are appended to the previous valid item's
   2923 --- message with a newline. (default: false)
   2924 --- @field merge_lines? boolean
   2925 
   2926 --- Convert a list of quickfix items to a list of diagnostics.
   2927 ---
   2928 ---@param list vim.quickfix.entry[] List of quickfix items from |getqflist()| or |getloclist()|.
   2929 ---@param opts? vim.diagnostic.fromqflist.Opts
   2930 ---@return vim.Diagnostic[]
   2931 function M.fromqflist(list, opts)
   2932  vim.validate('list', list, 'table')
   2933 
   2934  opts = opts or {}
   2935  local merge = opts.merge_lines
   2936 
   2937  local diagnostics = {} --- @type vim.Diagnostic[]
   2938  local last_diag --- @type vim.Diagnostic?
   2939  for _, item in ipairs(list) do
   2940    if item.valid == 1 then
   2941      local lnum = math.max(0, item.lnum - 1)
   2942      local col = math.max(0, item.col - 1)
   2943      local end_lnum = item.end_lnum > 0 and (item.end_lnum - 1) or lnum
   2944      local end_col = item.end_col > 0 and (item.end_col - 1) or col
   2945      local code = item.nr > 0 and item.nr or nil
   2946      local severity = item.type ~= '' and M.severity[item.type:upper()] or M.severity.ERROR
   2947      local diag = {
   2948        bufnr = item.bufnr,
   2949        lnum = lnum,
   2950        col = col,
   2951        end_lnum = end_lnum,
   2952        end_col = end_col,
   2953        severity = severity,
   2954        message = item.text,
   2955        code = code,
   2956      }
   2957      diagnostics[#diagnostics + 1] = diag
   2958      last_diag = diag
   2959    elseif merge and last_diag then
   2960      last_diag.message = last_diag.message .. '\n' .. item.text
   2961    end
   2962  end
   2963  return diagnostics
   2964 end
   2965 
   2966 local hl_map = {
   2967  [M.severity.ERROR] = 'DiagnosticSignError',
   2968  [M.severity.WARN] = 'DiagnosticSignWarn',
   2969  [M.severity.INFO] = 'DiagnosticSignInfo',
   2970  [M.severity.HINT] = 'DiagnosticSignHint',
   2971 }
   2972 
   2973 --- Returns formatted string with diagnostics for the current buffer.
   2974 --- The severities with 0 diagnostics are left out.
   2975 --- Example `E:2 W:3 I:4 H:5`
   2976 ---
   2977 --- To customise appearance, set diagnostic text for each severity with
   2978 --- ```lua
   2979 --- vim.diagnostic.config({
   2980 ---   status = { text = { [vim.diagnostic.severity.ERROR] = 'e', ... } }
   2981 --- })
   2982 --- ```
   2983 ---@param bufnr? integer Buffer number to get diagnostics from.
   2984 ---                      Defaults to 0 for the current buffer
   2985 ---
   2986 ---@return string
   2987 function M.status(bufnr)
   2988  vim.validate('bufnr', bufnr, 'number', true)
   2989  bufnr = bufnr or 0
   2990  local counts = M.count(bufnr)
   2991  local user_signs = vim.tbl_get(M.config() --[[@as vim.diagnostic.Opts]], 'status', 'text') or {}
   2992  local signs = vim.tbl_extend('keep', user_signs, { 'E', 'W', 'I', 'H' })
   2993  local result_str = vim
   2994    .iter(pairs(counts))
   2995    :map(function(severity, count)
   2996      return ('%%#%s#%s:%s'):format(hl_map[severity], signs[severity], count)
   2997    end)
   2998    :join(' ')
   2999 
   3000  if result_str:len() > 0 then
   3001    result_str = result_str .. '%##'
   3002  end
   3003 
   3004  return result_str
   3005 end
   3006 
   3007 api.nvim_create_autocmd('DiagnosticChanged', {
   3008  group = api.nvim_create_augroup('nvim.diagnostic.status', {}),
   3009  callback = function(ev)
   3010    if api.nvim_buf_is_loaded(ev.buf) then
   3011      api.nvim__redraw({ buf = ev.buf, statusline = true })
   3012    end
   3013  end,
   3014  desc = 'diagnostics component for the statusline',
   3015 })
   3016 
   3017 return M