From 1d33a817517eb6cd3844b045def38eecb253f3c5 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 13 Jun 2026 01:40:05 +0200 Subject: [PATCH] test(autoread): cleanup - Merged into the main "reloads on external change" test. - Reduce duplication. - Wall-clock down from ~4.4s to ~1.4s (dropped 3s debounce test). --- runtime/doc/options.txt | 13 +- runtime/lua/vim/_meta/options.gen.lua | 13 +- src/nvim/options.lua | 13 +- test/functional/options/autoread_spec.lua | 152 +++++++--------------- 4 files changed, 67 insertions(+), 124 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 8cea4e2671..005b6b2b8b 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -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< diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua index 5bc33d2ede..2b8c298972 100644 --- a/runtime/lua/vim/_meta/options.gen.lua +++ b/runtime/lua/vim/_meta/options.gen.lua @@ -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: --- diff --git a/src/nvim/options.lua b/src/nvim/options.lua index f02318a546..d3cd1fc85d 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -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< diff --git a/test/functional/options/autoread_spec.lua b/test/functional/options/autoread_spec.lua index 0491b90f43..cee3747097 100644 --- a/test/functional/options/autoread_spec.lua +++ b/test/functional/options/autoread_spec.lua @@ -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,18 +17,23 @@ local function is_watching(bufnr) end, bufnr) 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) - 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 @@ -41,47 +44,53 @@ describe('autoread file watcher', function() 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) + -- Give the watcher time to fire; the buffer must NOT be reloaded because + -- it has unsaved changes (autoread only reloads unmodified buffers). sleep(200) -- Also do a manual checktime to be sure command('silent! checktime') @@ -90,11 +99,8 @@ 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()) @@ -109,64 +115,24 @@ 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) + -- TODO: revisit this test it('handles rapid changes with debouncing', function() local path = testdir .. '/test_debounce.txt' write_file(path, 'v1\n') @@ -188,14 +154,9 @@ describe('autoread file watcher', function() 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 +164,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)