2023-11-19 06:25:32 -07:00
|
|
|
local uv = vim.uv
|
2023-03-04 23:52:27 -07:00
|
|
|
|
2024-02-07 04:24:44 -07:00
|
|
|
local M = {}
|
|
|
|
|
|
|
|
--- @enum vim._watch.FileChangeType
|
|
|
|
--- Types of events watchers will emit.
|
|
|
|
M.FileChangeType = {
|
2023-03-04 23:52:27 -07:00
|
|
|
Created = 1,
|
|
|
|
Changed = 2,
|
|
|
|
Deleted = 3,
|
2023-11-19 06:25:32 -07:00
|
|
|
}
|
|
|
|
|
2024-02-07 04:24:44 -07:00
|
|
|
--- @class vim._watch.Opts
|
2023-03-04 23:52:27 -07:00
|
|
|
---
|
2024-02-07 04:24:44 -07:00
|
|
|
--- @field debounce? integer ms
|
|
|
|
---
|
|
|
|
--- An |lpeg| pattern. Only changes to files whose full paths match the pattern
|
|
|
|
--- will be reported. Only matches against non-directoriess, all directories will
|
|
|
|
--- be watched for new potentially-matching files. exclude_pattern can be used to
|
|
|
|
--- filter out directories. When nil, matches any file name.
|
|
|
|
--- @field include_pattern? vim.lpeg.Pattern
|
2023-03-04 23:52:27 -07:00
|
|
|
---
|
2024-02-07 04:24:44 -07:00
|
|
|
--- An |lpeg| pattern. Only changes to files and directories whose full path does
|
|
|
|
--- not match the pattern will be reported. Matches against both files and
|
|
|
|
--- directories. When nil, matches nothing.
|
|
|
|
--- @field exclude_pattern? vim.lpeg.Pattern
|
|
|
|
|
|
|
|
--- @alias vim._watch.Callback fun(path: string, change_type: vim._watch.FileChangeType)
|
|
|
|
|
|
|
|
--- @class vim._watch.watch.Opts : vim._watch.Opts
|
|
|
|
--- @field uvflags? uv.fs_event_start.flags
|
|
|
|
|
|
|
|
--- @param path string
|
|
|
|
--- @param opts? vim._watch.Opts
|
|
|
|
local function skip(path, opts)
|
|
|
|
if not opts then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
if opts.include_pattern and opts.include_pattern:match(path) == nil then
|
|
|
|
return true
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
2024-02-07 04:24:44 -07:00
|
|
|
|
|
|
|
if opts.exclude_pattern and opts.exclude_pattern:match(path) ~= nil then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
return false
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
--- Initializes and starts a |uv_fs_event_t|
|
|
|
|
---
|
2024-02-07 04:24:44 -07:00
|
|
|
--- @param path string The path to watch
|
|
|
|
--- @param opts vim._watch.watch.Opts? Additional options:
|
|
|
|
--- - uvflags (table|nil)
|
|
|
|
--- Same flags as accepted by |uv.fs_event_start()|
|
|
|
|
--- @param callback vim._watch.Callback Callback for new events
|
|
|
|
--- @return fun() cancel Stops the watcher
|
2023-03-04 23:52:27 -07:00
|
|
|
function M.watch(path, opts, callback)
|
|
|
|
vim.validate({
|
|
|
|
path = { path, 'string', false },
|
|
|
|
opts = { opts, 'table', true },
|
|
|
|
callback = { callback, 'function', false },
|
|
|
|
})
|
|
|
|
|
2024-02-07 04:24:44 -07:00
|
|
|
opts = opts or {}
|
|
|
|
|
2023-03-04 23:52:27 -07:00
|
|
|
path = vim.fs.normalize(path)
|
|
|
|
local uvflags = opts and opts.uvflags or {}
|
2024-02-07 04:24:44 -07:00
|
|
|
local handle = assert(uv.new_fs_event())
|
|
|
|
|
2023-03-04 23:52:27 -07:00
|
|
|
local _, start_err = handle:start(path, uvflags, function(err, filename, events)
|
|
|
|
assert(not err, err)
|
|
|
|
local fullpath = path
|
|
|
|
if filename then
|
2024-02-07 04:24:44 -07:00
|
|
|
fullpath = vim.fs.normalize(vim.fs.joinpath(fullpath, filename))
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
2024-02-07 04:24:44 -07:00
|
|
|
|
|
|
|
if skip(fullpath, opts) then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
--- @type vim._watch.FileChangeType
|
|
|
|
local change_type
|
2023-03-04 23:52:27 -07:00
|
|
|
if events.rename then
|
2024-02-07 04:24:44 -07:00
|
|
|
local _, staterr, staterrname = uv.fs_stat(fullpath)
|
2023-03-04 23:52:27 -07:00
|
|
|
if staterrname == 'ENOENT' then
|
|
|
|
change_type = M.FileChangeType.Deleted
|
|
|
|
else
|
|
|
|
assert(not staterr, staterr)
|
|
|
|
change_type = M.FileChangeType.Created
|
|
|
|
end
|
2024-02-07 04:24:44 -07:00
|
|
|
elseif events.change then
|
|
|
|
change_type = M.FileChangeType.Changed
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
|
|
|
callback(fullpath, change_type)
|
|
|
|
end)
|
2024-02-07 04:24:44 -07:00
|
|
|
|
2023-03-04 23:52:27 -07:00
|
|
|
assert(not start_err, start_err)
|
2024-02-07 04:24:44 -07:00
|
|
|
|
2023-03-04 23:52:27 -07:00
|
|
|
return function()
|
2024-02-07 04:24:44 -07:00
|
|
|
local _, stop_err = handle:stop()
|
|
|
|
assert(not stop_err, stop_err)
|
|
|
|
local is_closing, close_err = handle:is_closing()
|
|
|
|
assert(not close_err, close_err)
|
|
|
|
if not is_closing then
|
|
|
|
handle:close()
|
|
|
|
end
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-02-07 04:24:44 -07:00
|
|
|
--- Initializes and starts a |uv_fs_event_t| recursively watching every directory underneath the
|
|
|
|
--- directory at path.
|
|
|
|
---
|
|
|
|
--- @param path string The path to watch. Must refer to a directory.
|
|
|
|
--- @param opts vim._watch.Opts? Additional options
|
|
|
|
--- @param callback vim._watch.Callback Callback for new events
|
|
|
|
--- @return fun() cancel Stops the watcher
|
|
|
|
function M.watchdirs(path, opts, callback)
|
|
|
|
vim.validate({
|
|
|
|
path = { path, 'string', false },
|
|
|
|
opts = { opts, 'table', true },
|
|
|
|
callback = { callback, 'function', false },
|
|
|
|
})
|
2023-06-14 03:40:11 -07:00
|
|
|
|
2023-11-19 06:25:32 -07:00
|
|
|
opts = opts or {}
|
|
|
|
local debounce = opts.debounce or 500
|
2024-02-07 04:24:44 -07:00
|
|
|
|
2023-11-19 06:25:32 -07:00
|
|
|
---@type table<string, uv.uv_fs_event_t> handle by fullpath
|
|
|
|
local handles = {}
|
|
|
|
|
|
|
|
local timer = assert(uv.new_timer())
|
|
|
|
|
2024-02-07 04:27:37 -07:00
|
|
|
--- Map of file path to boolean indicating if the file has been changed
|
|
|
|
--- at some point within the debounce cycle.
|
|
|
|
--- @type table<string, boolean>
|
|
|
|
local filechanges = {}
|
2023-11-19 06:25:32 -07:00
|
|
|
|
2024-02-07 04:24:44 -07:00
|
|
|
local process_changes --- @type fun()
|
2023-03-04 23:52:27 -07:00
|
|
|
|
2024-02-07 04:24:44 -07:00
|
|
|
--- @param filepath string
|
|
|
|
--- @return uv.fs_event_start.callback
|
2023-11-19 06:25:32 -07:00
|
|
|
local function create_on_change(filepath)
|
|
|
|
return function(err, filename, events)
|
|
|
|
assert(not err, err)
|
|
|
|
local fullpath = vim.fs.joinpath(filepath, filename)
|
2024-02-07 04:24:44 -07:00
|
|
|
if skip(fullpath, opts) then
|
|
|
|
return
|
2023-11-19 06:25:32 -07:00
|
|
|
end
|
2024-02-07 04:24:44 -07:00
|
|
|
|
2024-02-07 04:27:37 -07:00
|
|
|
if not filechanges[fullpath] then
|
|
|
|
filechanges[fullpath] = events.change or false
|
|
|
|
end
|
2024-02-07 04:24:44 -07:00
|
|
|
timer:start(debounce, 0, process_changes)
|
2023-04-17 09:50:05 -07:00
|
|
|
end
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
|
|
|
|
2023-11-19 06:25:32 -07:00
|
|
|
process_changes = function()
|
2024-02-07 04:27:37 -07:00
|
|
|
-- Since the callback is debounced it may have also been deleted later on
|
|
|
|
-- so we always need to check the existence of the file:
|
|
|
|
-- stat succeeds, changed=true -> Changed
|
|
|
|
-- stat succeeds, changed=false -> Created
|
|
|
|
-- stat fails -> Removed
|
|
|
|
for fullpath, changed in pairs(filechanges) do
|
2023-11-21 09:46:19 -07:00
|
|
|
uv.fs_stat(fullpath, function(_, stat)
|
|
|
|
---@type vim._watch.FileChangeType
|
|
|
|
local change_type
|
|
|
|
if stat then
|
2024-02-07 04:27:37 -07:00
|
|
|
change_type = changed and M.FileChangeType.Changed or M.FileChangeType.Created
|
2023-11-21 09:46:19 -07:00
|
|
|
if stat.type == 'directory' then
|
|
|
|
local handle = handles[fullpath]
|
|
|
|
if not handle then
|
|
|
|
handle = assert(uv.new_fs_event())
|
|
|
|
handles[fullpath] = handle
|
2024-02-07 04:24:44 -07:00
|
|
|
handle:start(fullpath, {}, create_on_change(fullpath))
|
2023-11-21 09:46:19 -07:00
|
|
|
end
|
2023-11-19 06:25:32 -07:00
|
|
|
end
|
2023-11-21 09:46:19 -07:00
|
|
|
else
|
2024-02-07 04:24:44 -07:00
|
|
|
change_type = M.FileChangeType.Deleted
|
2023-11-21 09:46:19 -07:00
|
|
|
local handle = handles[fullpath]
|
|
|
|
if handle then
|
|
|
|
if not handle:is_closing() then
|
|
|
|
handle:close()
|
|
|
|
end
|
|
|
|
handles[fullpath] = nil
|
2023-11-19 06:25:32 -07:00
|
|
|
end
|
2023-06-14 03:40:11 -07:00
|
|
|
end
|
2023-11-21 09:46:19 -07:00
|
|
|
callback(fullpath, change_type)
|
|
|
|
end)
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
2024-02-07 04:27:37 -07:00
|
|
|
filechanges = {}
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
2024-02-07 04:24:44 -07:00
|
|
|
|
2023-11-19 06:25:32 -07:00
|
|
|
local root_handle = assert(uv.new_fs_event())
|
|
|
|
handles[path] = root_handle
|
2024-02-07 04:24:44 -07:00
|
|
|
root_handle:start(path, {}, create_on_change(path))
|
2023-11-19 06:25:32 -07:00
|
|
|
|
|
|
|
--- "640K ought to be enough for anyone"
|
|
|
|
--- Who has folders this deep?
|
|
|
|
local max_depth = 100
|
|
|
|
|
|
|
|
for name, type in vim.fs.dir(path, { depth = max_depth }) do
|
2024-05-12 12:09:06 -07:00
|
|
|
if type == 'directory' then
|
|
|
|
local filepath = vim.fs.joinpath(path, name)
|
|
|
|
if not skip(filepath, opts) then
|
|
|
|
local handle = assert(uv.new_fs_event())
|
|
|
|
handles[filepath] = handle
|
|
|
|
handle:start(filepath, {}, create_on_change(filepath))
|
|
|
|
end
|
2023-11-19 06:25:32 -07:00
|
|
|
end
|
|
|
|
end
|
2024-02-07 04:24:44 -07:00
|
|
|
|
2023-11-19 06:25:32 -07:00
|
|
|
local function cancel()
|
|
|
|
for fullpath, handle in pairs(handles) do
|
|
|
|
if not handle:is_closing() then
|
|
|
|
handle:close()
|
|
|
|
end
|
|
|
|
handles[fullpath] = nil
|
|
|
|
end
|
|
|
|
timer:stop()
|
|
|
|
timer:close()
|
|
|
|
end
|
2023-03-04 23:52:27 -07:00
|
|
|
|
2024-02-07 04:24:44 -07:00
|
|
|
return cancel
|
2023-03-04 23:52:27 -07:00
|
|
|
end
|
|
|
|
|
2024-02-07 04:28:35 -07:00
|
|
|
--- @param data string
|
|
|
|
--- @param opts vim._watch.Opts?
|
|
|
|
--- @param callback vim._watch.Callback
|
2024-07-06 02:44:19 -07:00
|
|
|
local function on_inotifywait_output(data, opts, callback)
|
2024-02-07 04:28:35 -07:00
|
|
|
local d = vim.split(data, '%s+')
|
|
|
|
|
|
|
|
-- only consider the last reported event
|
2024-07-06 02:44:19 -07:00
|
|
|
local path, event, file = d[1], d[2], d[#d]
|
|
|
|
local fullpath = vim.fs.joinpath(path, file)
|
2024-02-07 04:28:35 -07:00
|
|
|
|
|
|
|
if skip(fullpath, opts) then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
--- @type integer
|
|
|
|
local change_type
|
|
|
|
|
2024-07-06 02:44:19 -07:00
|
|
|
if event == 'CREATE' then
|
2024-02-07 04:28:35 -07:00
|
|
|
change_type = M.FileChangeType.Created
|
2024-07-06 02:44:19 -07:00
|
|
|
elseif event == 'DELETE' then
|
2024-02-07 04:28:35 -07:00
|
|
|
change_type = M.FileChangeType.Deleted
|
2024-07-06 02:44:19 -07:00
|
|
|
elseif event == 'MODIFY' then
|
2024-02-07 04:28:35 -07:00
|
|
|
change_type = M.FileChangeType.Changed
|
2024-07-06 02:44:19 -07:00
|
|
|
elseif event == 'MOVED_FROM' then
|
|
|
|
change_type = M.FileChangeType.Deleted
|
|
|
|
elseif event == 'MOVED_TO' then
|
|
|
|
change_type = M.FileChangeType.Created
|
2024-02-07 04:28:35 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
if change_type then
|
|
|
|
callback(fullpath, change_type)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--- @param path string The path to watch. Must refer to a directory.
|
|
|
|
--- @param opts vim._watch.Opts?
|
|
|
|
--- @param callback vim._watch.Callback Callback for new events
|
|
|
|
--- @return fun() cancel Stops the watcher
|
2024-07-06 02:44:19 -07:00
|
|
|
function M.inotify(path, opts, callback)
|
2024-02-07 04:28:35 -07:00
|
|
|
local obj = vim.system({
|
2024-07-06 02:44:19 -07:00
|
|
|
'inotifywait',
|
|
|
|
'--quiet', -- suppress startup messages
|
|
|
|
'--no-dereference', -- don't follow symlinks
|
|
|
|
'--monitor', -- keep listening for events forever
|
2024-02-07 04:28:35 -07:00
|
|
|
'--recursive',
|
2024-07-06 02:44:19 -07:00
|
|
|
'--event',
|
|
|
|
'create',
|
|
|
|
'--event',
|
|
|
|
'delete',
|
|
|
|
'--event',
|
|
|
|
'modify',
|
|
|
|
'--event',
|
|
|
|
'move',
|
2024-08-01 07:00:48 -07:00
|
|
|
string.format('@%s/.git', path), -- ignore git directory
|
2024-02-07 04:28:35 -07:00
|
|
|
path,
|
|
|
|
}, {
|
2024-03-03 04:54:39 -07:00
|
|
|
stderr = function(err, data)
|
|
|
|
if err then
|
|
|
|
error(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
if data and #vim.trim(data) > 0 then
|
|
|
|
vim.schedule(function()
|
2024-07-06 02:44:19 -07:00
|
|
|
if vim.fn.has('linux') == 1 and vim.startswith(data, 'Failed to watch') then
|
|
|
|
data = 'inotify(7) limit reached, see :h inotify-limitations for more info.'
|
2024-03-12 00:15:55 -07:00
|
|
|
end
|
|
|
|
|
2024-07-06 02:44:19 -07:00
|
|
|
vim.notify('inotify: ' .. data, vim.log.levels.ERROR)
|
2024-03-03 04:54:39 -07:00
|
|
|
end)
|
|
|
|
end
|
|
|
|
end,
|
2024-02-07 04:28:35 -07:00
|
|
|
stdout = function(err, data)
|
|
|
|
if err then
|
|
|
|
error(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
for line in vim.gsplit(data or '', '\n', { plain = true, trimempty = true }) do
|
2024-07-06 02:44:19 -07:00
|
|
|
on_inotifywait_output(line, opts, callback)
|
2024-02-07 04:28:35 -07:00
|
|
|
end
|
|
|
|
end,
|
2024-03-09 09:10:58 -07:00
|
|
|
-- --latency is locale dependent but tostring() isn't and will always have '.' as decimal point.
|
|
|
|
env = { LC_NUMERIC = 'C' },
|
2024-02-07 04:28:35 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
return function()
|
|
|
|
obj:kill(2)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return M
|