chore(watchers): refactor events and make debouncer safe
- fs poll -> fs events - make debouncer safe and fix diagnostics events
This commit is contained in:
parent
26512c369f
commit
06e48c29c4
@ -263,6 +263,7 @@ Subsequent calls to setup will replace the previous configuration.
|
||||
diagnostics = {
|
||||
enable = false,
|
||||
show_on_dirs = false,
|
||||
debounce_delay = 50,
|
||||
icons = {
|
||||
hint = "",
|
||||
info = "",
|
||||
@ -328,6 +329,7 @@ Subsequent calls to setup will replace the previous configuration.
|
||||
all = false,
|
||||
config = false,
|
||||
copy_paste = false,
|
||||
dev = false,
|
||||
diagnostics = false,
|
||||
git = false,
|
||||
profile = false,
|
||||
@ -471,6 +473,10 @@ Show LSP and COC diagnostics in the signcolumn
|
||||
Enable/disable the feature.
|
||||
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*
|
||||
Show diagnostic icons on parent directories.
|
||||
Type: `boolean`, Default: `false`
|
||||
@ -888,6 +894,10 @@ Configuration for diagnostic logging.
|
||||
File copy and paste actions.
|
||||
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*
|
||||
LSP and COC processing, verbose.
|
||||
Type: `boolean`, Default: `false`
|
||||
|
||||
@ -395,11 +395,17 @@ local function setup_autocommands(opts)
|
||||
|
||||
if opts.diagnostics.enable then
|
||||
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", {
|
||||
pattern = "CocDiagnosticChange",
|
||||
callback = require("nvim-tree.diagnostics").update,
|
||||
callback = function()
|
||||
log.line("diagnostics", "CocDiagnosticChange")
|
||||
require("nvim-tree.diagnostics").update()
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
@ -511,6 +517,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
|
||||
diagnostics = {
|
||||
enable = false,
|
||||
show_on_dirs = false,
|
||||
debounce_delay = 50,
|
||||
icons = {
|
||||
hint = "",
|
||||
info = "",
|
||||
@ -576,6 +583,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
|
||||
all = false,
|
||||
config = false,
|
||||
copy_paste = false,
|
||||
dev = false,
|
||||
diagnostics = false,
|
||||
git = false,
|
||||
profile = false,
|
||||
|
||||
@ -10,7 +10,7 @@ local first_init_done = false
|
||||
|
||||
function M.init(foldername)
|
||||
if TreeExplorer then
|
||||
TreeExplorer:_clear_watchers()
|
||||
TreeExplorer:destroy()
|
||||
end
|
||||
TreeExplorer = explorer.Explorer.new(foldername)
|
||||
if not first_init_done then
|
||||
|
||||
@ -90,6 +90,7 @@ function M.update()
|
||||
if not M.enable or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then
|
||||
return
|
||||
end
|
||||
utils.debounce("diagnostics", M.debounce_delay, function()
|
||||
local ps = log.profile_start "diagnostics update"
|
||||
log.line("diagnostics", "update")
|
||||
|
||||
@ -127,6 +128,7 @@ function M.update()
|
||||
end
|
||||
end
|
||||
log.profile_end(ps, "diagnostics update")
|
||||
end)
|
||||
end
|
||||
|
||||
local links = {
|
||||
@ -138,6 +140,7 @@ local links = {
|
||||
|
||||
function M.setup(opts)
|
||||
M.enable = opts.diagnostics.enable
|
||||
M.debounce_delay = opts.diagnostics.debounce_delay
|
||||
|
||||
if M.enable then
|
||||
log.line("diagnostics", "setup")
|
||||
|
||||
@ -43,6 +43,16 @@ function M.update_git_status(node, parent_ignored, status)
|
||||
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)
|
||||
M.config = {
|
||||
git = opts.git,
|
||||
|
||||
@ -2,6 +2,7 @@ local uv = vim.loop
|
||||
|
||||
local git = require "nvim-tree.git"
|
||||
local watch = require "nvim-tree.explorer.watch"
|
||||
local common = require "nvim-tree.explorer.common"
|
||||
|
||||
local M = {}
|
||||
|
||||
@ -33,22 +34,16 @@ function Explorer:expand(node)
|
||||
self:_load(node)
|
||||
end
|
||||
|
||||
function Explorer.clear_watchers_for(root_node)
|
||||
function Explorer:destroy()
|
||||
local function iterate(node)
|
||||
if node.watcher then
|
||||
node.watcher:stop()
|
||||
common.node_destroy(node)
|
||||
if node.nodes then
|
||||
for _, child in pairs(node.nodes) do
|
||||
if child.watcher then
|
||||
iterate(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
iterate(root_node)
|
||||
end
|
||||
|
||||
function Explorer:_clear_watchers()
|
||||
Explorer.clear_watchers_for(self)
|
||||
iterate(self)
|
||||
end
|
||||
|
||||
function M.setup(opts)
|
||||
|
||||
@ -69,7 +69,12 @@ function M.reload(node, status)
|
||||
node.nodes = vim.tbl_map(
|
||||
update_status(nodes_by_path, node_ignored, status),
|
||||
vim.tbl_filter(function(n)
|
||||
if child_names[n.absolute_path] then
|
||||
return child_names[n.absolute_path]
|
||||
else
|
||||
common.node_destroy(n)
|
||||
return nil
|
||||
end
|
||||
end, node.nodes)
|
||||
)
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ function M.create_watcher(absolute_path)
|
||||
end
|
||||
|
||||
log.line("watcher", "node start '%s'", absolute_path)
|
||||
Watcher.new {
|
||||
return Watcher.new {
|
||||
absolute_path = absolute_path,
|
||||
interval = M.interval,
|
||||
on_event = function(opts)
|
||||
|
||||
@ -29,8 +29,8 @@ function M.reload_project(project_root, path)
|
||||
return
|
||||
end
|
||||
|
||||
if path and not path:match("^" .. project_root) then
|
||||
path = nil
|
||||
if path and path:find(project_root, 1, true) ~= 1 then
|
||||
return
|
||||
end
|
||||
|
||||
local git_status = Runner.run {
|
||||
@ -43,7 +43,7 @@ function M.reload_project(project_root, path)
|
||||
|
||||
if path then
|
||||
for p in pairs(project.files) do
|
||||
if p:match("^" .. path) then
|
||||
if p:find(path, 1, true) == 1 then
|
||||
project.files[p] = nil
|
||||
end
|
||||
end
|
||||
@ -138,10 +138,6 @@ function M.load_project_status(cwd)
|
||||
reload_tree_at(opts.project_root)
|
||||
end)
|
||||
end,
|
||||
on_event0 = function()
|
||||
log.line("watcher", "git event")
|
||||
M.reload_tree_at(project_root)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@ -147,11 +147,11 @@ function Runner.run(opts)
|
||||
log.profile_end(ps, "git job %s %s", opts.project_root, opts.path)
|
||||
|
||||
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
|
||||
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
|
||||
log.line("git", "job success")
|
||||
log.line("git", "job success %s %s", opts.project_root, opts.path)
|
||||
end
|
||||
|
||||
return self.output
|
||||
|
||||
@ -307,25 +307,57 @@ function M.key_by(tbl, key)
|
||||
return keyed
|
||||
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 timeout number ms to wait
|
||||
---@param callback function to execute on completion
|
||||
function M.debounce(context, timeout, callback)
|
||||
if M.debouncers[context] then
|
||||
pcall(uv.close, M.debouncers[context])
|
||||
-- all execution here is done in a synchronous context; no thread safety required
|
||||
|
||||
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
|
||||
|
||||
M.debouncers[context] = uv.new_timer()
|
||||
M.debouncers[context]:start(
|
||||
timeout,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
M.debouncers[context]:close()
|
||||
M.debouncers[context] = nil
|
||||
local timer = uv.new_timer()
|
||||
debouncer.timer = timer
|
||||
timer:start(timeout, 0, function()
|
||||
timer_stop_close(timer)
|
||||
|
||||
-- reschedule when callback is running
|
||||
if debouncer.executing then
|
||||
M.debounce(context, timeout, callback)
|
||||
return
|
||||
end
|
||||
|
||||
-- call back at a safe time
|
||||
debouncer.executing = true
|
||||
vim.schedule(function()
|
||||
callback()
|
||||
debouncer.executing = false
|
||||
|
||||
-- no other timer waiting
|
||||
if debouncer.timer == timer then
|
||||
M.debouncers[context] = nil
|
||||
end
|
||||
end)
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
function M.focus_file(path)
|
||||
|
||||
@ -9,6 +9,13 @@ local M = {
|
||||
local 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)
|
||||
for _, existing in ipairs(M._watchers) do
|
||||
if existing._opts.absolute_path == opts.absolute_path then
|
||||
@ -35,40 +42,47 @@ function Watcher:start()
|
||||
|
||||
local rc, _, name
|
||||
|
||||
self._p, _, name = uv.new_fs_poll()
|
||||
if not self._p then
|
||||
self._p = nil
|
||||
self._e, _, name = uv.new_fs_event()
|
||||
if not self._e then
|
||||
self._e = nil
|
||||
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
|
||||
end
|
||||
|
||||
local poll_cb = vim.schedule_wrap(function(err)
|
||||
local event_cb = vim.schedule_wrap(function(err, filename, events)
|
||||
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
|
||||
log.line("watcher", "event_cb '%s' '%s' %s", self._opts.absolute_path, filename, vim.inspect(events))
|
||||
self._opts.on_event(self._opts)
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Watcher:stop()
|
||||
log.line("watcher", "Watcher:stop '%s'", self._opts.absolute_path)
|
||||
if self._p then
|
||||
local rc, _, name = uv.fs_poll_stop(self._p)
|
||||
function Watcher:destroy()
|
||||
log.line("watcher", "Watcher:destroy '%s'", self._opts.absolute_path)
|
||||
if self._e then
|
||||
local rc, _, name = self._e:stop()
|
||||
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
|
||||
self._p = nil
|
||||
end
|
||||
end
|
||||
|
||||
@ -76,7 +90,7 @@ M.Watcher = Watcher
|
||||
|
||||
function M.purge_watchers()
|
||||
for _, watcher in pairs(M._watchers) do
|
||||
watcher:stop()
|
||||
watcher:destroy()
|
||||
end
|
||||
M._watchers = {}
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user