mirror of
https://github.com/neovim/neovim.git
synced 2026-07-22 00:51:33 +00:00
feat(autoread): use filewatchers for OS-driven change detection #37971
Problem: The 'autoread' option only checks for file changes reactively — on FocusGained, :checktime, CmdlineEnter, etc. — by polling timestamps. External changes are not detected until the user interacts with Neovim. Solution: Add a core module (runtime/lua/nvim/autoread.lua) enabled from runtime/plugin/autoread.lua that watches each buffer's file using vim._watch.watch() (libuv fs_event). On change detection it calls :checktime, which invokes the existing buf_check_timestamp() logic for reload/prompt handling. Watchers are managed via autocmds tied to buffer lifecycle events and respect the 'autoread' option (global and buffer-local).
This commit is contained in:
committed by
GitHub
parent
c622b454b5
commit
400f247397
149
runtime/lua/nvim/autoread.lua
Normal file
149
runtime/lua/nvim/autoread.lua
Normal file
@@ -0,0 +1,149 @@
|
||||
--- Watches buffer files for external changes using vim._watch.
|
||||
--- When 'autoread' is set, external changes are detected in real-time
|
||||
--- instead of only on FocusGained/:checktime.
|
||||
|
||||
local uv = vim.uv
|
||||
local watch = vim._watch
|
||||
local nvim_on = require('vim._core.util').nvim_on
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @type table<integer, fun()> bufnr -> cancel function
|
||||
local watchers = {}
|
||||
|
||||
--- @type table<integer, uv.uv_timer_t> bufnr -> debounce timer
|
||||
local timers = {}
|
||||
|
||||
local DEBOUNCE_MS = 100
|
||||
|
||||
--- Returns the effective 'autoread' value for a buffer.
|
||||
--- 'autoread' is global-local: vim.bo[bufnr].autoread is nil when not set locally,
|
||||
--- so we must fall back to the global value.
|
||||
--- @param bufnr integer
|
||||
--- @return boolean
|
||||
local function buf_autoread(bufnr)
|
||||
local local_val = vim.bo[bufnr].autoread
|
||||
if local_val ~= nil then
|
||||
return local_val
|
||||
end
|
||||
return vim.go.autoread
|
||||
end
|
||||
|
||||
--- Returns true if the buffer should be watched.
|
||||
--- @param bufnr integer
|
||||
--- @return boolean
|
||||
local function should_watch(bufnr)
|
||||
if not vim.api.nvim_buf_is_loaded(bufnr) then
|
||||
return false
|
||||
end
|
||||
-- Skip special buffers (terminal, help, quickfix, etc.)
|
||||
if vim.bo[bufnr].buftype ~= '' then
|
||||
return false
|
||||
end
|
||||
-- Must have a file name that exists on disk
|
||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
||||
if name == '' or not uv.fs_stat(name) then
|
||||
return false
|
||||
end
|
||||
if not buf_autoread(bufnr) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Stops and cleans up the watcher for a buffer.
|
||||
--- @param bufnr integer
|
||||
local function stop_watcher(bufnr)
|
||||
local cancel = watchers[bufnr]
|
||||
if cancel then
|
||||
cancel()
|
||||
watchers[bufnr] = nil
|
||||
end
|
||||
local timer = timers[bufnr]
|
||||
if timer then
|
||||
timer:stop()
|
||||
timer:close()
|
||||
timers[bufnr] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Ensures the buffer has an active file watcher if appropriate, or stops
|
||||
--- an existing one if the buffer should no longer be watched.
|
||||
--- @param bufnr integer
|
||||
local function ensure_watcher(bufnr)
|
||||
stop_watcher(bufnr)
|
||||
|
||||
if not should_watch(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
||||
local timer = assert(uv.new_timer())
|
||||
timers[bufnr] = timer
|
||||
|
||||
local cancel = watch.watch(name, {}, function(_, change_type)
|
||||
-- Debounce: restart the same timer on each event, so only the last
|
||||
-- event in a rapid series (e.g. truncate + write) triggers checktime.
|
||||
timer:start(DEBOUNCE_MS, 0, function()
|
||||
vim.schedule(function()
|
||||
if not vim.api.nvim_buf_is_loaded(bufnr) or not buf_autoread(bufnr) then
|
||||
return
|
||||
end
|
||||
vim.cmd.checktime(bufnr)
|
||||
-- On rename events (e.g. atomic save by another editor), the watcher
|
||||
-- is now stale (watching the old inode). Re-establish it.
|
||||
if change_type ~= watch.FileChangeType.Changed then
|
||||
ensure_watcher(bufnr)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
watchers[bufnr] = cancel
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param bufnr integer
|
||||
--- @return boolean
|
||||
function M._is_watching(bufnr)
|
||||
return watchers[bufnr] ~= nil
|
||||
end
|
||||
|
||||
function M.enable()
|
||||
local group = vim.api.nvim_create_augroup('nvim.autoread', { clear = true })
|
||||
|
||||
-- (Re)start watcher when a file is loaded or written.
|
||||
nvim_on({ 'BufReadPost', 'BufWritePost' }, group, function(args)
|
||||
ensure_watcher(args.buf)
|
||||
end)
|
||||
|
||||
-- Stop watcher when buffer is unloaded or wiped out.
|
||||
nvim_on({ 'BufUnload', 'BufWipeout' }, group, function(args)
|
||||
stop_watcher(args.buf)
|
||||
end)
|
||||
|
||||
-- Clean up all watchers on exit to avoid dangling handles in the event loop.
|
||||
nvim_on('VimLeavePre', group, function()
|
||||
for bufnr in pairs(watchers) do
|
||||
stop_watcher(bufnr)
|
||||
end
|
||||
end)
|
||||
|
||||
-- React to 'autoread' option changes.
|
||||
nvim_on('OptionSet', group, { pattern = 'autoread' }, function()
|
||||
if vim.v.option_type == 'global' then
|
||||
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
|
||||
ensure_watcher(bufnr)
|
||||
end
|
||||
else
|
||||
ensure_watcher(vim.api.nvim_get_current_buf())
|
||||
end
|
||||
end)
|
||||
|
||||
-- Attach to buffers that were already loaded before enable() ran.
|
||||
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
|
||||
ensure_watcher(bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
3
runtime/lua/vim/_meta/options.gen.lua
generated
3
runtime/lua/vim/_meta/options.gen.lua
generated
@@ -172,7 +172,8 @@ vim.bo.ai = vim.bo.autoindent
|
||||
--- it has not been changed inside of Vim, automatically read it again.
|
||||
--- When the file has been deleted this is not done, so you have the text
|
||||
--- from before it was deleted. When it appears again then it is read.
|
||||
--- `timestamp`
|
||||
--- Nvim uses file system watchers to detect changes in real-time for all
|
||||
--- loaded buffers; see `timestamp` for details.
|
||||
--- If this option has a local value, use this command to switch back to
|
||||
--- using the global value:
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user