mirror of
https://github.com/neovim/neovim.git
synced 2025-11-25 11:40:40 +00:00
Merge pull request #27347 from lewis6991/fswatch
feat(lsp): add fswatch watchfunc backend
This commit is contained in:
@@ -2,72 +2,113 @@ 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 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
|
||||
|
||||
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
|
||||
|
||||
local root_dir = vim.uv.fs_mkdtemp(vim.fs.dirname(helpers.tmpname()) .. '/nvim_XXXXXXXXXX')
|
||||
|
||||
local result = exec_lua(
|
||||
local expected_events = 0
|
||||
|
||||
local function wait_for_event()
|
||||
expected_events = expected_events + 1
|
||||
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
|
||||
|
||||
local unwatched_path = root_dir .. '/file.unwatched'
|
||||
local watched_path = root_dir .. '/file'
|
||||
|
||||
exec_lua(
|
||||
[[
|
||||
local root_dir = ...
|
||||
local root_dir, watchfunc = ...
|
||||
|
||||
local events = {}
|
||||
_G.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 watched_path = root_dir .. '/file'
|
||||
local watched, err = io.open(watched_path, 'w')
|
||||
assert(not err, err)
|
||||
|
||||
expected_events = expected_events + 1
|
||||
wait_for_events()
|
||||
|
||||
watched:close()
|
||||
os.remove(watched_path)
|
||||
|
||||
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
|
||||
_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
|
||||
root_dir,
|
||||
watchfunc
|
||||
)
|
||||
|
||||
local expected = {
|
||||
if watchfunc ~= 'watch' then
|
||||
vim.uv.sleep(200)
|
||||
end
|
||||
|
||||
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)
|
||||
touch(watched_path)
|
||||
vim.uv.sleep(100)
|
||||
os.remove(watched_path)
|
||||
vim.uv.sleep(100)
|
||||
|
||||
eq({
|
||||
{
|
||||
change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
|
||||
path = root_dir .. '/file',
|
||||
@@ -76,106 +117,11 @@ describe('vim._watch', function()
|
||||
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,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
eq(expected, result)
|
||||
}, exec_lua [[return _G.events]])
|
||||
end)
|
||||
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 result = exec_lua(
|
||||
[[
|
||||
local root_dir = ...
|
||||
local lpeg = vim.lpeg
|
||||
|
||||
local events = {}
|
||||
|
||||
local debounce = 100
|
||||
local wait_ms = debounce + 200
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
}, function(path, change_type)
|
||||
table.insert(events, { path = path, change_type = change_type })
|
||||
end)
|
||||
|
||||
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)
|
||||
|
||||
expected_events = expected_events + 1
|
||||
wait_for_events()
|
||||
|
||||
watched:close()
|
||||
os.remove(watched_path)
|
||||
unwatched:close()
|
||||
os.remove(unwatched_path)
|
||||
|
||||
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)
|
||||
|
||||
watched:close()
|
||||
os.remove(watched_path)
|
||||
|
||||
return events
|
||||
]],
|
||||
root_dir
|
||||
)
|
||||
|
||||
local created = exec_lua([[return vim._watch.FileChangeType.Created]])
|
||||
local deleted = exec_lua([[return vim._watch.FileChangeType.Deleted]])
|
||||
local expected = {
|
||||
{
|
||||
change_type = created,
|
||||
path = root_dir .. '/file',
|
||||
},
|
||||
{
|
||||
change_type = deleted,
|
||||
path = root_dir .. '/file',
|
||||
},
|
||||
}
|
||||
eq(expected, result)
|
||||
end)
|
||||
end)
|
||||
run('watch')
|
||||
run('watchdirs')
|
||||
run('fswatch')
|
||||
end)
|
||||
|
||||
@@ -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)
|
||||
if watchfunc ~= 'watch' then
|
||||
vim.wait(100)
|
||||
end
|
||||
|
||||
local path = root_dir .. '/watch'
|
||||
local tmp = vim.fn.tempname()
|
||||
io.open(tmp, 'w'):close()
|
||||
vim.uv.fs_rename(tmp, path)
|
||||
|
||||
wait_for_message()
|
||||
|
||||
os.remove(path)
|
||||
|
||||
wait_for_message()
|
||||
|
||||
vim.lsp.stop_client(client_id)
|
||||
|
||||
return server.messages
|
||||
]],
|
||||
root_dir,
|
||||
watchfunc
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
local path = root_dir .. '/watch'
|
||||
local file = io.open(path, 'w')
|
||||
file:close()
|
||||
|
||||
expected_messages = expected_messages + 1
|
||||
wait_for_messages()
|
||||
|
||||
os.remove(path)
|
||||
|
||||
expected_messages = expected_messages + 1
|
||||
wait_for_messages()
|
||||
|
||||
return server.messages
|
||||
]],
|
||||
root_dir
|
||||
)
|
||||
end
|
||||
|
||||
local function watched_uri(fname)
|
||||
return exec_lua(
|
||||
[[
|
||||
local root_dir, fname = ...
|
||||
return vim.uri_from_fname(root_dir .. '/' .. fname)
|
||||
]],
|
||||
root_dir,
|
||||
fname
|
||||
)
|
||||
end
|
||||
|
||||
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)
|
||||
test_filechanges('watch')
|
||||
test_filechanges('watchdirs')
|
||||
test_filechanges('fswatch')
|
||||
|
||||
it('correctly registers and unregisters', function()
|
||||
local root_dir = '/some_dir'
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user