Skip to content

Commit 96a783f

Browse files
iusmacalex-courtis
andauthored
fix(nvim-tree#2519): Diagnostics Not Updated When Tree Not Visible (nvim-tree#2597)
* fix(nvim-tree#2519): diagnostics overhaul Signed-off-by: iusmac <[email protected]> * 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 <[email protected]> * Assign diagnostic version per node to reduce overhead Signed-off-by: iusmac <[email protected]> * Require renderer once Signed-off-by: iusmac <[email protected]> * Revert "Require renderer once" Causes circular requires after the previous commit. This reverts commit 7413041. * Rename `buffer_severity_dict` to `BUFFER_SEVERITY` Signed-off-by: iusmac <[email protected]> * Log diagnostics update properly Signed-off-by: iusmac <[email protected]> * Implement error handling for coc.nvim Signed-off-by: iusmac <[email protected]> * CI style fixes Signed-off-by: iusmac <[email protected]> * Capture `Keyboard interrupt` when handling coc exceptions Signed-off-by: iusmac <[email protected]> * add more doc --------- Signed-off-by: iusmac <[email protected]> Co-authored-by: Alexander Courtis <[email protected]>
1 parent 50f30bc commit 96a783f

File tree

4 files changed

+149
-61
lines changed

4 files changed

+149
-61
lines changed

lua/nvim-tree/actions/moves/item.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local view = require "nvim-tree.view"
33
local core = require "nvim-tree.core"
44
local lib = require "nvim-tree.lib"
55
local explorer_node = require "nvim-tree.explorer.node"
6+
local diagnostics = require "nvim-tree.diagnostics"
67

78
local M = {}
89

@@ -33,7 +34,8 @@ function M.fn(opts)
3334
local git_status = explorer_node.get_git_status(node)
3435
valid = git_status ~= nil and (not opts.skip_gitignored or git_status[1] ~= "!!")
3536
elseif opts.what == "diag" then
36-
valid = node.diag_status ~= nil
37+
local diag_status = diagnostics.get_diag_status(node)
38+
valid = diag_status ~= nil and diag_status.value ~= nil
3739
elseif opts.what == "opened" then
3840
valid = vim.fn.bufloaded(node.absolute_path) ~= 0
3941
end

lua/nvim-tree/diagnostics.lua

Lines changed: 139 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
11
local utils = require "nvim-tree.utils"
22
local view = require "nvim-tree.view"
3-
local core = require "nvim-tree.core"
43
local log = require "nvim-tree.log"
54

65
local M = {}
76

8-
local severity_levels = {
7+
---TODO add "$VIMRUNTIME" to "workspace.library" and use the @enum instead of this integer
8+
---@alias lsp.DiagnosticSeverity integer
9+
10+
---COC severity level strings to LSP severity levels
11+
---@enum COC_SEVERITY_LEVELS
12+
local COC_SEVERITY_LEVELS = {
913
Error = 1,
1014
Warning = 2,
1115
Information = 3,
1216
Hint = 4,
1317
}
1418

15-
---@return table
19+
---Absolute Node path to LSP severity level
20+
---@alias NodeSeverities table<string, lsp.DiagnosticSeverity>
21+
22+
---@class DiagStatus
23+
---@field value lsp.DiagnosticSeverity|nil
24+
---@field cache_version integer
25+
26+
--- The buffer-severity mappings derived during the last diagnostic list update.
27+
---@type NodeSeverities
28+
local NODE_SEVERITIES = {}
29+
30+
---The cache version number of the buffer-severity mappings.
31+
---@type integer
32+
local NODE_SEVERITIES_VERSION = 0
33+
34+
---@param path string
35+
---@return string
36+
local function uniformize_path(path)
37+
return utils.canonical_path(path:gsub("\\", "/"))
38+
end
39+
40+
---Marshal severities from LSP. Does nothing when LSP disabled.
41+
---@return NodeSeverities
1642
local function from_nvim_lsp()
1743
local buffer_severity = {}
1844

@@ -25,103 +51,159 @@ local function from_nvim_lsp()
2551
for _, diagnostic in ipairs(vim.diagnostic.get(nil, { severity = M.severity })) do
2652
local buf = diagnostic.bufnr
2753
if vim.api.nvim_buf_is_valid(buf) then
28-
local bufname = vim.api.nvim_buf_get_name(buf)
29-
local lowest_severity = buffer_severity[bufname]
30-
if not lowest_severity or diagnostic.severity < lowest_severity then
31-
buffer_severity[bufname] = diagnostic.severity
32-
end
54+
local bufname = uniformize_path(vim.api.nvim_buf_get_name(buf))
55+
local severity = diagnostic.severity
56+
local highest_severity = buffer_severity[bufname] or severity
57+
buffer_severity[bufname] = math.min(highest_severity, severity)
3358
end
3459
end
3560
end
3661

3762
return buffer_severity
3863
end
3964

40-
---@param severity integer
65+
---Severity is within diagnostics.severity.min, diagnostics.severity.max
66+
---@param severity lsp.DiagnosticSeverity
4167
---@param config table
4268
---@return boolean
4369
local function is_severity_in_range(severity, config)
4470
return config.max <= severity and severity <= config.min
4571
end
4672

47-
---@return table
48-
local function from_coc()
49-
if vim.g.coc_service_initialized ~= 1 then
50-
return {}
73+
---Handle any COC exceptions, preventing any propagation
74+
---@param err string
75+
local function handle_coc_exception(err)
76+
log.line("diagnostics", "handle_coc_exception: %s", vim.inspect(err))
77+
local notify = true
78+
79+
-- avoid distractions on interrupts (CTRL-C)
80+
if err:find "Vim:Interrupt" or err:find "Keyboard interrupt" then
81+
notify = false
5182
end
5283

53-
local diagnostic_list = vim.fn.CocAction "diagnosticList"
54-
if type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then
55-
return {}
84+
if notify then
85+
require("nvim-tree.notify").error("Diagnostics update from coc.nvim failed. " .. vim.inspect(err))
5686
end
87+
end
5788

58-
local diagnostics = {}
59-
for _, diagnostic in ipairs(diagnostic_list) do
60-
local bufname = diagnostic.file
61-
local coc_severity = severity_levels[diagnostic.severity]
89+
---COC service initialized
90+
---@return boolean
91+
local function is_using_coc()
92+
return vim.g.coc_service_initialized == 1
93+
end
6294

63-
local serverity = diagnostics[bufname] or vim.diagnostic.severity.HINT
64-
diagnostics[bufname] = math.min(coc_severity, serverity)
95+
---Marshal severities from COC. Does nothing when COC service not started.
96+
---@return NodeSeverities
97+
local function from_coc()
98+
if not is_using_coc() then
99+
return {}
100+
end
101+
102+
local ok, diagnostic_list = xpcall(function()
103+
return vim.fn.CocAction "diagnosticList"
104+
end, handle_coc_exception)
105+
if not ok or type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then
106+
return {}
65107
end
66108

67109
local buffer_severity = {}
68-
for bufname, severity in pairs(diagnostics) do
69-
if is_severity_in_range(severity, M.severity) then
70-
buffer_severity[bufname] = severity
110+
for _, diagnostic in ipairs(diagnostic_list) do
111+
local bufname = uniformize_path(diagnostic.file)
112+
local coc_severity = COC_SEVERITY_LEVELS[diagnostic.severity]
113+
local highest_severity = buffer_severity[bufname] or coc_severity
114+
if is_severity_in_range(highest_severity, M.severity) then
115+
buffer_severity[bufname] = math.min(highest_severity, coc_severity)
71116
end
72117
end
73118

74119
return buffer_severity
75120
end
76121

77-
local function is_using_coc()
78-
return vim.g.coc_service_initialized == 1
122+
---Maybe retrieve severity level from the cache
123+
---@param node Node
124+
---@return DiagStatus
125+
local function from_cache(node)
126+
local nodepath = uniformize_path(node.absolute_path)
127+
local max_severity = nil
128+
if not node.nodes then
129+
-- direct cache hit for files
130+
max_severity = NODE_SEVERITIES[nodepath]
131+
else
132+
-- dirs should be searched in the list of cached buffer names by prefix
133+
for bufname, severity in pairs(NODE_SEVERITIES) do
134+
local node_contains_buf = vim.startswith(bufname, nodepath .. "/")
135+
if node_contains_buf then
136+
if severity == M.severity.max then
137+
max_severity = severity
138+
break
139+
else
140+
max_severity = math.min(max_severity or severity, severity)
141+
end
142+
end
143+
end
144+
end
145+
return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION }
79146
end
80147

148+
---Fired on DiagnosticChanged and CocDiagnosticChanged events:
149+
---debounced retrieval, cache update, version increment and draw
81150
function M.update()
82-
if not M.enable or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then
151+
if not M.enable then
83152
return
84153
end
85154
utils.debounce("diagnostics", M.debounce_delay, function()
86155
local profile = log.profile_start "diagnostics update"
87-
log.line("diagnostics", "update")
88-
89-
local buffer_severity
90156
if is_using_coc() then
91-
buffer_severity = from_coc()
157+
NODE_SEVERITIES = from_coc()
92158
else
93-
buffer_severity = from_nvim_lsp()
94-
end
95-
96-
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())
97-
for _, node in pairs(nodes_by_line) do
98-
node.diag_status = nil
159+
NODE_SEVERITIES = from_nvim_lsp()
99160
end
100-
101-
for bufname, severity in pairs(buffer_severity) do
102-
local bufpath = utils.canonical_path(bufname)
103-
log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity)
104-
if 0 < severity and severity < 5 then
105-
for line, node in pairs(nodes_by_line) do
106-
local nodepath = utils.canonical_path(node.absolute_path)
107-
log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath)
108-
109-
local node_contains_buf = vim.startswith(bufpath:gsub("\\", "/"), nodepath:gsub("\\", "/") .. "/")
110-
if M.show_on_dirs and node_contains_buf and (not node.open or M.show_on_open_dirs) then
111-
log.line("diagnostics", " matched fold node '%s'", node.absolute_path)
112-
node.diag_status = severity
113-
elseif nodepath == bufpath then
114-
log.line("diagnostics", " matched file node '%s'", node.absolute_path)
115-
node.diag_status = severity
116-
end
117-
end
161+
NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1
162+
if log.enabled "diagnostics" then
163+
for bufname, severity in pairs(NODE_SEVERITIES) do
164+
log.line("diagnostics", "Indexing bufname '%s' with severity %d", bufname, severity)
118165
end
119166
end
120167
log.profile_end(profile)
121-
require("nvim-tree.renderer").draw()
168+
if view.is_buf_valid(view.get_bufnr()) then
169+
require("nvim-tree.renderer").draw()
170+
end
122171
end)
123172
end
124173

