refactor(watch): general tidy up

- Rename watch.poll to watch.watchdirs
- Unify how include and exclude is applied
- Improve type hints
This commit is contained in:
Lewis Russell
2024-02-07 11:24:44 +00:00
parent b413f5d048
commit b87505e116
5 changed files with 332 additions and 366 deletions

View File

@@ -1,45 +1,61 @@
local M = {}
local uv = vim.uv
---@enum vim._watch.FileChangeType
local FileChangeType = {
local M = {}
--- @enum vim._watch.FileChangeType
--- Types of events watchers will emit.
M.FileChangeType = {
Created = 1,
Changed = 2,
Deleted = 3,
}
--- Enumeration describing the types of events watchers will emit.
M.FileChangeType = vim.tbl_add_reverse_lookup(FileChangeType)
--- Joins filepath elements by static '/' separator
--- @class vim._watch.Opts
---
---@param ... (string) The path elements.
---@return string
local function filepath_join(...)
return table.concat({ ... }, '/')
end
--- Stops and closes a libuv |uv_fs_event_t| or |uv_fs_poll_t| handle
--- @field debounce? integer ms
---
---@param handle (uv.uv_fs_event_t|uv.uv_fs_poll_t) The handle to stop
local function stop(handle)
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()
--- 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
---
--- 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
end
if opts.exclude_pattern and opts.exclude_pattern:match(path) ~= nil then
return true
end
return false
end
--- Initializes and starts a |uv_fs_event_t|
---
---@param path (string) The path to watch
---@param opts (table|nil) Additional options
--- - uvflags (table|nil)
--- Same flags as accepted by |uv.fs_event_start()|
---@param callback (function) The function called when new events
---@return (function) Stops the watcher
--- @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
function M.watch(path, opts, callback)
vim.validate({
path = { path, 'string', false },
@@ -47,49 +63,69 @@ function M.watch(path, opts, callback)
callback = { callback, 'function', false },
})
opts = opts or {}
path = vim.fs.normalize(path)
local uvflags = opts and opts.uvflags or {}
local handle, new_err = vim.uv.new_fs_event()
assert(not new_err, new_err)
handle = assert(handle)
local handle = assert(uv.new_fs_event())
local _, start_err = handle:start(path, uvflags, function(err, filename, events)
assert(not err, err)
local fullpath = path
if filename then
filename = filename:gsub('\\', '/')
fullpath = filepath_join(fullpath, filename)
fullpath = vim.fs.normalize(vim.fs.joinpath(fullpath, filename))
end
local change_type = events.change and M.FileChangeType.Changed or 0
if skip(fullpath, opts) then
return
end
--- @type vim._watch.FileChangeType
local change_type
if events.rename then
local _, staterr, staterrname = vim.uv.fs_stat(fullpath)
local _, staterr, staterrname = uv.fs_stat(fullpath)
if staterrname == 'ENOENT' then
change_type = M.FileChangeType.Deleted
else
assert(not staterr, staterr)
change_type = M.FileChangeType.Created
end
elseif events.change then
change_type = M.FileChangeType.Changed
end
callback(fullpath, change_type)
end)
assert(not start_err, start_err)
return function()
stop(handle)
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
end
end
--- @class watch.PollOpts
--- @field debounce? integer
--- @field include_pattern? vim.lpeg.Pattern
--- @field exclude_pattern? vim.lpeg.Pattern
--- 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 },
})
---@param path string
---@param opts watch.PollOpts
---@param callback function Called on new events
---@return function cancel stops the watcher
local function recurse_watch(path, opts, callback)
opts = opts or {}
local debounce = opts.debounce or 500
local uvflags = {}
---@type table<string, uv.uv_fs_event_t> handle by fullpath
local handles = {}
@@ -98,28 +134,23 @@ local function recurse_watch(path, opts, callback)
---@type table[]
local changesets = {}
local function is_included(filepath)
return opts.include_pattern and opts.include_pattern:match(filepath)
end
local function is_excluded(filepath)
return opts.exclude_pattern and opts.exclude_pattern:match(filepath)
end
local process_changes = function()
assert(false, "Replaced later. I'm only here as forward reference")
end
local process_changes --- @type fun()
--- @param filepath string
--- @return uv.fs_event_start.callback
local function create_on_change(filepath)
return function(err, filename, events)
assert(not err, err)
local fullpath = vim.fs.joinpath(filepath, filename)
if is_included(fullpath) and not is_excluded(filepath) then
table.insert(changesets, {
fullpath = fullpath,
events = events,
})
timer:start(debounce, 0, process_changes)
if skip(fullpath, opts) then
return
end
table.insert(changesets, {
fullpath = fullpath,
events = events,
})
timer:start(debounce, 0, process_changes)
end
end
@@ -128,7 +159,7 @@ local function recurse_watch(path, opts, callback)
local filechanges = vim.defaulttable()
for i, change in ipairs(changesets) do
changesets[i] = nil
if is_included(change.fullpath) and not is_excluded(change.fullpath) then
if not skip(change.fullpath) then
table.insert(filechanges[change.fullpath], change.events)
end
end
@@ -137,10 +168,10 @@ local function recurse_watch(path, opts, callback)
---@type vim._watch.FileChangeType
local change_type
if stat then
change_type = FileChangeType.Created
change_type = M.FileChangeType.Created
for _, event in ipairs(events_list) do
if event.change then
change_type = FileChangeType.Changed
change_type = M.FileChangeType.Changed
end
end
if stat.type == 'directory' then
@@ -148,10 +179,11 @@ local function recurse_watch(path, opts, callback)
if not handle then
handle = assert(uv.new_fs_event())
handles[fullpath] = handle
handle:start(fullpath, uvflags, create_on_change(fullpath))
handle:start(fullpath, {}, create_on_change(fullpath))
end
end
else
change_type = M.FileChangeType.Deleted
local handle = handles[fullpath]
if handle then
if not handle:is_closing() then
@@ -159,15 +191,15 @@ local function recurse_watch(path, opts, callback)
end
handles[fullpath] = nil
end
change_type = FileChangeType.Deleted
end
callback(fullpath, change_type)
end)
end
end
local root_handle = assert(uv.new_fs_event())
handles[path] = root_handle
root_handle:start(path, uvflags, create_on_change(path))
root_handle:start(path, {}, create_on_change(path))
--- "640K ought to be enough for anyone"
--- Who has folders this deep?
@@ -175,12 +207,13 @@ local function recurse_watch(path, opts, callback)
for name, type in vim.fs.dir(path, { depth = max_depth }) do
local filepath = vim.fs.joinpath(path, name)
if type == 'directory' and not is_excluded(filepath) then
if type == 'directory' and not skip(filepath, opts) then
local handle = assert(uv.new_fs_event())
handles[filepath] = handle
handle:start(filepath, uvflags, create_on_change(filepath))
handle:start(filepath, {}, create_on_change(filepath))
end
end
local function cancel()
for fullpath, handle in pairs(handles) do
if not handle:is_closing() then
@@ -191,34 +224,9 @@ local function recurse_watch(path, opts, callback)
timer:stop()
timer:close()
end
return cancel
end
--- Initializes and starts a |uv_fs_poll_t| recursively watching every file underneath the
--- directory at path.
---
---@param path (string) The path to watch. Must refer to a directory.
---@param opts (table|nil) Additional options
--- - debounce (number|nil)
--- Time events are debounced in ms. Defaults to 500
--- - include_pattern (LPeg pattern|nil)
--- 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.
--- - exclude_pattern (LPeg pattern|nil)
--- 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.
---@param callback (function) The function called when new events
---@return function Stops the watcher
function M.poll(path, opts, callback)
vim.validate({
path = { path, 'string', false },
opts = { opts, 'table', true },
callback = { callback, 'function', false },
})
return recurse_watch(path, opts, callback)
end
return M