diff options
Diffstat (limited to 'runtime/lua/vim/diagnostic.lua')
| -rw-r--r-- | runtime/lua/vim/diagnostic.lua | 545 | 
1 files changed, 313 insertions, 232 deletions
| diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index b30a678eeb..742ebf69b2 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -36,7 +36,34 @@ M.handlers = setmetatable({}, {    end,  }) --- Local functions {{{ +-- Metatable that automatically creates an empty table when assigning to a missing key +local bufnr_and_namespace_cacher_mt = { +  __index = function(t, bufnr) +    assert(bufnr > 0, "Invalid buffer number") +    t[bufnr] = {} +    return t[bufnr] +  end, +} + +local diagnostic_cache = setmetatable({}, { +  __index = function(t, bufnr) +    assert(bufnr > 0, "Invalid buffer number") +    vim.api.nvim_buf_attach(bufnr, false, { +      on_detach = function() +        rawset(t, bufnr, nil) -- clear cache +      end +    }) +    t[bufnr] = {} +    return t[bufnr] +  end, +}) + +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 = {}  ---@private  local function to_severity(severity) @@ -64,23 +91,22 @@ local function filter_by_severity(severity, diagnostics)  end  ---@private -local function prefix_source(source, diagnostics) -  vim.validate { source = {source, function(v) -    return v == "always" or v == "if_many" -  end, "'always' or 'if_many'" } } - -  if source == "if_many" then -    local sources = {} -    for _, d in pairs(diagnostics) do -      if d.source then -        sources[d.source] = true +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        end      end -    if #vim.tbl_keys(sources) <= 1 then -      return diagnostics -    end    end +  return count +end +---@private +local function prefix_source(diagnostics)    return vim.tbl_map(function(d)      if not d.source then        return d @@ -106,8 +132,6 @@ local function reformat_diagnostics(format, diagnostics)    return formatted  end -local all_namespaces = {} -  ---@private  local function enabled_value(option, namespace)    local ns = namespace and M.get_namespace(namespace) or {} @@ -213,38 +237,13 @@ local function get_bufnr(bufnr)    return bufnr  end --- Metatable that automatically creates an empty table when assigning to a missing key -local bufnr_and_namespace_cacher_mt = { -  __index = function(t, bufnr) -    if not bufnr or bufnr == 0 then -      bufnr = vim.api.nvim_get_current_buf() -    end - -    if rawget(t, bufnr) == nil then -      rawset(t, bufnr, {}) -    end - -    return rawget(t, bufnr) -  end, - -  __newindex = function(t, bufnr, v) -    if not bufnr or bufnr == 0 then -      bufnr = vim.api.nvim_get_current_buf() -    end - -    rawset(t, bufnr, v) -  end, -} - -local diagnostic_cleanup = setmetatable({}, bufnr_and_namespace_cacher_mt) -local diagnostic_cache = setmetatable({}, bufnr_and_namespace_cacher_mt) -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) -  ---@private  local function is_disabled(namespace, bufnr) +  local ns = M.get_namespace(namespace) +  if ns.disabled then +    return true +  end +    if type(diagnostic_disabled[bufnr]) == "table" then      return diagnostic_disabled[bufnr][namespace]    end @@ -272,6 +271,8 @@ end  ---@private  local function set_diagnostic_cache(namespace, bufnr, diagnostics)    for _, diagnostic in ipairs(diagnostics) do +    assert(diagnostic.lnum, "Diagnostic line number is required") +    assert(diagnostic.col, "Diagnostic column is required")      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 @@ -282,11 +283,6 @@ local function set_diagnostic_cache(namespace, bufnr, diagnostics)  end  ---@private -local function clear_diagnostic_cache(namespace, bufnr) -  diagnostic_cache[bufnr][namespace] = nil -end - ----@private  local function restore_extmarks(bufnr, last)    for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do      local extmarks_current = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true}) @@ -302,11 +298,6 @@ local function restore_extmarks(bufnr, last)        if not found[extmark[1]] then          local opts = extmark[4]          opts.id = extmark[1] -        -- HACK: end_row should be end_line -        if opts.end_row then -          opts.end_line = opts.end_row -          opts.end_row = nil -        end          pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)        end      end @@ -372,6 +363,71 @@ local function clear_scheduled_display(namespace, bufnr)  end  ---@private +local function get_diagnostics(bufnr, opts, clamp) +  opts = opts or {} + +  local namespace = opts.namespace +  local diagnostics = {} + +  -- Memoized results of buf_line_count per bufnr +  local buf_line_count = setmetatable({}, { +    __index = function(t, k) +      t[k] = vim.api.nvim_buf_line_count(k) +      return rawget(t, k) +    end, +  }) + +  ---@private +  local function add(b, d) +    if not opts.lnum or d.lnum == opts.lnum then +      if clamp and vim.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) then +          d = vim.deepcopy(d) +          d.lnum = math.max(math.min(d.lnum, line_count), 0) +          d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0) +        end +      end +      table.insert(diagnostics, d) +    end +  end + +  if namespace == nil and bufnr == nil then +    for b, t in pairs(diagnostic_cache) do +      for _, v in pairs(t) do +        for _, diagnostic in pairs(v) do +          add(b, diagnostic) +        end +      end +    end +  elseif namespace == nil then +    bufnr = get_bufnr(bufnr) +    for iter_namespace in pairs(diagnostic_cache[bufnr]) do +      for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do +        add(bufnr, diagnostic) +      end +    end +  elseif bufnr == nil then +    for b, t in pairs(diagnostic_cache) do +      for _, diagnostic in pairs(t[namespace] or {}) do +        add(b, diagnostic) +      end +    end +  else +    bufnr = get_bufnr(bufnr) +    for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do +      add(bufnr, diagnostic) +    end +  end + +  if opts.severity then +    diagnostics = filter_by_severity(opts.severity, diagnostics) +  end + +  return diagnostics +end + +---@private  local function set_list(loclist, opts)    opts = opts or {}    local open = vim.F.if_nil(opts.open, true) @@ -381,7 +437,9 @@ local function set_list(loclist, opts)    if loclist then      bufnr = vim.api.nvim_win_get_buf(winnr)    end -  local diagnostics = M.get(bufnr, opts) +  -- 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)    local items = M.toqflist(diagnostics)    if loclist then      vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items }) @@ -394,27 +452,12 @@ local function set_list(loclist, opts)  end  ---@private ---- To (slightly) improve performance, modifies diagnostics in place. -local function clamp_line_numbers(bufnr, diagnostics) -  local buf_line_count = vim.api.nvim_buf_line_count(bufnr) -  if buf_line_count == 0 then -    return -  end - -  for _, diagnostic in ipairs(diagnostics) do -    diagnostic.lnum = math.max(math.min(diagnostic.lnum, buf_line_count - 1), 0) -    diagnostic.end_lnum = math.max(math.min(diagnostic.end_lnum, buf_line_count - 1), 0) -  end -end - ----@private  local function next_diagnostic(position, search_forward, bufnr, opts, namespace)    position[1] = position[1] - 1    bufnr = get_bufnr(bufnr)    local wrap = vim.F.if_nil(opts.wrap, true)    local line_count = vim.api.nvim_buf_line_count(bufnr) -  local diagnostics = M.get(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace})) -  clamp_line_numbers(bufnr, diagnostics) +  local diagnostics = get_diagnostics(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace}), true)    local line_diagnostics = diagnostic_lines(diagnostics)    for i = 0, line_count do      local offset = i * (search_forward and 1 or -1) @@ -426,13 +469,14 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)        lnum = (lnum + line_count) % line_count      end      if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then +      local line_length = #vim.api.nvim_buf_get_lines(bufnr,  lnum, lnum + 1, true)[1]        local sort_diagnostics, is_next        if search_forward then          sort_diagnostics = function(a, b) return a.col < b.col end -        is_next = function(diagnostic) return diagnostic.col > position[2] end +        is_next = function(d) return math.min(d.col, line_length - 1) > position[2] end        else          sort_diagnostics = function(a, b) return a.col > b.col end -        is_next = function(diagnostic) return diagnostic.col < position[2] end +        is_next = function(d) return math.min(d.col, line_length - 1) < position[2] end        end        table.sort(line_diagnostics[lnum], sort_diagnostics)        if i == 0 then @@ -460,26 +504,28 @@ local function diagnostic_move_pos(opts, pos)      return    end -  -- Save position in the window's jumplist -  vim.api.nvim_win_call(win_id, function() vim.cmd("normal! m'") end) - -  vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) +  vim.api.nvim_win_call(win_id, function() +    -- Save position in the window's jumplist +    vim.cmd("normal! m'") +    vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) +    -- Open folds under the cursor +    vim.cmd("normal! zv") +  end)    if float then      local float_opts = type(float) == "table" and float or {}      vim.schedule(function()        M.open_float( -        vim.api.nvim_win_get_buf(win_id), -        vim.tbl_extend("keep", float_opts, {scope="cursor"}) +        vim.tbl_extend("keep", float_opts, { +          bufnr = vim.api.nvim_win_get_buf(win_id), +          scope = "cursor", +          focus = false, +        })        )      end)    end  end --- }}} - --- Public API {{{ -  --- Configure diagnostic options globally or for a specific diagnostic  --- namespace.  --- @@ -513,8 +559,10 @@ end  ---       - virtual_text: (default true) Use virtual text for diagnostics. Options:  ---                       * severity: Only show virtual text for diagnostics matching the given  ---                       severity |diagnostic-severity| ----                       * source: (string) Include the diagnostic source in virtual ----                       text. One of "always" or "if_many". +---                       * 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.  ---                       * 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: @@ -532,19 +580,7 @@ end  ---                * 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. ----       - float: Options for floating windows: ----                  * severity: See |diagnostic-severity|. ----                  * show_header: (boolean, default true) Show "Diagnostics:" header ----                  * source: (string) Include the diagnostic source in ----                            the message. One of "always" or "if_many". ----                  * 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. ----                  * prefix: (function or string) Prefix each diagnostic in the floating window. If ----                            a function, it must have the signature (diagnostic, i, total) -> string, ----                            where {i} is the index of the diagnostic being evaluated and {total} is ----                            the total number of diagnostics displayed in the window. The returned ----                            string is prepended to each diagnostic in the window. Otherwise, ----                            if {prefix} is a string, it is prepended to each diagnostic. +---       - float: Options for floating windows. See |vim.diagnostic.open_float()|.  ---       - 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 @@ -552,6 +588,7 @@ end  ---                         are displayed before lower severities (e.g. ERROR is displayed before WARN).  ---                         Options:  ---                         * reverse: (boolean) Reverse sort order +---  ---@param namespace number|nil Update the options for the given namespace. When omitted, update the  ---                            global diagnostic options.  function M.config(opts, namespace) @@ -605,33 +642,31 @@ function M.set(namespace, bufnr, diagnostics, opts)      opts = {opts, 't', true},    } +  bufnr = get_bufnr(bufnr) +    if vim.tbl_isempty(diagnostics) then -    clear_diagnostic_cache(namespace, bufnr) +    diagnostic_cache[bufnr][namespace] = nil    else -    if not diagnostic_cleanup[bufnr][namespace] then -      diagnostic_cleanup[bufnr][namespace] = true - -      -- Clean up our data when the buffer unloads. -      vim.api.nvim_buf_attach(bufnr, false, { -        on_detach = function(_, b) -          clear_diagnostic_cache(namespace, b) -          diagnostic_cleanup[b][namespace] = nil -        end -      }) -    end      set_diagnostic_cache(namespace, bufnr, diagnostics)    end    if vim.api.nvim_buf_is_loaded(bufnr) then -    M.show(namespace, bufnr, diagnostics, opts) +    M.show(namespace, bufnr, nil, opts)    end -  vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") +  vim.api.nvim_buf_call(bufnr, function() +    vim.api.nvim_command( +      string.format( +        "doautocmd <nomodeline> DiagnosticChanged %s", +        vim.fn.fnameescape(vim.api.nvim_buf_get_name(bufnr)) +      ) +    ) +  end)  end  --- Get namespace metadata.  --- ----@param ns number Diagnostic namespace +---@param namespace number Diagnostic namespace  ---@return table Namespace metadata  function M.get_namespace(namespace)    vim.validate { namespace = { namespace, 'n' } } @@ -677,49 +712,7 @@ function M.get(bufnr, opts)      opts = { opts, 't', true },    } -  opts = opts or {} - -  local namespace = opts.namespace -  local diagnostics = {} - -  ---@private -  local function add(d) -    if not opts.lnum or d.lnum == opts.lnum then -      table.insert(diagnostics, d) -    end -  end - -  if namespace == nil and bufnr == nil then -    for _, t in pairs(diagnostic_cache) do -      for _, v in pairs(t) do -        for _, diagnostic in pairs(v) do -          add(diagnostic) -        end -      end -    end -  elseif namespace == nil then -    for iter_namespace in pairs(diagnostic_cache[bufnr]) do -      for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do -        add(diagnostic) -      end -    end -  elseif bufnr == nil then -    for _, t in pairs(diagnostic_cache) do -      for _, diagnostic in pairs(t[namespace] or {}) do -        add(diagnostic) -      end -    end -  else -    for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do -      add(diagnostic) -    end -  end - -  if opts.severity then -    diagnostics = filter_by_severity(opts.severity, diagnostics) -  end - -  return diagnostics +  return get_diagnostics(bufnr, opts, false)  end  --- Get the previous diagnostic closest to the cursor position. @@ -795,7 +788,9 @@ end  ---         - severity: See |diagnostic-severity|.  ---         - float: (boolean or table, default true) If "true", call |vim.diagnostic.open_float()|  ---                    after moving. If a table, pass the table as the {opts} parameter to ----                    |vim.diagnostic.open_float()|. +---                    |vim.diagnostic.open_float()|. Unless overridden, the float will show +---                    diagnostics at the new cursor position (as if "cursor" were passed to +---                    the "scope" option).  ---         - win_id: (number, default 0) Window ID  function M.goto_next(opts)    return diagnostic_move_pos( @@ -931,8 +926,11 @@ M.handlers.virtual_text = {        if opts.virtual_text.format then          diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics)        end -      if opts.virtual_text.source then -        diagnostics = prefix_source(opts.virtual_text.source, diagnostics) +      if +        opts.virtual_text.source +        and (opts.virtual_text.source ~= "if_many" or count_sources(bufnr) > 1) +      then +        diagnostics = prefix_source(diagnostics)        end        if opts.virtual_text.severity then          severity = opts.virtual_text.severity @@ -1039,19 +1037,22 @@ end  ---  ---@param namespace number|nil Diagnostic namespace. When omitted, hide  ---                            diagnostics from all namespaces. ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When +---                        omitted, hide diagnostics in all buffers.  function M.hide(namespace, bufnr)    vim.validate {      namespace = { namespace, 'n', true },      bufnr = { bufnr, 'n', true },    } -  bufnr = get_bufnr(bufnr) -  local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[bufnr]) -  for _, iter_namespace in ipairs(namespaces) do -    for _, handler in pairs(M.handlers) do -      if handler.hide then -        handler.hide(iter_namespace, bufnr) +  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        end      end    end @@ -1061,12 +1062,14 @@ end  ---  ---@param namespace number|nil Diagnostic namespace. When omitted, show  ---                            diagnostics from all namespaces. ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When omitted, show +---                        diagnostics in all buffers.  ---@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 ----                             diagnostics. May not be used when {namespace} is nil. +---                             diagnostics. May not be used when {namespace} +---                             or {bufnr} is nil.  ---@param opts table|nil Display options. See |vim.diagnostic.config()|.  function M.show(namespace, bufnr, diagnostics, opts)    vim.validate { @@ -1076,11 +1079,18 @@ function M.show(namespace, bufnr, diagnostics, opts)      opts = { opts, 't', true },    } -  bufnr = get_bufnr(bufnr) -  if not namespace then -    assert(not diagnostics, "Cannot show diagnostics without a namespace") -    for iter_namespace in pairs(diagnostic_cache[bufnr]) do -      M.show(iter_namespace, bufnr, nil, opts) +  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      end      return    end @@ -1091,7 +1101,7 @@ function M.show(namespace, bufnr, diagnostics, opts)    M.hide(namespace, bufnr) -  diagnostics = diagnostics or M.get(bufnr, {namespace=namespace}) +  diagnostics = diagnostics or get_diagnostics(bufnr, {namespace=namespace}, true)    if not diagnostics or vim.tbl_isempty(diagnostics) then      return @@ -1117,8 +1127,6 @@ function M.show(namespace, bufnr, diagnostics, opts)      end    end -  clamp_line_numbers(bufnr, diagnostics) -    for handler_name, handler in pairs(M.handlers) do      if handler.show and opts[handler_name] then        handler.show(namespace, bufnr, diagnostics, opts) @@ -1128,12 +1136,15 @@ end  --- Show diagnostics in a floating window.  --- ----@param bufnr number|nil Buffer number. Defaults to the current buffer.  ---@param opts table|nil Configuration table with the same keys as  ---            |vim.lsp.util.open_floating_preview()| in addition to the following: +---            - bufnr: (number) Buffer number to show diagnostics from. +---                     Defaults to the current buffer.  ---            - namespace: (number) Limit diagnostics to the given namespace ----            - scope: (string, default "buffer") Show diagnostics from the whole buffer ("buffer"), +---            - scope: (string, default "line") Show diagnostics from the whole buffer ("buffer"),  ---                     the current cursor line ("line"), or the current cursor position ("cursor"). +---                     Shorthand versions are also accepted ("c" for "cursor", "l" for "line", "b" +---                     for "buffer").  ---            - 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. @@ -1141,25 +1152,45 @@ end  ---                             from |vim.diagnostic.config()|.  ---            - severity: See |diagnostic-severity|. Overrides the setting from  ---                        |vim.diagnostic.config()|. ----            - show_header: (boolean, default true) Show "Diagnostics:" header. Overrides the ----                           setting from |vim.diagnostic.config()|. ----            - source: (string) Include the diagnostic source in the message. One of "always" or ----                      "if_many". Overrides the setting from |vim.diagnostic.config()|. +---            - 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()|. +---            - 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 +---                      the diagnostic source. Overrides the setting from +---                      |vim.diagnostic.config()|.  ---            - 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.  ---                      Overrides the setting from |vim.diagnostic.config()|. ----            - prefix: (function or string) Prefix each diagnostic in the floating window. +---            - 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.  ---                      Overrides the setting from |vim.diagnostic.config()|.  ---@return tuple ({float_bufnr}, {win_id}) -function M.open_float(bufnr, opts) -  vim.validate { -    bufnr = { bufnr, 'n', true }, -    opts = { opts, 't', true }, -  } +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    opts = opts or {} -  bufnr = get_bufnr(bufnr) -  local scope = opts.scope or "buffer" +  bufnr = get_bufnr(bufnr or opts.bufnr) +  local scope = ({l = "line", c = "cursor", b = "buffer"})[opts.scope] or opts.scope or "line"    local lnum, col    if scope == "line" or scope == "cursor" then      if not opts.pos then @@ -1188,8 +1219,7 @@ function M.open_float(bufnr, opts)      opts = get_resolved_options({ float = float_opts }, nil, bufnr).float    end -  local diagnostics = M.get(bufnr, opts) -  clamp_line_numbers(bufnr, diagnostics) +  local diagnostics = get_diagnostics(bufnr, opts, true)    if scope == "line" then      diagnostics = vim.tbl_filter(function(d) @@ -1220,35 +1250,56 @@ function M.open_float(bufnr, opts)    local lines = {}    local highlights = {} -  local show_header = vim.F.if_nil(opts.show_header, true) -  if show_header then -    table.insert(lines, "Diagnostics:") -    table.insert(highlights, {0, "Bold"}) +  local header = if_nil(opts.header, "Diagnostics:") +  if header then +    vim.validate { header = { header, function(v) +      return type(v) == "string" or type(v) == "table" +    end, "'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]) +        table.insert(highlights, {0, header[2] or "Bold"}) +      end +    elseif #header > 0 then +      table.insert(lines, header) +      table.insert(highlights, {0, "Bold"}) +    end    end    if opts.format then      diagnostics = reformat_diagnostics(opts.format, diagnostics)    end -  if opts.source then -    diagnostics = prefix_source(opts.source, diagnostics) +  if opts.source and (opts.source ~= "if_many" or count_sources(bufnr) > 1) then +    diagnostics = prefix_source(diagnostics)    end    local prefix_opt = if_nil(opts.prefix, (scope == "cursor" and #diagnostics <= 1) and "" or function(_, i)        return string.format("%d. ", i)    end) + +  local prefix, prefix_hl_group    if prefix_opt then      vim.validate { prefix = { prefix_opt, function(v) -      return type(v) == "string" or type(v) == "function" -    end, "'string' or 'function'" } } +      return type(v) == "string" or type(v) == "table" or type(v) == "function" +    end, "'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    end    for i, diagnostic in ipairs(diagnostics) do -    local prefix = type(prefix_opt) == "string" and prefix_opt or prefix_opt(diagnostic, i, #diagnostics) +    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      local hiname = floating_highlight_map[diagnostic.severity]      local message_lines = vim.split(diagnostic.message, '\n')      table.insert(lines, prefix..message_lines[1]) -    table.insert(highlights, {#prefix, hiname}) +    table.insert(highlights, {#prefix, hiname, prefix_hl_group})      for j = 2, #message_lines do        table.insert(lines, string.rep(' ', #prefix) .. message_lines[j])        table.insert(highlights, {0, hiname}) @@ -1261,8 +1312,10 @@ function M.open_float(bufnr, opts)    end    local float_bufnr, winnr = require('vim.lsp.util').open_floating_preview(lines, 'plaintext', opts)    for i, hi in ipairs(highlights) do -    local prefixlen, hiname = unpack(hi) -    -- Start highlight after the prefix +    local prefixlen, hiname, prefix_hiname = unpack(hi) +    if prefix_hiname then +      vim.api.nvim_buf_add_highlight(float_bufnr, -1, prefix_hiname, i-1, 0, prefixlen) +    end      vim.api.nvim_buf_add_highlight(float_bufnr, -1, hiname, i-1, prefixlen, -1)    end @@ -1286,16 +1339,23 @@ function M.reset(namespace, bufnr)      bufnr = {bufnr, 'n', true},    } -  local buffers = bufnr and {bufnr} or vim.tbl_keys(diagnostic_cache) +  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 -      clear_diagnostic_cache(iter_namespace, iter_bufnr) +      diagnostic_cache[iter_bufnr][iter_namespace] = nil        M.hide(iter_namespace, iter_bufnr)      end    end -  vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") +  vim.api.nvim_buf_call(bufnr, function() +    vim.api.nvim_command( +      string.format( +        "doautocmd <nomodeline> DiagnosticChanged %s", +        vim.fn.fnameescape(vim.api.nvim_buf_get_name(bufnr)) +      ) +    ) +  end)  end  --- Add all diagnostics to the quickfix list. @@ -1323,44 +1383,67 @@ end  --- Disable diagnostics in the given buffer.  --- ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When +---                        omitted, disable diagnostics in all buffers.  ---@param namespace number|nil Only disable diagnostics for the given namespace.  function M.disable(bufnr, namespace)    vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } -  bufnr = get_bufnr(bufnr) -  if namespace == nil then -    diagnostic_disabled[bufnr] = true -    for ns in pairs(diagnostic_cache[bufnr]) do -      M.hide(ns, bufnr) +  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      end    else -    if type(diagnostic_disabled[bufnr]) ~= "table" then -      diagnostic_disabled[bufnr] = {} +    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      end -    diagnostic_disabled[bufnr][namespace] = true -    M.hide(namespace, bufnr)    end + +  M.hide(namespace, bufnr)  end  --- Enable diagnostics in the given buffer.  --- ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When +---                        omitted, enable diagnostics in all buffers.  ---@param namespace number|nil Only enable diagnostics for the given namespace.  function M.enable(bufnr, namespace)    vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } -  bufnr = get_bufnr(bufnr) -  if namespace == nil then -    diagnostic_disabled[bufnr] = nil -    for ns in pairs(diagnostic_cache[bufnr]) do -      M.show(ns, bufnr) +  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      end    else -    if type(diagnostic_disabled[bufnr]) ~= "table" then -      return +    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      end -    diagnostic_disabled[bufnr][namespace] = nil -    M.show(namespace, bufnr)    end + +  M.show(namespace, bufnr)  end  --- Parse a diagnostic from a string. @@ -1474,7 +1557,7 @@ function M.fromqflist(list)    for _, item in ipairs(list) do      if item.valid == 1 then        local lnum = math.max(0, item.lnum - 1) -      local col = item.col > 0 and (item.col - 1) or nil +      local col = math.max(0, item.col - 1)        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 @@ -1492,6 +1575,4 @@ function M.fromqflist(list)    return diagnostics  end --- }}} -  return M | 
