local api = vim.api local nvim_on = require('vim._core.util').nvim_on local severity_module = require('vim.diagnostic._severity') --- @class (private) vim.diagnostic._store local M = {} -- bufnr -> ns -> Diagnostic[] local diagnostic_cache = {} --- @type table> local group = api.nvim_create_augroup('nvim.diagnostic.buf_wipeout', {}) setmetatable(diagnostic_cache, { --- @param t table --- @param bufnr integer __index = function(t, bufnr) assert(bufnr > 0, 'Invalid buffer number') nvim_on('BufWipeout', group, { buf = bufnr }, function() rawset(t, bufnr, nil) end) t[bufnr] = {} return t[bufnr] end, }) --- @param bufnr integer --- @param namespace integer --- @param d vim.Diagnostic.Set local function norm_diag(bufnr, namespace, d) vim.validate('diagnostic.lnum', d.lnum, 'number') local d1 = d --[[@as vim.Diagnostic]] d1.severity = d.severity and severity_module.to_severity(d.severity) or vim.diagnostic.severity.ERROR d1.end_lnum = d.end_lnum or d.lnum d1.col = d.col or 0 d1.end_col = d.end_col or d.col or 0 d1.namespace = namespace d1.bufnr = bufnr end --- Execute a given function now if the given buffer is already loaded or once it is loaded later. --- --- @param bufnr integer Buffer number --- @param fn fun() --- @return integer? local function once_buf_loaded(bufnr, fn) if api.nvim_buf_is_loaded(bufnr) then fn() else return nvim_on('BufRead', nil, { buf = bufnr, once = true, }, function() fn() end) end end --- @param bufnr integer? --- @param opts vim.diagnostic.GetOpts? --- @param clamp boolean --- @return vim.Diagnostic[] function M.get_diagnostics(bufnr, opts, clamp) opts = opts or {} local namespace = opts.namespace if type(namespace) == 'number' then namespace = { namespace } end --- @cast namespace integer[] --- @type vim.Diagnostic[] local diagnostics = {} -- Memoized results of buf_line_count per bufnr --- @type table local buf_line_count = setmetatable({}, { --- @param t table --- @param k integer --- @return integer __index = function(t, k) t[k] = api.nvim_buf_line_count(k) return rawget(t, k) end, }) local match_severity = opts.severity and severity_module.severity_predicate(opts.severity) or function(_) return true end --- @param b integer --- @param d vim.Diagnostic local match_enablement = function(d, b) if opts.enabled == nil then return true end local enabled = vim.diagnostic.is_enabled({ bufnr = b, ns_id = d.namespace }) return (enabled and opts.enabled) or (not enabled and not opts.enabled) end --- @param b integer --- @param d vim.Diagnostic local function add(b, d) if match_severity(d) and match_enablement(d, b) and (not opts.lnum or (opts.lnum >= d.lnum and opts.lnum <= (d.end_lnum or d.lnum))) then if clamp and api.nvim_buf_is_loaded(b) then local line_count = buf_line_count[b] - 1 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 d = vim.deepcopy(d, true) d.lnum = math.max(math.min(d.lnum, line_count), 0) d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0) d.col = math.max(d.col, 0) d.end_col = math.max(d.end_col, 0) end end table.insert(diagnostics, d) end end --- @param buf integer --- @param diags vim.Diagnostic[] local function add_all_diags(buf, diags) for _, diagnostic0 in pairs(diags) do add(buf, diagnostic0) end end if not namespace and not bufnr then for buf, ns_diags in pairs(diagnostic_cache) do for _, diags in pairs(ns_diags) do add_all_diags(buf, diags) end end elseif not namespace then bufnr = vim._resolve_bufnr(bufnr) for iter_namespace in pairs(diagnostic_cache[bufnr]) do add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace]) end elseif bufnr == nil then for b, t in pairs(diagnostic_cache) do for _, iter_namespace in ipairs(namespace) do add_all_diags(b, t[iter_namespace] or {}) end end else bufnr = vim._resolve_bufnr(bufnr) for _, iter_namespace in ipairs(namespace) do add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace] or {}) end end return diagnostics end --- @return integer[] function M.get_bufnrs() return vim.tbl_keys(diagnostic_cache) end --- @param bufnr integer --- @return integer[] function M.get_buf_namespaces(bufnr) return vim.tbl_keys(diagnostic_cache[vim._resolve_bufnr(bufnr)]) end --- @param namespace integer --- @param bufnr integer function M.clear(namespace, bufnr) diagnostic_cache[vim._resolve_bufnr(bufnr)][namespace] = nil end --- @param bufnr integer function M.drop_buf(bufnr) diagnostic_cache[vim._resolve_bufnr(bufnr)] = nil end --- Set diagnostics for the given namespace and buffer. --- --- @param namespace integer The diagnostic namespace --- @param bufnr integer Buffer number --- @param diagnostics vim.Diagnostic.Set[] function M.set(namespace, bufnr, diagnostics) vim.validate('namespace', namespace, 'number') vim.validate('bufnr', bufnr, 'number') vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') bufnr = vim._resolve_bufnr(bufnr) for _, diagnostic0 in ipairs(diagnostics) do norm_diag(bufnr, namespace, diagnostic0) end --- @cast diagnostics vim.Diagnostic[] if vim.tbl_isempty(diagnostics) then diagnostic_cache[bufnr][namespace] = nil else diagnostic_cache[bufnr][namespace] = diagnostics end -- Compute positions, set them as extmarks, and store in diagnostic._extmark_id -- (used by get_logical_pos to adjust positions). once_buf_loaded(bufnr, function() local ns = vim.diagnostic.get_namespace(namespace) if not ns.user_data.location_ns then ns.user_data.location_ns = api.nvim_create_namespace(string.format('nvim.%s.diagnostic', ns.name)) end api.nvim_buf_clear_namespace(bufnr, ns.user_data.location_ns, 0, -1) local lines = api.nvim_buf_get_lines(bufnr, 0, -1, true) -- set extmarks at diagnostic locations to preserve logical positions despite text changes for _, diagnostic0 in ipairs(diagnostics) do local last_row = #lines - 1 local row = math.max(0, math.min(diagnostic0.lnum, last_row)) local row_len = #lines[row + 1] local col = math.max(0, math.min(diagnostic0.col, row_len - 1)) local end_row = math.max(0, math.min(diagnostic0.end_lnum or row, last_row)) local end_row_len = #lines[end_row + 1] local end_col = math.max(0, math.min(diagnostic0.end_col or col, end_row_len)) if end_row == row then -- avoid starting an extmark beyond end of the line if end_col == col then end_col = math.min(end_col + 1, end_row_len) end else -- avoid ending an extmark before start of the line if end_col == 0 then end_row = end_row - 1 local end_line = lines[end_row + 1] if not end_line then error( 'Failed to adjust diagnostic position to the end of a previous line. #lines in a buffer: ' .. #lines .. ', lnum: ' .. diagnostic0.lnum .. ', col: ' .. diagnostic0.col .. ', end_lnum: ' .. diagnostic0.end_lnum .. ', end_col: ' .. diagnostic0.end_col ) end end_col = #end_line end end diagnostic0._extmark_id = api.nvim_buf_set_extmark(bufnr, ns.user_data.location_ns, row, col, { end_row = end_row, end_col = end_col, invalidate = true, }) end end) end --- @param bufnr integer? Buffer number to get diagnostics from. Use 0 for --- current buffer or nil for all buffers. --- @param opts? vim.diagnostic.GetOpts --- @return vim.Diagnostic[] : Fields `bufnr`, `end_lnum`, `end_col`, and `severity` --- are guaranteed to be present. function M.get(bufnr, opts) vim.validate('bufnr', bufnr, 'number', true) vim.validate('opts', opts, 'table', true) return vim.deepcopy(M.get_diagnostics(bufnr, opts, false), true) end --- @param bufnr? integer Buffer number to get diagnostics from. Use 0 for --- current buffer or nil for all buffers. --- @param opts? vim.diagnostic.GetOpts --- @return table : Table with actually present severity values as keys --- (see |diagnostic-severity|) and integer counts as values. function M.count(bufnr, opts) vim.validate('bufnr', bufnr, 'number', true) vim.validate('opts', opts, 'table', true) local diagnostics = M.get_diagnostics(bufnr, opts, false) local count = {} --- @type table for _, d in ipairs(diagnostics) do count[d.severity] = (count[d.severity] or 0) + 1 end return count end return M