aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLewis Russell <lewis6991@gmail.com>2024-03-01 23:31:20 +0000
committerGitHub <noreply@github.com>2024-03-01 23:31:20 +0000
commit39928a7f24916b35577864e28e505dda74103fb8 (patch)
tree8887b5622dd5edae62a53748cb226a684e8b26b4
parenta5fe8f59d98398d04bed8586cee73864bbcdde92 (diff)
parent4ff3217bbd8747d2d44680a825ac29097faf9c4b (diff)
downloadrneovim-39928a7f24916b35577864e28e505dda74103fb8.tar.gz
rneovim-39928a7f24916b35577864e28e505dda74103fb8.tar.bz2
rneovim-39928a7f24916b35577864e28e505dda74103fb8.zip
Merge pull request #27347 from lewis6991/fswatch
feat(lsp): add fswatch watchfunc backend
-rwxr-xr-x.github/scripts/install_deps.sh4
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/lua/vim/_watch.lua294
-rw-r--r--runtime/lua/vim/lsp/_watchfiles.lua17
-rw-r--r--runtime/lua/vim/lsp/client.lua1
-rw-r--r--runtime/lua/vim/lsp/health.lua40
-rw-r--r--test/functional/lua/watch_spec.lua226
-rw-r--r--test/functional/plugin/lsp_spec.lua205
-rw-r--r--test/helpers.lua51
9 files changed, 471 insertions, 370 deletions
diff --git a/.github/scripts/install_deps.sh b/.github/scripts/install_deps.sh
index 9a782e9698..ad81e053f9 100755
--- a/.github/scripts/install_deps.sh
+++ b/.github/scripts/install_deps.sh
@@ -30,12 +30,12 @@ if [[ $os == Linux ]]; then
fi
if [[ -n $TEST ]]; then
- sudo apt-get install -y locales-all cpanminus attr libattr1-dev gdb
+ sudo apt-get install -y locales-all cpanminus attr libattr1-dev gdb fswatch
fi
elif [[ $os == Darwin ]]; then
brew update --quiet
brew install ninja
if [[ -n $TEST ]]; then
- brew install cpanminus
+ brew install cpanminus fswatch
fi
fi
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 50beb79adf..516ff6f0fe 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -369,6 +369,9 @@ The following changes to existing APIs or features add new behavior.
• The `workspace/didChangeWatchedFiles` LSP client capability is now enabled
by default.
+ • On Mac or Windows, `libuv.fs_watch` is used as the backend.
+ • On Linux, `fswatch` (recommended) is used as the backend if available,
+ otherwise `libuv.fs_event` is used on each subdirectory.
• |LspRequest| autocmd callbacks now contain additional information about the LSP
request status update that occurred.
diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua
index 092781826f..d199cf8e29 100644
--- a/runtime/lua/vim/_watch.lua
+++ b/runtime/lua/vim/_watch.lua
@@ -1,45 +1,61 @@
-local M = {}
local uv = vim.uv
----@enum vim._watch.FileChangeType
-local FileChangeType = {
+local M = {}
+
+--- @enum vim._watch.FileChangeType
+--- Types of events watchers will emit.
+M.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
+--- @class vim._watch.Opts
---
----@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
+--- @field debounce? integer ms
+---
+--- 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.
+--- @field include_pattern? vim.lpeg.Pattern
---
----@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()
+--- 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.
+--- @field exclude_pattern? vim.lpeg.Pattern
+
+--- @alias vim._watch.Callback fun(path: string, change_type: vim._watch.FileChangeType)
+
+--- @class vim._watch.watch.Opts : vim._watch.Opts
+--- @field uvflags? uv.fs_event_start.flags
+
+--- @param path string
+--- @param opts? vim._watch.Opts
+local function skip(path, opts)
+ if not opts then
+ return false
end
+
+ if opts.include_pattern and opts.include_pattern:match(path) == nil then
+ return true
+ end
+
+ if opts.exclude_pattern and opts.exclude_pattern:match(path) ~= nil then
+ return true
+ end
+
+ return false
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
+--- @param path string The path to watch
+--- @param opts vim._watch.watch.Opts? Additional options:
+--- - uvflags (table|nil)
+--- Same flags as accepted by |uv.fs_event_start()|
+--- @param callback vim._watch.Callback Callback for new events
+--- @return fun() cancel Stops the watcher
function M.watch(path, opts, callback)
vim.validate({
path = { path, 'string', false },
@@ -47,111 +63,120 @@ function M.watch(path, opts, callback)
callback = { callback, 'function', false },
})
+ opts = opts or {}
+
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)
- handle = assert(handle)
+ local handle = assert(uv.new_fs_event())
+
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)
+ fullpath = vim.fs.normalize(vim.fs.joinpath(fullpath, filename))
+ end
+
+ if skip(fullpath, opts) then
+ return
end
- local change_type = events.change and M.FileChangeType.Changed or 0
+
+ --- @type vim._watch.FileChangeType
+ local change_type
if events.rename then
- local _, staterr, staterrname = vim.uv.fs_stat(fullpath)
+ local _, staterr, staterrname = uv.fs_stat(fullpath)
if staterrname == 'ENOENT' then
change_type = M.FileChangeType.Deleted
else
assert(not staterr, staterr)
change_type = M.FileChangeType.Created
end
+ elseif events.change then
+ change_type = M.FileChangeType.Changed
end
callback(fullpath, change_type)
end)
+
assert(not start_err, start_err)
+
return 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
end
---- @class watch.PollOpts
---- @field debounce? integer
---- @field include_pattern? vim.lpeg.Pattern
---- @field exclude_pattern? vim.lpeg.Pattern
+--- Initializes and starts a |uv_fs_event_t| recursively watching every directory underneath the
+--- directory at path.
+---
+--- @param path string The path to watch. Must refer to a directory.
+--- @param opts vim._watch.Opts? Additional options
+--- @param callback vim._watch.Callback Callback for new events
+--- @return fun() cancel Stops the watcher
+function M.watchdirs(path, opts, callback)
+ vim.validate({
+ path = { path, 'string', false },
+ opts = { opts, 'table', true },
+ callback = { callback, 'function', false },
+ })
----@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 = {}
+ --- Map of file path to boolean indicating if the file has been changed
+ --- at some point within the debounce cycle.
+ --- @type table<string, boolean>
+ local filechanges = {}
- 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 process_changes --- @type fun()
+ --- @param filepath string
+ --- @return uv.fs_event_start.callback
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)
+ if skip(fullpath, opts) then
+ return
end
+
+ if not filechanges[fullpath] then
+ filechanges[fullpath] = events.change or false
+ end
+ timer:start(debounce, 0, process_changes)
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
+ -- Since the callback is debounced it may have also been deleted later on
+ -- so we always need to check the existence of the file:
+ -- stat succeeds, changed=true -> Changed
+ -- stat succeeds, changed=false -> Created
+ -- stat fails -> Removed
+ for fullpath, changed 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
+ change_type = changed and M.FileChangeType.Changed or M.FileChangeType.Created
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))
+ handle:start(fullpath, {}, create_on_change(fullpath))
end
end
else
+ change_type = M.FileChangeType.Deleted
local handle = handles[fullpath]
if handle then
if not handle:is_closing() then
@@ -159,15 +184,16 @@ local function recurse_watch(path, opts, callback)
end
handles[fullpath] = nil
end
- change_type = FileChangeType.Deleted
end
callback(fullpath, change_type)
end)
end
+ filechanges = {}
end
+
local root_handle = assert(uv.new_fs_event())
handles[path] = root_handle
- root_handle:start(path, uvflags, create_on_change(path))
+ root_handle:start(path, {}, create_on_change(path))
--- "640K ought to be enough for anyone"
--- Who has folders this deep?
@@ -175,12 +201,13 @@ local function recurse_watch(path, opts, callback)
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
+ if type == 'directory' and not skip(filepath, opts) then
local handle = assert(uv.new_fs_event())
handles[filepath] = handle
- handle:start(filepath, uvflags, create_on_change(filepath))
+ handle:start(filepath, {}, create_on_change(filepath))
end
end
+
local function cancel()
for fullpath, handle in pairs(handles) do
if not handle:is_closing() then
@@ -191,34 +218,85 @@ local function recurse_watch(path, opts, callback)
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 },
+--- @param data string
+--- @param opts vim._watch.Opts?
+--- @param callback vim._watch.Callback
+local function fswatch_output_handler(data, opts, callback)
+ local d = vim.split(data, '%s+')
+
+ -- only consider the last reported event
+ local fullpath, event = d[1], d[#d]
+
+ if skip(fullpath, opts) then
+ return
+ end
+
+ --- @type integer
+ local change_type
+
+ if event == 'Created' then
+ change_type = M.FileChangeType.Created
+ elseif event == 'Removed' then
+ change_type = M.FileChangeType.Deleted
+ elseif event == 'Updated' then
+ change_type = M.FileChangeType.Changed
+ elseif event == 'Renamed' then
+ local _, staterr, staterrname = 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
+
+ if change_type then
+ callback(fullpath, change_type)
+ end
+end
+
+--- @param path string The path to watch. Must refer to a directory.
+--- @param opts vim._watch.Opts?
+--- @param callback vim._watch.Callback Callback for new events
+--- @return fun() cancel Stops the watcher
+function M.fswatch(path, opts, callback)
+ -- debounce isn't the same as latency but close enough
+ local latency = 0.5 -- seconds
+ if opts and opts.debounce then
+ latency = opts.debounce / 1000
+ end
+
+ local obj = vim.system({
+ 'fswatch',
+ '--event=Created',
+ '--event=Removed',
+ '--event=Updated',
+ '--event=Renamed',
+ '--event-flags',
+ '--recursive',
+ '--latency=' .. tostring(latency),
+ '--exclude',
+ '/.git/',
+ path,
+ }, {
+ stdout = function(err, data)
+ if err then
+ error(err)
+ end
+
+ for line in vim.gsplit(data or '', '\n', { plain = true, trimempty = true }) do
+ fswatch_output_handler(line, opts, callback)
+ end
+ end,
})
- return recurse_watch(path, opts, callback)
+
+ return function()
+ obj:kill(2)
+ end
end
return M
diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua
index 6ca60b78cd..49328fbe9b 100644
--- a/runtime/lua/vim/lsp/_watchfiles.lua
+++ b/runtime/lua/vim/lsp/_watchfiles.lua
@@ -7,7 +7,13 @@ local lpeg = vim.lpeg
local M = {}
-M._watchfunc = (vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1) and watch.watch or watch.poll
+if vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1 then
+ M._watchfunc = watch.watch
+elseif vim.fn.executable('fswatch') == 1 then
+ M._watchfunc = watch.fswatch
+else
+ M._watchfunc = watch.watchdirs
+end
---@type table<integer, table<string, function[]>> client id -> registration id -> cancel function
local cancels = vim.defaulttable()
@@ -163,4 +169,13 @@ function M.unregister(unreg, ctx)
end
end
+--- @param client_id integer
+function M.cancel(client_id)
+ for _, reg_cancels in pairs(cancels[client_id]) do
+ for _, cancel in pairs(reg_cancels) do
+ cancel()
+ end
+ end
+end
+
return M
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index fcbaadbc57..dfb5137e2a 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -815,6 +815,7 @@ function Client:_stop(force)
rpc.terminate()
self._graceful_shutdown_failed = true
end
+ vim.lsp._watchfiles.cancel(self.id)
end)
end
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index 15e4555b55..797a1097f9 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -1,10 +1,9 @@
local M = {}
---- Performs a healthcheck for LSP
-function M.check()
- local report_info = vim.health.info
- local report_warn = vim.health.warn
+local report_info = vim.health.info
+local report_warn = vim.health.warn
+local function check_log()
local log = vim.lsp.log
local current_log_level = log.get_level()
local log_level_string = log.levels[current_log_level] ---@type string
@@ -27,9 +26,11 @@ function M.check()
local report_fn = (log_size / 1000000 > 100 and report_warn or report_info)
report_fn(string.format('Log size: %d KB', log_size / 1000))
+end
- local clients = vim.lsp.get_clients()
+local function check_active_clients()
vim.health.start('vim.lsp: Active Clients')
+ local clients = vim.lsp.get_clients()
if next(clients) then
for _, client in pairs(clients) do
local attached_to = table.concat(vim.tbl_keys(client.attached_buffers or {}), ',')
@@ -48,4 +49,33 @@ function M.check()
end
end
+local function check_watcher()
+ vim.health.start('vim.lsp: File watcher')
+ local watchfunc = vim.lsp._watchfiles._watchfunc
+ assert(watchfunc)
+ local watchfunc_name --- @type string
+ if watchfunc == vim._watch.watch then
+ watchfunc_name = 'libuv-watch'
+ elseif watchfunc == vim._watch.watchdirs then
+ watchfunc_name = 'libuv-watchdirs'
+ elseif watchfunc == vim._watch.fswatch then
+ watchfunc_name = 'fswatch'
+ else
+ local nm = debug.getinfo(watchfunc, 'S').source
+ watchfunc_name = string.format('Custom (%s)', nm)
+ end
+
+ report_info('File watch backend: ' .. watchfunc_name)
+ if watchfunc_name == 'libuv-watchdirs' then
+ report_warn('libuv-watchdirs has known performance issues. Consider installing fswatch.')
+ end
+end
+
+--- Performs a healthcheck for LSP
+function M.check()
+ check_log()
+ check_active_clients()
+ check_watcher()
+end
+
return M
diff --git a/test/functional/lua/watch_spec.lua b/test/functional/lua/watch_spec.lua
index 044d707499..115fee8091 100644
--- a/test/functional/lua/watch_spec.lua
+++ b/test/functional/lua/watch_spec.lua
@@ -2,180 +2,126 @@ local helpers = require('test.functional.helpers')(after_each)
local eq = helpers.eq
local exec_lua = helpers.exec_lua
local clear = helpers.clear
+local is_ci = helpers.is_ci
local is_os = helpers.is_os
local skip = helpers.skip
+-- Create a file via a rename to avoid multiple
+-- events which can happen with some backends on some platforms
+local function touch(path)
+ local tmp = helpers.tmpname()
+ io.open(tmp, 'w'):close()
+ assert(vim.uv.fs_rename(tmp, path))
+end
+
describe('vim._watch', function()
before_each(function()
clear()
end)
- describe('watch', function()
- it('detects file changes', function()
- skip(is_os('bsd'), 'Stopped working on bsd after 3ca967387c49c754561c3b11a574797504d40f38')
- local root_dir = vim.uv.fs_mkdtemp(vim.fs.dirname(helpers.tmpname()) .. '/nvim_XXXXXXXXXX')
-
- local result = exec_lua(
- [[
- local root_dir = ...
-
- local events = {}
-
- local expected_events = 0
- local function wait_for_events()
- assert(vim.wait(100, function() return #events == expected_events end), 'Timed out waiting for expected number of events. Current events seen so far: ' .. vim.inspect(events))
- end
-
- local stop = vim._watch.watch(root_dir, {}, function(path, change_type)
- table.insert(events, { path = path, change_type = change_type })
- end)
-
- -- Only BSD seems to need some extra time for the watch to be ready to respond to events
- if vim.fn.has('bsd') then
- vim.wait(50)
- end
+ local function run(watchfunc)
+ it('detects file changes (watchfunc=' .. watchfunc .. '())', function()
+ if watchfunc == 'fswatch' then
+ skip(is_os('mac'), 'flaky test on mac')
+ skip(
+ not is_ci() and helpers.fn.executable('fswatch') == 0,
+ 'fswatch not installed and not on CI'
+ )
+ skip(is_os('win'), 'not supported on windows')
+ end
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
+ if watchfunc == 'watch' then
+ skip(is_os('bsd'), 'Stopped working on bsd after 3ca967387c49c754561c3b11a574797504d40f38')
+ else
+ skip(
+ is_os('bsd'),
+ 'kqueue only reports events on watched folder itself, not contained files #26110'
+ )
+ end
- expected_events = expected_events + 1
- wait_for_events()
+ local root_dir = vim.uv.fs_mkdtemp(vim.fs.dirname(helpers.tmpname()) .. '/nvim_XXXXXXXXXX')
- watched:close()
- os.remove(watched_path)
+ local expected_events = 0
+ local function wait_for_event()
expected_events = expected_events + 1
- wait_for_events()
-
- stop()
- -- No events should come through anymore
-
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
-
- vim.wait(50)
-
- watched:close()
- os.remove(watched_path)
-
- vim.wait(50)
-
- return events
- ]],
- root_dir
- )
-
- local expected = {
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir .. '/file',
- },
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]),
- path = root_dir .. '/file',
- },
- }
-
- -- kqueue only reports events on the watched path itself, so creating a file within a
- -- watched directory results in a "rename" libuv event on the directory.
- if is_os('bsd') then
- expected = {
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir,
- },
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir,
- },
- }
+ exec_lua(
+ [[
+ local expected_events = ...
+ assert(
+ vim.wait(3000, function()
+ return #_G.events == expected_events
+ end),
+ string.format(
+ 'Timed out waiting for expected event no. %d. Current events seen so far: %s',
+ expected_events,
+ vim.inspect(events)
+ )
+ )
+ ]],
+ expected_events
+ )
end
- eq(expected, result)
- end)
- end)
-
- describe('poll', function()
- it('detects file changes', function()
- skip(
- is_os('bsd'),
- 'kqueue only reports events on watched folder itself, not contained files #26110'
- )
- local root_dir = vim.uv.fs_mkdtemp(vim.fs.dirname(helpers.tmpname()) .. '/nvim_XXXXXXXXXX')
+ local unwatched_path = root_dir .. '/file.unwatched'
+ local watched_path = root_dir .. '/file'
- local result = exec_lua(
+ exec_lua(
[[
- local root_dir = ...
- local lpeg = vim.lpeg
-
- local events = {}
-
- local debounce = 100
- local wait_ms = debounce + 200
+ local root_dir, watchfunc = ...
- local expected_events = 0
- local function wait_for_events()
- assert(vim.wait(wait_ms, function() return #events == expected_events end), 'Timed out waiting for expected number of events. Current events seen so far: ' .. vim.inspect(events))
- end
+ _G.events = {}
- local incl = lpeg.P(root_dir) * lpeg.P("/file")^-1
- local excl = lpeg.P(root_dir..'/file.unwatched')
- local stop = vim._watch.poll(root_dir, {
- debounce = debounce,
- include_pattern = incl,
- exclude_pattern = excl,
+ _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(events, { path = path, change_type = change_type })
- end)
+ table.insert(_G.events, { path = path, change_type = change_type })
+ end)
+ ]],
+ root_dir,
+ watchfunc
+ )
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
- local unwatched_path = root_dir .. '/file.unwatched'
- local unwatched, err = io.open(unwatched_path, 'w')
- assert(not err, err)
+ if watchfunc ~= 'watch' then
+ vim.uv.sleep(200)
+ end
- expected_events = expected_events + 1
- wait_for_events()
+ touch(watched_path)
+ touch(unwatched_path)
- watched:close()
- os.remove(watched_path)
- unwatched:close()
- os.remove(unwatched_path)
+ wait_for_event()
- expected_events = expected_events + 1
- wait_for_events()
+ os.remove(watched_path)
+ os.remove(unwatched_path)
- stop()
- -- No events should come through anymore
+ wait_for_event()
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
+ exec_lua [[_G.stop_watch()]]
- watched:close()
- os.remove(watched_path)
+ -- No events should come through anymore
- return events
- ]],
- root_dir
- )
+ vim.uv.sleep(100)
+ touch(watched_path)
+ vim.uv.sleep(100)
+ os.remove(watched_path)
+ vim.uv.sleep(100)
- local created = exec_lua([[return vim._watch.FileChangeType.Created]])
- local deleted = exec_lua([[return vim._watch.FileChangeType.Deleted]])
- local expected = {
+ eq({
{
- change_type = created,
+ change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
path = root_dir .. '/file',
},
{
- change_type = deleted,
+ change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]),
path = root_dir .. '/file',
},
- }
- eq(expected, result)
+ }, exec_lua [[return _G.events]])
end)
- end)
+ end
+
+ run('watch')
+ run('watchdirs')
+ run('fswatch')
end)
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index af31d9ccae..1e787d2b0c 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -205,8 +205,8 @@ describe('LSP', function()
client.stop()
end,
on_exit = function(code, signal)
- eq(0, code, 'exit code', fake_lsp_logfile)
- eq(0, signal, 'exit signal', fake_lsp_logfile)
+ eq(0, code, 'exit code')
+ eq(0, signal, 'exit signal')
end,
settings = {
dummy = 1,
@@ -4490,113 +4490,140 @@ describe('LSP', function()
end)
describe('vim.lsp._watchfiles', function()
- it('sends notifications when files change', function()
- skip(
- is_os('bsd'),
- 'kqueue only reports events on watched folder itself, not contained files #26110'
- )
- local root_dir = tmpname()
- os.remove(root_dir)
- mkdir(root_dir)
+ local function test_filechanges(watchfunc)
+ it(
+ string.format('sends notifications when files change (watchfunc=%s)', watchfunc),
+ function()
+ if watchfunc == 'fswatch' then
+ skip(
+ not is_ci() and fn.executable('fswatch') == 0,
+ 'fswatch not installed and not on CI'
+ )
+ skip(is_os('win'), 'not supported on windows')
+ skip(is_os('mac'), 'flaky')
+ end
- exec_lua(create_server_definition)
- local result = exec_lua(
- [[
- local root_dir = ...
+ skip(
+ is_os('bsd'),
+ 'kqueue only reports events on watched folder itself, not contained files #26110'
+ )
- local server = _create_server()
- local client_id = vim.lsp.start({
- name = 'watchfiles-test',
- cmd = server.cmd,
- root_dir = root_dir,
- capabilities = {
- workspace = {
- didChangeWatchedFiles = {
- dynamicRegistration = true,
+ local root_dir = tmpname()
+ os.remove(root_dir)
+ mkdir(root_dir)
+
+ exec_lua(create_server_definition)
+ local result = exec_lua(
+ [[
+ local root_dir, watchfunc = ...
+
+ local server = _create_server()
+ local client_id = vim.lsp.start({
+ name = 'watchfiles-test',
+ cmd = server.cmd,
+ root_dir = root_dir,
+ capabilities = {
+ workspace = {
+ didChangeWatchedFiles = {
+ dynamicRegistration = true,
+ },
},
},
- },
- })
+ })
- local expected_messages = 2 -- initialize, initialized
+ require('vim.lsp._watchfiles')._watchfunc = require('vim._watch')[watchfunc]
- local watchfunc = require('vim.lsp._watchfiles')._watchfunc
- local msg_wait_timeout = watchfunc == vim._watch.poll and 2500 or 200
- local function wait_for_messages()
- assert(vim.wait(msg_wait_timeout, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
- end
+ local expected_messages = 0
- wait_for_messages()
+ local msg_wait_timeout = watchfunc == 'watch' and 200 or 2500
- vim.lsp.handlers['client/registerCapability'](nil, {
- registrations = {
- {
- id = 'watchfiles-test-0',
- method = 'workspace/didChangeWatchedFiles',
- registerOptions = {
- watchers = {
- {
- globPattern = '**/watch',
- kind = 7,
+ local function wait_for_message(incr)
+ expected_messages = expected_messages + (incr or 1)
+ assert(
+ vim.wait(msg_wait_timeout, function()
+ return #server.messages == expected_messages
+ end),
+ 'Timed out waiting for expected number of messages. Current messages seen so far: '
+ .. vim.inspect(server.messages)
+ )
+ end
+
+ wait_for_message(2) -- initialize, initialized
+
+ vim.lsp.handlers['client/registerCapability'](nil, {
+ registrations = {
+ {
+ id = 'watchfiles-test-0',
+ method = 'workspace/didChangeWatchedFiles',
+ registerOptions = {
+ watchers = {
+ {
+ globPattern = '**/watch',
+ kind = 7,
+ },
},
},
},
},
- },
- }, { client_id = client_id })
+ }, { client_id = client_id })
- if watchfunc == vim._watch.poll then
- vim.wait(100)
- end
+ if watchfunc ~= 'watch' then
+ vim.wait(100)
+ end
- local path = root_dir .. '/watch'
- local file = io.open(path, 'w')
- file:close()
+ local path = root_dir .. '/watch'
+ local tmp = vim.fn.tempname()
+ io.open(tmp, 'w'):close()
+ vim.uv.fs_rename(tmp, path)
- expected_messages = expected_messages + 1
- wait_for_messages()
+ wait_for_message()
- os.remove(path)
+ os.remove(path)
- expected_messages = expected_messages + 1
- wait_for_messages()
+ wait_for_message()
- return server.messages
- ]],
- root_dir
- )
+ vim.lsp.stop_client(client_id)
- local function watched_uri(fname)
- return exec_lua(
- [[
- local root_dir, fname = ...
- return vim.uri_from_fname(root_dir .. '/' .. fname)
- ]],
- root_dir,
- fname
- )
- end
+ return server.messages
+ ]],
+ root_dir,
+ watchfunc
+ )
- eq(4, #result)
- eq('workspace/didChangeWatchedFiles', result[3].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('watch'),
- },
- },
- }, result[3].params)
- eq('workspace/didChangeWatchedFiles', result[4].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
- uri = watched_uri('watch'),
- },
- },
- }, result[4].params)
- end)
+ local uri = vim.uri_from_fname(root_dir .. '/watch')
+
+ eq(6, #result)
+
+ eq({
+ method = 'workspace/didChangeWatchedFiles',
+ params = {
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = uri,
+ },
+ },
+ },
+ }, result[3])
+
+ eq({
+ method = 'workspace/didChangeWatchedFiles',
+ params = {
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
+ uri = uri,
+ },
+ },
+ },
+ }, result[4])
+ end
+ )
+ end
+
+ test_filechanges('watch')
+ test_filechanges('watchdirs')
+ test_filechanges('fswatch')
it('correctly registers and unregisters', function()
local root_dir = '/some_dir'
diff --git a/test/helpers.lua b/test/helpers.lua
index 24ccead8b2..3d53aa3be9 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -372,6 +372,8 @@ function module.sysname()
return uv.os_uname().sysname:lower()
end
+--- @param s 'win'|'mac'|'freebsd'|'openbsd'|'bsd'
+--- @return boolean
function module.is_os(s)
if not (s == 'win' or s == 'mac' or s == 'freebsd' or s == 'openbsd' or s == 'bsd') then
error('unknown platform: ' .. tostring(s))
@@ -396,33 +398,32 @@ local function tmpdir_is_local(dir)
return not not (dir and dir:find('Xtest'))
end
+local tmpname_id = 0
+local tmpdir = tmpdir_get()
+
--- Creates a new temporary file for use by tests.
-module.tmpname = (function()
- local seq = 0
- local tmpdir = tmpdir_get()
- return function()
- if tmpdir_is_local(tmpdir) then
- -- Cannot control os.tmpname() dir, so hack our own tmpname() impl.
- seq = seq + 1
- -- "…/Xtest_tmpdir/T42.7"
- local fname = ('%s/%s.%d'):format(tmpdir, (_G._nvim_test_id or 'nvim-test'), seq)
- io.open(fname, 'w'):close()
- return fname
- else
- local fname = os.tmpname()
- if module.is_os('win') and fname:sub(1, 2) == '\\s' then
- -- In Windows tmpname() returns a filename starting with
- -- special sequence \s, prepend $TEMP path
- return tmpdir .. fname
- elseif fname:match('^/tmp') and module.is_os('mac') then
- -- In OS X /tmp links to /private/tmp
- return '/private' .. fname
- else
- return fname
- end
- end
+function module.tmpname()
+ if tmpdir_is_local(tmpdir) then
+ -- Cannot control os.tmpname() dir, so hack our own tmpname() impl.
+ tmpname_id = tmpname_id + 1
+ -- "…/Xtest_tmpdir/T42.7"
+ local fname = ('%s/%s.%d'):format(tmpdir, (_G._nvim_test_id or 'nvim-test'), tmpname_id)
+ io.open(fname, 'w'):close()
+ return fname
end
-end)()
+
+ local fname = os.tmpname()
+ if module.is_os('win') and fname:sub(1, 2) == '\\s' then
+ -- In Windows tmpname() returns a filename starting with
+ -- special sequence \s, prepend $TEMP path
+ return tmpdir .. fname
+ elseif module.is_os('mac') and fname:match('^/tmp') then
+ -- In OS X /tmp links to /private/tmp
+ return '/private' .. fname
+ end
+
+ return fname
+end
local function deps_prefix()
local env = os.getenv('DEPS_PREFIX')