1
1
local utils = require " nvim-tree.utils"
2
2
local view = require " nvim-tree.view"
3
- local core = require " nvim-tree.core"
4
3
local log = require " nvim-tree.log"
5
4
6
5
local M = {}
7
6
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 = {
9
13
Error = 1 ,
10
14
Warning = 2 ,
11
15
Information = 3 ,
12
16
Hint = 4 ,
13
17
}
14
18
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
16
42
local function from_nvim_lsp ()
17
43
local buffer_severity = {}
18
44
@@ -25,103 +51,159 @@ local function from_nvim_lsp()
25
51
for _ , diagnostic in ipairs (vim .diagnostic .get (nil , { severity = M .severity })) do
26
52
local buf = diagnostic .bufnr
27
53
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 )
33
58
end
34
59
end
35
60
end
36
61
37
62
return buffer_severity
38
63
end
39
64
40
- --- @param severity integer
65
+ --- Severity is within diagnostics.severity.min, diagnostics.severity.max
66
+ --- @param severity lsp.DiagnosticSeverity
41
67
--- @param config table
42
68
--- @return boolean
43
69
local function is_severity_in_range (severity , config )
44
70
return config .max <= severity and severity <= config .min
45
71
end
46
72
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
51
82
end
52
83
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 ))
56
86
end
87
+ end
57
88
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
62
94
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 {}
65
107
end
66
108
67
109
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 )
71
116
end
72
117
end
73
118
74
119
return buffer_severity
75
120
end
76
121
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 }
79
146
end
80
147
148
+ --- Fired on DiagnosticChanged and CocDiagnosticChanged events:
149
+ --- debounced retrieval, cache update, version increment and draw
81
150
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
83
152
return
84
153
end
85
154
utils .debounce (" diagnostics" , M .debounce_delay , function ()
86
155
local profile = log .profile_start " diagnostics update"
87
- log .line (" diagnostics" , " update" )
88
-
89
- local buffer_severity
90
156
if is_using_coc () then
91
- buffer_severity = from_coc ()
157
+ NODE_SEVERITIES = from_coc ()
92
158
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 ()
99
160
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 )
118
165
end
119
166
end
120
167
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
122
171
end )
123
172
end
124
173
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
+
125
207
function M .setup (opts )
126
208
M .enable = opts .diagnostics .enable
127
209
M .debounce_delay = opts .diagnostics .debounce_delay
0 commit comments