A Neovim plugin for enhanced diagnostic virtual text display, aiming to provide better performance and customization options.
NOTE: This code is currently in the testing phase and may contain bugs. If you encounter any issues, please let me know. I can't found it alone, so please help me to improve it.
vim.diagnostic.enable/disable
commands.You need to set vim.diagnostic.config({ virtual_text = false }), to not have all diagnostics in the buffer displayed conflict. May be in the future we will integrate it with native vim.diagnostic
Add the following to your init.lua
or init.vim
:
-- lazy.nvim
{
'sontungexpt/better-diagnostic-virtual-text',
"LspAttach"
config = function(_)
require('better-diagnostic-virtual-text').setup(opts)
end
}
-- or better ways configure in on_attach of lsp client
-- if use this way don't need to call setup function
{
'sontungexpt/better-diagnostic-virtual-text',
lazy = true,
}
M.on_attach = function(client, bufnr)
-- nil can replace with the options of each buffer
require("better-diagnostic-virtual-text.api").setup_buf(bufnr, {})
--- ... other config for lsp client
end
-- Can be applied to each buffer separately
local default_options = {
ui = {
wrap_line_after = false, -- wrap the line after this length to avoid the virtual text is too long
left_kept_space = 3, --- the number of spaces kept on the left side of the virtual text, make sure it enough to custom for each line
right_kept_space = 3, --- the number of spaces kept on the right side of the virtual text, make sure it enough to custom for each line
arrow = " ",
up_arrow = " ",
down_arrow = " ",
above = false, -- the virtual text will be displayed above the line
},
priority = 2003, -- the priority of virtual text
inline = true,
}
UI will has 4 parts: arrow, left_kept_space, message, right_kept_space orders:
| arrow | left_kept_space | message | right_kept_space |
Override this function before setup the plugin.
--- Format line chunks for virtual text display.
---
--- This function formats the line chunks for virtual text display, considering various options such as severity,
--- underline symbol, text offsets, and parts to be removed.
---
--- @param ui_opts table - The table of UI options. Should contain:
--- - arrow: The symbol used as the left arrow.
--- - up_arrow: The symbol used as the up arrow.
--- - down_arrow: The symbol used as the down arrow.
--- - left_kept_space: The space to keep on the left side.
--- - right_kept_space: The space to keep on the right side.
--- - wrap_line_after: The maximum line length to wrap after.
--- @param line_idx number - The index of the current line (1-based). It start from the cursor line to above or below depend on the above option.
--- @param line_msg string - The message to display on the line.
--- @param severity number - The severity level of the diagnostic (1 = Error, 2 = Warn, 3 = Info, 4 = Hint).
--- @param max_line_length number - The maximum length of the line.
--- @param lasted_line boolean - Whether this is the last line of the diagnostic message. Please check line_idx == 1 to know the first line before checking lasted_line because the first line can be the lasted line if the message has only one line.
--- @param virt_text_offset number - The offset for virtual text positioning.
--- @param should_display_below boolean - Whether to display the virtual text below the line. If above is true, this option will be whether the virtual text should be above
--- @param above_instead boolean - Display above or below
--- @param removed_parts table - A table indicating which parts should be deleted and make room for message (e.g., arrow, left_kept_space, right_kept_space).
--- @param diagnostic table - The diagnostic to display. see `:help vim.Diagnostic.` for more information.
--- @return table - A list of formatted chunks for virtual text display.
--- @see vim.api.nvim_buf_set_extmark
function M.format_line_chunks(
ui_opts,
line_idx,
line_msg,
severity,
max_line_length,
lasted_line,
virt_text_offset,
should_display_below,
above_instead,
removed_parts,
diagnostic
)
local chunks = {}
local first_line = line_idx == 1
local severity_suffix = SEVERITY_SUFFIXS[severity]
local function hls(extend_hl_groups)
local default_groups = {
"DiagnosticVirtualText" .. severity_suffix,
"BetterDiagnosticVirtualText" .. severity_suffix,
}
if extend_hl_groups then
for i, hl in ipairs(extend_hl_groups) do
default_groups[2 + i] = hl
end
end
return default_groups
end
local message_highlight = hls()
if should_display_below then
local arrow_symbol = (above_instead and ui_opts.down_arrow or ui_opts.up_arrow):match("^%s*(.*)")
local space_offset = space(virt_text_offset)
if first_line then
if not removed_parts.arrow then
tbl_insert(chunks, {
space_offset .. arrow_symbol,
hls({ "BetterDiagnosticVirtualTextArrow", "BetterDiagnosticVirtualTextArrow" .. severity_suffix }),
})
end
else
tbl_insert(chunks, {
space_offset .. space(strdisplaywidth(arrow_symbol)),
message_highlight,
})
end
else
local arrow_symbol = ui_opts.arrow
if first_line then
if not removed_parts.arrow then
tbl_insert(chunks, {
arrow_symbol,
hls({ "BetterDiagnosticVirtualTextArrow", "BetterDiagnosticVirtualTextArrow" .. severity_suffix }),
})
end
else
tbl_insert(chunks, {
space(virt_text_offset + strdisplaywidth(arrow_symbol)),
message_highlight,
})
end
end
if not removed_parts.left_kept_space then
local tree_symbol = " "
if first_line then
if not lasted_line then
tree_symbol = above_instead and " └ " or " ┌ "
end
elseif lasted_line then
tree_symbol = above_instead and " ┌ " or " └ "
else
tree_symbol = " │ "
end
tbl_insert(chunks, {
tree_symbol,
hls({ "BetterDiagnosticVirtualTextTree", "BetterDiagnosticVirtualTextTree" .. severity_suffix }),
})
end
tbl_insert(chunks, {
line_msg,
message_highlight,
})
if not removed_parts.right_kept_space then
local last_space = space(max_line_length - strdisplaywidth(line_msg) + ui_opts.right_kept_space)
tbl_insert(chunks, { last_space, message_highlight })
end
return chunks
end
You can enable and disable the plugin using the following commands:
vim.diagnostic.enable(true, { bufnr = vim.api.nvim_get_current_buf() }) -- Enable the plugin for the current buffer.
vim.diagnostic.enable(false, { bufnr = vim.api.nvim_get_current_buf() }) -- Disable the plugin for the current buffer.
The default highlight names for each severity level are:
DiagnosticVirtualTextError
DiagnosticVirtualTextWarn
DiagnosticVirtualTextInfo
DiagnosticVirtualTextHint
You can override the default highlight names with:
BetterDiagnosticVirtualTextError
BetterDiagnosticVirtualTextWarn
BetterDiagnosticVirtualTextInfo
BetterDiagnosticVirtualTextHint
For the arrow highlights, use:
BetterDiagnosticVirtualTextArrow
for all severity levels.BetterDiagnosticVirtualTextArrowError
BetterDiagnosticVirtualTextArrowWarn
BetterDiagnosticVirtualTextArrowInfo
BetterDiagnosticVirtualTextArrowHint
For the tree highlights, use:
BetterDiagnosticVirtualTextTree
for all severity levels.BetterDiagnosticVirtualTextTreeError
BetterDiagnosticVirtualTextTreeWarn
BetterDiagnosticVirtualTextTreeInfo
BetterDiagnosticVirtualTextTreeHint
Replace M
with the require("better-diagnostic-virtual-text.api")
.
NOTE : I was too lazy to write the complete API documentation, so I used ChatGPT to generate it. If there are any inaccuracies, please refer to the source for verification.
M.inspect_cache()
M.foreach_line(bufnr, callback)
Iterates through each line of diagnostics in a specified buffer and invokes a callback function for each line. Ensures compatibility with Lua versions older than 5.2 by using the default pairs
function directly, or with a custom pairs
function that handles diagnostic metadata.
local meta_pairs = function(t)
local metatable = getmetatable(t)
if metatable and metatable.__pairs then
return metatable.__pairs(t)
end
return pairs(t)
end
usage:
require("better-diagnostic-virtual-text.api").foreach_line(bufnr, function(line, diagnostics)
for _, diagnostic in meta_pairs(diagnostics) do
print(diagnostic.message)
end
end)
M.clear_extmark_cache(bufnr)
Clears the diagnostics extmarks for a buffer.
bufnr
(integer): The buffer number to clear the diagnostics for.M.update_diagnostics_cache(bufnr, line, diagnostic)
bufnr
(integer
): The buffer number.line
(integer
): The line number.diagnostic
(table
): The new diagnostic to track or list of diagnostics to update.M.fetch_diagnostics(bufnr, line, recompute, comparator, finish_soon)
Description: Retrieves diagnostics at a specific line in the specified buffer.
Parameters:
bufnr
(integer
): The buffer number.line
(integer
): The line number.recompute
(boolean|nil
): Whether to recompute the diagnostics.comparator
(function|nil
): The comparator function to sort the diagnostics. If not provided, the diagnostics are not sorted.finish_soon
(boolean|function|nil
): If true, stops processing sort when a finish_soon(d) return true or finish_soon is boolean and severity 1 diagnostic is found. When stop immediately the return value is the list with only found diagnostic. This parameter only work if comparator
is provided or `recompute`` = false
.Returns:
table
: List of diagnostics sorted by severity.integer
: Number of diagnostics.Note: if finish_soon == true, the list will only has one diagnostic fit the condition.
M.fetch_cursor_diagnostics(bufnr, current_line, current_col, recompute, comparator, finish_soon)
Description: Retrieves diagnostics at the cursor position in the specified buffer.
Parameters:
bufnr
(integer
): The buffer number.current_line
(integer
): Optional. The current line number. Defaults to cursor line.current_col
(integer
): Optional. The current column number. Defaults to cursor column.recompute
(boolean
): Optional. Whether to recompute diagnostics or use cached diagnostics. Defaults to false.comparator
(function|nil
): The comparator function to sort the diagnostics. If not provided, the diagnostics are not sorted.finish_soon
(boolean|function|nil
): If true, stops processing sort when a finish_soon(d) return true or finish_soon is boolean and severity 1 diagnostic is found under cursor. When stop immediately the return value is the list with only found diagnostic. This parameter only work if comparator
is provided or `recompute`` = falseReturns:
table
: Diagnostics at the cursor position sorted by severity.integer
: Number of diagnostics at the cursor position.table
: Full list of diagnostics for the line sorted by severity.integer
: Number of diagnostics in the line sorted by severity.Note: if finish_soon == true, the list will only has one diagnostic fit the condition.
M.fetch_top_cursor_diagnostic(bufnr, current_line, current_col, recompute)
bufnr
(integer
): The buffer number.current_line
(integer
): Optional. The current line number. Defaults to cursor line.current_col
(integer
): Optional. The current column number. Defaults to cursor column.recompute
(boolean
): Optional. Whether to recompute diagnostics or use cached diagnostics. Defaults to false.table
: Diagnostic at the cursor position.table
: Full list of diagnostics for the line.integer
: Number of diagnostics in the list.M.format_line_chunks(ui_opts, line_idx, line_msg, severity, max_line_length, lasted_line, virt_text_offset, should_display_below, removed_parts, diagnostic)
ui_opts
(table
): Table of UI options.line_idx
(number
): Index of the current line (1-based).line_msg
(string
): Message to display on the line.severity
(number
): Severity level of the diagnostic.max_line_length
(number
): Maximum length of the line.lasted_line
(boolean
): Whether this is the last line of the diagnostic message.virt_text_offset
(number
): Offset for virtual text positioning.should_display_below
(boolean
): Whether to display virtual text below the line.removed_parts
(table
): Table indicating parts to delete to make room for message.diagnostic
(table
): The diagnostic to display.table
: List of formatted chunks for virtual text display.M.exists_any_diagnostics(bufnr, line)
Checks if diagnostics exist for a buffer at a line.
Parameters:
bufnr
(integer): The buffer number to check.line
(integer): The line number to check.Returns:
exists
(boolean): True if diagnostics exist, false otherwise.M.clean_diagnostics(bufnr, lines_or_diagnostic)
Cleans diagnostics for a buffer.
Parameters:
bufnr
(integer): The buffer number.lines_or_diagnostic
(number|table): Specifies the lines or diagnostic to clean.Returns:
cleared
(boolean): True if any diagnostics were cleared, false otherwise.M.show_diagnostic(opts, bufnr, diagnostic, clean_opts)
Displays a diagnostic for a buffer, optionally cleaning existing diagnostics before showing the new one.
Parameters:
opts
(table): Options for displaying the diagnostic.bufnr
(integer): The buffer number.diagnostic
(table): The diagnostic to show.clean_opts
(number|table|nil): Options for cleaning diagnostics before showing the new one.recompute_ui
(boolean|nil) Whether to recompute the diagnostics. Defaults to false.Returns:
shown_line
(integer): The start line of the diagnostic where it was shown.diagnostic
(table): The diagnostic that was shown.M.show_top_severity_diagnostic(opts, bufnr, current_line, recompute, clean_opts)
Shows the highest severity diagnostic at the line for a buffer.
Parameters:
opts
(table): Options for displaying the diagnostic.bufnr
(integer): The buffer number.current_line
(integer): The current line number.recompute
(boolean): Whether to recompute the diagnostics.clean_opts
(number|table): Options for cleaning diagnostics before showing the new one.recompute_ui
(boolean|nil) Whether to recompute the diagnostics. Defaults to false.Returns:
line_number
(integer): The line number where the diagnostic was shown.diagnostic
(table): The diagnostic that was shown.diagnostics_list
(table): The list of diagnostics at the line.size
(integer): The size of the diagnostics list.M.show_cursor_diagnostic(opts, bufnr, current_line, current_col, recompute, clean_opts)
Shows the highest severity diagnostic at the cursor position in a buffer.
Parameters:
opts
(table): Options for displaying the diagnostic.bufnr
(integer): The buffer number.current_line
(integer): The current line number.current_col
(integer): The current column number.recompute
(boolean): Whether to recompute the diagnostics.clean_opts
(number|table): Options for cleaning diagnostics before showing the new one.recompute_ui
(boolean|nil) Whether to recompute the diagnostics. Defaults to false.Returns:
line_number
(integer): The line number where the diagnostic was shown.diagnostic
(table): The diagnostic that was shown.diagnostics_list
(table): The list of diagnostics at the cursor position.size
(integer): The size of the diagnostics list.M.get_shown_line_num(diagnostic)
Returns the line number where the diagnostic was shown.
Parameters:
diagnostic
(table): The diagnostic.Returns:
line_shown
(integer): The line number where the diagnostic was shown.M.when_enabled(bufnr, callback)
Invokes a callback function when the plugin is enabled for a buffer.
Parameters:
bufnr
(integer): The buffer number.callback
(function): The callback function to invoke.M.setup_buf(bufnr, opts)
Sets up the buffer to handle diagnostic rendering and interaction.
bufnr
(integer): The buffer number.opts
(table): Options for setting up the buffer.M.setup(opts)
Sets up the module to handle diagnostic rendering and interaction globally.
opts
(table): Options for setting up the module.MIT License