Skip to content

Commit

Permalink
feat(lsp): replace fswatch with inotifywait
Browse files Browse the repository at this point in the history
This patch replaces fswatch with inotifywait from inotify-toools:

https://github.com/inotify-tools/inotify-tools

fswatch takes ~1min to set up recursively for the Samba source code
directory. inotifywait needs less than a second to do the same thing.

emcrisostomo/fswatch#321

Also it fswatch seems to be unmaintained in the meantime.

Signed-off-by: Andreas Schneider <[email protected]>
  • Loading branch information
cryptomilk committed Jun 17, 2024
1 parent 20a7eeb commit 844093c
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 50 deletions.
8 changes: 4 additions & 4 deletions runtime/doc/lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -543,10 +543,10 @@ Example: File-change detection *watch-file*
vim.api.nvim_command(
"command! -nargs=1 Watch call luaeval('watch_file(_A)', expand('<args>'))")
<
*fswatch-limitations*
When on Linux and using fswatch, you may need to increase the maximum number
of `inotify` watches and queued events as the default limit can be too low. To
increase the limit, run: >sh
*inotify-limitations*
When on Linux and using inotifywait, you may need to increase the maximum
number of `inotify` watches and queued events as the default limit can be too
low. To increase the limit, run: >sh
sysctl fs.inotify.max_user_watches=100000
sysctl fs.inotify.max_queued_events=100000
<
Expand Down
4 changes: 3 additions & 1 deletion runtime/doc/news.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ EVENTS

LSP

• TODO
• The watchfunc backend was implemented with fswatch, but deactivated by
default. The fswatch backend was replaced with inotifywait from
inotify-tools. This is a much faster backend.

LUA

Expand Down
62 changes: 29 additions & 33 deletions runtime/lua/vim/_watch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,12 @@ end
--- @param data string
--- @param opts vim._watch.Opts?
--- @param callback vim._watch.Callback
local function fswatch_output_handler(data, opts, callback)
local function inotifywait_output_handler(data, opts, callback)
local d = vim.split(data, '%s+')

-- only consider the last reported event
local fullpath, event = d[1], d[#d]
local path, event, file = d[1], d[2], d[#d]
local fullpath = vim.fs.joinpath(path, file)

if skip(fullpath, opts) then
return
Expand All @@ -240,20 +241,16 @@ local function fswatch_output_handler(data, opts, callback)
--- @type integer
local change_type

if event == 'Created' then
if event == 'CREATE' then
change_type = M.FileChangeType.Created
elseif event == 'Removed' then
elseif event == 'DELETE' then
change_type = M.FileChangeType.Deleted
elseif event == 'Updated' then
elseif event == 'MODIFY' 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
elseif event == 'MOVED_FROM' then
change_type = M.FileChangeType.Deleted
elseif event == 'MOVED_TO' then
change_type = M.FileChangeType.Created
end

if change_type then
Expand All @@ -265,24 +262,23 @@ end
--- @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

function M.inotify(path, opts, callback)
-- FIXME Follwo symlinks, yes or no?
local obj = vim.system({
'fswatch',
'--event=Created',
'--event=Removed',
'--event=Updated',
'--event=Renamed',
'--event-flags',
'inotifywait',
'--quiet', -- suppress startup messages
'--no-dereference', -- don't follow symlinks
'--monitor', -- keep listening for events forever
'--recursive',
'--latency=' .. tostring(latency),
'--exclude',
'/.git/',
'--event',
'create',
'--event',
'delete',
'--event',
'modify',
'--event',
'move',
'@.git', -- ignore git directory
path,
}, {
stderr = function(err, data)
Expand All @@ -292,11 +288,11 @@ function M.fswatch(path, opts, callback)

if data and #vim.trim(data) > 0 then
vim.schedule(function()
if vim.fn.has('linux') == 1 and vim.startswith(data, 'Event queue overflow') then
data = 'inotify(7) limit reached, see :h fswatch-limitations for more info.'
if vim.fn.has('linux') == 1 and vim.startswith(data, 'Failed to watch') then
data = 'inotify(7) limit reached, see :h inotify-limitations for more info.'
end

vim.notify('fswatch: ' .. data, vim.log.levels.ERROR)
vim.notify('inotify: ' .. data, vim.log.levels.ERROR)
end)
end
end,
Expand All @@ -306,7 +302,7 @@ function M.fswatch(path, opts, callback)
end

for line in vim.gsplit(data or '', '\n', { plain = true, trimempty = true }) do
fswatch_output_handler(line, opts, callback)
inotifywait_output_handler(line, opts, callback)
end
end,
-- --latency is locale dependent but tostring() isn't and will always have '.' as decimal point.
Expand Down
4 changes: 2 additions & 2 deletions runtime/lua/vim/lsp/_watchfiles.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ local M = {}

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
elseif vim.fn.executable('inotifywait') == 1 then
M._watchfunc = watch.inotify
else
M._watchfunc = watch.watchdirs
end
Expand Down
6 changes: 3 additions & 3 deletions runtime/lua/vim/lsp/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,16 @@ local function check_watcher()
watchfunc_name = 'libuv-watch'
elseif watchfunc == vim._watch.watchdirs then
watchfunc_name = 'libuv-watchdirs'
elseif watchfunc == vim._watch.fswatch then
watchfunc_name = 'fswatch'
elseif watchfunc == vim._watch.inotifywait then
watchfunc_name = 'inotifywait'
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.')
report_warn('libuv-watchdirs has known performance issues. Consider installing inotify-tools.')
end
end

Expand Down
9 changes: 6 additions & 3 deletions test/functional/lua/watch_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ describe('vim._watch', function()

local function run(watchfunc)
it('detects file changes (watchfunc=' .. watchfunc .. '())', function()
if watchfunc == 'fswatch' then
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('fswatch') == 0, 'fswatch not installed and not on CI')
skip(
not is_ci() and n.fn.executable('inotifywait') == 0,
'inotify-tools not installed and not on CI'
)
end

if watchfunc == 'watch' then
Expand Down Expand Up @@ -123,5 +126,5 @@ describe('vim._watch', function()

run('watch')
run('watchdirs')
run('fswatch')
run('inotify')
end)
8 changes: 4 additions & 4 deletions test/functional/plugin/lsp_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5048,12 +5048,12 @@ describe('LSP', function()
it(
string.format('sends notifications when files change (watchfunc=%s)', watchfunc),
function()
if watchfunc == 'fswatch' then
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 fn.executable('fswatch') == 0,
'fswatch not installed and not on CI'
not is_ci() and fn.executable('inotifywait') == 0,
'inotify-tools not installed and not on CI'
)
end

Expand Down Expand Up @@ -5185,7 +5185,7 @@ describe('LSP', function()

test_filechanges('watch')
test_filechanges('watchdirs')
test_filechanges('fswatch')
test_filechanges('inotify')

it('correctly registers and unregisters', function()
local root_dir = '/some_dir'
Expand Down

0 comments on commit 844093c

Please sign in to comment.