diff options
| author | Rudrajeet Pal <rudrajeetpal64@gmail.com> | 2026-06-04 16:33:03 +0530 |
|---|---|---|
| committer | Justin M. Keyes <justinkz@gmail.com> | 2026-07-01 12:05:41 +0200 |
| commit | 4e04dff2287eed576cdb1063d5b7c00bf91b9fad (patch) | |
| tree | c0f6733b71c81bbf34307abfbcb8288bab49c5f7 /runtime | |
| parent | 0d81f257afd24373144873e8f030bf929c1900e2 (diff) | |
| download | rneovim-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.txt | 34 | ||||
| -rw-r--r-- | runtime/doc/news.txt | 3 | ||||
| -rw-r--r-- | runtime/lua/vim/fs.lua | 110 |
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 |