diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index e7ab7c85..142a32b2 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 e162f1b4..c41f1a8b 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 83f3db92..a61fea72 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 f2360570..fb16d3ca 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") + 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 + 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 14cef92b..e3c3e76d 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 5578331f..6b9a8c9e 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 e26a68b0..5d9299b7 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 2243df2a..79b50fc5 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 77f98588..761677d0 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 cd488a15..a62043c5 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 7ed406c5..e9eea742 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 15ebeb90..3ab7a42d 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