feat(explorer): add filesystem watchers (#1304)

* feat(explorer): add experimental watchers

This commit introduces watchers to update the tree.
This behavior is introduced behind an "filesystem_watchers" option
which should prevent instabilities.
It will become the default at some point.

Co-authored-by: Alexander Courtis <alex@courtis.org>
This commit is contained in:
Kiyan
2022-06-05 12:39:39 +02:00
committed by GitHub
parent a5793f1edb
commit b0d27c09b6
19 changed files with 379 additions and 38 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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
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

View File

@@ -165,7 +165,9 @@ local function do_paste(node, action_type, action_fn)
end
clipboard[action_type] = {}
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

View File

@@ -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)
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

View File

@@ -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 {}

View File

@@ -86,11 +86,14 @@ function M.fn(node)
events._dispatch_file_removed(node.absolute_path)
clear_buffer(node.absolute_path)
end
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

View File

@@ -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)
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

View File

@@ -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

View File

@@ -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)
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)
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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,6 +16,19 @@ function M.reload()
end
for project_root in pairs(M.projects) do
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,
@@ -23,10 +39,12 @@ function M.reload()
M.projects[project_root] = {
files = git_status,
dirs = git_utils.file_status_to_dir_status(git_status, project_root),
watcher = watcher,
}
end
end
return M.projects
function M.get_project(project_root)
return M.projects[project_root]
end
function M.get_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

View File

@@ -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

View File

@@ -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

77
lua/nvim-tree/watcher.lua Normal file
View File

@@ -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