diff --git a/README.md b/README.md index c3318760..cfd2eab2 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,10 @@ require'nvim-tree'.setup { -- BEGIN_DEFAULT_OPTS custom = {}, exclude = {}, }, + filesystem_watchers = { + enable = false, + interval = 100, + }, git = { enable = true, ignore = true, @@ -231,6 +235,7 @@ require'nvim-tree'.setup { -- BEGIN_DEFAULT_OPTS diagnostics = false, git = false, profile = false, + watcher = false, }, }, } -- END_DEFAULT_OPTS diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 3599d092..2a0a5188 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -200,6 +200,10 @@ Values may be functions. Warning: this may result in unexpected behaviour. custom = {}, exclude = {}, }, + filesystem_watchers = { + enable = false, + interval = 100, + }, git = { enable = true, ignore = true, @@ -249,6 +253,7 @@ Values may be functions. Warning: this may result in unexpected behaviour. diagnostics = false, git = false, profile = false, + watcher = false, }, }, } -- END_DEFAULT_OPTS @@ -419,6 +424,26 @@ Git integration with icons and colors. milliseconds but a few seconds), it will not render anything until the git process returned the data. + +*nvim-tree.filesystem_watchers* +Will use file system watcher (libuv fs_poll) to watch the filesystem for +changes. +Using this will disable BufEnter / BufWritePost events in nvim-tree which +were used to update the whole tree. With this feature, the tree will be +updated only for the appropriate folder change, resulting in better +performance. +This will be experimental for a few weeks and will become the default. + + *nvim-tree.filesystem_watchers.enable* + Enable / disable the feature. + Type: `boolean`, Default: `false` + + *nvim-tree.filesystem_watchers.interval* + Milliseconds between polls for each directory. + Increase to at least 1000ms if changes are not visible. See + https://github.com/luvit/luv/blob/master/docs.md#uvfs_poll_startfs_poll-path-interval-callback + Type: `number`, Default: `100` (ms) + *nvim-tree.view* Window / buffer setup. diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 0b9fd385..704b63be 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -300,13 +300,18 @@ local function setup_autocommands(opts) -- reset highlights when colorscheme is changed create_nvim_tree_autocmd("ColorScheme", { callback = M.reset_highlight }) - if opts.auto_reload_on_write then + local has_watchers = opts.filesystem_watchers.enable + + if opts.auto_reload_on_write and not has_watchers then create_nvim_tree_autocmd("BufWritePost", { callback = reloaders.reload_explorer }) end - create_nvim_tree_autocmd("User", { - pattern = { "FugitiveChanged", "NeogitStatusRefreshed" }, - callback = reloaders.reload_git, - }) + + if not has_watchers and opts.git.enable then + create_nvim_tree_autocmd("User", { + pattern = { "FugitiveChanged", "NeogitStatusRefreshed" }, + callback = reloaders.reload_git, + }) + end if opts.open_on_tab then create_nvim_tree_autocmd("TabEnter", { callback = M.tab_change }) @@ -339,7 +344,7 @@ local function setup_autocommands(opts) create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory }) end - if opts.reload_on_bufenter then + if opts.reload_on_bufenter and not has_watchers then create_nvim_tree_autocmd("BufEnter", { pattern = "NvimTree_*", callback = reloaders.reload_explorer }) end end @@ -456,6 +461,10 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS custom = {}, exclude = {}, }, + filesystem_watchers = { + enable = false, + interval = 100, + }, git = { enable = true, ignore = true, @@ -505,6 +514,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS diagnostics = false, git = false, profile = false, + watcher = false, }, }, } -- END_DEFAULT_OPTS diff --git a/lua/nvim-tree/actions/copy-paste.lua b/lua/nvim-tree/actions/copy-paste.lua index 72269ba2..77760b1c 100644 --- a/lua/nvim-tree/actions/copy-paste.lua +++ b/lua/nvim-tree/actions/copy-paste.lua @@ -165,7 +165,9 @@ local function do_paste(node, action_type, action_fn) end clipboard[action_type] = {} - return require("nvim-tree.actions.reloaders").reload_explorer() + if not M.enable_reload then + return require("nvim-tree.actions.reloaders").reload_explorer() + end end local function do_cut(source, destination) @@ -242,6 +244,7 @@ end function M.setup(opts) M.use_system_clipboard = opts.actions.use_system_clipboard + M.enable_reload = not opts.filesystem_watchers.enable end return M diff --git a/lua/nvim-tree/actions/create-file.lua b/lua/nvim-tree/actions/create-file.lua index 26b012b4..cda896c5 100644 --- a/lua/nvim-tree/actions/create-file.lua +++ b/lua/nvim-tree/actions/create-file.lua @@ -42,7 +42,7 @@ local function get_num_nodes(iter) end local function get_containing_folder(node) - local is_open = M.config.create_in_closed_folder or node.open + local is_open = M.create_in_closed_folder or node.open if node.nodes ~= nil and is_open then return utils.path_add_trailing(node.absolute_path) end @@ -107,13 +107,19 @@ function M.fn(node) a.nvim_out_write(new_file_path .. " was properly created\n") end events._dispatch_folder_created(new_file_path) - require("nvim-tree.actions.reloaders").reload_explorer() - focus_file(new_file_path) + if M.enable_reload then + require("nvim-tree.actions.reloaders").reload_explorer() + end + -- INFO: defer needed when reload is automatic (watchers) + vim.defer_fn(function() + focus_file(new_file_path) + end, 50) end) end function M.setup(opts) - M.config = opts + M.create_in_closed_folder = opts.create_in_closed_folder + M.enable_reload = not opts.filesystem_watchers.enable end return M diff --git a/lua/nvim-tree/actions/init.lua b/lua/nvim-tree/actions/init.lua index e86f2e5e..ceb2aea2 100644 --- a/lua/nvim-tree/actions/init.lua +++ b/lua/nvim-tree/actions/init.lua @@ -403,13 +403,14 @@ local DEFAULT_MAPPING_CONFIG = { } function M.setup(opts) - require("nvim-tree.actions.system-open").setup(opts.system_open) - require("nvim-tree.actions.trash").setup(opts.trash) + require("nvim-tree.actions.system-open").setup(opts) + require("nvim-tree.actions.trash").setup(opts) require("nvim-tree.actions.open-file").setup(opts) require("nvim-tree.actions.change-dir").setup(opts) + require("nvim-tree.actions.create-file").setup(opts) + require("nvim-tree.actions.rename-file").setup(opts) require("nvim-tree.actions.remove-file").setup(opts) require("nvim-tree.actions.copy-paste").setup(opts) - require("nvim-tree.actions.create-file").setup(opts) require("nvim-tree.actions.expand-all").setup(opts) local user_map_config = (opts.view or {}).mappings or {} diff --git a/lua/nvim-tree/actions/remove-file.lua b/lua/nvim-tree/actions/remove-file.lua index e1ea883a..3ddfeae5 100644 --- a/lua/nvim-tree/actions/remove-file.lua +++ b/lua/nvim-tree/actions/remove-file.lua @@ -86,11 +86,14 @@ function M.fn(node) events._dispatch_file_removed(node.absolute_path) clear_buffer(node.absolute_path) end - require("nvim-tree.actions.reloaders").reload_explorer() + if M.enable_reload then + require("nvim-tree.actions.reloaders").reload_explorer() + end end end function M.setup(opts) + M.enable_reload = not opts.filesystem_watchers.enable M.close_window = opts.actions.remove_file.close_window end diff --git a/lua/nvim-tree/actions/rename-file.lua b/lua/nvim-tree/actions/rename-file.lua index f699db89..5c19c727 100644 --- a/lua/nvim-tree/actions/rename-file.lua +++ b/lua/nvim-tree/actions/rename-file.lua @@ -37,9 +37,15 @@ function M.fn(with_sub) a.nvim_out_write(node.absolute_path .. " ➜ " .. new_file_path .. "\n") utils.rename_loaded_buffers(node.absolute_path, new_file_path) events._dispatch_node_renamed(abs_path, new_file_path) - require("nvim-tree.actions.reloaders").reload_explorer() + if M.enable_reload then + require("nvim-tree.actions.reloaders").reload_explorer() + end end) end end +function M.setup(opts) + M.enable_reload = not opts.filesystem_watchers.enable +end + return M diff --git a/lua/nvim-tree/actions/system-open.lua b/lua/nvim-tree/actions/system-open.lua index e44e357a..632ae9db 100644 --- a/lua/nvim-tree/actions/system-open.lua +++ b/lua/nvim-tree/actions/system-open.lua @@ -51,7 +51,7 @@ function M.fn(node) end function M.setup(opts) - M.config.system_open = opts or {} + M.config.system_open = opts.system_open or {} if #M.config.system_open.cmd == 0 then if M.config.is_windows then diff --git a/lua/nvim-tree/actions/trash.lua b/lua/nvim-tree/actions/trash.lua index 14289ec0..f2bc4e99 100644 --- a/lua/nvim-tree/actions/trash.lua +++ b/lua/nvim-tree/actions/trash.lua @@ -71,20 +71,25 @@ function M.fn(node) if node.nodes ~= nil and not node.link_to then trash_path(function() events._dispatch_folder_removed(node.absolute_path) - require("nvim-tree.actions.reloaders").reload_explorer() + if M.enable_reload then + require("nvim-tree.actions.reloaders").reload_explorer() + end end) else trash_path(function() events._dispatch_file_removed(node.absolute_path) clear_buffer(node.absolute_path) - require("nvim-tree.actions.reloaders").reload_explorer() + if M.enable_reload then + require("nvim-tree.actions.reloaders").reload_explorer() + end end) end end end function M.setup(opts) - M.config.trash = opts or {} + M.config.trash = opts.trash or {} + M.enable_reload = not opts.filesystem_watchers.enable end return M diff --git a/lua/nvim-tree/core.lua b/lua/nvim-tree/core.lua index faee0105..83f3db92 100644 --- a/lua/nvim-tree/core.lua +++ b/lua/nvim-tree/core.lua @@ -9,6 +9,9 @@ TreeExplorer = nil local first_init_done = false function M.init(foldername) + if TreeExplorer then + TreeExplorer:_clear_watchers() + end TreeExplorer = explorer.Explorer.new(foldername) if not first_init_done then events._dispatch_ready() diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua index 980358b2..05084c63 100644 --- a/lua/nvim-tree/explorer/init.lua +++ b/lua/nvim-tree/explorer/init.lua @@ -1,6 +1,7 @@ local uv = vim.loop local git = require "nvim-tree.git" +local watch = require "nvim-tree.explorer.watch" local M = {} @@ -15,6 +16,8 @@ function Explorer.new(cwd) local explorer = setmetatable({ absolute_path = cwd, nodes = {}, + watcher = watch.create_watcher(cwd), + open = true, }, Explorer) explorer:_load(explorer) return explorer @@ -30,11 +33,30 @@ function Explorer:expand(node) self:_load(node) end +function Explorer.clear_watchers_for(root_node) + local function iterate(node) + if node.watcher then + node.watcher:stop() + 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) +end + function M.setup(opts) require("nvim-tree.explorer.explore").setup(opts) require("nvim-tree.explorer.filters").setup(opts) require("nvim-tree.explorer.sorters").setup(opts) require("nvim-tree.explorer.reload").setup(opts) + require("nvim-tree.explorer.watch").setup(opts) end M.Explorer = Explorer diff --git a/lua/nvim-tree/explorer/node-builders.lua b/lua/nvim-tree/explorer/node-builders.lua index d4ee42f1..1d7c1f84 100644 --- a/lua/nvim-tree/explorer/node-builders.lua +++ b/lua/nvim-tree/explorer/node-builders.lua @@ -1,5 +1,6 @@ local uv = vim.loop local utils = require "nvim-tree.utils" +local watch = require "nvim-tree.explorer.watch" local M = { is_windows = vim.fn.has "win32" == 1, @@ -18,6 +19,7 @@ function M.folder(parent, absolute_path, name) nodes = {}, open = false, parent = parent, + watcher = watch.create_watcher(absolute_path), } end @@ -49,12 +51,13 @@ end function M.link(parent, absolute_path, name) --- I dont know if this is needed, because in my understanding, there isnt hard links in windows, but just to be sure i changed it. local link_to = uv.fs_realpath(absolute_path) - local open, nodes, has_children + local open, nodes, has_children, watcher if (link_to ~= nil) and uv.fs_stat(link_to).type == "directory" then local handle = uv.fs_scandir(link_to) has_children = handle and uv.fs_scandir_next(handle) ~= nil open = false nodes = {} + watcher = watch.create_watcher(link_to) end return { @@ -67,6 +70,7 @@ function M.link(parent, absolute_path, name) nodes = nodes, open = open, parent = parent, + watcher = watcher, } end diff --git a/lua/nvim-tree/explorer/reload.lua b/lua/nvim-tree/explorer/reload.lua index 4680f479..e26a68b0 100644 --- a/lua/nvim-tree/explorer/reload.lua +++ b/lua/nvim-tree/explorer/reload.lua @@ -37,8 +37,8 @@ function M.reload(node, status) local node_ignored = node.git_status == "!!" local nodes_by_path = utils.key_by(node.nodes, "absolute_path") while true do - local name, t = uv.fs_scandir_next(handle) - if not name then + local ok, name, t = pcall(uv.fs_scandir_next, handle) + if not ok or not name then break end @@ -48,12 +48,17 @@ function M.reload(node, status) child_names[abs] = true if not nodes_by_path[abs] then if t == "directory" and uv.fs_access(abs, "R") then - table.insert(node.nodes, builders.folder(node, abs, name)) + local folder = builders.folder(node, abs, name) + nodes_by_path[abs] = folder + table.insert(node.nodes, folder) elseif t == "file" then - table.insert(node.nodes, builders.file(node, abs, name)) + local file = builders.file(node, abs, name) + nodes_by_path[abs] = file + table.insert(node.nodes, file) elseif t == "link" then local link = builders.link(node, abs, name) if link.link_to ~= nil then + nodes_by_path[abs] = link table.insert(node.nodes, link) end end diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua new file mode 100644 index 00000000..7e7441c7 --- /dev/null +++ b/lua/nvim-tree/explorer/watch.lua @@ -0,0 +1,59 @@ +local log = require "nvim-tree.log" +local utils = require "nvim-tree.utils" +local git = require "nvim-tree.git" +local Watcher = require("nvim-tree.watcher").Watcher + +local M = {} + +local function reload_and_get_git_project(path) + local project_root = git.get_project_root(path) + git.reload_project(project_root) + return project_root, git.get_project(project_root) or {} +end + +local function update_parent_statuses(node, project, root) + while project and node and node.absolute_path ~= root do + require("nvim-tree.explorer.common").update_git_status(node, false, project) + node = node.parent + end +end + +local function is_git(path) + return path:match "%.git$" ~= nil or path:match(utils.path_add_trailing ".git") ~= nil +end + +function M.create_watcher(absolute_path) + if not M.enabled then + return nil + end + if is_git(absolute_path) then + return nil + end + + log.line("watcher", "node start '%s'", absolute_path) + Watcher.new { + absolute_path = absolute_path, + interval = M.interval, + on_event = function(path) + local n = utils.get_node_from_path(absolute_path) + if not n then + return + end + log.line("watcher", "node event '%s'", path) + + local node = utils.get_parent_of_group(n) + local project_root, project = reload_and_get_git_project(path) + require("nvim-tree.explorer.reload").reload(node, project) + update_parent_statuses(node, project, project_root) + + require("nvim-tree.renderer").draw() + end, + } +end + +function M.setup(opts) + M.enabled = opts.filesystem_watchers.enable + M.interval = opts.filesystem_watchers.interval +end + +return M diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua index f6dfd978..41d6f62a 100644 --- a/lua/nvim-tree/git/init.lua +++ b/lua/nvim-tree/git/init.lua @@ -1,5 +1,8 @@ +local log = require "nvim-tree.log" +local utils = require "nvim-tree.utils" local git_utils = require "nvim-tree.git.utils" local Runner = require "nvim-tree.git.runner" +local Watcher = require("nvim-tree.watcher").Watcher local M = { config = nil, @@ -13,22 +16,37 @@ function M.reload() end for project_root in pairs(M.projects) do - M.projects[project_root] = {} - local git_status = Runner.run { - project_root = project_root, - list_untracked = git_utils.should_show_untracked(project_root), - list_ignored = true, - timeout = M.config.timeout, - } - M.projects[project_root] = { - files = git_status, - dirs = git_utils.file_status_to_dir_status(git_status, project_root), - } + M.reload_project(project_root) end return M.projects end +function M.reload_project(project_root) + local project = M.projects[project_root] + if not project or not M.config.enable then + return + end + + local watcher = M.projects[project_root].watcher + M.projects[project_root] = {} + local git_status = Runner.run { + project_root = project_root, + list_untracked = git_utils.should_show_untracked(project_root), + list_ignored = true, + timeout = M.config.timeout, + } + M.projects[project_root] = { + files = git_status, + dirs = git_utils.file_status_to_dir_status(git_status, project_root), + watcher = watcher, + } +end + +function M.get_project(project_root) + return M.projects[project_root] +end + function M.get_project_root(cwd) if M.cwd_to_project_root[cwd] then return M.cwd_to_project_root[cwd] @@ -42,6 +60,35 @@ function M.get_project_root(cwd) return project_root end +function M.reload_tree_at(project_root) + local root_node = utils.get_node_from_path(project_root) + if not root_node then + return + end + + M.reload_project(project_root) + local project = M.get_project(project_root) + + local project_files = project.files and project.files or {} + local project_dirs = project.dirs and project.dirs or {} + + local function iterate(n) + local parent_ignored = n.git_status == "!!" + for _, node in pairs(n.nodes) do + node.git_status = project_dirs[node.absolute_path] or project_files[node.absolute_path] + if not node.git_status and parent_ignored then + node.git_status = "!!" + end + + if node.nodes and #node.nodes > 0 then + iterate(node) + end + end + end + + iterate(root_node) +end + function M.load_project_status(cwd) if not M.config.enable then return {} @@ -64,15 +111,32 @@ function M.load_project_status(cwd) list_ignored = true, timeout = M.config.timeout, } + + local watcher = nil + if M.config.watcher.enable then + log.line("watcher", "git start") + watcher = Watcher.new { + absolute_path = utils.path_join { project_root, ".git" }, + interval = M.config.watcher.interval, + on_event = function() + log.line("watcher", "git event") + M.reload_tree_at(project_root) + require("nvim-tree.renderer").draw() + end, + } + end + M.projects[project_root] = { files = git_status, dirs = git_utils.file_status_to_dir_status(git_status, project_root), + watcher = watcher, } return M.projects[project_root] end function M.setup(opts) M.config = opts.git + M.config.watcher = opts.filesystem_watchers end return M diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua index 880b72f0..3669e60c 100644 --- a/lua/nvim-tree/git/runner.lua +++ b/lua/nvim-tree/git/runner.lua @@ -53,7 +53,9 @@ end function Runner:_log_raw_output(output) if output and type(output) == "string" then - log.raw("git", "%s", output) + -- TODO put this back after watcher feature completed + -- log.raw("git", "%s", output) + log.line("git", "done") end end diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua index d0ddf2d2..aee28dc3 100644 --- a/lua/nvim-tree/utils.lua +++ b/lua/nvim-tree/utils.lua @@ -96,7 +96,8 @@ function M.get_user_input_char() return vim.fn.nr2char(c) end --- get the node from the tree that matches the predicate +-- get the node and index of the node from the tree that matches the predicate. +-- The explored nodes are those displayed on the view. -- @param nodes list of node -- @param fn function(node): boolean function M.find_node(nodes, fn) @@ -126,6 +127,46 @@ function M.find_node(nodes, fn) return node, i end +-- get the node in the tree state depending on the absolute path of the node +-- (grouped or hidden too) +function M.get_node_from_path(path) + local explorer = require("nvim-tree.core").get_explorer() + if explorer.absolute_path == path then + return explorer + end + + local function iterate(nodes) + for _, node in pairs(nodes) do + if node.absolute_path == path or node.link_to == path then + return node + end + if node.nodes then + local res = iterate(node.nodes) + if res then + return res + end + end + if node.group_next then + local res = iterate { node.group_next } + if res then + return res + end + end + end + end + + return iterate(explorer.nodes) +end + +-- get the highest parent of grouped nodes +function M.get_parent_of_group(node_) + local node = node_ + while node.parent and node.parent.group_next do + node = node.parent + end + return node +end + -- return visible nodes indexed by line -- @param nodes_all list of node -- @param line_start first index diff --git a/lua/nvim-tree/watcher.lua b/lua/nvim-tree/watcher.lua new file mode 100644 index 00000000..78021451 --- /dev/null +++ b/lua/nvim-tree/watcher.lua @@ -0,0 +1,77 @@ +local uv = vim.loop + +local log = require "nvim-tree.log" +local utils = require "nvim-tree.utils" + +local M = {} +local Watcher = { + _watchers = {}, +} +Watcher.__index = Watcher + +function Watcher.new(opts) + for _, existing in ipairs(Watcher._watchers) do + if existing._opts.absolute_path == opts.absolute_path then + log.line("watcher", "Watcher:new using existing '%s'", opts.absolute_path) + return existing + end + end + + log.line("watcher", "Watcher:new '%s'", opts.absolute_path) + + local watcher = setmetatable({ + _opts = opts, + }, Watcher) + + watcher = watcher:start() + + table.insert(Watcher._watchers, watcher) + + return watcher +end + +function Watcher:start() + log.line("watcher", "Watcher:start '%s'", self._opts.absolute_path) + + local rc, _, name + + self._p, _, name = uv.new_fs_poll() + if not self._p then + self._p = nil + utils.warn( + string.format("Could not initialize an fs_poll watcher for path %s : %s", self._opts.absolute_path, name) + ) + return nil + end + + local poll_cb = vim.schedule_wrap(function(err) + if err then + log.line("watcher", "poll_cb for %s fail : %s", self._opts.absolute_path, err) + else + self._opts.on_event(self._opts.absolute_path) + end + end) + + rc, _, name = uv.fs_poll_start(self._p, self._opts.absolute_path, self._opts.interval, poll_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)) + 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) + if rc ~= 0 then + utils.warn(string.format("Could not stop the fs_poll watcher for path %s : %s", self._opts.absolute_path, name)) + end + self._p = nil + end +end + +M.Watcher = Watcher + +return M