diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2026-07-01 07:18:08 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-01 07:18:08 -0400 |
| commit | d253acca35205a2277af8fae5cf4a3ea3e1bb302 (patch) | |
| tree | 85cb5f97c8bda004eb19b1dd6972b6255100eeda /runtime/lua/vim | |
| parent | 0d81f257afd24373144873e8f030bf929c1900e2 (diff) | |
| parent | 1035a9fb5df1ed7c5cab99fa4c0686c2c9ef0e37 (diff) | |
| download | rneovim-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.lua | 127 |
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 |