174+
---Maybe retrieve diagnostic status for a node.
175+
---Returns cached value when node's version matches.
176+
---@param node Node
177+
---@return DiagStatus|nil
178+
function M.get_diag_status(node)
179+
if not M.enable then
180+
return nil
181+
end
182+
183+
-- dir but we shouldn't show on dirs at all
184+
if node.nodes ~= nil and not M.show_on_dirs then
185+
return nil
186+
end
187+
188+
-- here, we do a lazy update of the diagnostic status carried by the node.
189+
-- This is by design, as diagnostics and nodes live in completely separate
190+
-- worlds, and this module is the link between the two
191+
if not node.diag_status or node.diag_status.cache_version < NODE_SEVERITIES_VERSION then
192+
node.diag_status = from_cache(node)
193+
end
194+
195+
-- file
196+
if not node.nodes then
197+
return node.diag_status
198+
end
199+
200+
-- dir is closed or we should show on open_dirs
201+
if not node.open or M.show_on_open_dirs then
202+
return node.diag_status
203+
end
204+
return nil
205+
end
206+
125207
function M.setup(opts)
126208
M.enable = opts.diagnostics.enable
127209
M.debounce_delay = opts.diagnostics.debounce_delay

lua/nvim-tree/node.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
---@field parent DirNode
1818
---@field type string
1919
---@field watcher function|nil
20+
---@field diag_status DiagStatus|nil
2021

