mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-06 11:48:23 +00:00
feat(explorer): file watching that works on all platforms
This commit is contained in:
parent
e6e9123deb
commit
8399465872
2 changed files with 73 additions and 84 deletions
|
@ -115,6 +115,7 @@ function Tree:close(path)
|
||||||
local dir = self:dir(path)
|
local dir = self:dir(path)
|
||||||
local node = self:find(dir)
|
local node = self:find(dir)
|
||||||
node.open = false
|
node.open = false
|
||||||
|
node.expanded = false -- clear expanded state
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param node snacks.picker.explorer.Node
|
---@param node snacks.picker.explorer.Node
|
||||||
|
|
|
@ -3,38 +3,58 @@ local M = {}
|
||||||
local Git = require("snacks.explorer.git")
|
local Git = require("snacks.explorer.git")
|
||||||
local Tree = require("snacks.explorer.tree")
|
local Tree = require("snacks.explorer.tree")
|
||||||
|
|
||||||
|
M._watches = {} ---@type table<string, uv.uv_fs_event_t>
|
||||||
|
|
||||||
local uv = vim.uv or vim.loop
|
local uv = vim.uv or vim.loop
|
||||||
|
|
||||||
---@alias snacks.explorer.Watcher fun(path:string, opts:vim._watch.watch.Opts?, callback:vim._watch.Callback):fun()
|
|
||||||
|
|
||||||
---@return snacks.explorer.Watcher?
|
|
||||||
function M.watcher()
|
|
||||||
if not vim._watch then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if vim.fn.has("win32") == 1 or vim.fn.has("mac") == 1 then
|
|
||||||
return vim._watch.watch
|
|
||||||
elseif vim.fn.executable("inotifywait") == 1 then
|
|
||||||
return vim._watch.inotify
|
|
||||||
end
|
|
||||||
-- This is horrible for performance. Don't use it!
|
|
||||||
-- return vim._watch.watchdirs
|
|
||||||
end
|
|
||||||
|
|
||||||
local running = {} ---@type table<string, fun()>
|
|
||||||
|
|
||||||
function M.abort()
|
|
||||||
for _, abort in pairs(running) do
|
|
||||||
pcall(abort)
|
|
||||||
end
|
|
||||||
running = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local timer = assert(uv.new_timer())
|
local timer = assert(uv.new_timer())
|
||||||
|
|
||||||
|
---@param path string
|
||||||
|
---@param cb? fun(file:string, events: uv.fs_event_start.callback.events)
|
||||||
|
function M.start(path, cb)
|
||||||
|
if M._watches[path] ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local handle = assert(vim.uv.new_fs_event())
|
||||||
|
local ok, err = handle:start(path, {}, function(_, file, events)
|
||||||
|
file = path .. "/" .. file
|
||||||
|
if cb then
|
||||||
|
cb(file, events)
|
||||||
|
else
|
||||||
|
Tree:refresh(vim.fs.dirname(file))
|
||||||
|
M.refresh()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
M._watches[path] = handle
|
||||||
|
if not ok then
|
||||||
|
Snacks.notify.error("Failed to watch " .. path .. ": " .. err)
|
||||||
|
if not handle:is_closing() then
|
||||||
|
handle:close()
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param path string
|
||||||
|
function M.stop(path)
|
||||||
|
local handle = M._watches[path]
|
||||||
|
if handle then
|
||||||
|
if not handle:is_closing() then
|
||||||
|
handle:close()
|
||||||
|
end
|
||||||
|
M._watches[path] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stop all watches
|
||||||
|
function M.abort()
|
||||||
|
for path in pairs(M._watches) do
|
||||||
|
M.stop(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- batch updates and give explorer the time to update before the watcher
|
||||||
function M.refresh()
|
function M.refresh()
|
||||||
-- batch updates and give explorer the time to update before the watcher
|
timer:start(100, 0, function()
|
||||||
timer:start(500, 0, function()
|
|
||||||
local picker = Snacks.picker.get({ source = "explorer" })[1]
|
local picker = Snacks.picker.get({ source = "explorer" })[1]
|
||||||
if picker and not picker.closed and Tree:is_dirty(picker:cwd(), picker.opts) then
|
if picker and not picker.closed and Tree:is_dirty(picker:cwd(), picker.opts) then
|
||||||
if not picker.list.target then
|
if not picker.list.target then
|
||||||
|
@ -45,68 +65,36 @@ function M.refresh()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param cwd string
|
|
||||||
function M.watch_git(cwd)
|
|
||||||
local root = Snacks.git.get_root(cwd)
|
|
||||||
if not root then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local handle = assert(vim.uv.new_fs_event())
|
|
||||||
handle:start(root .. "/.git", {}, function(_, file)
|
|
||||||
if file == "index" then
|
|
||||||
Git.refresh(root)
|
|
||||||
M.refresh()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
return function()
|
|
||||||
if handle and not handle:is_closing() then
|
|
||||||
handle:close()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param cwd string
|
|
||||||
function M.watch_files(cwd)
|
|
||||||
local watch = M.watcher()
|
|
||||||
if not watch then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return watch(cwd, {
|
|
||||||
uvflags = { recursive = true },
|
|
||||||
}, function(path, changes)
|
|
||||||
-- handle deletes
|
|
||||||
while not uv.fs_stat(path) do
|
|
||||||
local p = vim.fs.dirname(path)
|
|
||||||
if p == path then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
path = p
|
|
||||||
end
|
|
||||||
Tree:refresh(path)
|
|
||||||
M.refresh()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param cwd string
|
---@param cwd string
|
||||||
function M.watch(cwd)
|
function M.watch(cwd)
|
||||||
if running[cwd] then
|
-- Track used watches
|
||||||
return
|
local used = {} ---@type table<string, boolean>
|
||||||
|
|
||||||
|
-- Watch git index
|
||||||
|
local root = Snacks.git.get_root(cwd)
|
||||||
|
if root then
|
||||||
|
used[root .. "/.git"] = true
|
||||||
|
M.start(root .. "/.git", function(file)
|
||||||
|
if vim.fs.basename(file) == "index" then
|
||||||
|
Git.refresh(root)
|
||||||
|
M.refresh()
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local watchers = { M.watch_git, M.watch_files }
|
-- Watch open directories
|
||||||
local cancel = {} ---@type (fun())[]
|
Tree:walk(Tree:find(cwd), function(node)
|
||||||
|
if node.dir and node.open then
|
||||||
for _, watch in ipairs(watchers) do
|
used[node.path] = true
|
||||||
local ok, c = pcall(watch, cwd)
|
M.start(node.path)
|
||||||
if ok and c then
|
|
||||||
cancel[#cancel + 1] = c
|
|
||||||
end
|
end
|
||||||
end
|
end)
|
||||||
|
|
||||||
running[cwd] = function()
|
-- Stop unused watches
|
||||||
vim.tbl_map(pcall, cancel)
|
for path in pairs(M._watches) do
|
||||||
running[cwd] = nil
|
if not used[path] then
|
||||||
|
M.stop(path)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue