aboutsummaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
authorRudrajeet Pal <rudrajeetpal64@gmail.com>2026-06-04 16:33:03 +0530
committerJustin M. Keyes <justinkz@gmail.com>2026-07-01 12:05:41 +0200
commit4e04dff2287eed576cdb1063d5b7c00bf91b9fad (patch)
treec0f6733b71c81bbf34307abfbcb8288bab49c5f7 /runtime
parent0d81f257afd24373144873e8f030bf929c1900e2 (diff)
downloadrneovim-4e04dff2287eed576cdb1063d5b7c00bf91b9fad.tar.gz
rneovim-4e04dff2287eed576cdb1063d5b7c00bf91b9fad.tar.bz2
rneovim-4e04dff2287eed576cdb1063d5b7c00bf91b9fad.zip
feat(vim.fs): dir(), find() error-reporting
Problem: vim.fs.dir() and vim.fs.find() drop errors returned by uv.fs_scandir(). Solution: - vim.fs.dir(): - Return root scan failures as a secondary return value. - Propagate recursive scan failures through the iterator. This allows callers to distinguish unreadable directories from empty ones. - vim.fs.find(): Collect errors during search, and return the list as a second retval.
Diffstat (limited to 'runtime')
-rw-r--r--runtime/doc/lua.txt34
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/lua/vim/fs.lua110
3 files changed, 117 insertions, 30 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index aa9fb67ad6..130031ff4c 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -2596,6 +2596,20 @@ vim.fs.dir({path}, {opts}) *vim.fs.dir()*
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
+ if err then
+ -- Failed to scan a child directory
+ end
+ end
+<
+
Attributes: ~
Since: 0.8.0
@@ -2611,11 +2625,14 @@ vim.fs.dir({path}, {opts}) *vim.fs.dir()*
current directory. Only useful when depth > 1 Return an
iterator over the items located in {path}
- Return: ~
+ Return (multiple): ~
(`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".
+ "name" and "type", along with an optional value "err" "name" is the
+ basename of the item relative to {path}. "type" is one of the
+ following: "file", "directory", "link", "fifo", "socket", "char",
+ "block", "unknown". "err" is a string with the format {errname}:
+ {message}
+ (`string?`) err Error encountered while scanning the base directory
vim.fs.dirname({file}) *vim.fs.dirname()*
Gets the parent directory of the given path (not expanded/resolved, the
@@ -2708,9 +2725,12 @@ vim.fs.find({names}, {opts}) *vim.fs.find()*
through parent directories. Otherwise, search child
directories (recursively).
- Return: ~
- (`string[]`) Normalized paths |vim.fs.normalize()| of all matching
- items
+ Return (multiple): ~
+ (`string[]`) matches Normalized paths |vim.fs.normalize()| of all
+ matching items
+ (`table[]`) errors list of errors encountered during the search
+ • {err} (`string`) Error message
+ • {path} (`string`) Path at which error was encountered
vim.fs.joinpath({...}) *vim.fs.joinpath()*
Concatenates partial paths (one absolute or relative path followed by zero
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 09580a6644..6c1b58737a 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -253,6 +253,9 @@ LUA
• |vim.net.request()| can now accept `method` param overload for multiple HTTP methods.
• |writefile()| treats Lua strings as "blob", so it can be used to write
binary data.
+• |vim.fs.dir()| and |vim.fs.find()| report errors encountered while
+ scanning directories that cannot be read (e.g. due to permissions),
+ instead of treating them as empty.
• |vim.filetype.inspect()| returns a copy of the internal tables used for
filetype detection.
• Added `__eq` metamethod to |vim.VersionRange|. 2 distinct but representing
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index 7e85e80056..4a524aa467 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -187,17 +187,36 @@ end
--- (default: `false`)
--- @field follow? boolean
----@alias Iterator fun(): string?, string?
+---@alias Iterator fun(): string?, string?, string?
--- 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
+--- if err then
+--- -- Failed to scan a child directory
+--- 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".
+---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type", along
+--- with an optional value "err"
--- "name" is the basename of the item relative to {path}.
--- "type" is one of the following:
--- "file", "directory", "link", "fifo", "socket", "char", "block", "unknown".
+--- "err" is a string with the format {errname}: {message}
+---@return string? err Error encountered while scanning the base directory
function M.dir(path, opts)
opts = opts or {}
@@ -207,31 +226,37 @@ function M.dir(path, opts)
vim.validate('follow', opts.follow, 'boolean', true)
path = M.normalize(path)
+
+ local rootfs, rooterr = uv.fs_scandir(path)
+
+ if not rootfs then
+ local empty = function()
+ return nil
+ end
+
+ return empty, rooterr
+ end
+
if not opts.depth or opts.depth == 1 then
- local fs = uv.fs_scandir(path)
return function()
- if not fs then
- return
- end
- return fs_scandir_next(fs, path)
+ return fs_scandir_next(rootfs, path)
end
end
--- @async
- return coroutine.wrap(function()
- local dirs = { { path, 1 } }
+ local it = coroutine.wrap(function()
+ 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,11 +265,19 @@ 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 = err
+ else
+ dirs[#dirs + 1] = { f, level + 1, fs_next }
+ end
end
+ coroutine.yield(f, t, err_scan)
end
end
end)
+
+ return it, nil
end
--- @class vim.fs.find.Opts
@@ -271,6 +304,15 @@ end
--- (default: `false`)
--- @field follow? boolean
+--- @class vim.fs.find.Error
+--- @inlinedoc
+---
+--- Path at which error was encountered
+--- @field path string
+---
+--- Error message
+--- @field err string
+
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
---
--- Finds items given in {names} starting from {path}. If {upward} is "true"
@@ -308,7 +350,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[] matches # Normalized paths |vim.fs.normalize()| of all matching items
+---@return vim.fs.find.Error[] errors # list of errors encountered during the search
function M.find(names, opts)
opts = opts or {}
vim.validate('names', names, { 'string', 'table', 'function' })
@@ -328,6 +371,7 @@ function M.find(names, opts)
local limit = opts.limit or 1
local matches = {} --- @type string[]
+ local errors = {} --- @type vim.fs.find.Error[]
local function add(match)
matches[#matches + 1] = M.normalize(match)
@@ -342,9 +386,17 @@ function M.find(names, opts)
if type(names) == 'function' then
test = function(p)
local t = {}
- for name, type in M.dir(p) do
+ local it, base_err = M.dir(p)
+ if base_err ~= nil then
+ table.insert(errors, { path = p, err = base_err })
+ end
+ for name, type, err in it do
+ local f = M.joinpath(p, name)
+ if err ~= nil then
+ table.insert(errors, { path = f, err = err })
+ end
if (not opts.type or opts.type == type) and names(name, p) then
- table.insert(t, M.joinpath(p, name))
+ table.insert(t, f)
end
end
return t
@@ -352,6 +404,11 @@ function M.find(names, opts)
else
test = function(p)
local t = {} --- @type string[]
+ local _, base_err = M.dir(p)
+ if base_err ~= nil then -- Check if the directory itself is readable
+ table.insert(errors, { path = p, err = base_err })
+ return t
+ end
for _, name in ipairs(names) do
local f = M.joinpath(p, name)
local stat = uv.fs_stat(f)
@@ -366,7 +423,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 +434,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,19 +446,26 @@ function M.find(names, opts)
break
end
- for other, type_ in M.dir(dir) do
+ local it, base_err = M.dir(dir)
+ if base_err ~= nil then
+ table.insert(errors, { path = dir, err = base_err })
+ end
+ for other, type_, err in it do
local f = M.joinpath(dir, other)
+ if err ~= nil then
+ table.insert(errors, { path = f, err = 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
+ 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
+ return matches, errors
end
end
end
@@ -417,7 +481,7 @@ function M.find(names, opts)
end
end
- return matches
+ return matches, errors
end
--- Find the first parent directory containing a specific "marker", relative to a file path or