mirror of
https://github.com/neovim/neovim.git
synced 2025-12-16 03:15:39 +00:00
feat(lsp): implement workspace/didChangeWatchedFiles (#22405)
This commit is contained in:
174
runtime/lua/vim/_watch.lua
Normal file
174
runtime/lua/vim/_watch.lua
Normal file
@@ -0,0 +1,174 @@
|
||||
local M = {}
|
||||
|
||||
--- Enumeration describing the types of events watchers will emit.
|
||||
M.FileChangeType = vim.tbl_add_reverse_lookup({
|
||||
Created = 1,
|
||||
Changed = 2,
|
||||
Deleted = 3,
|
||||
})
|
||||
|
||||
---@private
|
||||
--- Joins filepath elements by static '/' separator
|
||||
---
|
||||
---@param ... (string) The path elements.
|
||||
local function filepath_join(...)
|
||||
return table.concat({ ... }, '/')
|
||||
end
|
||||
|
||||
---@private
|
||||
--- Stops and closes a libuv |uv_fs_event_t| or |uv_fs_poll_t| handle
|
||||
---
|
||||
---@param handle (uv_fs_event_t|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
|
||||
---@returns (function) A function to stop the watch
|
||||
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.loop.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.loop.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
|
||||
|
||||
local default_poll_interval_ms = 2000
|
||||
|
||||
---@private
|
||||
--- Implementation for poll, hiding internally-used parameters.
|
||||
---
|
||||
---@param watches (table|nil) A tree structure to maintain state for recursive watches.
|
||||
--- - handle (uv_fs_poll_t)
|
||||
--- The libuv handle
|
||||
--- - cancel (function)
|
||||
--- A function that cancels the handle and all children's handles
|
||||
--- - is_dir (boolean)
|
||||
--- Indicates whether the path is a directory (and the poll should
|
||||
--- be invoked recursively)
|
||||
--- - children (table|nil)
|
||||
--- A mapping of directory entry name to its recursive watches
|
||||
local function poll_internal(path, opts, callback, watches)
|
||||
path = vim.fs.normalize(path)
|
||||
local interval = opts and opts.interval or default_poll_interval_ms
|
||||
watches = watches or {
|
||||
is_dir = true,
|
||||
}
|
||||
|
||||
if not watches.handle then
|
||||
local poll, new_err = vim.loop.new_fs_poll()
|
||||
assert(not new_err, new_err)
|
||||
watches.handle = poll
|
||||
local _, start_err = poll:start(
|
||||
path,
|
||||
interval,
|
||||
vim.schedule_wrap(function(err)
|
||||
if err == 'ENOENT' then
|
||||
return
|
||||
end
|
||||
assert(not err, err)
|
||||
poll_internal(path, opts, callback, watches)
|
||||
callback(path, M.FileChangeType.Changed)
|
||||
end)
|
||||
)
|
||||
assert(not start_err, start_err)
|
||||
callback(path, M.FileChangeType.Created)
|
||||
end
|
||||
|
||||
watches.cancel = function()
|
||||
if watches.children then
|
||||
for _, w in pairs(watches.children) do
|
||||
w.cancel()
|
||||
end
|
||||
end
|
||||
stop(watches.handle)
|
||||
end
|
||||
|
||||
if watches.is_dir then
|
||||
watches.children = watches.children or {}
|
||||
local exists = {}
|
||||
for name, ftype in vim.fs.dir(path) do
|
||||
exists[name] = true
|
||||
if not watches.children[name] then
|
||||
watches.children[name] = {
|
||||
is_dir = ftype == 'directory',
|
||||
}
|
||||
poll_internal(filepath_join(path, name), opts, callback, watches.children[name])
|
||||
end
|
||||
end
|
||||
|
||||
local newchildren = {}
|
||||
for name, watch in pairs(watches.children) do
|
||||
if exists[name] then
|
||||
newchildren[name] = watch
|
||||
else
|
||||
watch.cancel()
|
||||
watches.children[name] = nil
|
||||
callback(path .. '/' .. name, M.FileChangeType.Deleted)
|
||||
end
|
||||
end
|
||||
watches.children = newchildren
|
||||
end
|
||||
|
||||
return watches.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
|
||||
--- - interval (number|nil)
|
||||
--- Polling interval in ms as passed to |uv.fs_poll_start()|. Defaults to 2000.
|
||||
---@param callback (function) The function called when new events
|
||||
---@returns (function) A function to stop the watch.
|
||||
function M.poll(path, opts, callback)
|
||||
vim.validate({
|
||||
path = { path, 'string', false },
|
||||
opts = { opts, 'table', true },
|
||||
callback = { callback, 'function', false },
|
||||
})
|
||||
return poll_internal(path, opts, callback, nil)
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user