mirror of
https://github.com/neovim/neovim.git
synced 2026-06-15 16:23:48 +00:00
feat(autoread): surface autoread activity via 'busy' flag
Problem:
Old 'autoread' only did `:checktime` on focus-change and shell (":!")
commands, and only for non-hidden buffers. Since 'autoread' is now
driven by OS filewatcher events, buffers are updated much more eagerly.
This should be surfaced to the user somehow, either via a carefully
placed notification, or a minimal UI indicator.
A "notification" would be noisy, unless it is conditional on specific
circumstances (e.g. when "many" buffers are updated).
Solution:
Use the existing 'busy' buffer-local option as a subtle hint about
activity.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
--- 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.
|
||||
--- Provides 'autoread' via OS filewatchers: watches 'autoread' buffer files for external changes
|
||||
--- using vim._watch. Complements the existing FocusGained/:checktime approach.
|
||||
|
||||
local uv = vim.uv
|
||||
local watch = vim._watch
|
||||
@@ -8,13 +7,16 @@ local nvim_on = require('vim._core.util').nvim_on
|
||||
|
||||
local M = {}
|
||||
|
||||
local debounce_ms = 100
|
||||
--- @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
|
||||
--- @type table<integer, true> bufnr -> true. Tracks pending autoreads (debounce window, or :checktime in flight),
|
||||
--- so we can surface activity via the 'busy' flag.
|
||||
local pending = {}
|
||||
--- @type table<integer, true> bufnr -> true. Tracks which `pending` buffers have set 'busy'.
|
||||
local pending_busy = {}
|
||||
|
||||
--- @private
|
||||
--- Test-only: override the debounce window so tests can run faster.
|
||||
@@ -30,6 +32,35 @@ function M._is_watching(bufnr)
|
||||
return watchers[bufnr] ~= nil
|
||||
end
|
||||
|
||||
--- Sets the 'busy' option on a `pending` buffer. Idempotent: if `pending` and `pending_busy`
|
||||
--- already agree, it's a no-op. Must run on main thread.
|
||||
---
|
||||
--- @param bufnr integer
|
||||
local function sync_busy(bufnr)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
pending_busy[bufnr] = nil
|
||||
return
|
||||
end
|
||||
local want = pending[bufnr] ~= nil
|
||||
local have = pending_busy[bufnr] ~= nil
|
||||
if want == have then
|
||||
return
|
||||
end
|
||||
vim.bo[bufnr].busy = math.max(0, vim.bo[bufnr].busy + (want and 1 or -1))
|
||||
pending_busy[bufnr] = want or nil
|
||||
end
|
||||
|
||||
--- Sends `pending` state for `bufnr`.
|
||||
---
|
||||
--- @param bufnr integer
|
||||
--- @param is_pending boolean
|
||||
local function set_pending(bufnr, is_pending)
|
||||
pending[bufnr] = is_pending or nil
|
||||
vim.schedule(function()
|
||||
sync_busy(bufnr)
|
||||
end)
|
||||
end
|
||||
|
||||
--- 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.
|
||||
@@ -68,6 +99,7 @@ end
|
||||
--- Stops and cleans up the watcher for a buffer.
|
||||
--- @param bufnr integer
|
||||
local function stop_watcher(bufnr)
|
||||
set_pending(bufnr, false)
|
||||
local cancel = watchers[bufnr]
|
||||
if cancel then
|
||||
cancel()
|
||||
@@ -96,14 +128,20 @@ local function ensure_watcher(bufnr)
|
||||
timers[bufnr] = timer
|
||||
|
||||
local cancel = watch.watch(name, {}, function(_, change_type)
|
||||
-- Set the 'busy' buffer option for the duration of the pending cycle. This is a small, "best
|
||||
-- effort" UX hint, not intended to be noticeable except when filewatcher activity is "noisy".
|
||||
set_pending(bufnr, true)
|
||||
-- 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()
|
||||
sync_busy(bufnr)
|
||||
if not vim.api.nvim_buf_is_loaded(bufnr) or not buf_autoread(bufnr) then
|
||||
set_pending(bufnr, false)
|
||||
return
|
||||
end
|
||||
vim.cmd.checktime(bufnr)
|
||||
set_pending(bufnr, false)
|
||||
-- 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
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('autoread file watcher', function()
|
||||
-- Give the watcher time to fire; the buffer must NOT be reloaded because
|
||||
-- it has unsaved changes (autoread only reloads unmodified buffers).
|
||||
sleep(50)
|
||||
-- Also do a manual checktime to be sure
|
||||
-- Also do a manual :checktime to be sure
|
||||
command('silent! checktime')
|
||||
-- Buffer should still have local changes (autoread doesn't override modified buffers)
|
||||
eq({ 'local change' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
@@ -111,7 +111,7 @@ describe('autoread file watcher', function()
|
||||
command('setlocal noautoread')
|
||||
eq(false, is_watching())
|
||||
|
||||
-- Modify externally while noautoread
|
||||
-- Modify externally while 'noautoread'.
|
||||
write_file(path, 'while disabled\n')
|
||||
sleep(50)
|
||||
eq({ 'original' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
@@ -167,6 +167,38 @@ describe('autoread file watcher', function()
|
||||
eq(1, n.exec_lua('return _G.reloads'))
|
||||
end)
|
||||
|
||||
it("bumps 'busy' on each watched buffer while a reload is pending", function()
|
||||
-- Use a longer debounce so we can sample 'busy' during pending autoreads.
|
||||
n.exec_lua([[require('nvim.autoread')._set_debounce(100)]])
|
||||
|
||||
local path1 = open_watched('a1\n')
|
||||
local buf1 = api.nvim_get_current_buf()
|
||||
command('enew')
|
||||
local path2 = open_watched('a2\n')
|
||||
local buf2 = api.nvim_get_current_buf()
|
||||
|
||||
eq(0, api.nvim_get_option_value('busy', { buf = buf1 }))
|
||||
eq(0, api.nvim_get_option_value('busy', { buf = buf2 }))
|
||||
|
||||
-- Trigger external changes on both watched files concurrently.
|
||||
write_file(path1, 'b1\n')
|
||||
write_file(path2, 'b2\n')
|
||||
|
||||
-- Confirm busy=1 during the debounce window.
|
||||
retry(nil, 1000, function()
|
||||
eq(1, api.nvim_get_option_value('busy', { buf = buf1 }))
|
||||
eq(1, api.nvim_get_option_value('busy', { buf = buf2 }))
|
||||
end)
|
||||
|
||||
-- Confirm busy=0 after the autoread.
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'b1' }, api.nvim_buf_get_lines(buf1, 0, -1, true))
|
||||
eq({ 'b2' }, api.nvim_buf_get_lines(buf2, 0, -1, true))
|
||||
eq(0, api.nvim_get_option_value('busy', { buf = buf1 }))
|
||||
eq(0, api.nvim_get_option_value('busy', { buf = buf2 }))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('detects changes after atomic rename (external editor save)', function()
|
||||
local path = open_watched('original\n')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user