2022-11-19 06:41:47 -07:00
local api , if_nil = vim.api , vim.F . if_nil
2021-11-14 18:40:11 -07:00
2021-09-06 19:21:18 -07:00
local M = { }
2022-11-21 14:02:18 -07:00
---@enum DiagnosticSeverity
2021-09-06 19:21:18 -07:00
M.severity = {
ERROR = 1 ,
WARN = 2 ,
INFO = 3 ,
HINT = 4 ,
}
vim.tbl_add_reverse_lookup ( M.severity )
2021-09-19 15:13:23 -07:00
-- Mappings from qflist/loclist error types to severities
M.severity . E = M.severity . ERROR
M.severity . W = M.severity . WARN
M.severity . I = M.severity . INFO
M.severity . N = M.severity . HINT
2021-09-06 19:21:18 -07:00
local global_diagnostic_options = {
signs = true ,
underline = true ,
virtual_text = true ,
2021-10-19 10:45:51 -07:00
float = true ,
2021-09-06 19:21:18 -07:00
update_in_insert = false ,
severity_sort = false ,
}
2021-10-29 18:47:34 -07:00
M.handlers = setmetatable ( { } , {
__newindex = function ( t , name , handler )
vim.validate ( { handler = { handler , ' t ' } } )
rawset ( t , name , handler )
2022-01-11 16:44:07 -07:00
if global_diagnostic_options [ name ] == nil then
2021-10-29 18:47:34 -07:00
global_diagnostic_options [ name ] = true
end
end ,
} )
2021-11-19 11:20:04 -07:00
-- Metatable that automatically creates an empty table when assigning to a missing key
local bufnr_and_namespace_cacher_mt = {
__index = function ( t , bufnr )
2021-11-22 08:47:30 -07:00
assert ( bufnr > 0 , ' Invalid buffer number ' )
t [ bufnr ] = { }
return t [ bufnr ]
2021-11-19 11:20:04 -07:00
end ,
}
2022-08-29 10:09:14 -07:00
local diagnostic_cache
do
2022-11-19 06:41:47 -07:00
local group = api.nvim_create_augroup ( ' DiagnosticBufWipeout ' , { } )
2022-08-29 10:09:14 -07:00
diagnostic_cache = setmetatable ( { } , {
__index = function ( t , bufnr )
assert ( bufnr > 0 , ' Invalid buffer number ' )
2022-11-19 06:41:47 -07:00
api.nvim_create_autocmd ( ' BufWipeout ' , {
2022-08-29 10:09:14 -07:00
group = group ,
buffer = bufnr ,
callback = function ( )
rawset ( t , bufnr , nil )
end ,
} )
t [ bufnr ] = { }
return t [ bufnr ]
end ,
} )
end
2021-11-21 18:40:06 -07:00
2021-11-19 11:20:04 -07:00
local diagnostic_cache_extmarks = setmetatable ( { } , bufnr_and_namespace_cacher_mt )
local diagnostic_attached_buffers = { }
local diagnostic_disabled = { }
local bufs_waiting_to_update = setmetatable ( { } , bufnr_and_namespace_cacher_mt )
local all_namespaces = { }
2021-09-06 19:21:18 -07:00
local function to_severity ( severity )
2021-10-08 11:28:02 -07:00
if type ( severity ) == ' string ' then
return assert (
M.severity [ string.upper ( severity ) ] ,
string.format ( ' Invalid severity: %s ' , severity )
2022-07-07 09:27:18 -07:00
)
2021-10-08 11:28:02 -07:00
end
return severity
2021-09-06 19:21:18 -07:00
end
local function filter_by_severity ( severity , diagnostics )
if not severity then
return diagnostics
end
if type ( severity ) ~= ' table ' then
severity = to_severity ( severity )
return vim.tbl_filter ( function ( t )
return t.severity == severity
end , diagnostics )
end
2023-08-16 06:49:14 -07:00
if severity.min or severity.max then
local min_severity = to_severity ( severity.min ) or M.severity . HINT
local max_severity = to_severity ( severity.max ) or M.severity . ERROR
return vim.tbl_filter ( function ( t )
return t.severity <= min_severity and t.severity >= max_severity
end , diagnostics )
end
local severities = { }
for _ , s in ipairs ( severity ) do
severities [ to_severity ( s ) ] = true
end
2021-09-06 19:21:18 -07:00
return vim.tbl_filter ( function ( t )
2023-08-16 06:49:14 -07:00
return severities [ t.severity ]
2021-09-06 19:21:18 -07:00
end , diagnostics )
end
2021-12-17 19:38:33 -07:00
local function count_sources ( bufnr )
local seen = { }
local count = 0
for _ , namespace_diagnostics in pairs ( diagnostic_cache [ bufnr ] ) do
for _ , diagnostic in ipairs ( namespace_diagnostics ) do
if diagnostic.source and not seen [ diagnostic.source ] then
seen [ diagnostic.source ] = true
count = count + 1
2021-09-18 14:00:32 -07:00
end
end
end
2021-12-17 19:38:33 -07:00
return count
end
2021-09-18 14:00:32 -07:00
2021-12-17 19:38:33 -07:00
local function prefix_source ( diagnostics )
2021-09-18 14:00:32 -07:00
return vim.tbl_map ( function ( d )
if not d.source then
return d
end
local t = vim.deepcopy ( d )
t.message = string.format ( ' %s: %s ' , d.source , d.message )
return t
end , diagnostics )
end
2021-09-22 12:20:15 -07:00
local function reformat_diagnostics ( format , diagnostics )
vim.validate ( {
format = { format , ' f ' } ,
diagnostics = { diagnostics , ' t ' } ,
} )
local formatted = vim.deepcopy ( diagnostics )
for _ , diagnostic in ipairs ( formatted ) do
diagnostic.message = format ( diagnostic )
end
return formatted
end
2021-10-17 07:18:35 -07:00
local function enabled_value ( option , namespace )
2021-10-29 18:47:34 -07:00
local ns = namespace and M.get_namespace ( namespace ) or { }
2021-10-19 10:45:51 -07:00
if ns.opts and type ( ns.opts [ option ] ) == ' table ' then
2021-10-17 07:18:35 -07:00
return ns.opts [ option ]
end
if type ( global_diagnostic_options [ option ] ) == ' table ' then
return global_diagnostic_options [ option ]
end
return { }
end
local function resolve_optional_value ( option , value , namespace , bufnr )
if not value then
return false
elseif value == true then
return enabled_value ( option , namespace )
elseif type ( value ) == ' function ' then
local val = value ( namespace , bufnr )
if val == true then
return enabled_value ( option , namespace )
else
return val
end
elseif type ( value ) == ' table ' then
return value
else
error ( ' Unexpected option type: ' .. vim.inspect ( value ) )
end
end
2021-09-06 19:21:18 -07:00
local function get_resolved_options ( opts , namespace , bufnr )
2021-10-29 18:47:34 -07:00
local ns = namespace and M.get_namespace ( namespace ) or { }
2021-10-19 10:45:51 -07:00
-- Do not use tbl_deep_extend so that an empty table can be used to reset to default values
local resolved = vim.tbl_extend ( ' keep ' , opts or { } , ns.opts or { } , global_diagnostic_options )
2021-09-06 19:21:18 -07:00
for k in pairs ( global_diagnostic_options ) do
if resolved [ k ] ~= nil then
2021-10-17 07:18:35 -07:00
resolved [ k ] = resolve_optional_value ( k , resolved [ k ] , namespace , bufnr )
2021-09-06 19:21:18 -07:00
end
end
return resolved
end
-- Default diagnostic highlights
local diagnostic_severities = {
[ M.severity . ERROR ] = { ctermfg = 1 , guifg = ' Red ' } ,
[ M.severity . WARN ] = { ctermfg = 3 , guifg = ' Orange ' } ,
[ M.severity . INFO ] = { ctermfg = 4 , guifg = ' LightBlue ' } ,
[ M.severity . HINT ] = { ctermfg = 7 , guifg = ' LightGrey ' } ,
}
-- Make a map from DiagnosticSeverity -> Highlight Name
local function make_highlight_map ( base_name )
local result = { }
for k in pairs ( diagnostic_severities ) do
local name = M.severity [ k ]
name = name : sub ( 1 , 1 ) .. name : sub ( 2 ) : lower ( )
result [ k ] = ' Diagnostic ' .. base_name .. name
end
return result
end
local virtual_text_highlight_map = make_highlight_map ( ' VirtualText ' )
local underline_highlight_map = make_highlight_map ( ' Underline ' )
local floating_highlight_map = make_highlight_map ( ' Floating ' )
local sign_highlight_map = make_highlight_map ( ' Sign ' )
---@private
local define_default_signs = ( function ( )
local signs_defined = false
return function ( )
if signs_defined then
return
end
for severity , sign_hl_name in pairs ( sign_highlight_map ) do
2021-09-17 07:42:06 -07:00
if vim.tbl_isempty ( vim.fn . sign_getdefined ( sign_hl_name ) ) then
local severity_name = M.severity [ severity ]
vim.fn . sign_define ( sign_hl_name , {
text = ( severity_name or ' U ' ) : sub ( 1 , 1 ) ,
texthl = sign_hl_name ,
linehl = ' ' ,
numhl = ' ' ,
} )
end
2021-09-06 19:21:18 -07:00
end
signs_defined = true
end
end ) ( )
local function get_bufnr ( bufnr )
if not bufnr or bufnr == 0 then
2022-11-19 06:41:47 -07:00
return api.nvim_get_current_buf ( )
2021-09-06 19:21:18 -07:00
end
return bufnr
end
local function diagnostic_lines ( diagnostics )
if not diagnostics then
2021-09-23 08:23:57 -07:00
return { }
2021-09-06 19:21:18 -07:00
end
local diagnostics_by_line = { }
for _ , diagnostic in ipairs ( diagnostics ) do
local line_diagnostics = diagnostics_by_line [ diagnostic.lnum ]
if not line_diagnostics then
line_diagnostics = { }
diagnostics_by_line [ diagnostic.lnum ] = line_diagnostics
end
table.insert ( line_diagnostics , diagnostic )
end
return diagnostics_by_line
end
2021-09-20 11:32:21 -07:00
local function set_diagnostic_cache ( namespace , bufnr , diagnostics )
2021-09-06 19:21:18 -07:00
for _ , diagnostic in ipairs ( diagnostics ) do
2021-12-16 11:20:18 -07:00
assert ( diagnostic.lnum , ' Diagnostic line number is required ' )
assert ( diagnostic.col , ' Diagnostic column is required ' )
2021-09-20 11:32:21 -07:00
diagnostic.severity = diagnostic.severity and to_severity ( diagnostic.severity )
or M.severity . ERROR
diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
diagnostic.end_col = diagnostic.end_col or diagnostic.col
2021-09-06 19:21:18 -07:00
diagnostic.namespace = namespace
diagnostic.bufnr = bufnr
end
diagnostic_cache [ bufnr ] [ namespace ] = diagnostics
end
local function restore_extmarks ( bufnr , last )
for ns , extmarks in pairs ( diagnostic_cache_extmarks [ bufnr ] ) do
2022-11-19 06:41:47 -07:00
local extmarks_current = api.nvim_buf_get_extmarks ( bufnr , ns , 0 , - 1 , { details = true } )
2021-09-06 19:21:18 -07:00
local found = { }
for _ , extmark in ipairs ( extmarks_current ) do
-- nvim_buf_set_lines will move any extmark to the line after the last
-- nvim_buf_set_text will move any extmark to the last line
if extmark [ 2 ] ~= last + 1 then
found [ extmark [ 1 ] ] = true
end
end
for _ , extmark in ipairs ( extmarks ) do
if not found [ extmark [ 1 ] ] then
local opts = extmark [ 4 ]
opts.id = extmark [ 1 ]
2022-11-19 06:41:47 -07:00
pcall ( api.nvim_buf_set_extmark , bufnr , ns , extmark [ 2 ] , extmark [ 3 ] , opts )
2021-09-06 19:21:18 -07:00
end
end
end
end
local function save_extmarks ( namespace , bufnr )
2021-11-09 14:33:01 -07:00
bufnr = get_bufnr ( bufnr )
2021-09-06 19:21:18 -07:00
if not diagnostic_attached_buffers [ bufnr ] then
2022-11-19 06:41:47 -07:00
api.nvim_buf_attach ( bufnr , false , {
2021-09-06 19:21:18 -07:00
on_lines = function ( _ , _ , _ , _ , _ , last )
restore_extmarks ( bufnr , last - 1 )
end ,
on_detach = function ( )
diagnostic_cache_extmarks [ bufnr ] = nil
end ,
} )
diagnostic_attached_buffers [ bufnr ] = true
end
diagnostic_cache_extmarks [ bufnr ] [ namespace ] =
2022-11-19 06:41:47 -07:00
api.nvim_buf_get_extmarks ( bufnr , namespace , 0 , - 1 , { details = true } )
2021-09-06 19:21:18 -07:00
end
local registered_autocmds = { }
local function make_augroup_key ( namespace , bufnr )
2021-10-29 18:47:34 -07:00
local ns = M.get_namespace ( namespace )
2021-09-06 19:21:18 -07:00
return string.format ( ' DiagnosticInsertLeave:%s:%s ' , bufnr , ns.name )
end
2022-07-09 09:40:32 -07:00
local function execute_scheduled_display ( namespace , bufnr )
local args = bufs_waiting_to_update [ bufnr ] [ namespace ]
if not args then
return
end
-- Clear the args so we don't display unnecessarily.
bufs_waiting_to_update [ bufnr ] [ namespace ] = nil
M.show ( namespace , bufnr , nil , args )
end
2021-09-06 19:21:18 -07:00
--- Table of autocmd events to fire the update for displaying new diagnostic information
local insert_leave_auto_cmds = { ' InsertLeave ' , ' CursorHoldI ' }
local function schedule_display ( namespace , bufnr , args )
bufs_waiting_to_update [ bufnr ] [ namespace ] = args
local key = make_augroup_key ( namespace , bufnr )
if not registered_autocmds [ key ] then
2022-11-19 06:41:47 -07:00
local group = api.nvim_create_augroup ( key , { clear = true } )
api.nvim_create_autocmd ( insert_leave_auto_cmds , {
2022-07-09 09:40:32 -07:00
group = group ,
buffer = bufnr ,
callback = function ( )
execute_scheduled_display ( namespace , bufnr )
end ,
2022-07-17 10:11:05 -07:00
desc = ' vim.diagnostic: display diagnostics ' ,
2022-07-09 09:40:32 -07:00
} )
2021-09-06 19:21:18 -07:00
registered_autocmds [ key ] = true
end
end
local function clear_scheduled_display ( namespace , bufnr )
local key = make_augroup_key ( namespace , bufnr )
if registered_autocmds [ key ] then
2022-11-19 06:41:47 -07:00
api.nvim_del_augroup_by_name ( key )
2021-09-06 19:21:18 -07:00
registered_autocmds [ key ] = nil
end
end
2021-11-19 11:20:04 -07:00
local function get_diagnostics ( bufnr , opts , clamp )
opts = opts or { }
local namespace = opts.namespace
local diagnostics = { }
2021-11-24 19:32:26 -07:00
-- Memoized results of buf_line_count per bufnr
local buf_line_count = setmetatable ( { } , {
__index = function ( t , k )
2022-11-19 06:41:47 -07:00
t [ k ] = api.nvim_buf_line_count ( k )
2021-11-24 19:32:26 -07:00
return rawget ( t , k )
end ,
} )
2021-11-19 11:20:04 -07:00
2021-11-24 19:32:26 -07:00
local function add ( b , d )
2021-11-19 11:20:04 -07:00
if not opts.lnum or d.lnum == opts.lnum then
2023-09-06 10:54:18 -07:00
d = vim.deepcopy ( d )
2022-11-19 06:41:47 -07:00
if clamp and api.nvim_buf_is_loaded ( b ) then
2021-11-24 19:32:26 -07:00
local line_count = buf_line_count [ b ] - 1
2022-06-08 11:55:39 -07:00
if
d.lnum > line_count
or d.end_lnum > line_count
or d.lnum < 0
or d.end_lnum < 0
or d.col < 0
or d.end_col < 0
then
2021-11-24 19:32:26 -07:00
d.lnum = math.max ( math.min ( d.lnum , line_count ) , 0 )
d.end_lnum = math.max ( math.min ( d.end_lnum , line_count ) , 0 )
2022-06-08 11:55:39 -07:00
d.col = math.max ( d.col , 0 )
d.end_col = math.max ( d.end_col , 0 )
2021-11-24 19:32:26 -07:00
end
2021-11-19 11:20:04 -07:00
end
table.insert ( diagnostics , d )
end
2021-09-20 11:32:21 -07:00
end
2023-01-03 03:07:27 -07:00
local function add_all_diags ( buf , diags )
for _ , diagnostic in pairs ( diags ) do
add ( buf , diagnostic )
end
end
2021-11-19 11:20:04 -07:00
if namespace == nil and bufnr == nil then
2021-11-24 19:32:26 -07:00
for b , t in pairs ( diagnostic_cache ) do
2021-11-19 11:20:04 -07:00
for _ , v in pairs ( t ) do
2023-01-03 03:07:27 -07:00
add_all_diags ( b , v )
2021-11-19 11:20:04 -07:00
end
end
elseif namespace == nil then
2021-11-22 09:22:08 -07:00
bufnr = get_bufnr ( bufnr )
2021-11-19 11:20:04 -07:00
for iter_namespace in pairs ( diagnostic_cache [ bufnr ] ) do
2023-01-03 03:07:27 -07:00
add_all_diags ( bufnr , diagnostic_cache [ bufnr ] [ iter_namespace ] )
2021-11-19 11:20:04 -07:00
end
elseif bufnr == nil then
2021-11-24 19:32:26 -07:00
for b , t in pairs ( diagnostic_cache ) do
2023-01-03 03:07:27 -07:00
add_all_diags ( b , t [ namespace ] or { } )
2021-11-19 11:20:04 -07:00
end
else
2021-11-22 09:22:08 -07:00
bufnr = get_bufnr ( bufnr )
2023-01-03 03:07:27 -07:00
add_all_diags ( bufnr , diagnostic_cache [ bufnr ] [ namespace ] or { } )
2021-11-19 11:20:04 -07:00
end
if opts.severity then
diagnostics = filter_by_severity ( opts.severity , diagnostics )
2021-09-20 11:32:21 -07:00
end
2021-11-19 11:20:04 -07:00
return diagnostics
2021-09-20 11:32:21 -07:00
end
2021-11-19 11:49:44 -07:00
local function set_list ( loclist , opts )
opts = opts or { }
local open = vim.F . if_nil ( opts.open , true )
local title = opts.title or ' Diagnostics '
local winnr = opts.winnr or 0
local bufnr
if loclist then
2022-11-19 06:41:47 -07:00
bufnr = api.nvim_win_get_buf ( winnr )
2021-11-19 11:49:44 -07:00
end
2021-11-24 19:57:27 -07:00
-- Don't clamp line numbers since the quickfix list can already handle line
-- numbers beyond the end of the buffer
local diagnostics = get_diagnostics ( bufnr , opts , false )
2021-11-19 11:49:44 -07:00
local items = M.toqflist ( diagnostics )
if loclist then
vim.fn . setloclist ( winnr , { } , ' ' , { title = title , items = items } )
else
vim.fn . setqflist ( { } , ' ' , { title = title , items = items } )
end
if open then
2022-12-30 11:23:54 -07:00
api.nvim_command ( loclist and ' lwindow ' or ' botright cwindow ' )
2021-11-19 11:49:44 -07:00
end
end
2021-09-17 18:57:14 -07:00
local function next_diagnostic ( position , search_forward , bufnr , opts , namespace )
position [ 1 ] = position [ 1 ] - 1
2021-09-20 11:32:21 -07:00
bufnr = get_bufnr ( bufnr )
2021-09-17 18:57:14 -07:00
local wrap = vim.F . if_nil ( opts.wrap , true )
2022-11-19 06:41:47 -07:00
local line_count = api.nvim_buf_line_count ( bufnr )
2021-11-19 11:20:04 -07:00
local diagnostics =
get_diagnostics ( bufnr , vim.tbl_extend ( ' keep ' , opts , { namespace = namespace } ) , true )
2021-09-20 11:32:21 -07:00
local line_diagnostics = diagnostic_lines ( diagnostics )
2023-03-30 06:49:58 -07:00
2021-09-17 18:57:14 -07:00
for i = 0 , line_count do
local offset = i * ( search_forward and 1 or - 1 )
local lnum = position [ 1 ] + offset
if lnum < 0 or lnum >= line_count then
if not wrap then
return
end
lnum = ( lnum + line_count ) % line_count
end
2021-09-20 11:32:21 -07:00
if line_diagnostics [ lnum ] and not vim.tbl_isempty ( line_diagnostics [ lnum ] ) then
2022-11-19 06:41:47 -07:00
local line_length = # api.nvim_buf_get_lines ( bufnr , lnum , lnum + 1 , true ) [ 1 ]
2021-09-17 18:57:14 -07:00
local sort_diagnostics , is_next
if search_forward then
sort_diagnostics = function ( a , b )
return a.col < b.col
end
2021-11-19 11:31:33 -07:00
is_next = function ( d )
return math.min ( d.col , line_length - 1 ) > position [ 2 ]
end
2021-09-17 18:57:14 -07:00
else
sort_diagnostics = function ( a , b )
return a.col > b.col
end
2021-11-19 11:31:33 -07:00
is_next = function ( d )
return math.min ( d.col , line_length - 1 ) < position [ 2 ]
end
2021-09-17 18:57:14 -07:00
end
2021-09-20 11:32:21 -07:00
table.sort ( line_diagnostics [ lnum ] , sort_diagnostics )
2021-09-17 18:57:14 -07:00
if i == 0 then
2021-09-20 11:32:21 -07:00
for _ , v in pairs ( line_diagnostics [ lnum ] ) do
2021-09-17 18:57:14 -07:00
if is_next ( v ) then
return v
end
end
else
2021-09-20 11:32:21 -07:00
return line_diagnostics [ lnum ] [ 1 ]
2021-09-17 18:57:14 -07:00
end
end
end
end
local function diagnostic_move_pos ( opts , pos )
opts = opts or { }
2021-10-19 10:45:51 -07:00
local float = vim.F . if_nil ( opts.float , true )
2022-11-19 06:41:47 -07:00
local win_id = opts.win_id or api.nvim_get_current_win ( )
2021-09-17 18:57:14 -07:00
if not pos then
2022-11-19 06:41:47 -07:00
api.nvim_echo ( { { ' No more valid diagnostics to move to ' , ' WarningMsg ' } } , true , { } )
2021-09-17 18:57:14 -07:00
return
end
2022-11-19 06:41:47 -07:00
api.nvim_win_call ( win_id , function ( )
2021-12-04 14:14:38 -07:00
-- Save position in the window's jumplist
vim.cmd ( " normal! m' " )
2022-11-19 06:41:47 -07:00
api.nvim_win_set_cursor ( win_id , { pos [ 1 ] + 1 , pos [ 2 ] } )
2021-12-04 14:14:38 -07:00
-- Open folds under the cursor
vim.cmd ( ' normal! zv ' )
end )
2021-09-17 18:57:14 -07:00
2021-10-19 10:45:51 -07:00
if float then
local float_opts = type ( float ) == ' table ' and float or { }
2021-09-17 18:57:14 -07:00
vim.schedule ( function ( )
2021-11-24 21:45:42 -07:00
M.open_float ( vim.tbl_extend ( ' keep ' , float_opts , {
2022-11-19 06:41:47 -07:00
bufnr = api.nvim_win_get_buf ( win_id ) ,
2021-11-24 21:45:42 -07:00
scope = ' cursor ' ,
2021-11-29 15:37:55 -07:00
focus = false ,
2021-11-24 21:45:42 -07:00
} ) )
2021-09-17 18:57:14 -07:00
end )
end
end
2021-09-06 19:21:18 -07:00
--- Configure diagnostic options globally or for a specific diagnostic
--- namespace.
---
2021-10-17 07:18:35 -07:00
--- Configuration can be specified globally, per-namespace, or ephemerally
--- (i.e. only for a single call to |vim.diagnostic.set()| or
--- |vim.diagnostic.show()|). Ephemeral configuration has highest priority,
--- followed by namespace configuration, and finally global configuration.
---
--- For example, if a user enables virtual text globally with
2023-09-14 06:23:01 -07:00
---
--- ```lua
--- vim.diagnostic.config({ virtual_text = true })
--- ```
2021-10-17 07:18:35 -07:00
---
--- and a diagnostic producer sets diagnostics with
2023-09-14 06:23:01 -07:00
---
--- ```lua
--- vim.diagnostic.set(ns, 0, diagnostics, { virtual_text = false })
--- ```
2021-10-17 07:18:35 -07:00
---
--- then virtual text will not be enabled for those diagnostics.
---
2021-09-06 19:21:18 -07:00
---@note Each of the configuration options below accepts one of the following:
--- - `false`: Disable this feature
--- - `true`: Enable this feature, use default settings.
2021-10-17 07:18:35 -07:00
--- - `table`: Enable this feature with overrides. Use an empty table to use default values.
2021-09-06 19:21:18 -07:00
--- - `function`: Function with signature (namespace, bufnr) that returns any of the above.
---
2022-01-11 16:44:31 -07:00
---@param opts table|nil When omitted or "nil", retrieve the current configuration. Otherwise, a
--- configuration table with the following keys:
2021-09-17 09:50:25 -07:00
--- - underline: (default true) Use underline for diagnostics. Options:
2023-08-09 02:06:13 -07:00
--- * severity: Only underline diagnostics matching the given
--- severity |diagnostic-severity|
2021-09-17 09:50:25 -07:00
--- - virtual_text: (default true) Use virtual text for diagnostics. If multiple diagnostics
2021-10-01 22:50:28 -07:00
--- are set for a namespace, one prefix per diagnostic + the last diagnostic
2023-08-16 06:21:32 -07:00
--- message are shown. In addition to the options listed below, the
--- "virt_text" options of |nvim_buf_set_extmark()| may also be used here
--- (e.g. "virt_text_pos" and "hl_mode").
2021-09-17 09:50:25 -07:00
--- Options:
--- * severity: Only show virtual text for diagnostics matching the given
--- severity |diagnostic-severity|
2021-12-17 19:38:33 -07:00
--- * source: (boolean or string) Include the diagnostic source in virtual
--- text. Use "if_many" to only show sources if there is more than
--- one diagnostic source in the buffer. Otherwise, any truthy value
--- means to always show the diagnostic source.
2021-09-06 19:21:18 -07:00
--- * spacing: (number) Amount of empty spaces inserted at the beginning
2021-09-17 09:50:25 -07:00
--- of the virtual text.
2023-04-17 04:53:34 -07:00
--- * prefix: (string or function) prepend diagnostic message with prefix.
--- If a function, it must have the signature (diagnostic) -> string,
--- where {diagnostic} is of type |diagnostic-structure|. This can be
--- used to render diagnostic symbols or error codes.
2022-11-20 16:57:36 -07:00
--- * suffix: (string or function) Append diagnostic message with suffix.
--- If a function, it must have the signature (diagnostic) ->
--- string, where {diagnostic} is of type |diagnostic-structure|.
--- This can be used to render an LSP diagnostic error code.
2021-09-22 12:20:15 -07:00
--- * format: (function) A function that takes a diagnostic as input and
--- returns a string. The return value is the text used to display
--- the diagnostic. Example:
2022-11-23 04:31:49 -07:00
--- <pre>lua
--- function(diagnostic)
--- if diagnostic.severity == vim.diagnostic.severity.ERROR then
--- return string.format("E: %s", diagnostic.message)
--- end
--- return diagnostic.message
2021-09-22 12:20:15 -07:00
--- end
--- </pre>
2021-09-17 09:50:25 -07:00
--- - signs: (default true) Use signs for diagnostics. Options:
2023-08-09 02:06:13 -07:00
--- * severity: Only show signs for diagnostics matching the given
--- severity |diagnostic-severity|
2021-10-01 18:07:55 -07:00
--- * priority: (number, default 10) Base priority to use for signs. When
--- {severity_sort} is used, the priority of a sign is adjusted based on
--- its severity. Otherwise, all signs use the same priority.
2021-10-19 10:45:51 -07:00
--- - float: Options for floating windows. See |vim.diagnostic.open_float()|.
2021-09-06 19:21:18 -07:00
--- - update_in_insert: (default false) Update diagnostics in Insert mode (if false,
--- diagnostics are updated on InsertLeave)
--- - severity_sort: (default false) Sort diagnostics by severity. This affects the order in
2021-09-17 13:59:30 -07:00
--- which signs and virtual text are displayed. When true, higher severities
--- are displayed before lower severities (e.g. ERROR is displayed before WARN).
--- Options:
2021-09-17 09:50:25 -07:00
--- * reverse: (boolean) Reverse sort order
2021-11-27 09:10:48 -07:00
---
2023-03-04 06:06:20 -07:00
---@param namespace integer|nil Update the options for the given namespace. When omitted, update the
2021-09-06 19:21:18 -07:00
--- global diagnostic options.
function M . config ( opts , namespace )
vim.validate ( {
2022-01-11 16:44:31 -07:00
opts = { opts , ' t ' , true } ,
2021-09-06 19:21:18 -07:00
namespace = { namespace , ' n ' , true } ,
} )
local t
if namespace then
2021-10-29 18:47:34 -07:00
local ns = M.get_namespace ( namespace )
2021-09-06 19:21:18 -07:00
t = ns.opts
else
t = global_diagnostic_options
end
2022-01-11 16:44:31 -07:00
if not opts then
-- Return current config
return vim.deepcopy ( t )
end
2022-01-11 16:43:47 -07:00
for k , v in pairs ( opts ) do
t [ k ] = v
2021-09-06 19:21:18 -07:00
end
if namespace then
for bufnr , v in pairs ( diagnostic_cache ) do
2022-11-19 06:41:47 -07:00
if api.nvim_buf_is_loaded ( bufnr ) and v [ namespace ] then
2021-09-06 19:21:18 -07:00
M.show ( namespace , bufnr )
end
end
else
for bufnr , v in pairs ( diagnostic_cache ) do
2022-11-19 06:41:47 -07:00
if api.nvim_buf_is_loaded ( bufnr ) then
2021-09-18 14:01:03 -07:00
for ns in pairs ( v ) do
M.show ( ns , bufnr )
end
2021-09-06 19:21:18 -07:00
end
end
end
end
--- Set diagnostics for the given namespace and buffer.
---
2023-03-04 06:06:20 -07:00
---@param namespace integer The diagnostic namespace
---@param bufnr integer Buffer number
2021-09-06 19:21:18 -07:00
---@param diagnostics table A list of diagnostic items |diagnostic-structure|
---@param opts table|nil Display options to pass to |vim.diagnostic.show()|
function M . set ( namespace , bufnr , diagnostics , opts )
vim.validate ( {
namespace = { namespace , ' n ' } ,
bufnr = { bufnr , ' n ' } ,
2022-01-01 12:58:34 -07:00
diagnostics = {
diagnostics ,
vim.tbl_islist ,
' a list of diagnostics ' ,
} ,
2021-09-06 19:21:18 -07:00
opts = { opts , ' t ' , true } ,
} )
2021-11-22 08:47:30 -07:00
bufnr = get_bufnr ( bufnr )
2021-09-06 19:21:18 -07:00
if vim.tbl_isempty ( diagnostics ) then
2021-11-21 18:40:06 -07:00
diagnostic_cache [ bufnr ] [ namespace ] = nil
2021-09-27 07:57:53 -07:00
else
set_diagnostic_cache ( namespace , bufnr , diagnostics )
2021-09-06 19:21:18 -07:00
end
2022-11-19 06:41:47 -07:00
if api.nvim_buf_is_loaded ( bufnr ) then
2021-11-27 12:32:40 -07:00
M.show ( namespace , bufnr , nil , opts )
2021-09-06 19:21:18 -07:00
end
2022-11-19 06:41:47 -07:00
api.nvim_exec_autocmds ( ' DiagnosticChanged ' , {
2022-04-20 07:16:47 -07:00
modeline = false ,
buffer = bufnr ,
2022-09-13 07:33:39 -07:00
data = { diagnostics = diagnostics } ,
2022-04-20 07:16:47 -07:00
} )
2021-09-06 19:21:18 -07:00
end
2021-10-29 18:47:34 -07:00
--- Get namespace metadata.
---
2023-03-04 06:06:20 -07:00
---@param namespace integer Diagnostic namespace
2021-10-29 18:47:34 -07:00
---@return table Namespace metadata
function M . get_namespace ( namespace )
vim.validate ( { namespace = { namespace , ' n ' } } )
if not all_namespaces [ namespace ] then
local name
2022-11-19 06:41:47 -07:00
for k , v in pairs ( api.nvim_get_namespaces ( ) ) do
2021-10-29 18:47:34 -07:00
if namespace == v then
name = k
break
end
end
assert ( name , ' namespace does not exist or is anonymous ' )
all_namespaces [ namespace ] = {
name = name ,
opts = { } ,
user_data = { } ,
}
end
return all_namespaces [ namespace ]
end
2021-10-01 22:50:28 -07:00
--- Get current diagnostic namespaces.
---
---@return table A list of active diagnostic namespaces |vim.diagnostic|.
function M . get_namespaces ( )
return vim.deepcopy ( all_namespaces )
end
2022-11-21 14:02:18 -07:00
---@class Diagnostic
2023-08-09 02:06:13 -07:00
---@field bufnr? integer
2023-03-04 06:06:20 -07:00
---@field lnum integer 0-indexed
2023-08-09 02:06:13 -07:00
---@field end_lnum? integer 0-indexed
2023-03-04 06:06:20 -07:00
---@field col integer 0-indexed
2023-08-09 02:06:13 -07:00
---@field end_col? integer 0-indexed
---@field severity? DiagnosticSeverity
2022-11-21 14:02:18 -07:00
---@field message string
2023-08-09 02:06:13 -07:00
---@field source? string
---@field code? string
---@field _tags? { deprecated: boolean, unnecessary: boolean}
---@field user_data? any arbitrary data plugins can add
2022-11-21 14:02:18 -07:00
2021-09-06 19:21:18 -07:00
--- Get current diagnostics.
---
2023-09-06 10:54:18 -07:00
--- Modifying diagnostics in the returned table has no effect. To set diagnostics in a buffer, use |vim.diagnostic.set()|.
---
2023-03-04 06:06:20 -07:00
---@param bufnr integer|nil Buffer number to get diagnostics from. Use 0 for
2021-09-06 19:21:18 -07:00
--- current buffer or nil for all buffers.
---@param opts table|nil A table with the following keys:
--- - namespace: (number) Limit diagnostics to the given namespace.
--- - lnum: (number) Limit diagnostics to the given line number.
--- - severity: See |diagnostic-severity|.
2023-06-27 08:14:17 -07:00
---@return Diagnostic[] table A list of diagnostic items |diagnostic-structure|. Keys `bufnr`, `end_lnum`, `end_col`, and `severity` are guaranteed to be present.
2021-09-06 19:21:18 -07:00
function M . get ( bufnr , opts )
vim.validate ( {
bufnr = { bufnr , ' n ' , true } ,
opts = { opts , ' t ' , true } ,
} )
2021-11-19 11:20:04 -07:00
return get_diagnostics ( bufnr , opts , false )
2021-09-06 19:21:18 -07:00
end
--- Get the previous diagnostic closest to the cursor position.
---
2022-11-21 14:02:18 -07:00
---@param opts nil|table See |vim.diagnostic.goto_next()|
---@return Diagnostic|nil Previous diagnostic
2021-09-06 19:21:18 -07:00
function M . get_prev ( opts )
opts = opts or { }
2022-11-19 06:41:47 -07:00
local win_id = opts.win_id or api.nvim_get_current_win ( )
local bufnr = api.nvim_win_get_buf ( win_id )
local cursor_position = opts.cursor_position or api.nvim_win_get_cursor ( win_id )
2021-09-06 19:21:18 -07:00
return next_diagnostic ( cursor_position , false , bufnr , opts , opts.namespace )
end
--- Return the position of the previous diagnostic in the current buffer.
---
2022-11-21 14:02:18 -07:00
---@param opts table|nil See |vim.diagnostic.goto_next()|
---@return table|false Previous diagnostic position as a (row, col) tuple or false if there is no
--- prior diagnostic
2021-09-06 19:21:18 -07:00
function M . get_prev_pos ( opts )
2021-09-16 10:26:45 -07:00
local prev = M.get_prev ( opts )
if not prev then
return false
end
return { prev.lnum , prev.col }
2021-09-06 19:21:18 -07:00
end
--- Move to the previous diagnostic in the current buffer.
2022-10-28 16:13:27 -07:00
---@param opts table|nil See |vim.diagnostic.goto_next()|
2021-09-06 19:21:18 -07:00
function M . goto_prev ( opts )
return diagnostic_move_pos ( opts , M.get_prev_pos ( opts ) )
end
--- Get the next diagnostic closest to the cursor position.
---
2022-11-21 14:02:18 -07:00
---@param opts table|nil See |vim.diagnostic.goto_next()|
---@return Diagnostic|nil Next diagnostic
2021-09-06 19:21:18 -07:00
function M . get_next ( opts )
opts = opts or { }
2022-11-19 06:41:47 -07:00
local win_id = opts.win_id or api.nvim_get_current_win ( )
local bufnr = api.nvim_win_get_buf ( win_id )
local cursor_position = opts.cursor_position or api.nvim_win_get_cursor ( win_id )
2021-09-06 19:21:18 -07:00
return next_diagnostic ( cursor_position , true , bufnr , opts , opts.namespace )
end
--- Return the position of the next diagnostic in the current buffer.
---
2022-11-21 14:02:18 -07:00
---@param opts table|nil See |vim.diagnostic.goto_next()|
---@return table|false Next diagnostic position as a (row, col) tuple or false if no next
--- diagnostic.
2021-09-06 19:21:18 -07:00
function M . get_next_pos ( opts )
2021-09-16 10:26:45 -07:00
local next = M.get_next ( opts )
if not next then
return false
end
return { next.lnum , next.col }
2021-09-06 19:21:18 -07:00
end
--- Move to the next diagnostic.
---
---@param opts table|nil Configuration table with the following keys:
--- - namespace: (number) Only consider diagnostics from the given namespace.
2023-08-09 02:06:13 -07:00
--- - cursor_position: (cursor position) Cursor position as a (row, col) tuple.
--- See |nvim_win_get_cursor()|. Defaults to the current cursor position.
2021-09-06 19:21:18 -07:00
--- - wrap: (boolean, default true) Whether to loop around file or not. Similar to 'wrapscan'.
--- - severity: See |diagnostic-severity|.
2021-10-19 10:45:51 -07:00
--- - float: (boolean or table, default true) If "true", call |vim.diagnostic.open_float()|
2023-08-09 02:06:13 -07:00
--- after moving. If a table, pass the table as the {opts} parameter
--- to |vim.diagnostic.open_float()|. Unless overridden, the float will show
2021-09-06 19:21:18 -07:00
--- diagnostics at the new cursor position (as if "cursor" were passed to
2021-10-19 10:45:51 -07:00
--- the "scope" option).
2021-09-06 19:21:18 -07:00
--- - win_id: (number, default 0) Window ID
function M . goto_next ( opts )
return diagnostic_move_pos ( opts , M.get_next_pos ( opts ) )
end
2021-10-29 18:47:34 -07:00
M.handlers . signs = {
show = function ( namespace , bufnr , diagnostics , opts )
vim.validate ( {
namespace = { namespace , ' n ' } ,
bufnr = { bufnr , ' n ' } ,
2022-01-01 12:58:34 -07:00
diagnostics = {
diagnostics ,
vim.tbl_islist ,
' a list of diagnostics ' ,
} ,
2021-10-29 18:47:34 -07:00
opts = { opts , ' t ' , true } ,
} )
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
bufnr = get_bufnr ( bufnr )
2022-01-11 16:39:15 -07:00
opts = opts or { }
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
if opts.signs and opts.signs . severity then
diagnostics = filter_by_severity ( opts.signs . severity , diagnostics )
end
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
define_default_signs ( )
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
-- 10 is the default sign priority when none is explicitly specified
local priority = opts.signs and opts.signs . priority or 10
local get_priority
if opts.severity_sort then
if type ( opts.severity_sort ) == ' table ' and opts.severity_sort . reverse then
get_priority = function ( severity )
return priority + ( severity - vim.diagnostic . severity.ERROR )
end
else
get_priority = function ( severity )
return priority + ( vim.diagnostic . severity.HINT - severity )
end
2021-09-26 16:02:18 -07:00
end
else
2021-10-29 18:47:34 -07:00
get_priority = function ( )
return priority
2021-09-26 16:02:18 -07:00
end
end
2021-10-29 18:47:34 -07:00
local ns = M.get_namespace ( namespace )
if not ns.user_data . sign_group then
ns.user_data . sign_group = string.format ( ' vim.diagnostic.%s ' , ns.name )
end
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
local sign_group = ns.user_data . sign_group
for _ , diagnostic in ipairs ( diagnostics ) do
vim.fn . sign_place ( 0 , sign_group , sign_highlight_map [ diagnostic.severity ] , bufnr , {
priority = get_priority ( diagnostic.severity ) ,
lnum = diagnostic.lnum + 1 ,
} )
end
end ,
hide = function ( namespace , bufnr )
local ns = M.get_namespace ( namespace )
2022-12-17 19:19:15 -07:00
if ns.user_data . sign_group and api.nvim_buf_is_valid ( bufnr ) then
2021-10-29 18:47:34 -07:00
vim.fn . sign_unplace ( ns.user_data . sign_group , { buffer = bufnr } )
end
end ,
}
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
M.handlers . underline = {
show = function ( namespace , bufnr , diagnostics , opts )
vim.validate ( {
namespace = { namespace , ' n ' } ,
bufnr = { bufnr , ' n ' } ,
2022-01-01 12:58:34 -07:00
diagnostics = {
diagnostics ,
vim.tbl_islist ,
' a list of diagnostics ' ,
} ,
2021-10-29 18:47:34 -07:00
opts = { opts , ' t ' , true } ,
} )
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
bufnr = get_bufnr ( bufnr )
2022-01-11 16:39:15 -07:00
opts = opts or { }
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
if opts.underline and opts.underline . severity then
diagnostics = filter_by_severity ( opts.underline . severity , diagnostics )
2021-09-06 19:21:18 -07:00
end
2021-10-29 18:47:34 -07:00
local ns = M.get_namespace ( namespace )
if not ns.user_data . underline_ns then
2022-11-19 06:41:47 -07:00
ns.user_data . underline_ns = api.nvim_create_namespace ( ' ' )
2021-10-29 18:47:34 -07:00
end
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
local underline_ns = ns.user_data . underline_ns
for _ , diagnostic in ipairs ( diagnostics ) do
local higroup = underline_highlight_map [ diagnostic.severity ]
2021-09-06 19:21:18 -07:00
2021-10-29 18:47:34 -07:00
if higroup == nil then
-- Default to error if we don't have a highlight associated
higroup = underline_highlight_map.Error
end
2021-09-06 19:21:18 -07:00
2023-03-30 06:49:58 -07:00
if diagnostic._tags then
-- TODO(lewis6991): we should be able to stack these.
if diagnostic._tags . unnecessary then
higroup = ' DiagnosticUnnecessary '
end
if diagnostic._tags . deprecated then
higroup = ' DiagnosticDeprecated '
end
end
2021-10-29 18:47:34 -07:00
vim.highlight . range (
bufnr ,
underline_ns ,
higroup ,
{ diagnostic.lnum , diagnostic.col } ,
2022-02-19 09:38:14 -07:00
{ diagnostic.end_lnum , diagnostic.end_col } ,
2022-02-21 13:21:42 -07:00
{ priority = vim.highlight . priorities.diagnostics }
2021-10-29 18:47:34 -07:00
)
end
2021-11-09 14:33:01 -07:00
save_extmarks ( underline_ns , bufnr )
2021-10-29 18:47:34 -07:00
end ,
hide = function ( namespace , bufnr )
local ns = M.get_namespace ( namespace )
if ns.user_data . underline_ns then
2021-11-09 14:33:01 -07:00
diagnostic_cache_extmarks [ bufnr ] [ ns.user_data . underline_ns ] = { }
2022-12-17 19:19:15 -07:00
if api.nvim_buf_is_valid ( bufnr ) then
api.nvim_buf_clear_namespace ( bufnr , ns.user_data . underline_ns , 0 , - 1 )
end
2021-10-29 18:47:34 -07:00
end
2021-09-22 12:20:15 -07:00
end ,
2021-10-29 18:47:34 -07:00
}
2021-09-22 12:20:15 -07:00
2021-10-29 18:47:34 -07:00
M.handlers . virtual_text = {
show = function ( namespace , bufnr , diagnostics , opts )
vim.validate ( {
namespace = { namespace , ' n ' } ,
bufnr = { bufnr , ' n ' } ,
2022-01-01 12:58:34 -07:00
diagnostics = {
diagnostics ,
vim.tbl_islist ,
' a list of diagnostics ' ,
} ,
2021-10-29 18:47:34 -07:00
opts = { opts , ' t ' , true } ,
} )
bufnr = get_bufnr ( bufnr )
2022-01-11 16:39:15 -07:00
opts = opts or { }
2021-09-18 14:00:32 -07:00
2021-10-29 18:47:34 -07:00
local severity
if opts.virtual_text then
if opts.virtual_text . format then
diagnostics = reformat_diagnostics ( opts.virtual_text . format , diagnostics )
end
2021-12-17 19:38:33 -07:00
if
opts.virtual_text . source
and ( opts.virtual_text . source ~= ' if_many ' or count_sources ( bufnr ) > 1 )
then
diagnostics = prefix_source ( diagnostics )
2021-10-29 18:47:34 -07:00
end
if opts.virtual_text . severity then
severity = opts.virtual_text . severity
end
2021-09-06 19:21:18 -07:00
end
2021-10-29 18:47:34 -07:00
local ns = M.get_namespace ( namespace )
if not ns.user_data . virt_text_ns then
2022-11-19 06:41:47 -07:00
ns.user_data . virt_text_ns = api.nvim_create_namespace ( ' ' )
2021-09-06 19:21:18 -07:00
end
2021-10-29 18:47:34 -07:00
local virt_text_ns = ns.user_data . virt_text_ns
local buffer_line_diagnostics = diagnostic_lines ( diagnostics )
for line , line_diagnostics in pairs ( buffer_line_diagnostics ) do
if severity then
line_diagnostics = filter_by_severity ( severity , line_diagnostics )
end
local virt_texts = M._get_virt_text_chunks ( line_diagnostics , opts.virtual_text )
if virt_texts then
2022-11-19 06:41:47 -07:00
api.nvim_buf_set_extmark ( bufnr , virt_text_ns , line , 0 , {
2023-08-16 06:21:32 -07:00
hl_mode = opts.virtual_text . hl_mode or ' combine ' ,
2021-10-29 18:47:34 -07:00
virt_text = virt_texts ,
2023-08-16 06:21:32 -07:00
virt_text_pos = opts.virtual_text . virt_text_pos ,
virt_text_hide = opts.virtual_text . virt_text_hide ,
virt_text_win_col = opts.virtual_text . virt_text_win_col ,
2021-10-29 18:47:34 -07:00
} )
end
end
2021-11-09 14:33:01 -07:00
save_extmarks ( virt_text_ns , bufnr )
2021-10-29 18:47:34 -07:00
end ,
hide = function ( namespace , bufnr )
local ns = M.get_namespace ( namespace )
if ns.user_data . virt_text_ns then
2021-11-09 14:33:01 -07:00
diagnostic_cache_extmarks [ bufnr ] [ ns.user_data . virt_text_ns ] = { }
2022-12-17 19:19:15 -07:00
if api.nvim_buf_is_valid ( bufnr ) then
api.nvim_buf_clear_namespace ( bufnr , ns.user_data . virt_text_ns , 0 , - 1 )
end
2021-10-29 18:47:34 -07:00
end
end ,
}
2021-09-06 19:21:18 -07:00
--- Get virtual text chunks to display using |nvim_buf_set_extmark()|.
---
2021-09-20 14:32:14 -07:00
--- Exported for backward compatibility with
--- vim.lsp.diagnostic.get_virtual_text_chunks_for_line(). When that function is eventually removed,
--- this can be made local.
---@private
function M . _get_virt_text_chunks ( line_diags , opts )
2021-09-06 19:21:18 -07:00
if # line_diags == 0 then
return nil
end
opts = opts or { }
local prefix = opts.prefix or ' ■ '
2022-11-20 16:57:36 -07:00
local suffix = opts.suffix or ' '
2021-09-06 19:21:18 -07:00
local spacing = opts.spacing or 4
-- Create a little more space between virtual text and contents
local virt_texts = { { string.rep ( ' ' , spacing ) } }
2023-04-17 04:53:34 -07:00
for i = 1 , # line_diags do
local resolved_prefix = prefix
if type ( prefix ) == ' function ' then
resolved_prefix = prefix ( line_diags [ i ] ) or ' '
end
table.insert (
virt_texts ,
{ resolved_prefix , virtual_text_highlight_map [ line_diags [ i ] . severity ] }
)
2021-09-06 19:21:18 -07:00
end
local last = line_diags [ # line_diags ]
-- TODO(tjdevries): Allow different servers to be shown first somehow?
-- TODO(tjdevries): Display server name associated with these?
if last.message then
2022-11-20 16:57:36 -07:00
if type ( suffix ) == ' function ' then
suffix = suffix ( last ) or ' '
end
2021-09-06 19:21:18 -07:00
table.insert ( virt_texts , {
2023-04-17 04:53:34 -07:00
string.format ( ' %s%s ' , last.message : gsub ( ' \r ' , ' ' ) : gsub ( ' \n ' , ' ' ) , suffix ) ,
2021-09-06 19:21:18 -07:00
virtual_text_highlight_map [ last.severity ] ,
} )
return virt_texts
end
end
--- Hide currently displayed diagnostics.
---
--- This only clears the decorations displayed in the buffer. Diagnostics can
--- be redisplayed with |vim.diagnostic.show()|. To completely remove
--- diagnostics, use |vim.diagnostic.reset()|.
---
--- To hide diagnostics and prevent them from re-displaying, use
--- |vim.diagnostic.disable()|.
---
2023-03-04 06:06:20 -07:00
---@param namespace integer|nil Diagnostic namespace. When omitted, hide
2021-11-09 14:33:01 -07:00
--- diagnostics from all namespaces.
2023-03-04 06:06:20 -07:00
---@param bufnr integer|nil Buffer number, or 0 for current buffer. When
2021-11-16 08:47:49 -07:00
--- omitted, hide diagnostics in all buffers.
2021-09-06 19:21:18 -07:00
function M . hide ( namespace , bufnr )
vim.validate ( {
2021-11-09 14:33:01 -07:00
namespace = { namespace , ' n ' , true } ,
2021-09-06 19:21:18 -07:00
bufnr = { bufnr , ' n ' , true } ,
} )
2021-11-16 08:47:49 -07:00
local buffers = bufnr and { get_bufnr ( bufnr ) } or vim.tbl_keys ( diagnostic_cache )
for _ , iter_bufnr in ipairs ( buffers ) do
local namespaces = namespace and { namespace } or vim.tbl_keys ( diagnostic_cache [ iter_bufnr ] )
for _ , iter_namespace in ipairs ( namespaces ) do
for _ , handler in pairs ( M.handlers ) do
if handler.hide then
handler.hide ( iter_namespace , iter_bufnr )
end
2021-11-09 14:33:01 -07:00
end
2021-10-29 18:47:34 -07:00
end
end
2021-09-06 19:21:18 -07:00
end
2023-01-12 09:57:39 -07:00
--- Check whether diagnostics are disabled in a given buffer.
---
2023-03-04 06:06:20 -07:00
---@param bufnr integer|nil Buffer number, or 0 for current buffer.
---@param namespace integer|nil Diagnostic namespace. When omitted, checks if
2023-01-12 09:57:39 -07:00
--- all diagnostics are disabled in {bufnr}.
--- Otherwise, only checks if diagnostics from
--- {namespace} are disabled.
---@return boolean
function M . is_disabled ( bufnr , namespace )
bufnr = get_bufnr ( bufnr )
if namespace and M.get_namespace ( namespace ) . disabled then
return true
end
if type ( diagnostic_disabled [ bufnr ] ) == ' table ' then
return diagnostic_disabled [ bufnr ] [ namespace ]
end
return diagnostic_disabled [ bufnr ] ~= nil
end
2021-09-06 19:21:18 -07:00
--- Display diagnostics for the given namespace and buffer.
---
2023-03-04 06:06:20 -07:00
---@param namespace integer|nil Diagnostic namespace. When omitted, show
2021-11-09 14:33:01 -07:00
--- diagnostics from all namespaces.
2023-03-04 06:06:20 -07:00
---@param bufnr integer|nil Buffer number, or 0 for current buffer. When omitted, show
2021-11-16 08:47:49 -07:00
--- diagnostics in all buffers.
2021-09-06 19:21:18 -07:00
---@param diagnostics table|nil The diagnostics to display. When omitted, use the
--- saved diagnostics for the given namespace and
--- buffer. This can be used to display a list of diagnostics
--- without saving them or to display only a subset of
2021-11-16 08:47:49 -07:00
--- diagnostics. May not be used when {namespace}
--- or {bufnr} is nil.
2021-09-06 19:21:18 -07:00
---@param opts table|nil Display options. See |vim.diagnostic.config()|.
function M . show ( namespace , bufnr , diagnostics , opts )
vim.validate ( {
2021-11-09 14:33:01 -07:00
namespace = { namespace , ' n ' , true } ,
2021-09-06 19:21:18 -07:00
bufnr = { bufnr , ' n ' , true } ,
2022-01-01 12:58:34 -07:00
diagnostics = {
diagnostics ,
function ( v )
return v == nil or vim.tbl_islist ( v )
end ,
' a list of diagnostics ' ,
} ,
2021-09-06 19:21:18 -07:00
opts = { opts , ' t ' , true } ,
} )
2021-11-16 08:47:49 -07:00
if not bufnr or not namespace then
assert ( not diagnostics , ' Cannot show diagnostics without a buffer and namespace ' )
if not bufnr then
for iter_bufnr in pairs ( diagnostic_cache ) do
M.show ( namespace , iter_bufnr , nil , opts )
end
else
-- namespace is nil
bufnr = get_bufnr ( bufnr )
for iter_namespace in pairs ( diagnostic_cache [ bufnr ] ) do
M.show ( iter_namespace , bufnr , nil , opts )
end
2021-11-09 14:33:01 -07:00
end
return
end
2023-01-12 09:57:39 -07:00
if M.is_disabled ( bufnr , namespace ) then
2021-09-06 19:21:18 -07:00
return
end
M.hide ( namespace , bufnr )
2021-11-19 11:20:04 -07:00
diagnostics = diagnostics or get_diagnostics ( bufnr , { namespace = namespace } , true )
2021-09-06 19:21:18 -07:00
if not diagnostics or vim.tbl_isempty ( diagnostics ) then
return
end
opts = get_resolved_options ( opts , namespace , bufnr )
if opts.update_in_insert then
clear_scheduled_display ( namespace , bufnr )
else
2022-11-19 06:41:47 -07:00
local mode = api.nvim_get_mode ( )
2021-09-06 19:21:18 -07:00
if string.sub ( mode.mode , 1 , 1 ) == ' i ' then
schedule_display ( namespace , bufnr , opts )
return
end
end
2021-09-17 09:50:25 -07:00
if vim.F . if_nil ( opts.severity_sort , false ) then
if type ( opts.severity_sort ) == ' table ' and opts.severity_sort . reverse then
table.sort ( diagnostics , function ( a , b )
return a.severity < b.severity
end )
2021-09-17 13:59:30 -07:00
else
table.sort ( diagnostics , function ( a , b )
return a.severity > b.severity
end )
2021-09-17 09:50:25 -07:00
end
end
2021-10-29 18:47:34 -07:00
for handler_name , handler in pairs ( M.handlers ) do
if handler.show and opts [ handler_name ] then
handler.show ( namespace , bufnr , diagnostics , opts )
end
2021-09-06 19:21:18 -07:00
end
end
2021-10-19 10:45:51 -07:00
--- Show diagnostics in a floating window.
2021-09-06 19:21:18 -07:00
---
2023-08-09 02:06:13 -07:00
---@param opts table|nil Configuration table with the same keys
--- as |vim.lsp.util.open_floating_preview()| in addition to the following:
2021-12-08 18:44:31 -07:00
--- - bufnr: (number) Buffer number to show diagnostics from.
--- Defaults to the current buffer.
2021-09-06 19:21:18 -07:00
--- - namespace: (number) Limit diagnostics to the given namespace
2021-11-28 09:42:29 -07:00
--- - scope: (string, default "line") Show diagnostics from the whole buffer ("buffer"),
2021-10-19 10:45:51 -07:00
--- the current cursor line ("line"), or the current cursor position ("cursor").
2021-12-08 18:44:31 -07:00
--- Shorthand versions are also accepted ("c" for "cursor", "l" for "line", "b"
--- for "buffer").
2021-10-19 10:45:51 -07:00
--- - pos: (number or table) If {scope} is "line" or "cursor", use this position rather
--- than the cursor position. If a number, interpreted as a line number;
--- otherwise, a (row, col) tuple.
--- - severity_sort: (default false) Sort diagnostics by severity. Overrides the setting
--- from |vim.diagnostic.config()|.
2023-08-09 02:06:13 -07:00
--- - severity: See |diagnostic-severity|. Overrides the setting
--- from |vim.diagnostic.config()|.
2021-11-15 08:56:10 -07:00
--- - header: (string or table) String to use as the header for the floating window. If a
--- table, it is interpreted as a [text, hl_group] tuple. Overrides the setting
--- from |vim.diagnostic.config()|.
2021-12-17 19:38:33 -07:00
--- - source: (boolean or string) Include the diagnostic source in the message.
--- Use "if_many" to only show sources if there is more than one source of
--- diagnostics in the buffer. Otherwise, any truthy value means to always show
2023-08-09 02:06:13 -07:00
--- the diagnostic source. Overrides the setting from |vim.diagnostic.config()|.
2021-09-22 12:20:15 -07:00
--- - format: (function) A function that takes a diagnostic as input and returns a
--- string. The return value is the text used to display the diagnostic.
2021-10-19 10:45:51 -07:00
--- Overrides the setting from |vim.diagnostic.config()|.
2021-11-15 08:55:31 -07:00
--- - prefix: (function, string, or table) Prefix each diagnostic in the floating
--- window. If a function, it must have the signature (diagnostic, i,
--- total) -> (string, string), where {i} is the index of the diagnostic
--- being evaluated and {total} is the total number of diagnostics
--- displayed in the window. The function should return a string which
--- is prepended to each diagnostic in the window as well as an
--- (optional) highlight group which will be used to highlight the
--- prefix. If {prefix} is a table, it is interpreted as a [text,
--- hl_group] tuple as in |nvim_echo()|; otherwise, if {prefix} is a
--- string, it is prepended to each diagnostic in the window with no
--- highlight.
2021-11-14 18:40:11 -07:00
--- Overrides the setting from |vim.diagnostic.config()|.
2022-11-20 13:09:35 -07:00
--- - suffix: Same as {prefix}, but appends the text to the diagnostic instead of
--- prepending it. Overrides the setting from |vim.diagnostic.config()|.
2023-03-04 06:06:20 -07:00
---@return integer|nil, integer|nil: ({float_bufnr}, {win_id})
2021-12-08 18:44:31 -07:00
function M . open_float ( opts , ... )
-- Support old (bufnr, opts) signature
local bufnr
if opts == nil or type ( opts ) == ' number ' then
bufnr = opts
opts = ...
else
vim.validate ( {
opts = { opts , ' t ' , true } ,
} )
end
2021-09-06 19:21:18 -07:00
opts = opts or { }
2021-12-08 18:44:31 -07:00
bufnr = get_bufnr ( bufnr or opts.bufnr )
2022-03-27 07:10:03 -07:00
do
-- Resolve options with user settings from vim.diagnostic.config
-- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
-- does not have a dedicated table for configuration options; instead, the options are mixed in
-- with its `opts` table which also includes "keyword" parameters. So we create a dedicated
-- options table that inherits missing keys from the global configuration before resolving.
local t = global_diagnostic_options.float
local float_opts = vim.tbl_extend ( ' keep ' , opts , type ( t ) == ' table ' and t or { } )
opts = get_resolved_options ( { float = float_opts } , nil , bufnr ) . float
end
2021-12-08 18:44:31 -07:00
local scope = ( { l = ' line ' , c = ' cursor ' , b = ' buffer ' } ) [ opts.scope ] or opts.scope or ' line '
2021-10-19 10:45:51 -07:00
local lnum , col
if scope == ' line ' or scope == ' cursor ' then
if not opts.pos then
2022-11-19 06:41:47 -07:00
local pos = api.nvim_win_get_cursor ( 0 )
2021-10-19 10:45:51 -07:00
lnum = pos [ 1 ] - 1
col = pos [ 2 ]
elseif type ( opts.pos ) == ' number ' then
lnum = opts.pos
elseif type ( opts.pos ) == ' table ' then
lnum , col = unpack ( opts.pos )
else
error ( " Invalid value for option 'pos' " )
end
elseif scope ~= ' buffer ' then
error ( " Invalid value for option 'scope' " )
2021-09-06 19:21:18 -07:00
end
2021-10-19 10:45:51 -07:00
2021-11-19 11:20:04 -07:00
local diagnostics = get_diagnostics ( bufnr , opts , true )
2021-09-06 19:21:18 -07:00
2021-10-19 10:45:51 -07:00
if scope == ' line ' then
diagnostics = vim.tbl_filter ( function ( d )
return d.lnum == lnum
end , diagnostics )
elseif scope == ' cursor ' then
2021-10-19 15:27:49 -07:00
-- LSP servers can send diagnostics with `end_col` past the length of the line
2022-11-19 06:41:47 -07:00
local line_length = # api.nvim_buf_get_lines ( bufnr , lnum , lnum + 1 , true ) [ 1 ]
2021-10-19 10:45:51 -07:00
diagnostics = vim.tbl_filter ( function ( d )
2021-10-19 15:27:49 -07:00
return d.lnum == lnum
and math.min ( d.col , line_length - 1 ) <= col
and ( d.end_col >= col or d.end_lnum > lnum )
2021-10-19 10:45:51 -07:00
end , diagnostics )
end
2021-09-06 19:21:18 -07:00
2021-10-19 10:45:51 -07:00
if vim.tbl_isempty ( diagnostics ) then
return
end
local severity_sort = vim.F . if_nil ( opts.severity_sort , global_diagnostic_options.severity_sort )
if severity_sort then
if type ( severity_sort ) == ' table ' and severity_sort.reverse then
table.sort ( diagnostics , function ( a , b )
return a.severity > b.severity
end )
else
table.sort ( diagnostics , function ( a , b )
return a.severity < b.severity
end )
end
end
local lines = { }
local highlights = { }
2021-11-15 08:56:10 -07:00
local header = if_nil ( opts.header , ' Diagnostics: ' )
if header then
vim.validate ( {
header = {
header ,
2022-11-19 06:41:47 -07:00
{ ' string ' , ' table ' } ,
2021-11-15 08:56:10 -07:00
" 'string' or 'table' " ,
} ,
} )
if type ( header ) == ' table ' then
-- Don't insert any lines for an empty string
if string.len ( if_nil ( header [ 1 ] , ' ' ) ) > 0 then
table.insert ( lines , header [ 1 ] )
2022-11-20 13:09:35 -07:00
table.insert ( highlights , { hlname = header [ 2 ] or ' Bold ' } )
2021-11-15 08:56:10 -07:00
end
elseif # header > 0 then
table.insert ( lines , header )
2022-11-20 13:09:35 -07:00
table.insert ( highlights , { hlname = ' Bold ' } )
2021-11-15 08:56:10 -07:00
end
2021-10-19 10:45:51 -07:00
end
if opts.format then
diagnostics = reformat_diagnostics ( opts.format , diagnostics )
end
2021-12-17 19:38:33 -07:00
if opts.source and ( opts.source ~= ' if_many ' or count_sources ( bufnr ) > 1 ) then
diagnostics = prefix_source ( diagnostics )
2021-10-19 10:45:51 -07:00
end
2021-11-14 18:40:11 -07:00
local prefix_opt =
if_nil ( opts.prefix , ( scope == ' cursor ' and # diagnostics <= 1 ) and ' ' or function ( _ , i )
return string.format ( ' %d. ' , i )
end )
2021-11-15 08:55:31 -07:00
local prefix , prefix_hl_group
2021-11-14 18:40:11 -07:00
if prefix_opt then
vim.validate ( {
prefix = {
prefix_opt ,
2022-11-19 06:41:47 -07:00
{ ' string ' , ' table ' , ' function ' } ,
2021-11-15 08:55:31 -07:00
" 'string' or 'table' or 'function' " ,
} ,
} )
if type ( prefix_opt ) == ' string ' then
prefix , prefix_hl_group = prefix_opt , ' NormalFloat '
elseif type ( prefix_opt ) == ' table ' then
prefix , prefix_hl_group = prefix_opt [ 1 ] or ' ' , prefix_opt [ 2 ] or ' NormalFloat '
end
2021-11-14 18:40:11 -07:00
end
2022-11-20 13:09:35 -07:00
local suffix_opt = if_nil ( opts.suffix , function ( diagnostic )
return diagnostic.code and string.format ( ' [%s] ' , diagnostic.code ) or ' '
end )
local suffix , suffix_hl_group
if suffix_opt then
vim.validate ( {
suffix = {
suffix_opt ,
{ ' string ' , ' table ' , ' function ' } ,
" 'string' or 'table' or 'function' " ,
} ,
} )
if type ( suffix_opt ) == ' string ' then
suffix , suffix_hl_group = suffix_opt , ' NormalFloat '
elseif type ( suffix_opt ) == ' table ' then
suffix , suffix_hl_group = suffix_opt [ 1 ] or ' ' , suffix_opt [ 2 ] or ' NormalFloat '
end
end
2021-10-19 10:45:51 -07:00
for i , diagnostic in ipairs ( diagnostics ) do
2021-11-15 08:55:31 -07:00
if prefix_opt and type ( prefix_opt ) == ' function ' then
prefix , prefix_hl_group = prefix_opt ( diagnostic , i , # diagnostics )
prefix , prefix_hl_group = prefix or ' ' , prefix_hl_group or ' NormalFloat '
end
2022-11-20 13:09:35 -07:00
if suffix_opt and type ( suffix_opt ) == ' function ' then
suffix , suffix_hl_group = suffix_opt ( diagnostic , i , # diagnostics )
suffix , suffix_hl_group = suffix or ' ' , suffix_hl_group or ' NormalFloat '
end
2021-10-19 10:45:51 -07:00
local hiname = floating_highlight_map [ diagnostic.severity ]
local message_lines = vim.split ( diagnostic.message , ' \n ' )
2022-11-20 13:09:35 -07:00
for j = 1 , # message_lines do
local pre = j == 1 and prefix or string.rep ( ' ' , # prefix )
local suf = j == # message_lines and suffix or ' '
table.insert ( lines , pre .. message_lines [ j ] .. suf )
table.insert ( highlights , {
hlname = hiname ,
prefix = {
length = j == 1 and # prefix or 0 ,
hlname = prefix_hl_group ,
} ,
suffix = {
length = j == # message_lines and # suffix or 0 ,
hlname = suffix_hl_group ,
} ,
} )
2021-10-19 10:45:51 -07:00
end
end
2021-10-19 15:29:52 -07:00
-- Used by open_floating_preview to allow the float to be focused
if not opts.focus_id then
opts.focus_id = scope
end
2021-10-19 10:45:51 -07:00
local float_bufnr , winnr = require ( ' vim.lsp.util ' ) . open_floating_preview ( lines , ' plaintext ' , opts )
2022-11-20 13:09:35 -07:00
for i , hl in ipairs ( highlights ) do
local line = lines [ i ]
local prefix_len = hl.prefix and hl.prefix . length or 0
local suffix_len = hl.suffix and hl.suffix . length or 0
if prefix_len > 0 then
api.nvim_buf_add_highlight ( float_bufnr , - 1 , hl.prefix . hlname , i - 1 , 0 , prefix_len )
end
api.nvim_buf_add_highlight ( float_bufnr , - 1 , hl.hlname , i - 1 , prefix_len , # line - suffix_len )
if suffix_len > 0 then
api.nvim_buf_add_highlight ( float_bufnr , - 1 , hl.suffix . hlname , i - 1 , # line - suffix_len , - 1 )
2021-11-15 08:55:31 -07:00
end
2021-10-19 10:45:51 -07:00
end
return float_bufnr , winnr
2021-09-06 19:21:18 -07:00
end
--- Remove all diagnostics from the given namespace.
---
--- Unlike |vim.diagnostic.hide()|, this function removes all saved
--- diagnostics. They cannot be redisplayed using |vim.diagnostic.show()|. To
--- simply remove diagnostic decorations in a way that they can be
--- re-displayed, use |vim.diagnostic.hide()|.
---
2023-03-04 06:06:20 -07:00
---@param namespace integer|nil Diagnostic namespace. When omitted, remove
2021-11-09 14:33:01 -07:00
--- diagnostics from all namespaces.
2023-03-04 06:06:20 -07:00
---@param bufnr integer|nil Remove diagnostics for the given buffer. When omitted,
2021-09-06 19:21:18 -07:00
--- diagnostics are removed for all buffers.
function M . reset ( namespace , bufnr )
2021-11-09 14:33:01 -07:00
vim.validate ( {
namespace = { namespace , ' n ' , true } ,
bufnr = { bufnr , ' n ' , true } ,
} )
2021-11-22 08:47:30 -07:00
local buffers = bufnr and { get_bufnr ( bufnr ) } or vim.tbl_keys ( diagnostic_cache )
2021-11-09 14:33:01 -07:00
for _ , iter_bufnr in ipairs ( buffers ) do
local namespaces = namespace and { namespace } or vim.tbl_keys ( diagnostic_cache [ iter_bufnr ] )
for _ , iter_namespace in ipairs ( namespaces ) do
2021-11-21 18:40:06 -07:00
diagnostic_cache [ iter_bufnr ] [ iter_namespace ] = nil
2021-11-09 14:33:01 -07:00
M.hide ( iter_namespace , iter_bufnr )
2021-09-06 19:21:18 -07:00
end
2022-12-17 19:19:15 -07:00
if api.nvim_buf_is_valid ( iter_bufnr ) then
api.nvim_exec_autocmds ( ' DiagnosticChanged ' , {
modeline = false ,
buffer = iter_bufnr ,
data = { diagnostics = { } } ,
} )
else
diagnostic_cache [ iter_bufnr ] = nil
end
2022-01-03 06:48:01 -07:00
end
2021-09-06 19:21:18 -07:00
end
--- Add all diagnostics to the quickfix list.
---
---@param opts table|nil Configuration table with the following keys:
--- - namespace: (number) Only add diagnostics from the given namespace.
--- - open: (boolean, default true) Open quickfix list after setting.
--- - title: (string) Title of quickfix list. Defaults to "Diagnostics".
--- - severity: See |diagnostic-severity|.
function M . setqflist ( opts )
set_list ( false , opts )
end
--- Add buffer diagnostics to the location list.
---
---@param opts table|nil Configuration table with the following keys:
--- - namespace: (number) Only add diagnostics from the given namespace.
--- - winnr: (number, default 0) Window number to set location list for.
--- - open: (boolean, default true) Open the location list after setting.
--- - title: (string) Title of the location list. Defaults to "Diagnostics".
--- - severity: See |diagnostic-severity|.
function M . setloclist ( opts )
set_list ( true , opts )
end
--- Disable diagnostics in the given buffer.
---
2023-03-04 06:06:20 -07:00
---@param bufnr integer|nil Buffer number, or 0 for current buffer. When
2021-11-16 08:47:49 -07:00
--- omitted, disable diagnostics in all buffers.
2023-03-04 06:06:20 -07:00
---@param namespace integer|nil Only disable diagnostics for the given namespace.
2021-09-06 19:21:18 -07:00
function M . disable ( bufnr , namespace )
vim.validate ( { bufnr = { bufnr , ' n ' , true } , namespace = { namespace , ' n ' , true } } )
2021-11-16 08:47:49 -07:00
if bufnr == nil then
if namespace == nil then
-- Disable everything (including as yet non-existing buffers and
-- namespaces) by setting diagnostic_disabled to an empty table and set
-- its metatable to always return true. This metatable is removed
-- in enable()
diagnostic_disabled = setmetatable ( { } , {
__index = function ( )
return true
end ,
} )
else
local ns = M.get_namespace ( namespace )
ns.disabled = true
2021-09-06 19:21:18 -07:00
end
else
2021-11-16 08:47:49 -07:00
bufnr = get_bufnr ( bufnr )
if namespace == nil then
diagnostic_disabled [ bufnr ] = true
else
if type ( diagnostic_disabled [ bufnr ] ) ~= ' table ' then
diagnostic_disabled [ bufnr ] = { }
end
diagnostic_disabled [ bufnr ] [ namespace ] = true
2021-09-06 19:21:18 -07:00
end
end
2021-11-16 08:47:49 -07:00
M.hide ( namespace , bufnr )
2021-09-06 19:21:18 -07:00
end
--- Enable diagnostics in the given buffer.
---
2023-03-04 06:06:20 -07:00
---@param bufnr integer|nil Buffer number, or 0 for current buffer. When
2021-11-16 08:47:49 -07:00
--- omitted, enable diagnostics in all buffers.
2023-03-04 06:06:20 -07:00
---@param namespace integer|nil Only enable diagnostics for the given namespace.
2021-09-06 19:21:18 -07:00
function M . enable ( bufnr , namespace )
vim.validate ( { bufnr = { bufnr , ' n ' , true } , namespace = { namespace , ' n ' , true } } )
2021-11-16 08:47:49 -07:00
if bufnr == nil then
if namespace == nil then
-- Enable everything by setting diagnostic_disabled to an empty table
diagnostic_disabled = { }
else
local ns = M.get_namespace ( namespace )
ns.disabled = false
2021-09-06 19:21:18 -07:00
end
else
2021-11-16 08:47:49 -07:00
bufnr = get_bufnr ( bufnr )
if namespace == nil then
diagnostic_disabled [ bufnr ] = nil
else
if type ( diagnostic_disabled [ bufnr ] ) ~= ' table ' then
return
end
diagnostic_disabled [ bufnr ] [ namespace ] = nil
2021-09-06 19:21:18 -07:00
end
end
2021-11-16 08:47:49 -07:00
M.show ( namespace , bufnr )
2021-09-06 19:21:18 -07:00
end
2021-09-19 15:13:23 -07:00
--- Parse a diagnostic from a string.
---
--- For example, consider a line of output from a linter:
2023-09-14 06:23:01 -07:00
---
--- ```
2021-09-19 15:13:23 -07:00
--- WARNING filename:27:3: Variable 'foo' does not exist
2023-09-14 06:23:01 -07:00
--- ```
2021-10-17 07:18:35 -07:00
---
2021-09-19 15:13:23 -07:00
--- This can be parsed into a diagnostic |diagnostic-structure|
--- with:
2023-09-14 06:23:01 -07:00
---
--- ```lua
--- local s = "WARNING filename:27:3: Variable 'foo' does not exist"
--- local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
--- local groups = { "severity", "lnum", "col", "message" }
--- vim.diagnostic.match(s, pattern, groups, { WARNING = vim.diagnostic.WARN })
--- ```
2021-09-19 15:13:23 -07:00
---
---@param str string String to parse diagnostics from.
---@param pat string Lua pattern with capture groups.
---@param groups table List of fields in a |diagnostic-structure| to
--- associate with captures from {pat}.
---@param severity_map table A table mapping the severity field from {groups}
--- with an item from |vim.diagnostic.severity|.
---@param defaults table|nil Table of default values for any fields not listed in {groups}.
--- When omitted, numeric values default to 0 and "severity" defaults to
--- ERROR.
2022-11-21 14:02:18 -07:00
---@return Diagnostic|nil: |diagnostic-structure| or `nil` if {pat} fails to match {str}.
2021-09-19 15:13:23 -07:00
function M . match ( str , pat , groups , severity_map , defaults )
vim.validate ( {
str = { str , ' s ' } ,
pat = { pat , ' s ' } ,
groups = { groups , ' t ' } ,
severity_map = { severity_map , ' t ' , true } ,
defaults = { defaults , ' t ' , true } ,
} )
severity_map = severity_map or M.severity
local diagnostic = { }
local matches = { string.match ( str , pat ) }
if vim.tbl_isempty ( matches ) then
return
end
for i , match in ipairs ( matches ) do
local field = groups [ i ]
if field == ' severity ' then
match = severity_map [ match ]
elseif field == ' lnum ' or field == ' end_lnum ' or field == ' col ' or field == ' end_col ' then
match = assert ( tonumber ( match ) ) - 1
end
diagnostic [ field ] = match
end
2021-09-06 19:21:18 -07:00
2021-09-19 15:13:23 -07:00
diagnostic = vim.tbl_extend ( ' keep ' , diagnostic , defaults or { } )
diagnostic.severity = diagnostic.severity or M.severity . ERROR
diagnostic.col = diagnostic.col or 0
diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
diagnostic.end_col = diagnostic.end_col or diagnostic.col
return diagnostic
end
local errlist_type_map = {
[ M.severity . ERROR ] = ' E ' ,
[ M.severity . WARN ] = ' W ' ,
[ M.severity . INFO ] = ' I ' ,
[ M.severity . HINT ] = ' N ' ,
}
--- Convert a list of diagnostics to a list of quickfix items that can be
--- passed to |setqflist()| or |setloclist()|.
---
---@param diagnostics table List of diagnostics |diagnostic-structure|.
2022-11-21 14:02:18 -07:00
---@return table[] of quickfix list items |setqflist-what|
2021-09-19 15:13:23 -07:00
function M . toqflist ( diagnostics )
2022-01-01 12:58:34 -07:00
vim.validate ( {
diagnostics = {
diagnostics ,
vim.tbl_islist ,
' a list of diagnostics ' ,
} ,
} )
2021-09-19 15:13:23 -07:00
local list = { }
for _ , v in ipairs ( diagnostics ) do
local item = {
bufnr = v.bufnr ,
lnum = v.lnum + 1 ,
col = v.col and ( v.col + 1 ) or nil ,
end_lnum = v.end_lnum and ( v.end_lnum + 1 ) or nil ,
end_col = v.end_col and ( v.end_col + 1 ) or nil ,
text = v.message ,
type = errlist_type_map [ v.severity ] or ' E ' ,
}
table.insert ( list , item )
end
table.sort ( list , function ( a , b )
if a.bufnr == b.bufnr then
2022-12-18 08:17:15 -07:00
if a.lnum == b.lnum then
return a.col < b.col
else
return a.lnum < b.lnum
end
2021-09-19 15:13:23 -07:00
else
return a.bufnr < b.bufnr
end
end )
return list
end
--- Convert a list of quickfix items to a list of diagnostics.
---
2023-08-09 02:06:13 -07:00
---@param list table[] List of quickfix items from |getqflist()| or |getloclist()|.
2022-11-21 14:02:18 -07:00
---@return Diagnostic[] array of |diagnostic-structure|
2021-09-19 15:13:23 -07:00
function M . fromqflist ( list )
2022-01-01 12:58:34 -07:00
vim.validate ( {
list = {
list ,
vim.tbl_islist ,
' a list of quickfix items ' ,
} ,
} )
2021-09-19 15:13:23 -07:00
local diagnostics = { }
for _ , item in ipairs ( list ) do
if item.valid == 1 then
local lnum = math.max ( 0 , item.lnum - 1 )
2021-11-18 12:27:46 -07:00
local col = math.max ( 0 , item.col - 1 )
2021-09-19 15:13:23 -07:00
local end_lnum = item.end_lnum > 0 and ( item.end_lnum - 1 ) or lnum
local end_col = item.end_col > 0 and ( item.end_col - 1 ) or col
local severity = item.type ~= ' ' and M.severity [ item.type ] or M.severity . ERROR
table.insert ( diagnostics , {
bufnr = item.bufnr ,
lnum = lnum ,
col = col ,
end_lnum = end_lnum ,
end_col = end_col ,
severity = severity ,
message = item.text ,
} )
end
end
return diagnostics
end
2021-09-06 19:21:18 -07:00
return M