mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	feat(fs.lua): add vim.fs.rm()
Analogous to the shell `rm` command.
This commit is contained in:
		 Lewis Russell
					Lewis Russell
				
			
				
					committed by
					
						 Lewis Russell
						Lewis Russell
					
				
			
			
				
	
			
			
			 Lewis Russell
						Lewis Russell
					
				
			
						parent
						
							29bceb4f75
						
					
				
				
					commit
					511b991e66
				
			| @@ -3021,6 +3021,16 @@ vim.fs.parents({start})                                     *vim.fs.parents()* | |||||||
|         (`nil`) |         (`nil`) | ||||||
|         (`string?`) |         (`string?`) | ||||||
|  |  | ||||||
|  | vim.fs.rm({path}, {opts})                                        *vim.fs.rm()* | ||||||
|  |     Remove files or directories | ||||||
|  |  | ||||||
|  |     Parameters: ~ | ||||||
|  |       • {path}  (`string`) Path to remove | ||||||
|  |       • {opts}  (`table?`) A table with the following fields: | ||||||
|  |                 • {recursive}? (`boolean`) Remove directories and their | ||||||
|  |                   contents recursively | ||||||
|  |                 • {force}? (`boolean`) Ignore nonexistent files and arguments | ||||||
|  |  | ||||||
| vim.fs.root({source}, {marker})                                *vim.fs.root()* | vim.fs.root({source}, {marker})                                *vim.fs.root()* | ||||||
|     Find the first parent directory containing a specific "marker", relative |     Find the first parent directory containing a specific "marker", relative | ||||||
|     to a file path or buffer. |     to a file path or buffer. | ||||||
|   | |||||||
| @@ -152,7 +152,7 @@ LSP | |||||||
|  |  | ||||||
| LUA | LUA | ||||||
|  |  | ||||||
| • TODO | • |vim.fs.rm()| can delete files and directories. | ||||||
|  |  | ||||||
| OPTIONS | OPTIONS | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
|  | local uv = vim.uv | ||||||
|  |  | ||||||
| local M = {} | local M = {} | ||||||
|  |  | ||||||
| local iswin = vim.uv.os_uname().sysname == 'Windows_NT' | local iswin = uv.os_uname().sysname == 'Windows_NT' | ||||||
| local os_sep = iswin and '\\' or '/' | local os_sep = iswin and '\\' or '/' | ||||||
|  |  | ||||||
| --- Iterate over all the parents of the given path. | --- Iterate over all the parents of the given path. | ||||||
| @@ -122,12 +124,12 @@ function M.dir(path, opts) | |||||||
|  |  | ||||||
|   path = M.normalize(path) |   path = M.normalize(path) | ||||||
|   if not opts.depth or opts.depth == 1 then |   if not opts.depth or opts.depth == 1 then | ||||||
|     local fs = vim.uv.fs_scandir(path) |     local fs = uv.fs_scandir(path) | ||||||
|     return function() |     return function() | ||||||
|       if not fs then |       if not fs then | ||||||
|         return |         return | ||||||
|       end |       end | ||||||
|       return vim.uv.fs_scandir_next(fs) |       return uv.fs_scandir_next(fs) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -138,9 +140,9 @@ function M.dir(path, opts) | |||||||
|       --- @type string, integer |       --- @type string, integer | ||||||
|       local dir0, level = unpack(table.remove(dirs, 1)) |       local dir0, level = unpack(table.remove(dirs, 1)) | ||||||
|       local dir = level == 1 and dir0 or M.joinpath(path, dir0) |       local dir = level == 1 and dir0 or M.joinpath(path, dir0) | ||||||
|       local fs = vim.uv.fs_scandir(dir) |       local fs = uv.fs_scandir(dir) | ||||||
|       while fs do |       while fs do | ||||||
|         local name, t = vim.uv.fs_scandir_next(fs) |         local name, t = uv.fs_scandir_next(fs) | ||||||
|         if not name then |         if not name then | ||||||
|           break |           break | ||||||
|         end |         end | ||||||
| @@ -234,7 +236,7 @@ function M.find(names, opts) | |||||||
|     names = { names } |     names = { names } | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   local path = opts.path or assert(vim.uv.cwd()) |   local path = opts.path or assert(uv.cwd()) | ||||||
|   local stop = opts.stop |   local stop = opts.stop | ||||||
|   local limit = opts.limit or 1 |   local limit = opts.limit or 1 | ||||||
|  |  | ||||||
| @@ -265,7 +267,7 @@ function M.find(names, opts) | |||||||
|         local t = {} --- @type string[] |         local t = {} --- @type string[] | ||||||
|         for _, name in ipairs(names) do |         for _, name in ipairs(names) do | ||||||
|           local f = M.joinpath(p, name) |           local f = M.joinpath(p, name) | ||||||
|           local stat = vim.uv.fs_stat(f) |           local stat = uv.fs_stat(f) | ||||||
|           if stat and (not opts.type or opts.type == stat.type) then |           if stat and (not opts.type or opts.type == stat.type) then | ||||||
|             t[#t + 1] = f |             t[#t + 1] = f | ||||||
|           end |           end | ||||||
| @@ -365,7 +367,7 @@ function M.root(source, marker) | |||||||
|     path = source |     path = source | ||||||
|   elseif type(source) == 'number' then |   elseif type(source) == 'number' then | ||||||
|     if vim.bo[source].buftype ~= '' then |     if vim.bo[source].buftype ~= '' then | ||||||
|       path = assert(vim.uv.cwd()) |       path = assert(uv.cwd()) | ||||||
|     else |     else | ||||||
|       path = vim.api.nvim_buf_get_name(source) |       path = vim.api.nvim_buf_get_name(source) | ||||||
|     end |     end | ||||||
| @@ -552,7 +554,7 @@ function M.normalize(path, opts) | |||||||
|  |  | ||||||
|   -- Expand ~ to users home directory |   -- Expand ~ to users home directory | ||||||
|   if vim.startswith(path, '~') then |   if vim.startswith(path, '~') then | ||||||
|     local home = vim.uv.os_homedir() or '~' |     local home = uv.os_homedir() or '~' | ||||||
|     if home:sub(-1) == os_sep_local then |     if home:sub(-1) == os_sep_local then | ||||||
|       home = home:sub(1, -2) |       home = home:sub(1, -2) | ||||||
|     end |     end | ||||||
| @@ -561,7 +563,7 @@ function M.normalize(path, opts) | |||||||
|  |  | ||||||
|   -- Expand environment variables if `opts.expand_env` isn't `false` |   -- Expand environment variables if `opts.expand_env` isn't `false` | ||||||
|   if opts.expand_env == nil or opts.expand_env then |   if opts.expand_env == nil or opts.expand_env then | ||||||
|     path = path:gsub('%$([%w_]+)', vim.uv.os_getenv) |     path = path:gsub('%$([%w_]+)', uv.os_getenv) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   if win then |   if win then | ||||||
| @@ -609,4 +611,55 @@ function M.normalize(path, opts) | |||||||
|   return path |   return path | ||||||
| end | end | ||||||
|  |  | ||||||
|  | --- @param path string Path to remove | ||||||
|  | --- @param ty string type of path | ||||||
|  | --- @param recursive? boolean | ||||||
|  | --- @param force? boolean | ||||||
|  | local function rm(path, ty, recursive, force) | ||||||
|  |   --- @diagnostic disable-next-line:no-unknown | ||||||
|  |   local rm_fn | ||||||
|  |  | ||||||
|  |   if ty == 'directory' then | ||||||
|  |     if recursive then | ||||||
|  |       for file, fty in vim.fs.dir(path) do | ||||||
|  |         rm(M.joinpath(path, file), fty, true, force) | ||||||
|  |       end | ||||||
|  |     elseif not force then | ||||||
|  |       error(string.format('%s is a directory', path)) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     rm_fn = uv.fs_rmdir | ||||||
|  |   else | ||||||
|  |     rm_fn = uv.fs_unlink | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   local ret, err, errnm = rm_fn(path) | ||||||
|  |   if ret == nil and (not force or errnm ~= 'ENOENT') then | ||||||
|  |     error(err) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  |  | ||||||
|  | --- @class vim.fs.rm.Opts | ||||||
|  | --- @inlinedoc | ||||||
|  | --- | ||||||
|  | --- Remove directories and their contents recursively | ||||||
|  | --- @field recursive? boolean | ||||||
|  | --- | ||||||
|  | --- Ignore nonexistent files and arguments | ||||||
|  | --- @field force? boolean | ||||||
|  |  | ||||||
|  | --- Remove files or directories | ||||||
|  | --- @param path string Path to remove | ||||||
|  | --- @param opts? vim.fs.rm.Opts | ||||||
|  | function M.rm(path, opts) | ||||||
|  |   opts = opts or {} | ||||||
|  |  | ||||||
|  |   local stat, err, errnm = uv.fs_stat(path) | ||||||
|  |   if stat then | ||||||
|  |     rm(path, stat.type, opts.recursive, opts.force) | ||||||
|  |   elseif not opts.force or errnm ~= 'ENOENT' then | ||||||
|  |     error(err) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  |  | ||||||
| return M | return M | ||||||
|   | |||||||
| @@ -652,6 +652,7 @@ function M.rename(old_fname, new_fname, opts) | |||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | --- @param change lsp.CreateFile | ||||||
| local function create_file(change) | local function create_file(change) | ||||||
|   local opts = change.options or {} |   local opts = change.options or {} | ||||||
|   -- from spec: Overwrite wins over `ignoreIfExists` |   -- from spec: Overwrite wins over `ignoreIfExists` | ||||||
| @@ -666,23 +667,15 @@ local function create_file(change) | |||||||
|   vim.fn.bufadd(fname) |   vim.fn.bufadd(fname) | ||||||
| end | end | ||||||
|  |  | ||||||
|  | --- @param change lsp.DeleteFile | ||||||
| local function delete_file(change) | local function delete_file(change) | ||||||
|   local opts = change.options or {} |   local opts = change.options or {} | ||||||
|   local fname = vim.uri_to_fname(change.uri) |   local fname = vim.uri_to_fname(change.uri) | ||||||
|   local stat = uv.fs_stat(fname) |  | ||||||
|   if opts.ignoreIfNotExists and not stat then |  | ||||||
|     return |  | ||||||
|   end |  | ||||||
|   assert(stat, 'Cannot delete not existing file or folder ' .. fname) |  | ||||||
|   local flags |  | ||||||
|   if stat and stat.type == 'directory' then |  | ||||||
|     flags = opts.recursive and 'rf' or 'd' |  | ||||||
|   else |  | ||||||
|     flags = '' |  | ||||||
|   end |  | ||||||
|   local bufnr = vim.fn.bufadd(fname) |   local bufnr = vim.fn.bufadd(fname) | ||||||
|   local result = tonumber(vim.fn.delete(fname, flags)) |   vim.fs.rm(fname, { | ||||||
|   assert(result == 0, 'Could not delete file: ' .. fname .. ', stat: ' .. vim.inspect(stat)) |     force = opts.ignoreIfNotExists, | ||||||
|  |     recursive = opts.recursive, | ||||||
|  |   }) | ||||||
|   api.nvim_buf_delete(bufnr, { force = true }) |   api.nvim_buf_delete(bufnr, { force = true }) | ||||||
| end | end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -759,58 +759,21 @@ function M.assert_visible(bufnr, visible) | |||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
| --- @param path string |  | ||||||
| local function do_rmdir(path) |  | ||||||
|   local stat = uv.fs_stat(path) |  | ||||||
|   if stat == nil then |  | ||||||
|     return |  | ||||||
|   end |  | ||||||
|   if stat.type ~= 'directory' then |  | ||||||
|     error(string.format('rmdir: not a directory: %s', path)) |  | ||||||
|   end |  | ||||||
|   for file in vim.fs.dir(path) do |  | ||||||
|     if file ~= '.' and file ~= '..' then |  | ||||||
|       local abspath = path .. '/' .. file |  | ||||||
|       if t.isdir(abspath) then |  | ||||||
|         do_rmdir(abspath) -- recurse |  | ||||||
|       else |  | ||||||
|         local ret, err = os.remove(abspath) |  | ||||||
|         if not ret then |  | ||||||
|           if not session then |  | ||||||
|             error('os.remove: ' .. err) |  | ||||||
|           else |  | ||||||
|             -- Try Nvim delete(): it handles `readonly` attribute on Windows, |  | ||||||
|             -- and avoids Lua cross-version/platform incompatibilities. |  | ||||||
|             if -1 == M.call('delete', abspath) then |  | ||||||
|               local hint = (is_os('win') and ' (hint: try :%bwipeout! before rmdir())' or '') |  | ||||||
|               error('delete() failed' .. hint .. ': ' .. abspath) |  | ||||||
|             end |  | ||||||
|           end |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
|   local ret, err = uv.fs_rmdir(path) |  | ||||||
|   if not ret then |  | ||||||
|     error('luv.fs_rmdir(' .. path .. '): ' .. err) |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  |  | ||||||
| local start_dir = uv.cwd() | local start_dir = uv.cwd() | ||||||
|  |  | ||||||
| function M.rmdir(path) | function M.rmdir(path) | ||||||
|   local ret, _ = pcall(do_rmdir, path) |   local ret, _ = pcall(vim.fs.rm, path, { recursive = true, force = true }) | ||||||
|   if not ret and is_os('win') then |   if not ret and is_os('win') then | ||||||
|     -- Maybe "Permission denied"; try again after changing the nvim |     -- Maybe "Permission denied"; try again after changing the nvim | ||||||
|     -- process to the top-level directory. |     -- process to the top-level directory. | ||||||
|     M.command([[exe 'cd '.fnameescape(']] .. start_dir .. "')") |     M.command([[exe 'cd '.fnameescape(']] .. start_dir .. "')") | ||||||
|     ret, _ = pcall(do_rmdir, path) |     ret, _ = pcall(vim.fs.rm, path, { recursive = true, force = true }) | ||||||
|   end |   end | ||||||
|   -- During teardown, the nvim process may not exit quickly enough, then rmdir() |   -- During teardown, the nvim process may not exit quickly enough, then rmdir() | ||||||
|   -- will fail (on Windows). |   -- will fail (on Windows). | ||||||
|   if not ret then -- Try again. |   if not ret then -- Try again. | ||||||
|     sleep(1000) |     sleep(1000) | ||||||
|     do_rmdir(path) |     vim.fs.rm(path, { recursive = true, force = true }) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user