mirror of
https://github.com/neovim/neovim.git
synced 2026-06-15 08:13:45 +00:00
Merge #40220 from justinmk/fixautoread
This commit is contained in:
@@ -809,12 +809,13 @@ A jump table for the options with a short description can be found at |Q_op|.
|
||||
*'autoread'* *'ar'* *'noautoread'* *'noar'*
|
||||
'autoread' 'ar' boolean (default on)
|
||||
global or local to buffer |global-local|
|
||||
When a file has been detected to have been changed outside of Vim and
|
||||
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.
|
||||
Nvim uses file system watchers to detect changes in real-time for all
|
||||
loaded buffers; see |timestamp| for details.
|
||||
When a file was changed outside of Nvim, automatically read it again.
|
||||
Skipped if the file was deleted, so you have the text from before it
|
||||
was deleted. If the file appears again then it is read. |timestamp|
|
||||
|
||||
This is partially driven by OS filewatcher events |uv_fs_event_t|, so
|
||||
even the current buffer may be updated.
|
||||
|
||||
If this option has a local value, use this command to switch back to
|
||||
using the global value: >vim
|
||||
set autoread<
|
||||
|
||||
@@ -14,7 +14,21 @@ local watchers = {}
|
||||
--- @type table<integer, uv.uv_timer_t> bufnr -> debounce timer
|
||||
local timers = {}
|
||||
|
||||
local DEBOUNCE_MS = 100
|
||||
local debounce_ms = 100
|
||||
|
||||
--- @private
|
||||
--- Test-only: override the debounce window so tests can run faster.
|
||||
--- @param ms integer
|
||||
function M._set_debounce(ms)
|
||||
debounce_ms = ms
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param bufnr integer
|
||||
--- @return boolean
|
||||
function M._is_watching(bufnr)
|
||||
return watchers[bufnr] ~= nil
|
||||
end
|
||||
|
||||
--- Returns the effective 'autoread' value for a buffer.
|
||||
--- 'autoread' is global-local: vim.bo[bufnr].autoread is nil when not set locally,
|
||||
@@ -84,7 +98,7 @@ local function ensure_watcher(bufnr)
|
||||
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()
|
||||
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
|
||||
@@ -102,13 +116,6 @@ local function ensure_watcher(bufnr)
|
||||
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 })
|
||||
|
||||
|
||||
13
runtime/lua/vim/_meta/options.gen.lua
generated
13
runtime/lua/vim/_meta/options.gen.lua
generated
@@ -168,12 +168,13 @@ vim.o.ai = vim.o.autoindent
|
||||
vim.bo.autoindent = vim.o.autoindent
|
||||
vim.bo.ai = vim.bo.autoindent
|
||||
|
||||
--- When a file has been detected to have been changed outside of Vim and
|
||||
--- 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.
|
||||
--- Nvim uses file system watchers to detect changes in real-time for all
|
||||
--- loaded buffers; see `timestamp` for details.
|
||||
--- When a file was changed outside of Nvim, automatically read it again.
|
||||
--- Skipped if the file was deleted, so you have the text from before it
|
||||
--- was deleted. If the file appears again then it is read. `timestamp`
|
||||
---
|
||||
--- This is partially driven by OS filewatcher events `uv_fs_event_t`, so
|
||||
--- even the current buffer may be updated.
|
||||
---
|
||||
--- If this option has a local value, use this command to switch back to
|
||||
--- using the global value:
|
||||
---
|
||||
|
||||
@@ -301,12 +301,13 @@ local options = {
|
||||
abbreviation = 'ar',
|
||||
defaults = true,
|
||||
desc = [=[
|
||||
When a file has been detected to have been changed outside of Vim and
|
||||
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.
|
||||
Nvim uses file system watchers to detect changes in real-time for all
|
||||
loaded buffers; see |timestamp| for details.
|
||||
When a file was changed outside of Nvim, automatically read it again.
|
||||
Skipped if the file was deleted, so you have the text from before it
|
||||
was deleted. If the file appears again then it is read. |timestamp|
|
||||
|
||||
This is partially driven by OS filewatcher events |uv_fs_event_t|, so
|
||||
even the current buffer may be updated.
|
||||
|
||||
If this option has a local value, use this command to switch back to
|
||||
using the global value: >vim
|
||||
set autoread<
|
||||
|
||||
@@ -9,8 +9,6 @@ local retry = t.retry
|
||||
local write_file = t.write_file
|
||||
local sleep = vim.uv.sleep
|
||||
|
||||
local testdir = 'Xtest-autoread'
|
||||
|
||||
--- Returns true if the autoread module is watching the given buffer
|
||||
--- (defaults to the current buffer).
|
||||
local function is_watching(bufnr)
|
||||
@@ -19,70 +17,88 @@ local function is_watching(bufnr)
|
||||
end, bufnr)
|
||||
end
|
||||
|
||||
--- Shortens the 'autoread' debounce window so each test doesn't pay the 100ms time-cost.
|
||||
local function shorten_debounce()
|
||||
n.exec_lua([[require('nvim.autoread')._set_debounce(10)]])
|
||||
end
|
||||
|
||||
--- Edits a fresh tempfile with the given initial content and asserts the watcher attached.
|
||||
--- Returns the file path.
|
||||
local function open_watched(content)
|
||||
local path = t.tmpname()
|
||||
write_file(path, content)
|
||||
command('edit ' .. path)
|
||||
eq(true, is_watching())
|
||||
return path
|
||||
end
|
||||
|
||||
describe('autoread file watcher', function()
|
||||
before_each(function()
|
||||
n.mkdir_p(testdir)
|
||||
clear({ args = { '--clean' } })
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
n.rmdir(testdir)
|
||||
shorten_debounce()
|
||||
end)
|
||||
|
||||
it('watches file opened on startup (nvim foo.txt)', function()
|
||||
local path = testdir .. '/test_startup.txt'
|
||||
local path = t.tmpname()
|
||||
write_file(path, 'startup original\n')
|
||||
|
||||
-- Spawn nvim with the file passed on the command line. This exercises the
|
||||
-- boot order: plugins must load before the initial file is read so that
|
||||
-- the BufReadPost autocmd is registered in time to attach a watcher.
|
||||
clear({ args = { '--clean', path } })
|
||||
shorten_debounce()
|
||||
|
||||
eq({ 'startup original' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
eq(true, is_watching())
|
||||
|
||||
-- Modify file externally; watcher should already be active.
|
||||
write_file(path, 'startup changed\n')
|
||||
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'startup changed' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('reloads buffer when file changes externally', function()
|
||||
local path = testdir .. '/test_reload.txt'
|
||||
write_file(path, 'original content\n')
|
||||
it('reloads on external change; survives hide; undoable; bdelete stops watch', function()
|
||||
local path = open_watched('original content\n')
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
|
||||
command('edit ' .. path)
|
||||
eq({ 'original content' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
eq(true, is_watching())
|
||||
|
||||
-- Modify file externally
|
||||
-- 1. Plain external change reloads the visible buffer.
|
||||
write_file(path, 'new content\n')
|
||||
|
||||
-- The watcher + debounce should trigger checktime
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'new content' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
|
||||
-- 2. Hide the buffer; watcher stays attached and still reloads.
|
||||
command('set hidden')
|
||||
command('enew')
|
||||
eq(true, is_watching(bufnr))
|
||||
write_file(path, 'while hidden\n')
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'while hidden' }, api.nvim_buf_get_lines(bufnr, 0, -1, true))
|
||||
end)
|
||||
|
||||
-- 3. The reload is undoable. Done last so the resulting modified state
|
||||
-- (buffer ≠ disk) doesn't block earlier auto-reload assertions.
|
||||
command('buffer ' .. bufnr)
|
||||
command('silent undo')
|
||||
eq({ 'new content' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
|
||||
-- 4. bdelete stops the watcher.
|
||||
command('enew!')
|
||||
command('bdelete! ' .. bufnr)
|
||||
eq(false, is_watching(bufnr))
|
||||
end)
|
||||
|
||||
it('does not reload when buffer has unsaved changes (conflict)', function()
|
||||
local path = testdir .. '/test_conflict.txt'
|
||||
write_file(path, 'original\n')
|
||||
local path = open_watched('original\n')
|
||||
|
||||
command('edit ' .. path)
|
||||
eq(true, is_watching())
|
||||
|
||||
-- Make a local change so the buffer is modified
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'local change' })
|
||||
eq(true, api.nvim_get_option_value('modified', { buf = 0 }))
|
||||
|
||||
-- Modify file externally
|
||||
write_file(path, 'external change\n')
|
||||
|
||||
-- Give watcher time to fire; buffer should NOT be reloaded
|
||||
-- because it has unsaved changes (autoread only reloads unmodified buffers)
|
||||
sleep(200)
|
||||
-- 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
|
||||
command('silent! checktime')
|
||||
-- Buffer should still have local changes (autoread doesn't override modified buffers)
|
||||
@@ -90,17 +106,14 @@ describe('autoread file watcher', function()
|
||||
end)
|
||||
|
||||
it('tracks autoread option changes', function()
|
||||
local path = testdir .. '/test_reenable.txt'
|
||||
write_file(path, 'original\n')
|
||||
local path = open_watched('original\n')
|
||||
|
||||
command('edit ' .. path)
|
||||
eq(true, is_watching())
|
||||
command('setlocal noautoread')
|
||||
eq(false, is_watching())
|
||||
|
||||
-- Modify externally while noautoread
|
||||
write_file(path, 'while disabled\n')
|
||||
sleep(200)
|
||||
sleep(50)
|
||||
eq({ 'original' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
|
||||
-- Re-enable autoread
|
||||
@@ -109,93 +122,55 @@ describe('autoread file watcher', function()
|
||||
|
||||
-- Modify again
|
||||
write_file(path, 'after reenable\n')
|
||||
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'after reenable' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('stops watcher on bdelete', function()
|
||||
local path = testdir .. '/test_bdelete.txt'
|
||||
write_file(path, 'content\n')
|
||||
|
||||
command('edit ' .. path)
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
eq(true, is_watching(bufnr))
|
||||
|
||||
command('enew')
|
||||
command('bdelete ' .. bufnr)
|
||||
eq(false, is_watching(bufnr))
|
||||
end)
|
||||
|
||||
it('reloads hidden buffer when file changes', function()
|
||||
local path = testdir .. '/test_hidden.txt'
|
||||
write_file(path, 'original\n')
|
||||
|
||||
command('set hidden')
|
||||
command('edit ' .. path)
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
eq(true, is_watching(bufnr))
|
||||
|
||||
-- Switch to a different buffer (hides the first one)
|
||||
command('enew')
|
||||
eq(true, is_watching(bufnr))
|
||||
|
||||
-- Modify file externally
|
||||
write_file(path, 'updated hidden\n')
|
||||
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'updated hidden' }, api.nvim_buf_get_lines(bufnr, 0, -1, true))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles file deletion gracefully', function()
|
||||
local path = testdir .. '/test_delete.txt'
|
||||
write_file(path, 'will be deleted\n')
|
||||
local path = open_watched('will be deleted\n')
|
||||
|
||||
command('edit ' .. path)
|
||||
eq({ 'will be deleted' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
eq(true, is_watching())
|
||||
|
||||
-- Delete the file
|
||||
os.remove(path)
|
||||
|
||||
retry(nil, 3000, function()
|
||||
eq(false, is_watching())
|
||||
end)
|
||||
-- Buffer content should remain unchanged
|
||||
-- Buffer content remains unchanged.
|
||||
eq({ 'will be deleted' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
|
||||
it('handles rapid changes with debouncing', function()
|
||||
local path = testdir .. '/test_debounce.txt'
|
||||
write_file(path, 'v1\n')
|
||||
it('coalesces rapid changes via debouncing', function()
|
||||
local path = open_watched('v1\n')
|
||||
|
||||
command('edit ' .. path)
|
||||
eq({ 'v1' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
eq(true, is_watching())
|
||||
-- Count buffer reloads triggered by the watcher.
|
||||
n.exec_lua([[
|
||||
_G.reloads = 0
|
||||
vim.api.nvim_create_autocmd('FileChangedShellPost', {
|
||||
callback = function() _G.reloads = _G.reloads + 1 end,
|
||||
})
|
||||
]])
|
||||
|
||||
-- Make several rapid changes
|
||||
-- 4 back-to-back writes well inside one debounce window.
|
||||
write_file(path, 'v2\n')
|
||||
write_file(path, 'v3\n')
|
||||
write_file(path, 'v4\n')
|
||||
write_file(path, 'final\n')
|
||||
|
||||
-- Should eventually settle on final content
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'final' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
|
||||
-- Let any late-arriving event flush, then assert all 4 writes coalesced.
|
||||
-- Every fs_event restarts the debounce timer, and 4 sub-millisecond
|
||||
-- write_file calls fit well inside one window, so the timer fires once.
|
||||
sleep(50)
|
||||
eq(1, n.exec_lua('return _G.reloads'))
|
||||
end)
|
||||
|
||||
it('detects changes after atomic rename (external editor save)', function()
|
||||
local path = testdir .. '/test_rename.txt'
|
||||
write_file(path, 'original\n')
|
||||
local path = open_watched('original\n')
|
||||
|
||||
command('edit ' .. path)
|
||||
eq({ 'original' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
eq(true, is_watching())
|
||||
|
||||
-- Simulate atomic save: write to temp file, rename over target
|
||||
-- Atomic save: write to temp file, rename over target.
|
||||
local tmp = path .. '.tmp'
|
||||
write_file(tmp, 'after rename\n')
|
||||
assert(vim.uv.fs_rename(tmp, path))
|
||||
@@ -203,34 +178,13 @@ describe('autoread file watcher', function()
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'after rename' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
-- Watcher should have been re-established on the new inode.
|
||||
-- Watcher re-established on the new inode.
|
||||
eq(true, is_watching())
|
||||
|
||||
-- Verify the watcher still works for subsequent plain writes
|
||||
-- Subsequent plain writes still reload.
|
||||
write_file(path, 'second change\n')
|
||||
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'second change' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('auto-reload is undoable', function()
|
||||
local path = testdir .. '/test_undo.txt'
|
||||
write_file(path, 'original\n')
|
||||
|
||||
command('edit ' .. path)
|
||||
eq({ 'original' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
eq(true, is_watching())
|
||||
|
||||
-- Modify externally
|
||||
write_file(path, 'changed externally\n')
|
||||
|
||||
retry(nil, 3000, function()
|
||||
eq({ 'changed externally' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
|
||||
-- Undo should restore original content
|
||||
command('silent undo')
|
||||
eq({ 'original' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user