From 971a0a0fe0393cca8ec389a661ca1ab290b18dfd Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 1 Jul 2026 12:05:15 +0200 Subject: feat(vim.fs): dir() ergonomics --- runtime/lua/vim/fs.lua | 101 +++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 54 deletions(-) (limited to 'runtime/lua/vim/fs.lua') 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 -- cgit