chore(watchers): refactor events and make debouncer safe
- fs poll -> fs events - make debouncer safe and fix diagnostics events
This commit is contained in:
committed by
GitHub
parent
26512c369f
commit
06e48c29c4
@@ -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`
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user