diff options
author | Justin M. Keyes <justinkz@gmail.com> | 2024-09-30 15:25:44 +0200 |
---|---|---|
committer | Justin M. Keyes <justinkz@gmail.com> | 2024-10-02 16:41:01 +0200 |
commit | 9e23b4e1852f9ad6418b45f827d1fb160611d8cf (patch) | |
tree | 6b4a20275919f355d3c3bdc3e0d4867da176938e | |
parent | 391c5084d1181a6742ebd8551efda4db7e8ca46e (diff) | |
download | rneovim-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.lua | 27 | ||||
-rw-r--r-- | test/functional/lua/watch_spec.lua | 92 |
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) |