aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/fs.lua
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2026-07-01 12:05:15 +0200
committerJustin M. Keyes <justinkz@gmail.com>2026-07-01 13:00:06 +0200
commit971a0a0fe0393cca8ec389a661ca1ab290b18dfd (patch)
tree20fda6f989a62e38df177ff64523009f84399027 /runtime/lua/vim/fs.lua
parentf141916e3d1ea61ddccb43b32a3d37b16cb640a0 (diff)
downloadrneovim-971a0a0fe0393cca8ec389a661ca1ab290b18dfd.tar.gz
rneovim-971a0a0fe0393cca8ec389a661ca1ab290b18dfd.tar.bz2
rneovim-971a0a0fe0393cca8ec389a661ca1ab290b18dfd.zip
feat(vim.fs): dir() ergonomics
Diffstat (limited to 'runtime/lua/vim/fs.lua')
-rw-r--r--runtime/lua/vim/fs.lua101
1 files changed, 47 insertions, 54 deletions
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index 5dd87816b0..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,21 +191,14 @@ end
--- (default: `false`)
--- @field follow? boolean
-
--- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|).
---
--- Example:
---
--- ```lua
---- local it, err = vim.fs.dir(path)
----
---- if err then
---- -- Failed to scan the root directory
---- end
----
---- for name, type, err in it do
+--- for name, type, err in vim.fs.dir(path, { err = true }) do
--- if err then
---- -- Failed to scan a child directory
+--- -- Failed to scan directory {name} (may be the root {path} itself).
--- end
--- end
--- ```
@@ -209,29 +206,36 @@ 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 fun(): string?, string?, string? # Iterator over items in {path}. Each iteration yields three values: name, type, err.
+---@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.
----@return string? err Error encountered while scanning the base directory
+--- - 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)
local rootfs, rooterr = uv.fs_scandir(path)
if not rootfs then
- local empty = function()
- return nil
+ -- 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 done then
+ return nil
+ end
+ done = true
+ return path, nil, rooterr
end
-
- return empty, rooterr
end
if not opts.depth or opts.depth == 1 then
@@ -241,7 +245,7 @@ function M.dir(path, opts)
end
--- @async
- local it = coroutine.wrap(function()
+ return coroutine.wrap(function()
local dirs = { { path, 1, rootfs } }
while #dirs > 0 do
--- @type string, integer, any
@@ -264,7 +268,7 @@ function M.dir(path, opts)
then
local fs_next, err = uv.fs_scandir(M.joinpath(path, f))
if not fs_next then
- err_scan = err
+ err_scan = opts.err and err or nil
else
dirs[#dirs + 1] = { f, level + 1, fs_next }
end
@@ -273,8 +277,6 @@ function M.dir(path, opts)
end
end
end)
-
- return it, nil
end
--- @class vim.fs.find.Opts
@@ -338,8 +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[] matches # Normalized paths |vim.fs.normalize()| of all matching items.
----@return string[] errors # Errors collected while searching.
+---@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' })
@@ -374,17 +376,11 @@ function M.find(names, opts)
if type(names) == 'function' then
test = function(p)
local t = {}
- local it, base_err = M.dir(p)
- if base_err ~= nil then
- table.insert(errors, base_err)
- end
- for name, type, err in it do
- local f = M.joinpath(p, name)
+ for name, type, err in M.dir(p, { err = true }) do
if err ~= nil then
table.insert(errors, err)
- end
- if (not opts.type or opts.type == type) and names(name, p) then
- table.insert(t, f)
+ elseif (not opts.type or opts.type == type) and names(name, p) then
+ table.insert(t, M.joinpath(p, name))
end
end
return t
@@ -392,7 +388,7 @@ function M.find(names, opts)
else
test = function(p)
local t = {} --- @type string[]
- local ok, aerr = uv.fs_access(p, 'R') -- Check if the directory itself is readable
+ 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
@@ -434,36 +430,33 @@ function M.find(names, opts)
break
end
- local it, base_err = M.dir(dir)
- if base_err ~= nil then
- table.insert(errors, base_err)
- end
- for other, type_, err in it do
- local f = M.joinpath(dir, other)
+ for other, type_, err in M.dir(dir, { err = true }) do
if err ~= nil then
table.insert(errors, err)
- end
- if type(names) == 'function' then
- if (not opts.type or opts.type == type_) and names(other, dir) then
- if add(f) then
- return matches, errors
- end
- end
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, 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