mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-03 17:24:29 +00:00 
			
		
		
		
	refactor(diagnostic): make display handlers generic (#16137)
Rather than treating virtual_text, signs, and underline specially, introduce the concept of generic "handlers", of which those three are simply the defaults bundled with Nvim. Handlers are called in `vim.diagnostic.show()` and `vim.diagnostic.hide()` and are used to handle how diagnostics are displayed.
This commit is contained in:
		@@ -74,6 +74,100 @@ Functions that take a severity as an optional parameter (e.g.
 | 
			
		||||
 | 
			
		||||
The latter form allows users to specify a range of severities.
 | 
			
		||||
 | 
			
		||||
==============================================================================
 | 
			
		||||
HANDLERS						*diagnostic-handlers*
 | 
			
		||||
 | 
			
		||||
Diagnostics are shown to the user with |vim.diagnostic.show()|. The display of
 | 
			
		||||
diagnostics is managed through handlers. A handler is a table with a "show"
 | 
			
		||||
and (optionally) a "hide" function. The "show" function has the signature
 | 
			
		||||
>
 | 
			
		||||
	function(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
<
 | 
			
		||||
and is responsible for displaying or otherwise handling the given
 | 
			
		||||
diagnostics. The "hide" function takes care of "cleaning up" any actions taken
 | 
			
		||||
by the "show" function and has the signature
 | 
			
		||||
>
 | 
			
		||||
	function(namespace, bufnr)
 | 
			
		||||
<
 | 
			
		||||
Handlers can be configured with |vim.diagnostic.config()| and added by
 | 
			
		||||
creating a new key in `vim.diagnostic.handlers` (see
 | 
			
		||||
|diagnostic-handlers-example|).
 | 
			
		||||
 | 
			
		||||
The {opts} table passed to a handler is the full set of configuration options
 | 
			
		||||
(that is, it is not limited to just the options for the handler itself). The
 | 
			
		||||
values in the table are already resolved (i.e. if a user specifies a
 | 
			
		||||
function for a config option, the function has already been evaluated).
 | 
			
		||||
 | 
			
		||||
Nvim provides these handlers by default: "virtual_text", "signs", and
 | 
			
		||||
"underline".
 | 
			
		||||
 | 
			
		||||
						*diagnostic-handlers-example*
 | 
			
		||||
The example below creates a new handler that notifies the user of diagnostics
 | 
			
		||||
with |vim.notify()|: >
 | 
			
		||||
 | 
			
		||||
	-- It's good practice to namespace custom handlers to avoid collisions
 | 
			
		||||
	vim.diagnostic.handlers["my/notify"] = {
 | 
			
		||||
	  show = function(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
	    -- In our example, the opts table has a "log_level" option
 | 
			
		||||
	    local level = opts["my/notify"].log_level
 | 
			
		||||
 | 
			
		||||
	    local name = vim.diagnostic.get_namespace(namespace).name
 | 
			
		||||
	    local msg = string.format("%d diagnostics in buffer %d from %s",
 | 
			
		||||
	                              #diagnostics,
 | 
			
		||||
	                              bufnr,
 | 
			
		||||
	                              name)
 | 
			
		||||
	    vim.notify(msg, level)
 | 
			
		||||
	  end,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	-- Users can configure the handler
 | 
			
		||||
	vim.diagnostic.config({
 | 
			
		||||
	  ["my/notify"] = {
 | 
			
		||||
	    log_level = vim.log.levels.INFO
 | 
			
		||||
	  }
 | 
			
		||||
	})
 | 
			
		||||
<
 | 
			
		||||
In this example, there is nothing to do when diagnostics are hidden, so we
 | 
			
		||||
omit the "hide" function.
 | 
			
		||||
 | 
			
		||||
Existing handlers can be overriden. For example, use the following to only
 | 
			
		||||
show a sign for the highest severity diagnostic on a given line: >
 | 
			
		||||
 | 
			
		||||
	-- Create a custom namespace. This will aggregate signs from all other
 | 
			
		||||
	-- namespaces and only show the one with the highest severity on a
 | 
			
		||||
	-- given line
 | 
			
		||||
	local ns = vim.api.nvim_create_namespace("my_namespace")
 | 
			
		||||
 | 
			
		||||
	-- Get a reference to the original signs handler
 | 
			
		||||
	local orig_signs_handler = vim.diagnostic.handlers.signs
 | 
			
		||||
 | 
			
		||||
	-- Override the built-in signs handler
 | 
			
		||||
	vim.diagnostic.handlers.signs = {
 | 
			
		||||
	  show = function(_, bufnr, _, opts)
 | 
			
		||||
	    -- Get all diagnostics from the whole buffer rather than just the
 | 
			
		||||
	    -- diagnostics passed to the handler
 | 
			
		||||
	    local diagnostics = vim.diagnostic.get(bufnr)
 | 
			
		||||
 | 
			
		||||
	    -- Find the "worst" diagnostic per line
 | 
			
		||||
	    local max_severity_per_line = {}
 | 
			
		||||
	    for _, d in pairs(diagnostics) do
 | 
			
		||||
	      local m = max_severity_per_line[d.lnum]
 | 
			
		||||
	      if not m or d.severity < m.severity then
 | 
			
		||||
	        max_severity_per_line[d.lnum] = d
 | 
			
		||||
	      end
 | 
			
		||||
	    end
 | 
			
		||||
 | 
			
		||||
	    -- Pass the filtered diagnostics (with our custom namespace) to
 | 
			
		||||
	    -- the original handler
 | 
			
		||||
	    local filtered_diagnostics = vim.tbl_values(max_severity_per_line)
 | 
			
		||||
	    orig_signs_handler.show(ns, bufnr, filtered_diagnostics, opts)
 | 
			
		||||
	  end,
 | 
			
		||||
	  hide = function(_, bufnr)
 | 
			
		||||
	    orig_signs_handler.hide(ns, bufnr)
 | 
			
		||||
	  end,
 | 
			
		||||
	}
 | 
			
		||||
<
 | 
			
		||||
 | 
			
		||||
==============================================================================
 | 
			
		||||
HIGHLIGHTS						*diagnostic-highlights*
 | 
			
		||||
 | 
			
		||||
@@ -202,51 +296,6 @@ Example: >
 | 
			
		||||
	autocmd User DiagnosticsChanged lua vim.diagnostic.setqflist({open = false })
 | 
			
		||||
<
 | 
			
		||||
==============================================================================
 | 
			
		||||
CUSTOMIZATION						*diagnostic-config*
 | 
			
		||||
 | 
			
		||||
If you need more customization over the way diagnostics are displayed than the
 | 
			
		||||
built-in configuration options provide, you can override the display handler
 | 
			
		||||
explicitly. For example, use the following to only show a sign for the highest
 | 
			
		||||
severity diagnostic on a given line: >
 | 
			
		||||
 | 
			
		||||
	-- Disable the default signs handler
 | 
			
		||||
	vim.diagnostic.config({signs = false})
 | 
			
		||||
 | 
			
		||||
	-- Create a namespace. This won't be used to add any diagnostics,
 | 
			
		||||
	-- only to display them.
 | 
			
		||||
	local ns = vim.api.nvim_create_namespace("my_namespace")
 | 
			
		||||
 | 
			
		||||
	-- Create a reference to the original function
 | 
			
		||||
	local orig_show = vim.diagnostic.show
 | 
			
		||||
 | 
			
		||||
	local function set_signs(bufnr)
 | 
			
		||||
	  -- Get all diagnostics from the current buffer
 | 
			
		||||
	  local diagnostics = vim.diagnostic.get(bufnr)
 | 
			
		||||
 | 
			
		||||
	  -- Find the "worst" diagnostic per line
 | 
			
		||||
	  local max_severity_per_line = {}
 | 
			
		||||
	  for _, d in pairs(diagnostics) do
 | 
			
		||||
	    local m = max_severity_per_line[d.lnum]
 | 
			
		||||
	    if not m or d.severity < m.severity then
 | 
			
		||||
	      max_severity_per_line[d.lnum] = d
 | 
			
		||||
	    end
 | 
			
		||||
	  end
 | 
			
		||||
 | 
			
		||||
	  -- Show the filtered diagnostics using the custom namespace. Use the
 | 
			
		||||
	  -- reference to the original function to avoid a loop.
 | 
			
		||||
	  local filtered_diagnostics = vim.tbl_values(max_severity_per_line)
 | 
			
		||||
	  orig_show(ns, bufnr, filtered_diagnostics, {
 | 
			
		||||
	    virtual_text=false,
 | 
			
		||||
	    underline=false,
 | 
			
		||||
	    signs=true
 | 
			
		||||
	  })
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	function vim.diagnostic.show(namespace, bufnr, ...)
 | 
			
		||||
	  orig_show(namespace, bufnr, ...)
 | 
			
		||||
	  set_signs(bufnr)
 | 
			
		||||
	end
 | 
			
		||||
<
 | 
			
		||||
==============================================================================
 | 
			
		||||
Lua module: vim.diagnostic                                    *diagnostic-api*
 | 
			
		||||
 | 
			
		||||
@@ -394,6 +443,15 @@ get({bufnr}, {opts})                                    *vim.diagnostic.get()*
 | 
			
		||||
                Return: ~
 | 
			
		||||
                    table A list of diagnostic items |diagnostic-structure|.
 | 
			
		||||
 | 
			
		||||
get_namespace({namespace})                    *vim.diagnostic.get_namespace()*
 | 
			
		||||
                Get namespace metadata.
 | 
			
		||||
 | 
			
		||||
                Parameters: ~
 | 
			
		||||
                    {ns}  number Diagnostic namespace
 | 
			
		||||
 | 
			
		||||
                Return: ~
 | 
			
		||||
                    table Namespace metadata
 | 
			
		||||
 | 
			
		||||
get_namespaces()                             *vim.diagnostic.get_namespaces()*
 | 
			
		||||
                Get current diagnostic namespaces.
 | 
			
		||||
 | 
			
		||||
@@ -619,7 +677,7 @@ show({namespace}, {bufnr}, {diagnostics}, {opts})
 | 
			
		||||
                Display diagnostics for the given namespace and buffer.
 | 
			
		||||
 | 
			
		||||
                Parameters: ~
 | 
			
		||||
                    {namespace}    number Diagnostic namespace
 | 
			
		||||
                    {namespace}    number Diagnostic namespace.
 | 
			
		||||
                    {bufnr}        number|nil Buffer number. Defaults to the
 | 
			
		||||
                                   current buffer.
 | 
			
		||||
                    {diagnostics}  table|nil The diagnostics to display. When
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,16 @@ local global_diagnostic_options = {
 | 
			
		||||
  severity_sort = false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
M.handlers = setmetatable({}, {
 | 
			
		||||
  __newindex = function(t, name, handler)
 | 
			
		||||
    vim.validate { handler = {handler, "t" } }
 | 
			
		||||
    rawset(t, name, handler)
 | 
			
		||||
    if not global_diagnostic_options[name] then
 | 
			
		||||
      global_diagnostic_options[name] = true
 | 
			
		||||
    end
 | 
			
		||||
  end,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
-- Local functions {{{
 | 
			
		||||
 | 
			
		||||
---@private
 | 
			
		||||
@@ -96,31 +106,9 @@ end
 | 
			
		||||
 | 
			
		||||
local all_namespaces = {}
 | 
			
		||||
 | 
			
		||||
---@private
 | 
			
		||||
local function get_namespace(ns)
 | 
			
		||||
  if not all_namespaces[ns] then
 | 
			
		||||
    local name
 | 
			
		||||
    for k, v in pairs(vim.api.nvim_get_namespaces()) do
 | 
			
		||||
      if ns == v then
 | 
			
		||||
        name = k
 | 
			
		||||
        break
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    assert(name, "namespace does not exist or is anonymous")
 | 
			
		||||
 | 
			
		||||
    all_namespaces[ns] = {
 | 
			
		||||
      name = name,
 | 
			
		||||
      sign_group = string.format("vim.diagnostic.%s", name),
 | 
			
		||||
      opts = {}
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
  return all_namespaces[ns]
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
---@private
 | 
			
		||||
local function enabled_value(option, namespace)
 | 
			
		||||
  local ns = namespace and get_namespace(namespace) or {}
 | 
			
		||||
  local ns = namespace and M.get_namespace(namespace) or {}
 | 
			
		||||
  if ns.opts and type(ns.opts[option]) == "table" then
 | 
			
		||||
    return ns.opts[option]
 | 
			
		||||
  end
 | 
			
		||||
@@ -154,7 +142,7 @@ end
 | 
			
		||||
 | 
			
		||||
---@private
 | 
			
		||||
local function get_resolved_options(opts, namespace, bufnr)
 | 
			
		||||
  local ns = namespace and get_namespace(namespace) or {}
 | 
			
		||||
  local ns = namespace and M.get_namespace(namespace) or {}
 | 
			
		||||
  -- 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)
 | 
			
		||||
  for k in pairs(global_diagnostic_options) do
 | 
			
		||||
@@ -343,7 +331,7 @@ local registered_autocmds = {}
 | 
			
		||||
 | 
			
		||||
---@private
 | 
			
		||||
local function make_augroup_key(namespace, bufnr)
 | 
			
		||||
  local ns = get_namespace(namespace)
 | 
			
		||||
  local ns = M.get_namespace(namespace)
 | 
			
		||||
  return string.format("DiagnosticInsertLeave:%s:%s", bufnr, ns.name)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@@ -566,7 +554,7 @@ function M.config(opts, namespace)
 | 
			
		||||
 | 
			
		||||
  local t
 | 
			
		||||
  if namespace then
 | 
			
		||||
    local ns = get_namespace(namespace)
 | 
			
		||||
    local ns = M.get_namespace(namespace)
 | 
			
		||||
    t = ns.opts
 | 
			
		||||
  else
 | 
			
		||||
    t = global_diagnostic_options
 | 
			
		||||
@@ -633,6 +621,32 @@ function M.set(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
  vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Get namespace metadata.
 | 
			
		||||
---
 | 
			
		||||
---@param ns number Diagnostic namespace
 | 
			
		||||
---@return table Namespace metadata
 | 
			
		||||
function M.get_namespace(namespace)
 | 
			
		||||
  vim.validate { namespace = { namespace, 'n' } }
 | 
			
		||||
  if not all_namespaces[namespace] then
 | 
			
		||||
    local name
 | 
			
		||||
    for k, v in pairs(vim.api.nvim_get_namespaces()) do
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
--- Get current diagnostic namespaces.
 | 
			
		||||
---
 | 
			
		||||
---@return table A list of active diagnostic namespaces |vim.diagnostic|.
 | 
			
		||||
@@ -782,156 +796,167 @@ function M.goto_next(opts)
 | 
			
		||||
  )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Diagnostic Setters {{{
 | 
			
		||||
M.handlers.signs = {
 | 
			
		||||
  show = function(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
    vim.validate {
 | 
			
		||||
      namespace = {namespace, 'n'},
 | 
			
		||||
      bufnr = {bufnr, 'n'},
 | 
			
		||||
      diagnostics = {diagnostics, 't'},
 | 
			
		||||
      opts = {opts, 't', true},
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
--- Set signs for given diagnostics.
 | 
			
		||||
---
 | 
			
		||||
---@param namespace number The diagnostic namespace
 | 
			
		||||
---@param bufnr number Buffer number
 | 
			
		||||
---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the
 | 
			
		||||
---                       current diagnostics in the given buffer are used.
 | 
			
		||||
---@param opts table Configuration table with the following keys:
 | 
			
		||||
---            - priority: Set the priority of the signs |sign-priority|.
 | 
			
		||||
---@private
 | 
			
		||||
function M._set_signs(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
  vim.validate {
 | 
			
		||||
    namespace = {namespace, 'n'},
 | 
			
		||||
    bufnr = {bufnr, 'n'},
 | 
			
		||||
    diagnostics = {diagnostics, 't'},
 | 
			
		||||
    opts = {opts, 't', true},
 | 
			
		||||
  }
 | 
			
		||||
    bufnr = get_bufnr(bufnr)
 | 
			
		||||
 | 
			
		||||
  bufnr = get_bufnr(bufnr)
 | 
			
		||||
  opts = get_resolved_options({ signs = opts }, namespace, bufnr)
 | 
			
		||||
    if opts.signs and opts.signs.severity then
 | 
			
		||||
      diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
  if opts.signs and opts.signs.severity then
 | 
			
		||||
    diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
 | 
			
		||||
  end
 | 
			
		||||
    define_default_signs()
 | 
			
		||||
 | 
			
		||||
  local ns = get_namespace(namespace)
 | 
			
		||||
 | 
			
		||||
  define_default_signs()
 | 
			
		||||
 | 
			
		||||
  -- 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)
 | 
			
		||||
    -- 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
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      get_priority = function(severity)
 | 
			
		||||
        return priority + (vim.diagnostic.severity.HINT - severity)
 | 
			
		||||
      get_priority = function()
 | 
			
		||||
        return priority
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  else
 | 
			
		||||
    get_priority = function()
 | 
			
		||||
      return priority
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  for _, diagnostic in ipairs(diagnostics) do
 | 
			
		||||
    vim.fn.sign_place(
 | 
			
		||||
      0,
 | 
			
		||||
      ns.sign_group,
 | 
			
		||||
      sign_highlight_map[diagnostic.severity],
 | 
			
		||||
      bufnr,
 | 
			
		||||
      {
 | 
			
		||||
        priority = get_priority(diagnostic.severity),
 | 
			
		||||
        lnum = diagnostic.lnum + 1
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Set underline for given diagnostics.
 | 
			
		||||
---
 | 
			
		||||
---@param namespace number The diagnostic namespace
 | 
			
		||||
---@param bufnr number Buffer number
 | 
			
		||||
---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the
 | 
			
		||||
---                       current diagnostics in the given buffer are used.
 | 
			
		||||
---@param opts table Configuration table. Currently unused.
 | 
			
		||||
---@private
 | 
			
		||||
function M._set_underline(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
  vim.validate {
 | 
			
		||||
    namespace = {namespace, 'n'},
 | 
			
		||||
    bufnr = {bufnr, 'n'},
 | 
			
		||||
    diagnostics = {diagnostics, 't'},
 | 
			
		||||
    opts = {opts, 't', true},
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bufnr = get_bufnr(bufnr)
 | 
			
		||||
  opts = get_resolved_options({ underline = opts }, namespace, bufnr).underline
 | 
			
		||||
 | 
			
		||||
  if opts and opts.severity then
 | 
			
		||||
    diagnostics = filter_by_severity(opts.severity, diagnostics)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  for _, diagnostic in ipairs(diagnostics) do
 | 
			
		||||
    local higroup = underline_highlight_map[diagnostic.severity]
 | 
			
		||||
 | 
			
		||||
    if higroup == nil then
 | 
			
		||||
      -- Default to error if we don't have a highlight associated
 | 
			
		||||
      higroup = underline_highlight_map.Error
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
    vim.highlight.range(
 | 
			
		||||
      bufnr,
 | 
			
		||||
      namespace,
 | 
			
		||||
      higroup,
 | 
			
		||||
      { diagnostic.lnum, diagnostic.col },
 | 
			
		||||
      { diagnostic.end_lnum, diagnostic.end_col }
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Set virtual text for given diagnostics.
 | 
			
		||||
---
 | 
			
		||||
---@param namespace number The diagnostic namespace
 | 
			
		||||
---@param bufnr number Buffer number
 | 
			
		||||
---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the
 | 
			
		||||
---                       current diagnostics in the given buffer are used.
 | 
			
		||||
---@param opts table|nil Configuration table with the following keys:
 | 
			
		||||
---            - prefix: (string) Prefix to display before virtual text on line.
 | 
			
		||||
---            - spacing: (number) Number of spaces to insert before virtual text.
 | 
			
		||||
---            - source: (string) Include the diagnostic source in virtual text. One of "always" or
 | 
			
		||||
---                      "if_many".
 | 
			
		||||
---@private
 | 
			
		||||
function M._set_virtual_text(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
  vim.validate {
 | 
			
		||||
    namespace = {namespace, 'n'},
 | 
			
		||||
    bufnr = {bufnr, 'n'},
 | 
			
		||||
    diagnostics = {diagnostics, 't'},
 | 
			
		||||
    opts = {opts, 't', true},
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bufnr = get_bufnr(bufnr)
 | 
			
		||||
  opts = get_resolved_options({ virtual_text = opts }, namespace, bufnr).virtual_text
 | 
			
		||||
 | 
			
		||||
  if opts and opts.format then
 | 
			
		||||
    diagnostics = reformat_diagnostics(opts.format, diagnostics)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if opts and opts.source then
 | 
			
		||||
    diagnostics = prefix_source(opts.source, diagnostics)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  local buffer_line_diagnostics = diagnostic_lines(diagnostics)
 | 
			
		||||
  for line, line_diagnostics in pairs(buffer_line_diagnostics) do
 | 
			
		||||
    if opts and opts.severity then
 | 
			
		||||
      line_diagnostics = filter_by_severity(opts.severity, line_diagnostics)
 | 
			
		||||
    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
 | 
			
		||||
    local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts)
 | 
			
		||||
  end,
 | 
			
		||||
  hide = function(namespace, bufnr)
 | 
			
		||||
    local ns = M.get_namespace(namespace)
 | 
			
		||||
    if ns.user_data.sign_group then
 | 
			
		||||
      vim.fn.sign_unplace(ns.user_data.sign_group, {buffer=bufnr})
 | 
			
		||||
    end
 | 
			
		||||
  end,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    if virt_texts then
 | 
			
		||||
      vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
 | 
			
		||||
        hl_mode = "combine",
 | 
			
		||||
        virt_text = virt_texts,
 | 
			
		||||
      })
 | 
			
		||||
M.handlers.underline = {
 | 
			
		||||
  show = function(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
    vim.validate {
 | 
			
		||||
      namespace = {namespace, 'n'},
 | 
			
		||||
      bufnr = {bufnr, 'n'},
 | 
			
		||||
      diagnostics = {diagnostics, 't'},
 | 
			
		||||
      opts = {opts, 't', true},
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bufnr = get_bufnr(bufnr)
 | 
			
		||||
 | 
			
		||||
    if opts.underline and opts.underline.severity then
 | 
			
		||||
      diagnostics = filter_by_severity(opts.underline.severity, diagnostics)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local ns = M.get_namespace(namespace)
 | 
			
		||||
    if not ns.user_data.underline_ns then
 | 
			
		||||
      ns.user_data.underline_ns = vim.api.nvim_create_namespace("")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local underline_ns = ns.user_data.underline_ns
 | 
			
		||||
    for _, diagnostic in ipairs(diagnostics) do
 | 
			
		||||
      local higroup = underline_highlight_map[diagnostic.severity]
 | 
			
		||||
 | 
			
		||||
      if higroup == nil then
 | 
			
		||||
        -- Default to error if we don't have a highlight associated
 | 
			
		||||
        higroup = underline_highlight_map.Error
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      vim.highlight.range(
 | 
			
		||||
        bufnr,
 | 
			
		||||
        underline_ns,
 | 
			
		||||
        higroup,
 | 
			
		||||
        { diagnostic.lnum, diagnostic.col },
 | 
			
		||||
        { diagnostic.end_lnum, diagnostic.end_col }
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end,
 | 
			
		||||
  hide = function(namespace, bufnr)
 | 
			
		||||
    local ns = M.get_namespace(namespace)
 | 
			
		||||
    if ns.user_data.underline_ns then
 | 
			
		||||
      vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
M.handlers.virtual_text = {
 | 
			
		||||
  show = function(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
    vim.validate {
 | 
			
		||||
      namespace = {namespace, 'n'},
 | 
			
		||||
      bufnr = {bufnr, 'n'},
 | 
			
		||||
      diagnostics = {diagnostics, 't'},
 | 
			
		||||
      opts = {opts, 't', true},
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bufnr = get_bufnr(bufnr)
 | 
			
		||||
 | 
			
		||||
    local severity
 | 
			
		||||
    if opts.virtual_text then
 | 
			
		||||
      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)
 | 
			
		||||
      end
 | 
			
		||||
      if opts.virtual_text.severity then
 | 
			
		||||
        severity = opts.virtual_text.severity
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local ns = M.get_namespace(namespace)
 | 
			
		||||
    if not ns.user_data.virt_text_ns then
 | 
			
		||||
      ns.user_data.virt_text_ns = vim.api.nvim_create_namespace("")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        vim.api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, {
 | 
			
		||||
          hl_mode = "combine",
 | 
			
		||||
          virt_text = virt_texts,
 | 
			
		||||
        })
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end,
 | 
			
		||||
  hide = function(namespace, bufnr)
 | 
			
		||||
    local ns = M.get_namespace(namespace)
 | 
			
		||||
    if ns.user_data.virt_text_ns then
 | 
			
		||||
      vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1)
 | 
			
		||||
    end
 | 
			
		||||
  end,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
--- Get virtual text chunks to display using |nvim_buf_set_extmark()|.
 | 
			
		||||
---
 | 
			
		||||
@@ -1011,19 +1036,16 @@ function M.hide(namespace, bufnr)
 | 
			
		||||
  bufnr = get_bufnr(bufnr)
 | 
			
		||||
  diagnostic_cache_extmarks[bufnr][namespace] = {}
 | 
			
		||||
 | 
			
		||||
  local ns = get_namespace(namespace)
 | 
			
		||||
 | 
			
		||||
  -- clear sign group
 | 
			
		||||
  vim.fn.sign_unplace(ns.sign_group, {buffer=bufnr})
 | 
			
		||||
 | 
			
		||||
  -- clear virtual text namespace
 | 
			
		||||
  vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
 | 
			
		||||
  for _, handler in pairs(M.handlers) do
 | 
			
		||||
    if handler.hide then
 | 
			
		||||
      handler.hide(namespace, bufnr)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--- Display diagnostics for the given namespace and buffer.
 | 
			
		||||
---
 | 
			
		||||
---@param namespace number Diagnostic namespace
 | 
			
		||||
---@param namespace number Diagnostic namespace.
 | 
			
		||||
---@param bufnr number|nil Buffer number. Defaults to the current buffer.
 | 
			
		||||
---@param diagnostics table|nil The diagnostics to display. When omitted, use the
 | 
			
		||||
---                             saved diagnostics for the given namespace and
 | 
			
		||||
@@ -1074,16 +1096,10 @@ function M.show(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
 | 
			
		||||
  clamp_line_numbers(bufnr, diagnostics)
 | 
			
		||||
 | 
			
		||||
  if opts.underline then
 | 
			
		||||
    M._set_underline(namespace, bufnr, diagnostics, opts.underline)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if opts.virtual_text then
 | 
			
		||||
    M._set_virtual_text(namespace, bufnr, diagnostics, opts.virtual_text)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if opts.signs then
 | 
			
		||||
    M._set_signs(namespace, bufnr, diagnostics, opts.signs)
 | 
			
		||||
  for handler_name, handler in pairs(M.handlers) do
 | 
			
		||||
    if handler.show and opts[handler_name] then
 | 
			
		||||
      handler.show(namespace, bufnr, diagnostics, opts)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  save_extmarks(namespace, bufnr)
 | 
			
		||||
 
 | 
			
		||||
@@ -146,7 +146,8 @@ local _client_namespaces = {}
 | 
			
		||||
function M.get_namespace(client_id)
 | 
			
		||||
  vim.validate { client_id = { client_id, 'n' } }
 | 
			
		||||
  if not _client_namespaces[client_id] then
 | 
			
		||||
    local name = string.format("vim.lsp.client-%d", client_id)
 | 
			
		||||
    local client = vim.lsp.get_client_by_id(client_id)
 | 
			
		||||
    local name = string.format("vim.lsp.%s.%d", client.name, client_id)
 | 
			
		||||
    _client_namespaces[client_id] = vim.api.nvim_create_namespace(name)
 | 
			
		||||
  end
 | 
			
		||||
  return _client_namespaces[client_id]
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user