aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2024-09-30 15:25:44 +0200
committerJustin M. Keyes <justinkz@gmail.com>2024-10-02 16:41:01 +0200
commit9e23b4e1852f9ad6418b45f827d1fb160611d8cf (patch)
tree6b4a20275919f355d3c3bdc3e0d4867da176938e
parent391c5084d1181a6742ebd8551efda4db7e8ca46e (diff)
downloadrneovim-9e23b4e1852f9ad6418b45f827d1fb160611d8cf.tar.gz
rneovim-9e23b4e1852f9ad6418b45f827d1fb160611d8cf.tar.bz2
rneovim-9e23b4e1852f9ad6418b45f827d1fb160611d8cf.zip
fix(watch): ignore nonexistent paths (ENOENT)
Problem: The `_watch.watch()` strategy may fail if the given path does not exist: …/vim/_watch.lua:101: ENOENT: no such file or directory stack traceback: [C]: in function 'assert' …/vim/_watch.lua:101: in function <…/vim/_watch.lua:61> [string "<nvim>"]:5: in main chunk - `_watch.watch()` actively asserts any error returned by `handle:start()`. - whereas `_watch.watchdirs()` just ignores the result of `root_handle:start()`. Servers may send "client/registerCapability" with "workspace/didChangeWatchedFiles" item(s) (`baseUri`) which do not actually exist on the filesystem: https://github.com/neovim/neovim/issues/28058#issuecomment-2189929424 { method = "client/registerCapability", params = { registrations = { { method = "workspace/didChangeWatchedFiles", registerOptions = { watchers = { { globPattern = { baseUri = "file:///Users/does/not/exist", pattern = "**/*.{ts,js,mts,mjs,cjs,cts,json,svelte}" } }, ... } Solution: - Remove the assert in `_watch.watch()`. - Show a once-only message for both cases. - More detailed logging is blocked until we have `nvim_log` / `vim.log`. fix #28058
-rw-r--r--runtime/lua/vim/_watch.lua27
-rw-r--r--test/functional/lua/watch_spec.lua92
2 files changed, 88 insertions, 31 deletions
diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua
index 3c090af3ff..11f6742941 100644
--- a/runtime/lua/vim/_watch.lua
+++ b/runtime/lua/vim/_watch.lua
@@ -30,6 +30,8 @@ M.FileChangeType = {
--- @class vim._watch.watch.Opts : vim._watch.Opts
--- @field uvflags? uv.fs_event_start.flags
+--- Decides if `path` should be skipped.
+---
--- @param path string
--- @param opts? vim._watch.Opts
local function skip(path, opts)
@@ -69,7 +71,7 @@ function M.watch(path, opts, callback)
local uvflags = opts and opts.uvflags or {}
local handle = assert(uv.new_fs_event())
- local _, start_err = handle:start(path, uvflags, function(err, filename, events)
+ local _, start_err, start_errname = handle:start(path, uvflags, function(err, filename, events)
assert(not err, err)
local fullpath = path
if filename then
@@ -96,7 +98,15 @@ function M.watch(path, opts, callback)
callback(fullpath, change_type)
end)
- assert(not start_err, start_err)
+ if start_err then
+ if start_errname == 'ENOENT' then
+ -- Server may send "workspace/didChangeWatchedFiles" with nonexistent `baseUri` path.
+ -- This is mostly a placeholder until we have `nvim_log` API.
+ vim.notify_once(('watch.watch: %s'):format(start_err), vim.log.levels.INFO)
+ end
+ -- TODO(justinmk): log important errors once we have `nvim_log` API.
+ return function() end
+ end
return function()
local _, stop_err = handle:stop()
@@ -193,7 +203,18 @@ function M.watchdirs(path, opts, callback)
local root_handle = assert(uv.new_fs_event())
handles[path] = root_handle
- root_handle:start(path, {}, create_on_change(path))
+ local _, start_err, start_errname = root_handle:start(path, {}, create_on_change(path))
+
+ if start_err then
+ if start_errname == 'ENOENT' then
+ -- Server may send "workspace/didChangeWatchedFiles" with nonexistent `baseUri` path.
+ -- This is mostly a placeholder until we have `nvim_log` API.
+ vim.notify_once(('watch.watchdirs: %s'):format(start_err), vim.log.levels.INFO)
+ end
+ -- TODO(justinmk): log important errors once we have `nvim_log` API.
+
+ -- Continue. vim.fs.dir() will return nothing, so the code below is harmless.
+ end
--- "640K ought to be enough for anyone"
--- Who has folders this deep?
diff --git a/test/functional/lua/watch_spec.lua b/test/functional/lua/watch_spec.lua
index ab6b1416aa..a1e4a42ebe 100644
--- a/test/functional/lua/watch_spec.lua
+++ b/test/functional/lua/watch_spec.lua
@@ -21,16 +21,72 @@ describe('vim._watch', function()
end)
local function run(watchfunc)
- it('detects file changes (watchfunc=' .. watchfunc .. '())', function()
+ -- Monkey-patches vim.notify_once so we can "spy" on it.
+ local function spy_notify_once()
+ exec_lua [[
+ _G.__notify_once_msgs = {}
+ vim.notify_once = (function(overridden)
+ return function(msg, level, opts)
+ table.insert(_G.__notify_once_msgs, msg)
+ return overridden(msg, level, opts)
+ end
+ end)(vim.notify_once)
+ ]]
+ end
+
+ local function last_notify_once_msg()
+ return exec_lua 'return _G.__notify_once_msgs[#_G.__notify_once_msgs]'
+ end
+
+ local function do_watch(root_dir, watchfunc_)
+ exec_lua(
+ [[
+ local root_dir, watchfunc = ...
+
+ _G.events = {}
+
+ _G.stop_watch = vim._watch[watchfunc](root_dir, {
+ debounce = 100,
+ include_pattern = vim.lpeg.P(root_dir) * vim.lpeg.P("/file") ^ -1,
+ exclude_pattern = vim.lpeg.P(root_dir .. '/file.unwatched'),
+ }, function(path, change_type)
+ table.insert(_G.events, { path = path, change_type = change_type })
+ end)
+ ]],
+ root_dir,
+ watchfunc_
+ )
+ end
+
+ it(watchfunc .. '() ignores nonexistent paths', function()
+ if watchfunc == 'inotify' then
+ skip(n.fn.executable('inotifywait') == 0, 'inotifywait not found')
+ skip(is_os('bsd'), 'inotifywait on bsd CI seems to expect path to exist?')
+ end
+
+ local msg = ('watch.%s: ENOENT: no such file or directory'):format(watchfunc)
+
+ spy_notify_once()
+ do_watch('/i am /very/funny.go', watchfunc)
+
+ if watchfunc ~= 'inotify' then -- watch.inotify() doesn't (currently) call vim.notify_once.
+ t.retry(nil, 2000, function()
+ t.eq(msg, last_notify_once_msg())
+ end)
+ end
+ eq(0, exec_lua [[return #_G.events]])
+
+ exec_lua [[_G.stop_watch()]]
+ end)
+
+ it(watchfunc .. '() detects file changes', function()
if watchfunc == 'inotify' then
skip(is_os('win'), 'not supported on windows')
skip(is_os('mac'), 'flaky test on mac')
- skip(
- not is_ci() and n.fn.executable('inotifywait') == 0,
- 'inotify-tools not installed and not on CI'
- )
+ skip(not is_ci() and n.fn.executable('inotifywait') == 0, 'inotifywait not found')
end
+ -- Note: because this is not `elseif`, BSD is skipped for *all* cases...?
if watchfunc == 'watch' then
skip(is_os('mac'), 'flaky test on mac')
skip(is_os('bsd'), 'Stopped working on bsd after 3ca967387c49c754561c3b11a574797504d40f38')
@@ -41,10 +97,8 @@ describe('vim._watch', function()
)
end
- local root_dir = vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX')
-
local expected_events = 0
-
+ --- Waits for a new event, or fails if no events are triggered.
local function wait_for_event()
expected_events = expected_events + 1
exec_lua(
@@ -65,26 +119,11 @@ describe('vim._watch', function()
)
end
+ local root_dir = vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX')
local unwatched_path = root_dir .. '/file.unwatched'
local watched_path = root_dir .. '/file'
- exec_lua(
- [[
- local root_dir, watchfunc = ...
-
- _G.events = {}
-
- _G.stop_watch = vim._watch[watchfunc](root_dir, {
- debounce = 100,
- include_pattern = vim.lpeg.P(root_dir) * vim.lpeg.P("/file") ^ -1,
- exclude_pattern = vim.lpeg.P(root_dir .. '/file.unwatched'),
- }, function(path, change_type)
- table.insert(_G.events, { path = path, change_type = change_type })
- end)
- ]],
- root_dir,
- watchfunc
- )
+ do_watch(root_dir, watchfunc)
if watchfunc ~= 'watch' then
vim.uv.sleep(200)
@@ -92,16 +131,13 @@ describe('vim._watch', function()
touch(watched_path)
touch(unwatched_path)
-
wait_for_event()
os.remove(watched_path)
os.remove(unwatched_path)
-
wait_for_event()
exec_lua [[_G.stop_watch()]]
-
-- No events should come through anymore
vim.uv.sleep(100)