Skip to content

Commit 06e48c2

Browse files
authored
chore(watchers): refactor events and make debouncer safe
- fs poll -> fs events - make debouncer safe and fix diagnostics events
1 parent 26512c3 commit 06e48c2

File tree

12 files changed

+156
-83
lines changed

12 files changed

+156
-83
lines changed

doc/nvim-tree-lua.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ Subsequent calls to setup will replace the previous configuration.
263263
diagnostics = {
264264
enable = false,
265265
show_on_dirs = false,
266+
debounce_delay = 50,
266267
icons = {
267268
hint = "",
268269
info = "",
@@ -328,6 +329,7 @@ Subsequent calls to setup will replace the previous configuration.
328329
all = false,
329330
config = false,
330331
copy_paste = false,
332+
dev = false,
331333
diagnostics = false,
332334
git = false,
333335
profile = false,
@@ -471,6 +473,10 @@ Show LSP and COC diagnostics in the signcolumn
471473
Enable/disable the feature.
472474
Type: `boolean`, Default: `false`
473475

476+
*nvim-tree.diagnostics.debounce_delay*
477+
Idle milliseconds between diagnostic event and update.
478+
Type: `number`, Default: `50` (ms)
479+
474480
*nvim-tree.diagnostics.show_on_dirs*
475481
Show diagnostic icons on parent directories.
476482
Type: `boolean`, Default: `false`
@@ -888,6 +894,10 @@ Configuration for diagnostic logging.
888894
File copy and paste actions.
889895
Type: `boolean`, Default: `false`
890896

897+
*nvim-tree.log.types.dev*
898+
Used for local development only. Not useful for users.
899+
Type: `boolean`, Default: `false`
900+
891901
*nvim-tree.log.types.diagnostics*
892902
LSP and COC processing, verbose.
893903
Type: `boolean`, Default: `false`

lua/nvim-tree.lua

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,17 @@ local function setup_autocommands(opts)
395395

396396
if opts.diagnostics.enable then
397397
create_nvim_tree_autocmd("DiagnosticChanged", {
398-
callback = require("nvim-tree.diagnostics").update,
398+
callback = function()
399+
log.line("diagnostics", "DiagnosticChanged")
400+
require("nvim-tree.diagnostics").update()
401+
end,
399402
})
400403
create_nvim_tree_autocmd("User", {
401404
pattern = "CocDiagnosticChange",
402-
callback = require("nvim-tree.diagnostics").update,
405+
callback = function()
406+
log.line("diagnostics", "CocDiagnosticChange")
407+
require("nvim-tree.diagnostics").update()
408+
end,
403409
})
404410
end
405411
end
@@ -511,6 +517,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
511517
diagnostics = {
512518
enable = false,
513519
show_on_dirs = false,
520+
debounce_delay = 50,
514521
icons = {
515522
hint = "",
516523
info = "",
@@ -576,6 +583,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
576583
all = false,
577584
config = false,
578585
copy_paste = false,
586+
dev = false,
579587
diagnostics = false,
580588
git = false,
581589
profile = false,

lua/nvim-tree/core.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ local first_init_done = false
1010

1111
function M.init(foldername)
1212
if TreeExplorer then
13-
TreeExplorer:_clear_watchers()
13+
TreeExplorer:destroy()
1414
end
1515
TreeExplorer = explorer.Explorer.new(foldername)
1616
if not first_init_done then

lua/nvim-tree/diagnostics.lua

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -90,43 +90,45 @@ function M.update()
9090
if not M.enable or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then
9191
return
9292
end
93-
local ps = log.profile_start "diagnostics update"
94-
log.line("diagnostics", "update")
95-
96-
local buffer_severity
97-
if is_using_coc() then
98-
buffer_severity = from_coc()
99-
else
100-
buffer_severity = from_nvim_lsp()
101-
end
93+
utils.debounce("diagnostics", M.debounce_delay, function()
94+
local ps = log.profile_start "diagnostics update"
95+
log.line("diagnostics", "update")
96+
97+
local buffer_severity
98+
if is_using_coc() then
99+
buffer_severity = from_coc()
100+
else
101+
buffer_severity = from_nvim_lsp()
102+
end
102103

103-
M.clear()
104+
M.clear()
104105

105-
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())
106-
for _, node in pairs(nodes_by_line) do
107-
node.diag_status = nil
108-
end
106+
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())
107+
for _, node in pairs(nodes_by_line) do
108+
node.diag_status = nil
109+
end
109110

110-
for bufname, severity in pairs(buffer_severity) do
111-
local bufpath = utils.canonical_path(bufname)
112-
log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity)
113-
if 0 < severity and severity < 5 then
114-
for line, node in pairs(nodes_by_line) do
115-
local nodepath = utils.canonical_path(node.absolute_path)
116-
log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath)
117-
if M.show_on_dirs and vim.startswith(bufpath, nodepath) then
118-
log.line("diagnostics", " matched fold node '%s'", node.absolute_path)
119-
node.diag_status = severity
120-
add_sign(line, severity)
121-
elseif nodepath == bufpath then
122-
log.line("diagnostics", " matched file node '%s'", node.absolute_path)
123-
node.diag_status = severity
124-
add_sign(line, severity)
111+
for bufname, severity in pairs(buffer_severity) do
112+
local bufpath = utils.canonical_path(bufname)
113+
log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity)
114+
if 0 < severity and severity < 5 then
115+
for line, node in pairs(nodes_by_line) do
116+
local nodepath = utils.canonical_path(node.absolute_path)
117+
log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath)
118+
if M.show_on_dirs and vim.startswith(bufpath, nodepath) then
119+
log.line("diagnostics", " matched fold node '%s'", node.absolute_path)
120+
node.diag_status = severity
121+
add_sign(line, severity)
122+
elseif nodepath == bufpath then
123+
log.line("diagnostics", " matched file node '%s'", node.absolute_path)
124+
node.diag_status = severity
125+
add_sign(line, severity)
126+
end
125127
end
126128
end
127129
end
128-
end
129-
log.profile_end(ps, "diagnostics update")
130+
log.profile_end(ps, "diagnostics update")
131+
end)
130132
end
131133

