aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2026-07-01 07:18:08 -0400
committerGitHub <noreply@github.com>2026-07-01 07:18:08 -0400
commitd253acca35205a2277af8fae5cf4a3ea3e1bb302 (patch)
tree85cb5f97c8bda004eb19b1dd6972b6255100eeda /runtime/lua/vim
parent0d81f257afd24373144873e8f030bf929c1900e2 (diff)
parent1035a9fb5df1ed7c5cab99fa4c0686c2c9ef0e37 (diff)
downloadrneovim-d253acca35205a2277af8fae5cf4a3ea3e1bb302.tar.gz
rneovim-d253acca35205a2277af8fae5cf4a3ea3e1bb302.tar.bz2
rneovim-d253acca35205a2277af8fae5cf4a3ea3e1bb302.zip
Merge #40114 feat(vim.fs): dir(), find() error-reporting
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/fs.lua127
1 files changed, 86 insertions, 41 deletions
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index 7e85e80056..fc18d525dd 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -177,6 +177,10 @@ end
--- (default: `1`)
--- @field depth? integer
---
+--- Report errors via the iterator's third value ("err"), instead of silently skipping.
+--- (default: `false`)
+--- @field err? boolean
+---
--- Predicate to control traversal.
--- Return false to stop searching the current directory.
--- Only useful when depth > 1
@@ -187,51 +191,73 @@ end
--- (default: `false`)
--- @field follow? boolean
----@alias Iterator fun(): string?, string?
-
--- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|).
---
+--- Example:
+---
+--- ```lua
+--- for name, type, err in vim.fs.dir(path, { err = true }) do
+--- if err then
+--- -- Failed to scan directory {name} (may be the root {path} itself).
+--- end
+--- end
+--- ```
+---
---@since 10
---@param path (string) Directory to iterate over, normalized via |vim.fs.normalize()|.
---@param opts? vim.fs.dir.Opts Optional keyword arguments:
----@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
---- "name" is the basename of the item relative to {path}.
---- "type" is one of the following:
---- "file", "directory", "link", "fifo", "socket", "char", "block", "unknown".
+---@return fun(): string?, string?, string? # Iterator over items in {path}, yielding (name, type, err):
+--- - name: Basename of the item relative to {path}.
+--- - type: One of: "file", "directory", "link", "fifo", "socket", "char", "block", "unknown".
+--- - err: Error string, or nil. Only if `opts.err=true`. If the root {path} itself could not
+--- be scanned, yields a single (name, nil, err) item.
function M.dir(path, opts)
opts = opts or {}
vim.validate('path', path, 'string')
vim.validate('depth', opts.depth, 'number', true)
- vim.validate('skip', opts.skip, 'function', true)
+ vim.validate('err', opts.err, 'boolean', true)
vim.validate('follow', opts.follow, 'boolean', true)
+ vim.validate('skip', opts.skip, 'function', true)
path = M.normalize(path)
- if not opts.depth or opts.depth == 1 then
- local fs = uv.fs_scandir(path)
+
+ local rootfs, rooterr = uv.fs_scandir(path)
+
+ if not rootfs then
+ -- Root scan failed:
+ -- - If opts.err=false, behave as an empty listing (back-compat).
+ -- - If opts.err=true, surface yield a single (name, nil, err) result.
+ local done = not opts.err
return function()
- if not fs then
- return
+ if done then
+ return nil
end
- return fs_scandir_next(fs, path)
+ done = true
+ return path, nil, rooterr
+ end
+ end
+
+ if not opts.depth or opts.depth == 1 then
+ return function()
+ return fs_scandir_next(rootfs, path)
end
end
--- @async
return coroutine.wrap(function()
- local dirs = { { path, 1 } }
+ local dirs = { { path, 1, rootfs } }
while #dirs > 0 do
- --- @type string, integer
- local dir0, level = unpack(table.remove(dirs, 1))
+ --- @type string, integer, any
+ local dir0, level, fs = unpack(table.remove(dirs, 1))
local dir = level == 1 and dir0 or M.joinpath(path, dir0)
- local fs = uv.fs_scandir(dir)
while fs do
local name, t = fs_scandir_next(fs, dir)
if not name then
break
end
local f = level == 1 and name or M.joinpath(dir0, name)
- coroutine.yield(f, t)
+ local err_scan = nil
if
opts.depth
and level < opts.depth
@@ -240,8 +266,14 @@ function M.dir(path, opts)
).type == 'directory'))
and (not opts.skip or opts.skip(f) ~= false)
then
- dirs[#dirs + 1] = { f, level + 1 }
+ local fs_next, err = uv.fs_scandir(M.joinpath(path, f))
+ if not fs_next then
+ err_scan = opts.err and err or nil
+ else
+ dirs[#dirs + 1] = { f, level + 1, fs_next }
+ end
end
+ coroutine.yield(f, t, err_scan)
end
end
end)
@@ -308,7 +340,8 @@ end
--- The function should return `true` if the given item is considered a match.
---
---@param opts? vim.fs.find.Opts Optional keyword arguments:
----@return (string[]) # Normalized paths |vim.fs.normalize()| of all matching items
+---@return string[] # Normalized paths |vim.fs.normalize()| of all matching items.
+---@return string[] # Errors collected while searching.
function M.find(names, opts)
opts = opts or {}
vim.validate('names', names, { 'string', 'table', 'function' })
@@ -328,6 +361,7 @@ function M.find(names, opts)
local limit = opts.limit or 1
local matches = {} --- @type string[]
+ local errors = {} --- @type string[]
local function add(match)
matches[#matches + 1] = M.normalize(match)
@@ -342,8 +376,10 @@ function M.find(names, opts)
if type(names) == 'function' then
test = function(p)
local t = {}
- for name, type in M.dir(p) do
- if (not opts.type or opts.type == type) and names(name, p) then
+ for name, type, err in M.dir(p, { err = true }) do
+ if err ~= nil then
+ table.insert(errors, err)
+ elseif (not opts.type or opts.type == type) and names(name, p) then
table.insert(t, M.joinpath(p, name))
end
end
@@ -352,6 +388,11 @@ function M.find(names, opts)
else
test = function(p)
local t = {} --- @type string[]
+ local ok, aerr = uv.fs_access(p, 'R') -- Check if the root dir is readable.
+ if not ok then
+ table.insert(errors, aerr)
+ return t
+ end
for _, name in ipairs(names) do
local f = M.joinpath(p, name)
local stat = uv.fs_stat(f)
@@ -366,7 +407,7 @@ function M.find(names, opts)
for _, match in ipairs(test(path)) do
if add(match) then
- return matches
+ return matches, errors
end
end
@@ -377,7 +418,7 @@ function M.find(names, opts)
for _, match in ipairs(test(parent)) do
if add(match) then
- return matches
+ return matches, errors
end
end
end
@@ -389,35 +430,39 @@ function M.find(names, opts)
break
end
- for other, type_ in M.dir(dir) do
- local f = M.joinpath(dir, other)
- if type(names) == 'function' then
- if (not opts.type or opts.type == type_) and names(other, dir) then
- if add(f) then
- return matches
- end
- end
+ for other, type_, err in M.dir(dir, { err = true }) do
+ if err ~= nil then
+ table.insert(errors, err)
else
- for _, name in ipairs(names) do
- if name == other and (not opts.type or opts.type == type_) then
+ local f = M.joinpath(dir, other)
+ if type(names) == 'function' then
+ if (not opts.type or opts.type == type_) and names(other, dir) then
if add(f) then
- return matches
+ return matches, errors
+ end
+ end
+ else
+ for _, name in ipairs(names) do
+ if name == other and (not opts.type or opts.type == type_) then
+ if add(f) then
+ return matches, errors
+ end
end
end
end
- end
- if
- type_ == 'directory'
- or (type_ == 'link' and opts.follow and (uv.fs_stat(f) or {}).type == 'directory')
- then
- dirs[#dirs + 1] = f
+ if
+ type_ == 'directory'
+ or (type_ == 'link' and opts.follow and (uv.fs_stat(f) or {}).type == 'directory')
+ then
+ dirs[#dirs + 1] = f
+ end
end
end
end
end
- return matches
+ return matches, errors
end
--- Find the first parent directory containing a specific "marker", relative to a file path or