aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/_watch.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2023-11-29 22:40:31 +0000
committerJosh Rahm <joshuarahm@gmail.com>2023-11-29 22:40:31 +0000
commit339e2d15cc26fe86988ea06468d912a46c8d6f29 (patch)
treea6167fc8fcfc6ae2dc102f57b2473858eac34063 /runtime/lua/vim/_watch.lua
parent067dc73729267c0262438a6fdd66e586f8496946 (diff)
parent4a8bf24ac690004aedf5540fa440e788459e5e34 (diff)
downloadrneovim-339e2d15cc26fe86988ea06468d912a46c8d6f29.tar.gz
rneovim-339e2d15cc26fe86988ea06468d912a46c8d6f29.tar.bz2
rneovim-339e2d15cc26fe86988ea06468d912a46c8d6f29.zip
Merge remote-tracking branch 'upstream/master' into fix_repeatcmdline
Diffstat (limited to 'runtime/lua/vim/_watch.lua')
-rw-r--r--runtime/lua/vim/_watch.lua223
1 files changed, 223 insertions, 0 deletions
diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua
new file mode 100644
index 0000000000..43fce3bf7f
--- /dev/null
+++ b/runtime/lua/vim/_watch.lua
@@ -0,0 +1,223 @@
+local M = {}
+local uv = vim.uv
+
+---@enum vim._watch.FileChangeType
+local 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
+---
+---@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
+---
+---@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()
+ end
+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
+function M.watch(path, opts, callback)
+ vim.validate({
+ path = { path, 'string', false },
+ opts = { opts, 'table', true },
+ callback = { callback, 'function', false },
+ })
+
+ 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)
+ 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)
+ end
+ local change_type = events.change and M.FileChangeType.Changed or 0
+ if events.rename then
+ local _, staterr, staterrname = vim.uv.fs_stat(fullpath)
+ if staterrname == 'ENOENT' then
+ change_type = M.FileChangeType.Deleted
+ else
+ assert(not staterr, staterr)
+ change_type = M.FileChangeType.Created
+ end
+ end
+ callback(fullpath, change_type)
+ end)
+ assert(not start_err, start_err)
+ return function()
+ stop(handle)
+ end
+end
+
+--- @class watch.PollOpts
+--- @field debounce? integer
+--- @field include_pattern? vim.lpeg.Pattern
+--- @field exclude_pattern? vim.lpeg.Pattern
+
+---@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 = {}
+
+ local timer = assert(uv.new_timer())
+
+ ---@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 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)
+ end
+ end
+ end
+
+ process_changes = function()
+ ---@type table<string, table[]>
+ 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
+ table.insert(filechanges[change.fullpath], change.events)
+ end
+ end
+ for fullpath, events_list in pairs(filechanges) do
+ uv.fs_stat(fullpath, function(_, stat)
+ ---@type vim._watch.FileChangeType
+ local change_type
+ if stat then
+ change_type = FileChangeType.Created
+ for _, event in ipairs(events_list) do
+ if event.change then
+ change_type = FileChangeType.Changed
+ end
+ end
+ if stat.type == 'directory' then
+ local handle = handles[fullpath]
+ if not handle then
+ handle = assert(uv.new_fs_event())
+ handles[fullpath] = handle
+ handle:start(fullpath, uvflags, create_on_change(fullpath))
+ end
+ end
+ else
+ local handle = handles[fullpath]
+ if handle then
+ if not handle:is_closing() then
+ handle:close()
+ 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))
+
+ --- "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
+ local filepath = vim.fs.joinpath(path, name)
+ if type == 'directory' and not is_excluded(filepath) then
+ local handle = assert(uv.new_fs_event())
+ handles[filepath] = handle
+ handle:start(filepath, uvflags, create_on_change(filepath))
+ end
+ end
+ 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
+ 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