132134
local links = {
@@ -138,6 +140,7 @@ local links = {
138140

139141
function M.setup(opts)
140142
M.enable = opts.diagnostics.enable
143+
M.debounce_delay = opts.diagnostics.debounce_delay
141144

142145
if M.enable then
143146
log.line("diagnostics", "setup")

lua/nvim-tree/explorer/common.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ function M.update_git_status(node, parent_ignored, status)
4343
end
4444
end
4545

46+
function M.node_destroy(node)
47+
if not node then
48+
return
49+
end
50+
51+
if node.watcher then
52+
node.watcher:destroy()
53+
end
54+
end
55+
4656
function M.setup(opts)
4757
M.config = {
4858
git = opts.git,

lua/nvim-tree/explorer/init.lua

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local uv = vim.loop
22

33
local git = require "nvim-tree.git"
44
local watch = require "nvim-tree.explorer.watch"
5+
local common = require "nvim-tree.explorer.common"
56

67
local M = {}
78

@@ -33,22 +34,16 @@ function Explorer:expand(node)
3334
self:_load(node)
3435
end
3536

36-
function Explorer.clear_watchers_for(root_node)
37+
function Explorer:destroy()
3738
local function iterate(node)
38-
if node.watcher then
39-
node.watcher:stop()
39+
common.node_destroy(node)
40+
if node.nodes then
4041
for _, child in pairs(node.nodes) do
41-
if child.watcher then
42-
iterate(child)
43-
end
42+
iterate(child)
4443
end
4544
end
4645
end
47-
iterate(root_node)
48-
end
49-
50-
function Explorer:_clear_watchers()
51-
Explorer.clear_watchers_for(self)
46+
iterate(self)
5247
end
5348

5449
function M.setup(opts)

lua/nvim-tree/explorer/reload.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,12 @@ function M.reload(node, status)
6969
node.nodes = vim.tbl_map(
7070
update_status(nodes_by_path, node_ignored, status),
7171
vim.tbl_filter(function(n)
72-
return child_names[n.absolute_path]
72+
if child_names[n.absolute_path] then
73+
return child_names[n.absolute_path]
74+
else
75+
common.node_destroy(n)
76+
return nil
77+
end
7378
end, node.nodes)
7479
)
7580

lua/nvim-tree/explorer/watch.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function M.create_watcher(absolute_path)
4646
end
4747

4848
log.line("watcher", "node start '%s'", absolute_path)
49-
Watcher.new {
49+
return Watcher.new {
5050
absolute_path = absolute_path,
5151
interval = M.interval,
5252
on_event = function(opts)

lua/nvim-tree/git/init.lua

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ function M.reload_project(project_root, path)
2929
return
3030
end
3131

32-
if path and not path:match("^" .. project_root) then
33-
path = nil
32+
if path and path:find(project_root, 1, true) ~= 1 then
33+
return
3434
end
3535

3636
local git_status = Runner.run {
@@ -43,7 +43,7 @@ function M.reload_project(project_root, path)
4343

4444
if path then
4545
for p in pairs(project.files) do
46-
if p:match("^" .. path) then
46+
if p:find(path, 1, true) == 1 then
4747
project.files[p] = nil
4848
end
4949
end
@@ -138,10 +138,6 @@ function M.load_project_status(cwd)
138138
reload_tree_at(opts.project_root)
139139
end)
140140
end,
141-
on_event0 = function()
142-
log.line("watcher", "git event")
143-
M.reload_tree_at(project_root)
144-
end,
145141
}
146142
end
147143

lua/nvim-tree/git/runner.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,11 @@ function Runner.run(opts)
147147
log.profile_end(ps, "git job %s %s", opts.project_root, opts.path)
148148

149149
if self.rc == -1 then
150-
log.line("git", "job timed out")
150+
log.line("git", "job timed out %s %s", opts.project_root, opts.path)
151151
elseif self.rc ~= 0 then
152-
log.line("git", "job failed with return code %d", self.rc)
152+
log.line("git", "job fail rc %d %s %s", self.rc, opts.project_root, opts.path)
153153
else
154-
log.line("git", "job success")
154+
log.line("git", "job success %s %s", opts.project_root, opts.path)
155155
end
156156

157157
return self.output

lua/nvim-tree/utils.lua

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -307,25 +307,57 @@ function M.key_by(tbl, key)
307307
return keyed
308308
end
309309

310-
---Execute callback timeout ms after the lastest invocation with context. Waiting invocations for that context will be discarded. Caller should this ensure that callback performs the same or functionally equivalent actions.
310+
local function timer_stop_close(timer)
311+
if timer:is_active() then
312+
timer:stop()
313+
end
314+
if not timer:is_closing() then
315+
timer:close()
316+
end
317+
end
318+
319+
---Execute callback timeout ms after the lastest invocation with context.
320+
---Waiting invocations for that context will be discarded.
321+
---Invocation will be rescheduled while a callback is being executed.
322+
---Caller must ensure that callback performs the same or functionally equivalent actions.
323+
---
311324
---@param context string identifies the callback to debounce
312325
---@param timeout number ms to wait
313326
---@param callback function to execute on completion
314327
function M.debounce(context, timeout, callback)
315-
if M.debouncers[context] then
316-
pcall(uv.close, M.debouncers[context])
328+
-- all execution here is done in a synchronous context; no thread safety required
329+
330+
M.debouncers[context] = M.debouncers[context] or {}
331+
local debouncer = M.debouncers[context]
332+
333+
-- cancel waiting or executing timer
334+
if debouncer.timer then
335+
timer_stop_close(debouncer.timer)
317336
end
318337

319-
M.debouncers[context] = uv.new_timer()
320-
M.debouncers[context]:start(
321-
timeout,
322-
0,
323-
vim.schedule_wrap(function()
324-
M.debouncers[context]:close()
325-
M.debouncers[context] = nil
338+
local timer = uv.new_timer()
339+
debouncer.timer = timer
340+
timer:start(timeout, 0, function()
341+
timer_stop_close(timer)
342+
343+
-- reschedule when callback is running
344+
if debouncer.executing then
345+
M.debounce(context, timeout, callback)
346+
return
347+
end
348+
349+
-- call back at a safe time
350+
debouncer.executing = true
351+
vim.schedule(function()
326352
callback()
353+
debouncer.executing = false
354+
355+
-- no other timer waiting
356+
if debouncer.timer == timer then
357+
M.debouncers[context] = nil
358+
end
327359
end)
328-
)
360+
end)
329361
end
330362

331363
function M.focus_file(path)

0 commit comments

Comments
 (0)