chore(watchers): refactor events and make debouncer safe

- fs poll -> fs events
- make debouncer safe and fix diagnostics events
This commit is contained in:
Alexander Courtis
2022-07-17 16:50:24 +10:00
committed by GitHub
parent 26512c369f
commit 06e48c29c4
12 changed files with 155 additions and 82 deletions

View File

@@ -263,6 +263,7 @@ Subsequent calls to setup will replace the previous configuration.
diagnostics = { diagnostics = {
enable = false, enable = false,
show_on_dirs = false, show_on_dirs = false,
debounce_delay = 50,
icons = { icons = {
hint = "", hint = "",
info = "", info = "",
@@ -328,6 +329,7 @@ Subsequent calls to setup will replace the previous configuration.
all = false, all = false,
config = false, config = false,
copy_paste = false, copy_paste = false,
dev = false,
diagnostics = false, diagnostics = false,
git = false, git = false,
profile = false, profile = false,
@@ -471,6 +473,10 @@ Show LSP and COC diagnostics in the signcolumn
Enable/disable the feature. Enable/disable the feature.
Type: `boolean`, Default: `false` Type: `boolean`, Default: `false`
*nvim-tree.diagnostics.debounce_delay*
Idle milliseconds between diagnostic event and update.
Type: `number`, Default: `50` (ms)
*nvim-tree.diagnostics.show_on_dirs* *nvim-tree.diagnostics.show_on_dirs*
Show diagnostic icons on parent directories. Show diagnostic icons on parent directories.
Type: `boolean`, Default: `false` Type: `boolean`, Default: `false`
@@ -888,6 +894,10 @@ Configuration for diagnostic logging.
File copy and paste actions. File copy and paste actions.
Type: `boolean`, Default: `false` Type: `boolean`, Default: `false`
*nvim-tree.log.types.dev*
Used for local development only. Not useful for users.
Type: `boolean`, Default: `false`
*nvim-tree.log.types.diagnostics* *nvim-tree.log.types.diagnostics*
LSP and COC processing, verbose. LSP and COC processing, verbose.
Type: `boolean`, Default: `false` Type: `boolean`, Default: `false`

View File

@@ -395,11 +395,17 @@ local function setup_autocommands(opts)
if opts.diagnostics.enable then if opts.diagnostics.enable then
create_nvim_tree_autocmd("DiagnosticChanged", { create_nvim_tree_autocmd("DiagnosticChanged", {
callback = require("nvim-tree.diagnostics").update, callback = function()
log.line("diagnostics", "DiagnosticChanged")
require("nvim-tree.diagnostics").update()
end,
}) })
create_nvim_tree_autocmd("User", { create_nvim_tree_autocmd("User", {
pattern = "CocDiagnosticChange", pattern = "CocDiagnosticChange",
callback = require("nvim-tree.diagnostics").update, callback = function()
log.line("diagnostics", "CocDiagnosticChange")
require("nvim-tree.diagnostics").update()
end,
}) })
end end
end end
@@ -511,6 +517,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
diagnostics = { diagnostics = {
enable = false, enable = false,
show_on_dirs = false, show_on_dirs = false,
debounce_delay = 50,
icons = { icons = {
hint = "", hint = "",
info = "", info = "",
@@ -576,6 +583,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
all = false, all = false,
config = false, config = false,
copy_paste = false, copy_paste = false,
dev = false,
diagnostics = false, diagnostics = false,
git = false, git = false,
profile = false, profile = false,

View File

@@ -10,7 +10,7 @@ local first_init_done = false
function M.init(foldername) function M.init(foldername)
if TreeExplorer then if TreeExplorer then
TreeExplorer:_clear_watchers() TreeExplorer:destroy()
end end
TreeExplorer = explorer.Explorer.new(foldername) TreeExplorer = explorer.Explorer.new(foldername)
if not first_init_done then if not first_init_done then

View File

@@ -90,43 +90,45 @@ 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 or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then
return return
end end
local ps = log.profile_start "diagnostics update" utils.debounce("diagnostics", M.debounce_delay, function()
log.line("diagnostics", "update") local ps = log.profile_start "diagnostics update"
log.line("diagnostics", "update")
local buffer_severity local buffer_severity
if is_using_coc() then if is_using_coc() then
buffer_severity = from_coc() buffer_severity = from_coc()
else else
buffer_severity = from_nvim_lsp() buffer_severity = from_nvim_lsp()
end end
M.clear() M.clear()
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line()) 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 for _, node in pairs(nodes_by_line) do
node.diag_status = nil node.diag_status = nil
end end
for bufname, severity in pairs(buffer_severity) do for bufname, severity in pairs(buffer_severity) do
local bufpath = utils.canonical_path(bufname) local bufpath = utils.canonical_path(bufname)
log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity) log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity)
if 0 < severity and severity < 5 then if 0 < severity and severity < 5 then
for line, node in pairs(nodes_by_line) do for line, node in pairs(nodes_by_line) do
local nodepath = utils.canonical_path(node.absolute_path) local nodepath = utils.canonical_path(node.absolute_path)
log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath) log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath)
if M.show_on_dirs and vim.startswith(bufpath, nodepath) then if M.show_on_dirs and vim.startswith(bufpath, nodepath) then
log.line("diagnostics", " matched fold node '%s'", node.absolute_path) log.line("diagnostics", " matched fold node '%s'", node.absolute_path)
node.diag_status = severity node.diag_status = severity
add_sign(line, severity) add_sign(line, severity)
elseif nodepath == bufpath then elseif nodepath == bufpath then
log.line("diagnostics", " matched file node '%s'", node.absolute_path) log.line("diagnostics", " matched file node '%s'", node.absolute_path)
node.diag_status = severity node.diag_status = severity
add_sign(line, severity) add_sign(line, severity)
end
end end
end end
end end
end log.profile_end(ps, "diagnostics update")
log.profile_end(ps, "diagnostics update") end)
end end
local links = { local links = {
@@ -138,6 +140,7 @@ local links = {
function M.setup(opts) function M.setup(opts)
M.enable = opts.diagnostics.enable M.enable = opts.diagnostics.enable
M.debounce_delay = opts.diagnostics.debounce_delay
if M.enable then if M.enable then
log.line("diagnostics", "setup") log.line("diagnostics", "setup")

View File

@@ -43,6 +43,16 @@ function M.update_git_status(node, parent_ignored, status)
end end
end end
function M.node_destroy(node)
if not node then
return
end
if node.watcher then
node.watcher:destroy()
end
end
function M.setup(opts) function M.setup(opts)
M.config = { M.config = {
git = opts.git, git = opts.git,

View File

@@ -2,6 +2,7 @@ local uv = vim.loop
local git = require "nvim-tree.git" local git = require "nvim-tree.git"
local watch = require "nvim-tree.explorer.watch" local watch = require "nvim-tree.explorer.watch"
local common = require "nvim-tree.explorer.common"
local M = {} local M = {}
@@ -33,22 +34,16 @@ function Explorer:expand(node)
self:_load(node) self:_load(node)
end end
function Explorer.clear_watchers_for(root_node) function Explorer:destroy()
local function iterate(node) local function iterate(node)
if node.watcher then common.node_destroy(node)
node.watcher:stop() if node.nodes then
for _, child in pairs(node.nodes) do for _, child in pairs(node.nodes) do
if child.watcher then iterate(child)
iterate(child)
end
end end
end end
end end
iterate(root_node) iterate(self)
end
function Explorer:_clear_watchers()
Explorer.clear_watchers_for(self)
end end
function M.setup(opts) function M.setup(opts)

View File

@@ -69,7 +69,12 @@ function M.reload(node, status)
node.nodes = vim.tbl_map( node.nodes = vim.tbl_map(
update_status(nodes_by_path, node_ignored, status), update_status(nodes_by_path, node_ignored, status),
vim.tbl_filter(function(n) vim.tbl_filter(function(n)
return child_names[n.absolute_path] if child_names[n.absolute_path] then
return child_names[n.absolute_path]
else
common.node_destroy(n)
return nil
end
end, node.nodes) end, node.nodes)
) )

View File

@@ -46,7 +46,7 @@ function M.create_watcher(absolute_path)
end end
log.line("watcher", "node start '%s'", absolute_path) log.line("watcher", "node start '%s'", absolute_path)
Watcher.new { return Watcher.new {
absolute_path = absolute_path, absolute_path = absolute_path,
interval = M.interval, interval = M.interval,
on_event = function(opts) on_event = function(opts)

View File

@@ -29,8 +29,8 @@ function M.reload_project(project_root, path)
return return
end end
if path and not path:match("^" .. project_root) then if path and path:find(project_root, 1, true) ~= 1 then
path = nil return
end end
local git_status = Runner.run { local git_status = Runner.run {
@@ -43,7 +43,7 @@ function M.reload_project(project_root, path)
if path then if path then
for p in pairs(project.files) do for p in pairs(project.files) do
if p:match("^" .. path) then if p:find(path, 1, true) == 1 then
project.files[p] = nil project.files[p] = nil
end end
end end
@@ -138,10 +138,6 @@ function M.load_project_status(cwd)
reload_tree_at(opts.project_root) reload_tree_at(opts.project_root)
end) end)
end, end,
on_event0 = function()
log.line("watcher", "git event")
M.reload_tree_at(project_root)
end,
} }
end end

View File

@@ -147,11 +147,11 @@ function Runner.run(opts)
log.profile_end(ps, "git job %s %s", opts.project_root, opts.path) log.profile_end(ps, "git job %s %s", opts.project_root, opts.path)
if self.rc == -1 then if self.rc == -1 then
log.line("git", "job timed out") log.line("git", "job timed out %s %s", opts.project_root, opts.path)
elseif self.rc ~= 0 then elseif self.rc ~= 0 then
log.line("git", "job failed with return code %d", self.rc) log.line("git", "job fail rc %d %s %s", self.rc, opts.project_root, opts.path)
else else
log.line("git", "job success") log.line("git", "job success %s %s", opts.project_root, opts.path)
end end
return self.output return self.output

View File

@@ -307,25 +307,57 @@ function M.key_by(tbl, key)
return keyed return keyed
end end
---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. local function timer_stop_close(timer)
if timer:is_active() then
timer:stop()
end
if not timer:is_closing() then
timer:close()
end
end
---Execute callback timeout ms after the lastest invocation with context.
---Waiting invocations for that context will be discarded.
---Invocation will be rescheduled while a callback is being executed.
---Caller must ensure that callback performs the same or functionally equivalent actions.
---
---@param context string identifies the callback to debounce ---@param context string identifies the callback to debounce
---@param timeout number ms to wait ---@param timeout number ms to wait
---@param callback function to execute on completion ---@param callback function to execute on completion
function M.debounce(context, timeout, callback) function M.debounce(context, timeout, callback)
if M.debouncers[context] then -- all execution here is done in a synchronous context; no thread safety required
pcall(uv.close, M.debouncers[context])
M.debouncers[context] = M.debouncers[context] or {}
local debouncer = M.debouncers[context]
-- cancel waiting or executing timer
if debouncer.timer then
timer_stop_close(debouncer.timer)
end end
M.debouncers[context] = uv.new_timer() local timer = uv.new_timer()
M.debouncers[context]:start( debouncer.timer = timer
timeout, timer:start(timeout, 0, function()
0, timer_stop_close(timer)
vim.schedule_wrap(function()
M.debouncers[context]:close() -- reschedule when callback is running
M.debouncers[context] = nil if debouncer.executing then
M.debounce(context, timeout, callback)
return
end
-- call back at a safe time
debouncer.executing = true
vim.schedule(function()
callback() callback()
debouncer.executing = false
-- no other timer waiting
if debouncer.timer == timer then
M.debouncers[context] = nil
end
end) end)
) end)
end end
function M.focus_file(path) function M.focus_file(path)

View File

@@ -9,6 +9,13 @@ local M = {
local Watcher = {} local Watcher = {}
Watcher.__index = Watcher Watcher.__index = Watcher
local FS_EVENT_FLAGS = {
-- inotify or equivalent will be used; fallback to stat has not yet been implemented
stat = false,
-- recursive is not functional in neovim's libuv implementation
recursive = false,
}
function Watcher.new(opts) function Watcher.new(opts)
for _, existing in ipairs(M._watchers) do for _, existing in ipairs(M._watchers) do
if existing._opts.absolute_path == opts.absolute_path then if existing._opts.absolute_path == opts.absolute_path then
@@ -35,40 +42,47 @@ function Watcher:start()
local rc, _, name local rc, _, name
self._p, _, name = uv.new_fs_poll() self._e, _, name = uv.new_fs_event()
if not self._p then if not self._e then
self._p = nil self._e = nil
utils.warn( utils.warn(
string.format("Could not initialize an fs_poll watcher for path %s : %s", self._opts.absolute_path, name) string.format("Could not initialize an fs_event watcher for path %s : %s", self._opts.absolute_path, name)
) )
return nil return nil
end end
local poll_cb = vim.schedule_wrap(function(err) local event_cb = vim.schedule_wrap(function(err, filename, events)
if err then if err then
log.line("watcher", "poll_cb for %s fail : %s", self._opts.absolute_path, err) log.line("watcher", "event_cb for %s fail : %s", self._opts.absolute_path, err)
else else
log.line("watcher", "event_cb '%s' '%s' %s", self._opts.absolute_path, filename, vim.inspect(events))
self._opts.on_event(self._opts) self._opts.on_event(self._opts)
end end
end) end)
rc, _, name = uv.fs_poll_start(self._p, self._opts.absolute_path, self._opts.interval, poll_cb) rc, _, name = self._e:start(self._opts.absolute_path, FS_EVENT_FLAGS, event_cb)
if rc ~= 0 then if rc ~= 0 then
utils.warn(string.format("Could not start the fs_poll watcher for path %s : %s", self._opts.absolute_path, name)) utils.warn(string.format("Could not start the fs_event watcher for path %s : %s", self._opts.absolute_path, name))
return nil return nil
end end
return self return self
end end
function Watcher:stop() function Watcher:destroy()
log.line("watcher", "Watcher:stop '%s'", self._opts.absolute_path) log.line("watcher", "Watcher:destroy '%s'", self._opts.absolute_path)
if self._p then if self._e then
local rc, _, name = uv.fs_poll_stop(self._p) local rc, _, name = self._e:stop()
if rc ~= 0 then if rc ~= 0 then
utils.warn(string.format("Could not stop the fs_poll watcher for path %s : %s", self._opts.absolute_path, name)) utils.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self._opts.absolute_path, name))
end
self._e = nil
end
for i, w in ipairs(M._watchers) do
if w == self then
table.remove(M._watchers, i)
break
end end
self._p = nil
end end
end end
@@ -76,7 +90,7 @@ M.Watcher = Watcher
function M.purge_watchers() function M.purge_watchers()
for _, watcher in pairs(M._watchers) do for _, watcher in pairs(M._watchers) do
watcher:stop() watcher:destroy()
end end
M._watchers = {} M._watchers = {}
end end