From 14c708634efec514463bb495d9648c78828ee198 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Thu, 5 Feb 2026 12:02:54 +0200 Subject: [PATCH] fix(vim.fs): make `rm()` work with symlink to a directory --- runtime/doc/lua.txt | 4 +++- runtime/lua/vim/fs.lua | 5 +++-- test/functional/lua/fs_spec.lua | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 0aebcaf98b..a71334eab7 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2696,7 +2696,9 @@ vim.fs.rm({path}, {opts}) *vim.fs.rm()* Since: 0.11.0 Parameters: ~ - • {path} (`string`) Path to remove + • {path} (`string`) Path to remove. Removes symlinks without touching + the origin. To remove the origin, resolve explicitly with + |uv.fs_realpath()|. • {opts} (`table?`) A table with the following fields: • {recursive}? (`boolean`) Remove directories and their contents recursively diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 0deacc536c..9b57ca2790 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -735,12 +735,13 @@ end --- Remove files or directories --- @since 13 ---- @param path string Path to remove +--- @param path string Path to remove. Removes symlinks without touching the origin. +---To remove the origin, resolve explicitly with |uv.fs_realpath()|. --- @param opts? vim.fs.rm.Opts function M.rm(path, opts) opts = opts or {} - local stat, err, errnm = uv.fs_stat(path) + local stat, err, errnm = uv.fs_lstat(path) if stat then rm(path, stat.type, opts.recursive, opts.force) elseif not opts.force or errnm ~= 'ENOENT' then diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index ca04f8edec..8c826a211d 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -9,6 +9,7 @@ local rmdir = n.rmdir local nvim_dir = n.nvim_dir local command = n.command local api = n.api +local fn = n.fn local test_build_dir = t.paths.test_build_dir local test_source_path = t.paths.test_source_path local nvim_prog = n.nvim_prog @@ -714,4 +715,41 @@ describe('vim.fs', function() end end) end) + + describe('rm()', function() + before_each(function() + t.mkdir('Xtest_fs-rm') + t.write_file('Xtest_fs-rm/file-to-link', 'File to link') + t.mkdir('Xtest_fs-rm/dir-to-link') + t.write_file('Xtest_fs-rm/dir-to-link/file', 'File in dir to link') + end) + + after_each(function() + vim.uv.fs_unlink('Xtest_fs-rm/dir-to-link/file') + vim.uv.fs_rmdir('Xtest_fs-rm/dir-to-link') + vim.uv.fs_unlink('Xtest_fs-rm/file-to-link') + vim.uv.fs_rmdir('Xtest_fs-rm') + end) + + it('works with symlink', function() + -- File + vim.uv.fs_symlink('Xtest_fs-rm/file-to-link', 'Xtest_fs-rm/file-as-link') + vim.fs.rm('Xtest_fs-rm/file-as-link') + eq(vim.uv.fs_stat('Xtest_fs-rm/file-as-link'), nil) + eq({ 'File to link' }, fn.readfile('Xtest_fs-rm/file-to-link')) + + -- Directory + local function assert_rm_symlinked_dir(opts) + vim.uv.fs_symlink('Xtest_fs-rm/dir-to-link', 'Xtest_fs-rm/dir-as-link') + vim.fs.rm('Xtest_fs-rm/dir-as-link', opts) + eq(vim.uv.fs_stat('Xtest_fs-rm/dir-as-link'), nil) + eq({ 'File in dir to link' }, fn.readfile('Xtest_fs-rm/dir-to-link/file')) + end + + assert_rm_symlinked_dir({}) + assert_rm_symlinked_dir({ force = true }) + assert_rm_symlinked_dir({ recursive = true }) + assert_rm_symlinked_dir({ recursive = true, force = true }) + end) + end) end)