Skip to content

chore(watchers): Watcher shares single fs_event from Event, node watchers use unique path prefixed debounce context #1453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions lua/nvim-tree/explorer/watch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,23 @@ function M.create_watcher(absolute_path)
return nil
end

log.line("watcher", "node start '%s'", absolute_path)
return Watcher.new {
absolute_path = absolute_path,
on_event = function(opts)
log.line("watcher", "node event scheduled '%s'", opts.absolute_path)
utils.debounce("explorer:watch:" .. opts.absolute_path, M.debounce_delay, function()
refresh_path(opts.absolute_path)
end)
end,
}
local function callback(watcher)
log.line("watcher", "node event scheduled %s", watcher.context)
utils.debounce(watcher.context, M.debounce_delay, function()
refresh_path(watcher._path)
end)
end

M.uid = M.uid + 1
return Watcher:new(absolute_path, callback, {
context = "explorer:watch:" .. absolute_path .. ":" .. M.uid,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique debouncer context added so that the real/link jobs do not clobber each other.

})
end

function M.setup(opts)
M.enabled = opts.filesystem_watchers.enable
M.debounce_delay = opts.filesystem_watchers.debounce_delay
M.uid = 0
end

return M
19 changes: 10 additions & 9 deletions lua/nvim-tree/git/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,17 @@ function M.load_project_status(cwd)
local watcher = nil
if M.config.filesystem_watchers.enable then
log.line("watcher", "git start")
watcher = Watcher.new {
absolute_path = utils.path_join { project_root, ".git" },

local callback = function(w)
log.line("watcher", "git event scheduled '%s'", w.project_root)
utils.debounce("git:watcher:" .. w.project_root, M.config.filesystem_watchers.debounce_delay, function()
reload_tree_at(w.project_root)
end)
end

watcher = Watcher:new(utils.path_join { project_root, ".git" }, callback, {
project_root = project_root,
on_event = function(opts)
log.line("watcher", "git event scheduled '%s'", opts.project_root)
utils.debounce("git:watcher:" .. opts.project_root, M.config.filesystem_watchers.debounce_delay, function()
reload_tree_at(opts.project_root)
end)
end,
}
})
end

