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