fix(#2519): Diagnostics Not Updated When Tree Not Visible (#2597)

* fix(#2519): diagnostics overhaul

Signed-off-by: iusmac <iusico.maxim@libero.it>

* fix: Properly filter diagnostics from coc

Also, while we're at it, refactor the lsp function for consistency.
There should be no functional change, just cosmetic.

Signed-off-by: iusmac <iusico.maxim@libero.it>

* Assign diagnostic version per node to reduce overhead

Signed-off-by: iusmac <iusico.maxim@libero.it>

* Require renderer once

Signed-off-by: iusmac <iusico.maxim@libero.it>

* Revert "Require renderer once"

Causes circular requires after the previous commit.

This reverts commit 7413041630.

* Rename `buffer_severity_dict` to `BUFFER_SEVERITY`

Signed-off-by: iusmac <iusico.maxim@libero.it>

* Log diagnostics update properly

Signed-off-by: iusmac <iusico.maxim@libero.it>

* Implement error handling for coc.nvim

Signed-off-by: iusmac <iusico.maxim@libero.it>

* CI style fixes

Signed-off-by: iusmac <iusico.maxim@libero.it>

* Capture `Keyboard interrupt` when handling coc exceptions

Signed-off-by: iusmac <iusico.maxim@libero.it>

* add more doc

---------

Signed-off-by: iusmac <iusico.maxim@libero.it>
Co-authored-by: Alexander Courtis <alex@courtis.org>
This commit is contained in:
Max 2023-12-30 04:30:07 +01:00 committed by GitHub
parent 50f30bcd8c
commit 96a783fbd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 149 additions and 61 deletions

View File

@ -3,6 +3,7 @@ local view = require "nvim-tree.view"
local core = require "nvim-tree.core"
local lib = require "nvim-tree.lib"
local explorer_node = require "nvim-tree.explorer.node"
local diagnostics = require "nvim-tree.diagnostics"
local M = {}
@ -33,7 +34,8 @@ function M.fn(opts)
local git_status = explorer_node.get_git_status(node)
valid = git_status ~= nil and (not opts.skip_gitignored or git_status[1] ~= "!!")
elseif opts.what == "diag" then
valid = node.diag_status ~= nil
local diag_status = diagnostics.get_diag_status(node)
valid = diag_status ~= nil and diag_status.value ~= nil
elseif opts.what == "opened" then
valid = vim.fn.bufloaded(node.absolute_path) ~= 0
end

View File

@ -1,18 +1,44 @@
local utils = require "nvim-tree.utils"
local view = require "nvim-tree.view"
local core = require "nvim-tree.core"
local log = require "nvim-tree.log"
local M = {}
local severity_levels = {
---TODO add "$VIMRUNTIME" to "workspace.library" and use the @enum instead of this integer
---@alias lsp.DiagnosticSeverity integer
---COC severity level strings to LSP severity levels
---@enum COC_SEVERITY_LEVELS
local COC_SEVERITY_LEVELS = {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4,
}
---@return table
---Absolute Node path to LSP severity level
---@alias NodeSeverities table<string, lsp.DiagnosticSeverity>
---@class DiagStatus
---@field value lsp.DiagnosticSeverity|nil
---@field cache_version integer
--- The buffer-severity mappings derived during the last diagnostic list update.
---@type NodeSeverities
local NODE_SEVERITIES = {}
---The cache version number of the buffer-severity mappings.
---@type integer
local NODE_SEVERITIES_VERSION = 0
---@param path string
---@return string
local function uniformize_path(path)
return utils.canonical_path(path:gsub("\\", "/"))
end
---Marshal severities from LSP. Does nothing when LSP disabled.
---@return NodeSeverities
local function from_nvim_lsp()
local buffer_severity = {}
@ -25,11 +51,10 @@ local function from_nvim_lsp()
for _, diagnostic in ipairs(vim.diagnostic.get(nil, { severity = M.severity })) do
local buf = diagnostic.bufnr
if vim.api.nvim_buf_is_valid(buf) then
local bufname = vim.api.nvim_buf_get_name(buf)
local lowest_severity = buffer_severity[bufname]
if not lowest_severity or diagnostic.severity < lowest_severity then
buffer_severity[bufname] = diagnostic.severity
end
local bufname = uniformize_path(vim.api.nvim_buf_get_name(buf))
local severity = diagnostic.severity
local highest_severity = buffer_severity[bufname] or severity
buffer_severity[bufname] = math.min(highest_severity, severity)
end
end
end
@ -37,91 +62,148 @@ local function from_nvim_lsp()
return buffer_severity
end
---@param severity integer
---Severity is within diagnostics.severity.min, diagnostics.severity.max
---@param severity lsp.DiagnosticSeverity
---@param config table
---@return boolean
local function is_severity_in_range(severity, config)
return config.max <= severity and severity <= config.min
end
---@return table
---Handle any COC exceptions, preventing any propagation
---@param err string
local function handle_coc_exception(err)
log.line("diagnostics", "handle_coc_exception: %s", vim.inspect(err))
local notify = true
-- avoid distractions on interrupts (CTRL-C)
if err:find "Vim:Interrupt" or err:find "Keyboard interrupt" then
notify = false
end
if notify then
require("nvim-tree.notify").error("Diagnostics update from coc.nvim failed. " .. vim.inspect(err))
end
end
---COC service initialized
---@return boolean
local function is_using_coc()
return vim.g.coc_service_initialized == 1
end
---Marshal severities from COC. Does nothing when COC service not started.
---@return NodeSeverities
local function from_coc()
if vim.g.coc_service_initialized ~= 1 then
if not is_using_coc() then
return {}
end
local diagnostic_list = vim.fn.CocAction "diagnosticList"
if type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then
local ok, diagnostic_list = xpcall(function()
return vim.fn.CocAction "diagnosticList"
end, handle_coc_exception)
if not ok or type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then
return {}
end
local diagnostics = {}
for _, diagnostic in ipairs(diagnostic_list) do
local bufname = diagnostic.file
local coc_severity = severity_levels[diagnostic.severity]
local serverity = diagnostics[bufname] or vim.diagnostic.severity.HINT
diagnostics[bufname] = math.min(coc_severity, serverity)
end
local buffer_severity = {}
for bufname, severity in pairs(diagnostics) do
if is_severity_in_range(severity, M.severity) then
buffer_severity[bufname] = severity
for _, diagnostic in ipairs(diagnostic_list) do
local bufname = uniformize_path(diagnostic.file)
local coc_severity = COC_SEVERITY_LEVELS[diagnostic.severity]
local highest_severity = buffer_severity[bufname] or coc_severity
if is_severity_in_range(highest_severity, M.severity) then
buffer_severity[bufname] = math.min(highest_severity, coc_severity)
end
end
return buffer_severity
end
local function is_using_coc()
return vim.g.coc_service_initialized == 1
---Maybe retrieve severity level from the cache
---@param node Node
---@return DiagStatus
local function from_cache(node)
local nodepath = uniformize_path(node.absolute_path)
local max_severity = nil
if not node.nodes then
-- direct cache hit for files
max_severity = NODE_SEVERITIES[nodepath]
else
-- dirs should be searched in the list of cached buffer names by prefix
for bufname, severity in pairs(NODE_SEVERITIES) do
local node_contains_buf = vim.startswith(bufname, nodepath .. "/")
if node_contains_buf then
if severity == M.severity.max then
max_severity = severity
break
else
max_severity = math.min(max_severity or severity, severity)
end
end
end
end
return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION }
end
---Fired on DiagnosticChanged and CocDiagnosticChanged events:
---debounced retrieval, cache update, version increment and draw
function M.update()
if not M.enable or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then
if not M.enable then
return
end
utils.debounce("diagnostics", M.debounce_delay, function()
local profile = log.profile_start "diagnostics update"
log.line("diagnostics", "update")
local buffer_severity
if is_using_coc() then
buffer_severity = from_coc()
NODE_SEVERITIES = from_coc()
else
buffer_severity = from_nvim_lsp()
NODE_SEVERITIES = from_nvim_lsp()
end
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())
for _, node in pairs(nodes_by_line) do
node.diag_status = nil
end
for bufname, severity in pairs(buffer_severity) do
local bufpath = utils.canonical_path(bufname)
log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity)
if 0 < severity and severity < 5 then
for line, node in pairs(nodes_by_line) do
local nodepath = utils.canonical_path(node.absolute_path)
log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath)
local node_contains_buf = vim.startswith(bufpath:gsub("\\", "/"), nodepath:gsub("\\", "/") .. "/")
if M.show_on_dirs and node_contains_buf and (not node.open or M.show_on_open_dirs) then
log.line("diagnostics", " matched fold node '%s'", node.absolute_path)
node.diag_status = severity
elseif nodepath == bufpath then
log.line("diagnostics", " matched file node '%s'", node.absolute_path)
node.diag_status = severity
end
end
NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1
if log.enabled "diagnostics" then
for bufname, severity in pairs(NODE_SEVERITIES) do
log.line("diagnostics", "Indexing bufname '%s' with severity %d", bufname, severity)
end
end
log.profile_end(profile)
require("nvim-tree.renderer").draw()
if view.is_buf_valid(view.get_bufnr()) then
require("nvim-tree.renderer").draw()
end
end)
end
---Maybe retrieve diagnostic status for a node.
---Returns cached value when node's version matches.
---@param node Node
---@return DiagStatus|nil
function M.get_diag_status(node)
if not M.enable then
return nil
end
-- dir but we shouldn't show on dirs at all
if node.nodes ~= nil and not M.show_on_dirs then
return nil
end
-- here, we do a lazy update of the diagnostic status carried by the node.
-- This is by design, as diagnostics and nodes live in completely separate
-- worlds, and this module is the link between the two
if not node.diag_status or node.diag_status.cache_version < NODE_SEVERITIES_VERSION then
node.diag_status = from_cache(node)
end
-- file
if not node.nodes then
return node.diag_status
end
-- dir is closed or we should show on open_dirs
if not node.open or M.show_on_open_dirs then
return node.diag_status
end
return nil
end
function M.setup(opts)
M.enable = opts.diagnostics.enable
M.debounce_delay = opts.diagnostics.debounce_delay

View File

@ -17,6 +17,7 @@
---@field parent DirNode
---@field type string
---@field watcher function|nil
---@field diag_status DiagStatus|nil
---@class DirNode: BaseNode
---@field has_children boolean

View File

@ -1,4 +1,5 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local diagnostics = require "nvim-tree.diagnostics"
local M = {
HS_FILE = {},
@ -17,10 +18,11 @@ function M.get_highlight(node)
end
local group
local diag_status = diagnostics.get_diag_status(node)
if node.nodes then
group = M.HS_FOLDER[node.diag_status]
group = M.HS_FOLDER[diag_status and diag_status.value]
else
group = M.HS_FILE[node.diag_status]
group = M.HS_FILE[diag_status and diag_status.value]
end
if group then
@ -35,7 +37,8 @@ end
---@return HighlightedString|nil modified icon
function M.get_icon(node)
if node and M.config.diagnostics.enable and M.config.renderer.icons.show.diagnostics then
return M.ICON[node.diag_status]
local diag_status = diagnostics.get_diag_status(node)
return M.ICON[diag_status and diag_status.value]
end
end