diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 127025ef72..586ded37ba 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -147,6 +147,29 @@ function M.joinpath(...) return (path:gsub(iswin and '[/\\][/\\]*' or '//+', '/')) end +--- Wrapper around `uv.fs_scandir_next()` that ensures a file type is returned. +--- +--- @param fs uv.uv_fs_t +--- @param path string +--- @return string? +--- @return string? +local function fs_scandir_next(fs, path) + -- use uv.fs_lstat instead of uv.fs_stat to avoid descending into a symlink entry as a directory/file + local name, etype = uv.fs_scandir_next(fs) + + if not name then + return + end + + if etype == nil then + local stat = vim.uv.fs_lstat(M.joinpath(path, name)) + -- Workaround #39612 https://github.com/luvit/luv/issues/660 + etype = stat and stat.type or 'unknown' + end + + return name, etype +end + --- @class vim.fs.dir.Opts --- @inlinedoc --- @@ -190,7 +213,7 @@ function M.dir(path, opts) if not fs then return end - return uv.fs_scandir_next(fs) + return fs_scandir_next(fs, path) end end @@ -203,7 +226,7 @@ function M.dir(path, opts) local dir = level == 1 and dir0 or M.joinpath(path, dir0) local fs = uv.fs_scandir(dir) while fs do - local name, t = uv.fs_scandir_next(fs) + local name, t = fs_scandir_next(fs, dir) if not name then break end diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index 8ab73650a6..8434a36373 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -252,6 +252,41 @@ describe('vim.fs', function() end) ) end) + + describe('fs_scandir_next fallback', function() + before_each(function() + mkdir('testdir') + t.write_file('testdir/test.txt', 'test file') + end) + + after_each(function() + rmdir('testdir') + end) + + it('falls back to fs_lstat when fs_scandir_next returns nil type', function() + local result = n.exec_lua([[ + local orig = vim.uv.fs_scandir_next + + vim.uv.fs_scandir_next = function(fs) + local name = orig(fs) + if name then + return name, nil + end + return name + end + + local out = {} + for name, etype in vim.fs.dir('testdir') do + out[name] = etype + end + + vim.uv.fs_scandir_next = orig + return out + ]]) + + eq('file', result['test.txt']) + end) + end) end) describe('find()', function()