From ac4e5460b1a4b1bd9bbfe6d948f0c368593b74f9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 25 Jul 2026 17:59:35 -0500 Subject: [PATCH] feat(vim.fs): dir(opts.normalize) --- runtime/doc/lua.txt | 6 +++++- runtime/lua/vim/fs.lua | 13 +++++++++++-- test/functional/lua/fs_spec.lua | 26 ++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 8a5cb3a327..ad4eb169ea 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2639,7 +2639,7 @@ vim.fs.dir({path}, {opts}) *vim.fs.dir()* Parameters: ~ • {path} (`string`) Directory to iterate over, normalized via - |vim.fs.normalize()|. + |vim.fs.normalize()| unless `opts.normalize=false`. • {opts} (`table?`) Optional keyword arguments: • {depth}? (`integer`, default: `1`) How deep to traverse. • {err}? (`boolean`, default: `false`) Report errors via the @@ -2647,6 +2647,10 @@ vim.fs.dir({path}, {opts}) *vim.fs.dir()* skipping. • {follow}? (`boolean`, default: `false`) Follow symbolic links. + • {normalize}? (`boolean`, default: `true`) Normalize {path} + via |vim.fs.normalize()|. Set `false` to use {path} + literally, e.g. to list a directory whose name contains `~` + or `$`. • {skip}? (`fun(dir_name: string): boolean`) Predicate to control traversal. Return false to stop searching the current directory. Only useful when depth > 1 Return an diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index fbcb46d77d..57e0da4650 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -190,6 +190,11 @@ end --- Follow symbolic links. --- (default: `false`) --- @field follow? boolean +--- +--- Normalize {path} via |vim.fs.normalize()|. Set `false` to use {path} literally, e.g. to list a +--- directory whose name contains `~` or `$`. +--- (default: `true`) +--- @field normalize? boolean --- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|). --- @@ -204,7 +209,8 @@ end --- ``` --- ---@since 10 ----@param path (string) Directory to iterate over, normalized via |vim.fs.normalize()|. +---@param path (string) Directory to iterate over, normalized via |vim.fs.normalize()| unless +--- `opts.normalize=false`. ---@param opts? vim.fs.dir.Opts Optional keyword arguments: ---@return fun(): string?, string?, string? # Iterator over items in {path}, yielding (name, type, err): --- - name: Basename of the item relative to {path}. @@ -219,8 +225,11 @@ function M.dir(path, opts) vim.validate('err', opts.err, 'boolean', true) vim.validate('follow', opts.follow, 'boolean', true) vim.validate('skip', opts.skip, 'function', true) + vim.validate('normalize', opts.normalize, 'boolean', true) - path = M.normalize(path) + if opts.normalize ~= false then + path = M.normalize(path) + end local rootfs, rooterr = uv.fs_scandir(path) diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index db8ff2a9c0..5a328303e6 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -344,6 +344,32 @@ describe('vim.fs', function() -- nil: with depth=2 we don't scan testdir/a/noaccess. eq(nil, result['a/noaccess']) end) + + it('opts.normalize=false uses {path} literally', function() + mkdir('testdir') + mkdir('testdir/$XTEST_FS_DIR') + mkdir('testdir/expanded') + t.write_file('testdir/$XTEST_FS_DIR/literal.txt', '') + t.write_file('testdir/expanded/expanded.txt', '') + finally(function() + rmdir('testdir') + end) + + eq( + { { ['expanded.txt'] = 'file' }, { ['literal.txt'] = 'file' } }, + exec_lua(function() + vim.uv.os_setenv('XTEST_FS_DIR', 'expanded') + local out = {} ---@type table[] + for i, normalize in ipairs({ true, false }) do + out[i] = {} + for name, etype in vim.fs.dir('testdir/$XTEST_FS_DIR', { normalize = normalize }) do + out[i][name] = etype + end + end + return out + end) + ) + end) end) describe('find()', function()