diff --git a/runtime/lua/nvim/dir.lua b/runtime/lua/nvim/dir.lua index f8b0b6b2d3..0b60fe4d00 100644 --- a/runtime/lua/nvim/dir.lua +++ b/runtime/lua/nvim/dir.lua @@ -26,6 +26,7 @@ local M = {} ---@class (private) nvim.dir.State ---@field gen integer +---@field err? string ---@field provider? nvim.dir.Provider local listing_group = api.nvim_create_augroup('nvim.dir.listing', { clear = false }) @@ -212,7 +213,8 @@ function load(buf, name, provider, restore_view, setup, select) local list_gen = state.gen + 1 state.gen = list_gen vim.b[buf].nvim_dir = state - -- Discard the listing state if the initial load fails, but preserve an existing listing on reload failure. + -- Render an empty listing (retried by "R" or :edit) if the initial load fails, + -- but preserve an existing listing on reload failure. local first_render = state.provider == nil local done = false @@ -229,10 +231,12 @@ function load(buf, name, provider, restore_view, setup, select) end if err ~= nil then notify_error(err) - if first_render then - close_listing(buf) + if not first_render then + current_state.err = err + vim.b[buf].nvim_dir = current_state + return end - return + entries = {} end ---@cast entries nvim.dir.Entry[] @@ -248,7 +252,7 @@ function load(buf, name, provider, restore_view, setup, select) if select and api.nvim_get_current_buf() == buf then select_entry(select) end - current_state.provider = provider + current_state.err, current_state.provider = err, provider vim.b[buf].nvim_dir = current_state if not setup then diff --git a/test/functional/plugin/dir_spec.lua b/test/functional/plugin/dir_spec.lua index 1f178932c8..b71d43a604 100644 --- a/test/functional/plugin/dir_spec.lua +++ b/test/functional/plugin/dir_spec.lua @@ -235,8 +235,13 @@ describe('nvim.dir', function() exec_lua(function() require('nvim.dir').open(0, 'custom://error', { - list = function(_, _, cb) - cb('simulated error') + list = function(buf, _, cb) + vim.b[buf].custom_fail = (vim.b[buf].custom_fail or 0) + 1 + if vim.b[buf].custom_fail == 1 then + cb('simulated error') + else + cb(nil, { { name = 'recovered.txt', dir = false } }) + end end, open = function() end, open_parent = function() end, @@ -244,7 +249,49 @@ describe('nvim.dir', function() end) ok(exec_capture('messages'):find('simulated error', 1, true) ~= nil) - eq(false, exec_lua([[return vim.b.nvim_dir ~= nil]])) + -- A failed initial load renders an empty listing that "R" can retry. + eq('simulated error', exec_lua([[return vim.b.nvim_dir.err]])) + eq({ '' }, lines()) + eq('nowrite', bufopt('buftype')) + eq(false, bufopt('modifiable')) + feed('R') + poke_eventloop() + eq({ 'recovered.txt' }, lines()) + eq(vim.NIL, exec_lua([[return vim.b.nvim_dir.err]])) + end) + + it('failed directory open does not repeat the error on every BufEnter', function() + t.skip(t.is_os('win'), 'directory permissions are POSIX-only') + t.skip(vim.uv.getuid() == 0, 'root ignores directory permissions') + n.clear({ args = { '--clean' } }) + make_fixture() + local locked = root .. '/locked' + t.mkdir(locked) + finally(function() + vim.uv.fs_chmod(locked, tonumber('755', 8)) + end) + vim.uv.fs_chmod(locked, 0) + + edit(root) + api.nvim_win_set_cursor(0, { line_of('locked/'), 0 }) + feed('') + poke_eventloop() + local function count_errors() + local _, n_errors = exec_capture('messages'):gsub('EACCES', '') + return n_errors + end + eq(1, count_errors()) + ok(exec_lua([[return vim.b.nvim_dir.err]]):find('EACCES', 1, true) ~= nil) + eq('nowrite', bufopt('buftype')) + edit(file) + feed('') + poke_eventloop() + eq(1, count_errors()) + vim.uv.fs_chmod(locked, tonumber('755', 8)) + t.write_file(locked .. '/inside.txt', 'inside', true) + feed('R') + poke_eventloop() + eq({ 'inside.txt' }, lines()) end) it('reloads custom listing providers', function() @@ -587,6 +634,7 @@ describe('nvim.dir', function() poke_eventloop() ok(exec_capture('messages'):find('ENOENT', 1, true) ~= nil) + ok(exec_lua([[return vim.b.nvim_dir.err]]):find('ENOENT', 1, true) ~= nil) assert_directory(subdir) end)