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
196 changes: 139 additions & 57 deletions lua/nvim-tree/diagnostics.lua
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

@alex-courtis alex-courtis Dec 30, 2023

Choose a reason for hiding this comment

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

That one's a work in progress #2546

---@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)
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

---Marshal severities from LSP. Does nothing when LSP disabled.
---@return NodeSeverities
local function from_nvim_lsp()
local buffer_severity = {}

Expand All @@ -25,103 +51,159 @@ 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

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
local function from_coc()
if vim.g.coc_service_initialized ~= 1 then
return {}
---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

local diagnostic_list = vim.fn.CocAction "diagnosticList"
if type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then
return {}
if notify then
require("nvim-tree.notify").error("Diagnostics update from coc.nvim failed. " .. vim.inspect(err))
end
end

local diagnostics = {}
for _, diagnostic in ipairs(diagnostic_list) do
local bufname = diagnostic.file
local coc_severity = severity_levels[diagnostic.severity]
---COC service initialized
---@return boolean
local function is_using_coc()
return vim.g.coc_service_initialized == 1
end

local serverity = diagnostics[bufname] or vim.diagnostic.severity.HINT
diagnostics[bufname] = math.min(coc_severity, serverity)
---Marshal severities from COC. Does nothing when COC service not started.
---@return NodeSeverities
local function from_coc()
if not is_using_coc() then
return {}
end

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 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()
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
NODE_SEVERITIES = 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
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()
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

---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
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