Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 30, 2023
4 changes: 3 additions & 1 deletion lua/nvim-tree/actions/moves/item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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
Expand Down
161 changes: 112 additions & 49 deletions lua/nvim-tree/diagnostics.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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 = {}
Expand All @@ -12,6 +11,24 @@ local severity_levels = {
Hint = 4,
}

---@class DiagStatus
---@field value integer|nil
---@field cache_version integer

--- The buffer-severity mappings derived during the last diagnostic list update.
---@type table
local BUFFER_SEVERITY = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good; we have a cache similar to git's.

It'll take me a while to properly review and test this one.


--- The cache version number of the buffer-severity mappings.
---@type integer
local BUFFER_SEVERITY_VERSION = 0

---@param path string
---@return string
local function uniformize_path(path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice name!

return utils.canonical_path(path:gsub("\\", "/"))
end

---@return table
local function from_nvim_lsp()
local buffer_severity = {}
Expand All @@ -25,11 +42,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
Expand All @@ -44,30 +60,41 @@ local function is_severity_in_range(severity, config)
return config.max <= severity and severity <= config.min
end

---@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") then
notify = false
end

if notify then
require("nvim-tree.notify").error("Diagnostics update from coc.nvim failed. " .. vim.inspect(err))
end
end

---@return table
local function from_coc()
if vim.g.coc_service_initialized ~= 1 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 = {}
local buffer_severity = {}
for _, diagnostic in ipairs(diagnostic_list) do
local bufname = diagnostic.file
local bufname = uniformize_path(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
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

Expand All @@ -78,50 +105,86 @@ local function is_using_coc()
return vim.g.coc_service_initialized == 1
end

---@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 = BUFFER_SEVERITY[nodepath]
else
-- dirs should be searched in the list of cached buffer names by prefix
for bufname, severity in pairs(BUFFER_SEVERITY) 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 = BUFFER_SEVERITY_VERSION }
end

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()
BUFFER_SEVERITY = from_coc()
else
buffer_severity = 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
BUFFER_SEVERITY = from_nvim_lsp()
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
BUFFER_SEVERITY_VERSION = BUFFER_SEVERITY_VERSION + 1
if log.enabled("diagnostics") then
for bufname, severity in pairs(BUFFER_SEVERITY) 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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ nit please require once at the start of the file

end
end)
end

---@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 < BUFFER_SEVERITY_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
Expand Down
1 change: 1 addition & 0 deletions lua/nvim-tree/node.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions lua/nvim-tree/renderer/components/diagnostics.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local diagnostics = require "nvim-tree.diagnostics"

local M = {
HS_FILE = {},
Expand All @@ -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
Expand All @@ -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

Expand Down