M.projects[project_root] = {
Expand Down
37 changes: 29 additions & 8 deletions lua/nvim-tree/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,17 @@ end
---Matching executable files in Windows.
---@param ext string
---@return boolean
local PATHEXT = vim.env.PATHEXT or ""
local wexe = vim.split(PATHEXT:gsub("%.", ""), ";")
local pathexts = {}
for _, v in pairs(wexe) do
pathexts[v] = true
end

function M.is_windows_exe(ext)
return pathexts[ext:upper()]
if not M.pathexts then
local PATHEXT = vim.env.PATHEXT or ""
local wexe = vim.split(PATHEXT:gsub("%.", ""), ";")
M.pathexts = {}
for _, v in pairs(wexe) do
M.pathexts[v] = true
end
end

return M.pathexts[ext:upper()]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to the change, however this hurt my eyes and I could not help tidying it.

end

function M.rename_loaded_buffers(old_path, new_path)
Expand Down Expand Up @@ -379,4 +381,23 @@ function M.clear_prompt()
end
end

-- return a new table with values from array
function M.array_shallow_clone(array)
local to = {}
for _, v in ipairs(array) do
table.insert(to, v)
end
return to
end

-- remove item from array if it exists
function M.array_remove(array, item)
for i, v in ipairs(array) do
if v == item then
table.remove(array, i)
break
end
end
end

return M
156 changes: 102 additions & 54 deletions lua/nvim-tree/watcher.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ local uv = vim.loop
local log = require "nvim-tree.log"
local utils = require "nvim-tree.utils"

local M = {
local M = {}

local Event = {
_events = {},
}
Event.__index = Event

local Watcher = {
_watchers = {},
}
local Watcher = {}
Watcher.__index = Watcher

local FS_EVENT_FLAGS = {
Expand All @@ -16,87 +22,129 @@ local FS_EVENT_FLAGS = {
recursive = false,
}

function Watcher.new(opts)
for _, existing in ipairs(M._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)
function Event:new(path)
log.line("watcher", "Event:new '%s'", path)

local watcher = setmetatable({
_opts = opts,
}, Watcher)
local e = setmetatable({
_path = path,
_fs_event = nil,
_listeners = {},
}, Event)

watcher = watcher:start()

table.insert(M._watchers, watcher)

return watcher
if e:start() then
Event._events[path] = e
return e
else
return nil
end
end

function Watcher:start()
log.line("watcher", "Watcher:start '%s'", self._opts.absolute_path)
function Event:start()
log.line("watcher", "Event:start '%s'", self._path)

local rc, _, name

self._e, _, name = uv.new_fs_event()
if not self._e then
self._e = nil
utils.notify.warn(
string.format("Could not initialize an fs_event watcher for path %s : %s", self._opts.absolute_path, name)
)
return nil
self._fs_event, _, name = uv.new_fs_event()
if not self._fs_event then
self._fs_event = nil
utils.notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self._path, name))
return false
end

local event_cb = vim.schedule_wrap(function(err, filename, events)
local event_cb = vim.schedule_wrap(function(err, filename)
if err then
log.line("watcher", "event_cb for %s fail : %s", self._opts.absolute_path, err)
log.line("watcher", "event_cb for %s fail : %s", self._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)
log.line("watcher", "event_cb '%s' '%s'", self._path, filename)
for _, listener in ipairs(self._listeners) do
listener()
end
end
end)

rc, _, name = self._e:start(self._opts.absolute_path, FS_EVENT_FLAGS, event_cb)
rc, _, name = self._fs_event:start(self._path, FS_EVENT_FLAGS, event_cb)
if rc ~= 0 then
utils.notify.warn(
string.format("Could not start the fs_event watcher for path %s : %s", self._opts.absolute_path, name)
)
return nil
utils.notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self._path, name))
return false
end

return self
return true
end

function Watcher:destroy()
log.line("watcher", "Watcher:destroy '%s'", self._opts.absolute_path)
if self._e then
local rc, _, name = self._e:stop()
function Event:add(listener)
table.insert(self._listeners, listener)
end

function Event:remove(listener)
utils.array_remove(self._listeners, listener)
if #self._listeners == 0 then
self:destroy()
end
end

function Event:destroy()
log.line("watcher", "Event:destroy '%s'", self._path)

if self._fs_event then
local rc, _, name = self._fs_event:stop()
if rc ~= 0 then
utils.notify.warn(
string.format("Could not stop the fs_event watcher for path %s : %s", self._opts.absolute_path, name)
)
utils.notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self._path, name))
end
self._e = nil
self._fs_event = nil
end
for i, w in ipairs(M._watchers) do
if w == self then
table.remove(M._watchers, i)
break
end

Event._events[self._path] = nil
end

function Watcher:new(path, callback, data)
log.line("watcher", "Watcher:new '%s'", path)

local w = setmetatable(data, Watcher)

w._event = Event._events[path] or Event:new(path)
w._listener = nil
w._path = path
w._callback = callback

if not w._event then
return nil
end

w:start()

table.insert(Watcher._watchers, w)

return w
end

function Watcher:start()
self._listener = function()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to call back to a member function like Watcher:on_event however it doesn't seem possible.

self._callback(self)
end

self._event:add(self._listener)
end

function Watcher:destroy()
log.line("watcher", "Watcher:destroy '%s'", self._path)

self._event:remove(self._listener)

utils.array_remove(Watcher._watchers, self)
end

M.Watcher = Watcher

function M.purge_watchers()
for _, watcher in pairs(M._watchers) do
watcher:destroy()
log.line("watcher", "purge_watchers")

for _, w in ipairs(utils.array_shallow_clone(Watcher._watchers)) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure i understand why a shallow clone is needed here ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watcher:destroy will remove self from Watcher._watchers. That confuses the iterator.

w:destroy()
end

for _, e in pairs(Event._events) do
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be necessary however it does not hurt.

e:destroy()
end
M._watchers = {}
end

return M