2122
---@class DirNode: BaseNode
2223
---@field has_children boolean

lua/nvim-tree/renderer/components/diagnostics.lua

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local HL_POSITION = require("nvim-tree.enum").HL_POSITION
2+
local diagnostics = require "nvim-tree.diagnostics"
23

34
local M = {
45
HS_FILE = {},
@@ -17,10 +18,11 @@ function M.get_highlight(node)
1718
end
1819

1920
local group
21+
local diag_status = diagnostics.get_diag_status(node)
2022
if node.nodes then
21-
group = M.HS_FOLDER[node.diag_status]
23+
group = M.HS_FOLDER[diag_status and diag_status.value]
2224
else
23-
group = M.HS_FILE[node.diag_status]
25+
group = M.HS_FILE[diag_status and diag_status.value]
2426
end
2527

2628
if group then
@@ -35,7 +37,8 @@ end
3537
---@return HighlightedString|nil modified icon
3638
function M.get_icon(node)
3739
if node and M.config.diagnostics.enable and M.config.renderer.icons.show.diagnostics then
38-
return M.ICON[node.diag_status]
40+
local diag_status = diagnostics.get_diag_status(node)
41+
return M.ICON[diag_status and diag_status.value]
3942
end
4043
end
4144

0 commit comments

Comments
 (0)