diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 6f1116c1cea..40828d2d9cb 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -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` diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index e162f1b4d8d..c41f1a8be55 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -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, diff --git a/lua/nvim-tree/core.lua b/lua/nvim-tree/core.lua index 83f3db92877..a61fea72743 100644 --- a/lua/nvim-tree/core.lua +++ b/lua/nvim-tree/core.lua @@ -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 diff --git a/lua/nvim-tree/diagnostics.lua b/lua/nvim-tree/diagnostics.lua index f2360570640..fb16d3ca4bd 100644 --- a/lua/nvim-tree/diagnostics.lua +++ b/lua/nvim-tree/diagnostics.lua @@ -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 return end - local ps = log.profile_start "diagnostics update" - log.line("diagnostics", "update") - - local buffer_severity - if is_using_coc() then - buffer_severity = from_coc() - else - buffer_severity = from_nvim_lsp() - end + utils.debounce("diagnostics", M.debounce_delay, function() + local ps = log.profile_start "diagnostics update" + log.line("diagnostics", "update") + + local buffer_severity + if is_using_coc() then + buffer_severity = from_coc() + else + buffer_severity = from_nvim_lsp() + end - M.clear() + M.clear() - 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 - node.diag_status = nil - end + 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 + node.diag_status = nil + end - for bufname, severity in pairs(buffer_severity) do - local bufpath = utils.canonical_path(bufname) - log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity) - if 0 < severity and severity < 5 then - for line, node in pairs(nodes_by_line) do - local nodepath = utils.canonical_path(node.absolute_path) - log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath) - if M.show_on_dirs and vim.startswith(bufpath, nodepath) then - log.line("diagnostics", " matched fold node '%s'", node.absolute_path) - node.diag_status = severity - add_sign(line, severity) - elseif nodepath == bufpath then - log.line("diagnostics", " matched file node '%s'", node.absolute_path) - node.diag_status = severity - add_sign(line, severity) + for bufname, severity in pairs(buffer_severity) do + local bufpath = utils.canonical_path(bufname) + log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity) + if 0 < severity and severity < 5 then + for line, node in pairs(nodes_by_line) do + local nodepath = utils.canonical_path(node.absolute_path) + log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath) + if M.show_on_dirs and vim.startswith(bufpath, nodepath) then + log.line("diagnostics", " matched fold node '%s'", node.absolute_path) + node.diag_status = severity + add_sign(line, severity) + elseif nodepath == bufpath then + log.line("diagnostics", " matched file node '%s'", node.absolute_path) + node.diag_status = severity + add_sign(line, severity) + end end end end - end - log.profile_end(ps, "diagnostics update") + 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") diff --git a/lua/nvim-tree/explorer/common.lua b/lua/nvim-tree/explorer/common.lua index 14cef92b92a..e3c3e76d32a 100644 --- a/lua/nvim-tree/explorer/common.lua +++ b/lua/nvim-tree/explorer/common.lua @@ -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, diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua index 5578331f67c..6b9a8c9e5cc 100644 --- a/lua/nvim-tree/explorer/init.lua +++ b/lua/nvim-tree/explorer/init.lua @@ -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 + iterate(child) end end end - iterate(root_node) -end - -function Explorer:_clear_watchers() - Explorer.clear_watchers_for(self) + iterate(self) end function M.setup(opts) diff --git a/lua/nvim-tree/explorer/reload.lua b/lua/nvim-tree/explorer/reload.lua index e26a68b0a92..5d9299b7830 100644 --- a/lua/nvim-tree/explorer/reload.lua +++ b/lua/nvim-tree/explorer/reload.lua @@ -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) - 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) ) diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua index 2243df2ada8..79b50fc57c0 100644 --- a/lua/nvim-tree/explorer/watch.lua +++ b/lua/nvim-tree/explorer/watch.lua @@ -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) diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua index 77f9858889e..761677d042e 100644 --- a/lua/nvim-tree/git/init.lua +++ b/lua/nvim-tree/git/init.lua @@ -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 diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua index cd488a15349..a62043c5220 100644 --- a/lua/nvim-tree/git/runner.lua +++ b/lua/nvim-tree/git/runner.lua @@ -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 diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua index 7ed406c5ca0..e9eea7428f1 100644 --- a/lua/nvim-tree/utils.lua +++ b/lua/nvim-tree/utils.lua @@ -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) diff --git a/lua/nvim-tree/watcher.lua b/lua/nvim-tree/watcher.lua index 15ebeb90296..3ab7a42d8b2 100644 --- a/lua/nvim-tree/watcher.lua +++ b/lua/nvim-tree/watcher.lua @@ -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