11local utils = require " nvim-tree.utils"
22local view = require " nvim-tree.view"
3- local core = require " nvim-tree.core"
43local log = require " nvim-tree.log"
54
65local 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
1642local 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
3863end
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
4369local function is_severity_in_range (severity , config )
4470 return config .max <= severity and severity <= config .min
4571end
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
75120end
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 }
79146end
80147
148+ --- Fired on DiagnosticChanged and CocDiagnosticChanged events:
149+ --- debounced retrieval, cache update, version increment and draw
81150function 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 )
123172end
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+
125207function M .setup (opts )
126208 M .enable = opts .diagnostics .enable
127209 M .debounce_delay = opts .diagnostics .debounce_delay
0 commit comments