From 5732aa706c639b3d775573d91d1139f24624629c Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Sat, 25 Feb 2023 03:07:18 -0600 Subject: feat(lsp): implement workspace/didChangeWatchedFiles (#21293) --- runtime/lua/vim/_watch.lua | 174 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 runtime/lua/vim/_watch.lua (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua new file mode 100644 index 0000000000..dba1522ec8 --- /dev/null +++ b/runtime/lua/vim/_watch.lua @@ -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 -- cgit From f0f27e9aef7c237dd55fbb5c2cd47c2f42d01742 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Sat, 25 Feb 2023 11:17:28 +0100 Subject: Revert "feat(lsp): implement workspace/didChangeWatchedFiles (#21293)" This reverts commit 5732aa706c639b3d775573d91d1139f24624629c. Causes editor to freeze in projects with many watcher registrations --- runtime/lua/vim/_watch.lua | 174 --------------------------------------------- 1 file changed, 174 deletions(-) delete mode 100644 runtime/lua/vim/_watch.lua (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua deleted file mode 100644 index dba1522ec8..0000000000 --- a/runtime/lua/vim/_watch.lua +++ /dev/null @@ -1,174 +0,0 @@ -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 -- cgit From ac69ba5fa0081026f2c5e6e29d5788802479b7b9 Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Sun, 5 Mar 2023 00:52:27 -0600 Subject: feat(lsp): implement workspace/didChangeWatchedFiles (#22405) --- runtime/lua/vim/_watch.lua | 174 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 runtime/lua/vim/_watch.lua (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua new file mode 100644 index 0000000000..dba1522ec8 --- /dev/null +++ b/runtime/lua/vim/_watch.lua @@ -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 -- cgit From 6cc76011ca28ff61f1c2f8de6d895d4c6d0a1ad8 Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Mon, 17 Apr 2023 11:50:05 -0500 Subject: fix(watchfiles): skip Created events when poll starts (#23139) --- runtime/lua/vim/_watch.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index dba1522ec8..dbffd726a2 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -88,6 +88,9 @@ local default_poll_interval_ms = 2000 --- be invoked recursively) --- - children (table|nil) --- A mapping of directory entry name to its recursive watches +-- - started (boolean|nil) +-- Whether or not the watcher has first been initialized. Used +-- to prevent a flood of Created events on startup. local function poll_internal(path, opts, callback, watches) path = vim.fs.normalize(path) local interval = opts and opts.interval or default_poll_interval_ms @@ -112,7 +115,9 @@ local function poll_internal(path, opts, callback, watches) end) ) assert(not start_err, start_err) - callback(path, M.FileChangeType.Created) + if watches.started then + callback(path, M.FileChangeType.Created) + end end watches.cancel = function() @@ -132,6 +137,7 @@ local function poll_internal(path, opts, callback, watches) if not watches.children[name] then watches.children[name] = { is_dir = ftype == 'directory', + started = watches.started, } poll_internal(filepath_join(path, name), opts, callback, watches.children[name]) end @@ -150,6 +156,8 @@ local function poll_internal(path, opts, callback, watches) watches.children = newchildren end + watches.started = true + return watches.cancel end -- cgit From 2db719f6c2b677fcbc197b02fe52764a851523b2 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 3 Jun 2023 11:06:00 +0100 Subject: feat(lua): rename vim.loop -> vim.uv (#22846) --- runtime/lua/vim/_watch.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index dbffd726a2..3bd8a56f6e 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -46,7 +46,7 @@ function M.watch(path, opts, callback) path = vim.fs.normalize(path) local uvflags = opts and opts.uvflags or {} - local handle, new_err = vim.loop.new_fs_event() + 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) @@ -57,7 +57,7 @@ function M.watch(path, opts, callback) end local change_type = events.change and M.FileChangeType.Changed or 0 if events.rename then - local _, staterr, staterrname = vim.loop.fs_stat(fullpath) + local _, staterr, staterrname = vim.uv.fs_stat(fullpath) if staterrname == 'ENOENT' then change_type = M.FileChangeType.Deleted else @@ -99,7 +99,7 @@ local function poll_internal(path, opts, callback, watches) } if not watches.handle then - local poll, new_err = vim.loop.new_fs_poll() + local poll, new_err = vim.uv.new_fs_poll() assert(not new_err, new_err) watches.handle = poll local _, start_err = poll:start( -- cgit From 79a5b89d66db74560e751561542064674e980146 Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Wed, 14 Jun 2023 05:40:11 -0500 Subject: perf(lsp): reduce polling handles for workspace/didChangeWatchedFiles (#23500) Co-authored-by: Lewis Russell --- runtime/lua/vim/_watch.lua | 73 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 3bd8a56f6e..d489cef9fc 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -11,6 +11,7 @@ M.FileChangeType = vim.tbl_add_reverse_lookup({ --- Joins filepath elements by static '/' separator --- ---@param ... (string) The path elements. +---@return string local function filepath_join(...) return table.concat({ ... }, '/') end @@ -36,7 +37,7 @@ end --- - 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 +---@return (function) A function to stop the watch function M.watch(path, opts, callback) vim.validate({ path = { path, 'string', false }, @@ -75,10 +76,25 @@ end local default_poll_interval_ms = 2000 +--- @class watch.Watches +--- @field is_dir boolean +--- @field children? table +--- @field cancel? fun() +--- @field started? boolean +--- @field handle? uv_fs_poll_t + +--- @class watch.PollOpts +--- @field interval? integer +--- @field include_pattern? userdata +--- @field exclude_pattern? userdata + ---@private --- Implementation for poll, hiding internally-used parameters. --- ----@param watches (table|nil) A tree structure to maintain state for recursive watches. +---@param path string +---@param opts watch.PollOpts +---@param callback fun(patch: string, filechangetype: integer) +---@param watches (watch.Watches|nil) A tree structure to maintain state for recursive watches. --- - handle (uv_fs_poll_t) --- The libuv handle --- - cancel (function) @@ -88,15 +104,36 @@ local default_poll_interval_ms = 2000 --- be invoked recursively) --- - children (table|nil) --- A mapping of directory entry name to its recursive watches --- - started (boolean|nil) --- Whether or not the watcher has first been initialized. Used --- to prevent a flood of Created events on startup. +--- - started (boolean|nil) +--- Whether or not the watcher has first been initialized. Used +--- to prevent a flood of Created events on startup. +---@return fun() Cancel function 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, } + watches.cancel = function() + if watches.children then + for _, w in pairs(watches.children) do + w.cancel() + end + end + if watches.handle then + stop(watches.handle) + end + end + + local function incl_match() + return not opts.include_pattern or opts.include_pattern:match(path) ~= nil + end + local function excl_match() + return opts.exclude_pattern and opts.exclude_pattern:match(path) ~= nil + end + if not watches.is_dir and not incl_match() or excl_match() then + return watches.cancel + end if not watches.handle then local poll, new_err = vim.uv.new_fs_poll() @@ -120,18 +157,9 @@ local function poll_internal(path, opts, callback, watches) end 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 = {} + local exists = {} --- @type table for name, ftype in vim.fs.dir(path) do exists[name] = true if not watches.children[name] then @@ -143,14 +171,16 @@ local function poll_internal(path, opts, callback, watches) end end - local newchildren = {} + local newchildren = {} ---@type table 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) + if watch.handle then + callback(path .. '/' .. name, M.FileChangeType.Deleted) + end end end watches.children = newchildren @@ -168,6 +198,15 @@ end ---@param opts (table|nil) Additional options --- - interval (number|nil) --- Polling interval in ms as passed to |uv.fs_poll_start()|. Defaults to 2000. +--- - 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 ---@returns (function) A function to stop the watch. function M.poll(path, opts, callback) -- cgit From 766f4978d6cb146511cf0b676c01e5327db46647 Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 10 Jul 2023 19:38:15 +0800 Subject: fix(lint): lint warnings #24226 --- runtime/lua/vim/_watch.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index d489cef9fc..00b7416098 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -37,7 +37,7 @@ end --- - uvflags (table|nil) --- Same flags as accepted by |uv.fs_event_start()| ---@param callback (function) The function called when new events ----@return (function) A function to stop the watch +---@return (function) Stops the watcher function M.watch(path, opts, callback) vim.validate({ path = { path, 'string', false }, @@ -208,7 +208,7 @@ end --- 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 ----@returns (function) A function to stop the watch. +---@return function Stops the watcher function M.poll(path, opts, callback) vim.validate({ path = { path, 'string', false }, -- cgit From be74807eef13ff8c90d55cf8b22b01d6d33b1641 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 18 Jul 2023 15:42:30 +0100 Subject: docs(lua): more improvements (#24387) * docs(lua): teach lua2dox how to table * docs(lua): teach gen_vimdoc.py about local functions No more need to mark local functions with @private * docs(lua): mention @nodoc and @meta in dev-lua-doc * fixup! Co-authored-by: Justin M. Keyes --------- Co-authored-by: Justin M. Keyes --- runtime/lua/vim/_watch.lua | 3 --- 1 file changed, 3 deletions(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 00b7416098..dc5f59f38b 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -7,7 +7,6 @@ M.FileChangeType = vim.tbl_add_reverse_lookup({ Deleted = 3, }) ----@private --- Joins filepath elements by static '/' separator --- ---@param ... (string) The path elements. @@ -16,7 +15,6 @@ 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 @@ -88,7 +86,6 @@ local default_poll_interval_ms = 2000 --- @field include_pattern? userdata --- @field exclude_pattern? userdata ----@private --- Implementation for poll, hiding internally-used parameters. --- ---@param path string -- cgit From c43c745a14dced87a23227d7be4f1c33d4455193 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Wed, 9 Aug 2023 11:06:13 +0200 Subject: fix(lua): improve annotations for stricter luals diagnostics (#24609) Problem: luals returns stricter diagnostics with bundled luarc.json Solution: Improve some function and type annotations: * use recognized uv.* types * disable diagnostic for global `vim` in shared.lua * docs: don't start comment lines with taglink (otherwise LuaLS will interpret it as a type) * add type alias for lpeg pattern * fix return annotation for `vim.secure.trust` * rename local Range object in vim.version (shadows `Range` in vim.treesitter) * fix some "missing fields" warnings * add missing required fields for test functions in eval.lua * rename lsp meta files for consistency --- runtime/lua/vim/_watch.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index dc5f59f38b..7230b31f6f 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -17,7 +17,7 @@ end --- 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 +---@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) @@ -79,7 +79,7 @@ local default_poll_interval_ms = 2000 --- @field children? table --- @field cancel? fun() --- @field started? boolean ---- @field handle? uv_fs_poll_t +--- @field handle? uv.uv_fs_poll_t --- @class watch.PollOpts --- @field interval? integer -- cgit From de28a0f84c577e264f37cd001b03d640db7d5ef9 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sun, 19 Nov 2023 14:25:32 +0100 Subject: perf(lsp): replace file polling on linux with per dir watcher (#26108) Should help with https://github.com/neovim/neovim/issues/23291 On linux `new_fs_event` doesn't support recursive watching, but we can still use it to watch folders. The downside of this approach is that we may end up sending some false `Deleted` events. For example, if you save a file named `foo` there will be a intermediate `foo~` due to the save mechanism of neovim. The events we get from vim.uv in that case are: - rename: foo~ - rename: foo~ - rename: foo - rename: foo - change: foo - change: foo The mechanism in this PR uses a debounce to reduce this to: - deleted: foo~ - changed: foo `foo~` will be the false positive. I suspect that for the LSP case this is good enough. If not, we may need to follow up on this and keep a table in memory that tracks available files. --- runtime/lua/vim/_watch.lua | 208 +++++++++++++++++++++++---------------------- 1 file changed, 106 insertions(+), 102 deletions(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 7230b31f6f..7870e1e867 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -1,11 +1,15 @@ local M = {} +local uv = vim.uv ---- Enumeration describing the types of events watchers will emit. -M.FileChangeType = vim.tbl_add_reverse_lookup({ +---@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 --- @@ -72,120 +76,120 @@ function M.watch(path, opts, callback) end end -local default_poll_interval_ms = 2000 - ---- @class watch.Watches ---- @field is_dir boolean ---- @field children? table ---- @field cancel? fun() ---- @field started? boolean ---- @field handle? uv.uv_fs_poll_t - --- @class watch.PollOpts ---- @field interval? integer ---- @field include_pattern? userdata ---- @field exclude_pattern? userdata +--- @field debounce? integer +--- @field include_pattern? vim.lpeg.Pattern +--- @field exclude_pattern? vim.lpeg.Pattern ---- Implementation for poll, hiding internally-used parameters. ---- ---@param path string ---@param opts watch.PollOpts ----@param callback fun(patch: string, filechangetype: integer) ----@param watches (watch.Watches|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 ---- - started (boolean|nil) ---- Whether or not the watcher has first been initialized. Used ---- to prevent a flood of Created events on startup. ----@return fun() Cancel function -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, - } - watches.cancel = function() - if watches.children then - for _, w in pairs(watches.children) do - w.cancel() - end - end - if watches.handle then - stop(watches.handle) - end +---@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 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 incl_match() - return not opts.include_pattern or opts.include_pattern:match(path) ~= nil - end - local function excl_match() - return opts.exclude_pattern and opts.exclude_pattern:match(path) ~= nil + local function is_excluded(filepath) + return opts.exclude_pattern and opts.exclude_pattern:match(filepath) end - if not watches.is_dir and not incl_match() or excl_match() then - return watches.cancel + + local process_changes = function() + assert(false, "Replaced later. I'm only here as forward reference") end - if not watches.handle then - local poll, new_err = vim.uv.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) - if watches.started then - callback(path, M.FileChangeType.Created) + 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 - if watches.is_dir then - watches.children = watches.children or {} - local exists = {} --- @type table - 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', - started = watches.started, - } - poll_internal(filepath_join(path, name), opts, callback, watches.children[name]) + process_changes = function() + ---@type 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 - - local newchildren = {} ---@type table - for name, watch in pairs(watches.children) do - if exists[name] then - newchildren[name] = watch + for fullpath, events_list in pairs(filechanges) do + local stat = uv.fs_stat(fullpath) + ---@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 - watch.cancel() - watches.children[name] = nil - if watch.handle then - callback(path .. '/' .. name, M.FileChangeType.Deleted) + 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 - watches.children = newchildren end - - watches.started = true - - return watches.cancel + 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 @@ -193,8 +197,8 @@ end --- ---@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. +--- - 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 @@ -212,7 +216,7 @@ function M.poll(path, opts, callback) opts = { opts, 'table', true }, callback = { callback, 'function', false }, }) - return poll_internal(path, opts, callback, nil) + return recurse_watch(path, opts, callback) end return M -- cgit From 7e97c773e3ba78fcddbb2a0b9b0d572c8210c83e Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Tue, 21 Nov 2023 17:46:19 +0100 Subject: perf(lsp): use async fs_stat for file watching on linux (#26123) --- runtime/lua/vim/_watch.lua | 51 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 25 deletions(-) (limited to 'runtime/lua/vim/_watch.lua') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 7870e1e867..43fce3bf7f 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -132,35 +132,36 @@ local function recurse_watch(path, opts, callback) end end for fullpath, events_list in pairs(filechanges) do - local stat = uv.fs_stat(fullpath) - ---@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 + 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 - 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)) + 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 - end - else - local handle = handles[fullpath] - if handle then - if not handle:is_closing() then - handle:close() + else + local handle = handles[fullpath] + if handle then + if not handle:is_closing() then + handle:close() + end + handles[fullpath] = nil end - handles[fullpath] = nil + change_type = FileChangeType.Deleted end - change_type = FileChangeType.Deleted - end - callback(fullpath, change_type) + callback(fullpath, change_type) + end) end end local root_handle = assert(uv.new_fs_event()) -- cgit