diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua index a8152f17..ff7b6566 100644 --- a/lua/plenary/path.lua +++ b/lua/plenary/path.lua @@ -476,30 +476,93 @@ function Path:rmdir() uv.fs_rmdir(self:absolute()) end -function Path:rename(opts) - opts = opts or {} - if not opts.new_name or opts.new_name == "" then - error "Please provide the new name!" +local function _check_path_empty(input_path) + if input_path == nil or (type(input_path) == "string" and string.match(input_path, "^%s*$")) then + error "Empty path input given. Please provide valid path." end +end - -- handles `.`, `..`, `./`, and `../` - if opts.new_name:match "^%.%.?/?\\?.+" then - opts.new_name = { - uv.fs_realpath(opts.new_name:sub(1, 3)), - opts.new_name:sub(4, #opts.new_name), - } +function Path:_get_override_input(dest_path, opts) + if opts.interactive and dest_path:exists() then + vim.ui.input( + { prompt = string.format("%s already exists. Overwrite with %s? [y/N] ", dest_path.filename, self.filename) }, + function(input) + opts.override = string.lower(input or "") == "y" + end + ) end + return opts.override +end + +function Path:_do_recursion(dest_path, opts, field, callback) + local success = {} + dest_path:mkdir { + parents = F.if_nil(opts.parents, false, opts.parents), + exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok), + } + local scan = require "plenary.scandir" + local data = scan.scan_dir(self.filename, { + respect_gitignore = F.if_nil(opts.respect_gitignore, false, opts.respect_gitignore), + hidden = F.if_nil(opts.hidden, true, opts.hidden), + depth = 1, + add_dirs = true, + }) + for _, entry in ipairs(data) do + local entry_path = Path:new(entry) + local suffix = table.remove(entry_path:_split()) + local new_dest = dest_path:joinpath(suffix) + -- clear destination as it might be Path table otherwise failing w/ extend + opts[field] = nil + local new_opts = vim.tbl_deep_extend("force", opts, { [field] = new_dest }) + -- nil: not overriden if `override = false` + _, success[new_dest] = pcall(callback, entry_path, new_opts) + end + return success +end + +--- Rename/move files or folders with defaults akin to GNU's `mv`. +---@class Path +---@param opts table: options to pass to toggling registered actions +---@field new_name string|Path: target file path to rename/move to +---@field recursive boolean: whether to rename/move folders recursively (default: true) +---@field override boolean: whether to override files (default: true) +---@field interactive boolean: confirm if rename/move would override; precedes `override` (default: false) +---@field respect_gitignore boolean: skip folders ignored by all detected `gitignore`s (default: false) +---@field hidden boolean: whether to add hidden files in recursively renaming/moving folders (default: true) +---@field parents boolean: whether to create possibly non-existing parent dirs of `opts.destination` (default: false) +---@field exists_ok boolean: whether ok if `opts.destination` exists, if so folders are merged (default: true) +---@return table {[Path of destination]: bool} indicating success of rename/move; nested tables constitute sub dirs +function Path:rename(opts) + opts = opts or {} + _check_path_empty(opts.new_name) + opts.recursive = F.if_nil(opts.recursive, true, opts.recursive) + opts.override = F.if_nil(opts.override, true, opts.override) - local new_path = Path:new(opts.new_name) + local dest = Path:new(opts.new_name) - if new_path:exists() then - error "File or directory already exists!" - end + local success = {} + if not self:is_dir() then + opts.override = self:_get_override_input(dest, opts) - local status = uv.fs_rename(self:absolute(), new_path:absolute()) - self.filename = new_path.filename + if dest:exists() then + if opts.override then + dest:rm() + else + return success + end + end + success[dest] = uv.fs_rename(self:absolute(), dest:absolute()) or false + self.filename = dest.filename + return success + end - return status + if opts.recursive then + success = self:_do_recursion(dest, opts, "new_name", Path.rename) + self:rmdir() + return success + else + error(string.format("Warning: %s was not copied as `recursive=false`", self:absolute())) + end end --- Copy files or folders with defaults akin to GNU's `cp`. @@ -514,62 +577,23 @@ end ---@field exists_ok bool: whether ok if `opts.destination` exists, if so folders are merged (default: true) ---@return table {[Path of destination]: bool} indicating success of copy; nested tables constitute sub dirs function Path:copy(opts) - opts = opts or {} + _check_path_empty(opts.destination) opts.recursive = F.if_nil(opts.recursive, false, opts.recursive) opts.override = F.if_nil(opts.override, true, opts.override) - local dest = opts.destination - -- handles `.`, `..`, `./`, and `../` - if not Path.is_path(dest) then - if type(dest) == "string" and dest:match "^%.%.?/?\\?.+" then - dest = { - uv.fs_realpath(dest:sub(1, 3)), - dest:sub(4, #dest), - } - end - dest = Path:new(dest) - end + local dest = Path:new(opts.destination) + -- success is true in case file is copied, false otherwise local success = {} if not self:is_dir() then - if opts.interactive and dest:exists() then - vim.ui.select( - { "Yes", "No" }, - { prompt = string.format("Overwrite existing %s?", dest:absolute()) }, - function(_, idx) - success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not (idx == 1) }) or false - end - ) - else - -- nil: not overriden if `override = false` - success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not opts.override }) or false - end + opts.override = self:_get_override_input(dest, opts) + -- nil: not overriden if `override = false` + success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not opts.override }) or false return success end -- dir if opts.recursive then - dest:mkdir { - parents = F.if_nil(opts.parents, false, opts.parents), - exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok), - } - local scan = require "plenary.scandir" - local data = scan.scan_dir(self.filename, { - respect_gitignore = F.if_nil(opts.respect_gitignore, false, opts.respect_gitignore), - hidden = F.if_nil(opts.hidden, true, opts.hidden), - depth = 1, - add_dirs = true, - }) - for _, entry in ipairs(data) do - local entry_path = Path:new(entry) - local suffix = table.remove(entry_path:_split()) - local new_dest = dest:joinpath(suffix) - -- clear destination as it might be Path table otherwise failing w/ extend - opts.destination = nil - local new_opts = vim.tbl_deep_extend("force", opts, { destination = new_dest }) - -- nil: not overriden if `override = false` - success[new_dest] = entry_path:copy(new_opts) or false - end - return success + return self:_do_recursion(dest, opts, "destination", Path.copy) else error(string.format("Warning: %s was not copied as `recursive=false`", self:absolute())) end diff --git a/tests/plenary/path_spec.lua b/tests/plenary/path_spec.lua index be9cae1c..2016470f 100644 --- a/tests/plenary/path_spec.lua +++ b/tests/plenary/path_spec.lua @@ -370,53 +370,202 @@ describe("Path", function() end) describe("rename", function() - it("can rename a file", function() - local p = Path:new "a_random_filename.lua" - assert(pcall(p.touch, p)) - assert(p:exists()) + local test_file + local test_name = "a_random_filename.lua" - assert(pcall(p.rename, p, { new_name = "not_a_random_filename.lua" })) - assert.are.same("not_a_random_filename.lua", p.filename) + before_each(function() + test_file = Path:new(test_name) + test_file:touch() + end) - p:rm() + after_each(function() + if test_file:exists() then + test_file:rm() + end + end) + + it("can rename a file", function() + local new_name = "not_a_random_filename.lua" + assert(pcall(test_file.rename, test_file, { new_name = new_name })) + assert.are.same(new_name, test_file.filename) end) it("can handle an invalid filename", function() - local p = Path:new "some_random_filename.lua" - assert(pcall(p.touch, p)) - assert(p:exists()) + local error_msg = "Empty path input given. Please provide valid path." + assert.has.errors(function() + test_file:rename { new_name = "" } + end, error_msg) + assert.has.errors(function() + test_file:rename { new_name = " " } + end, error_msg) + assert.has.errors(function() + test_file:rename() + end, error_msg) + assert.is.equal(test_file.filename, test_name) + end) + + it("can move to parent dir", function() + local new_name = "../some_random_filename.lua" + assert(pcall(test_file.rename, test_file, { new_name = new_name })) + assert.are.same(vim.loop.fs_realpath(Path:new(new_name):absolute()), test_file:absolute()) + end) - assert(not pcall(p.rename, p, { new_name = "" })) - assert(not pcall(p.rename, p)) - assert.are.same("some_random_filename.lua", p.filename) + it("cannot rename to an existing filename if override false", function() + local ovr_file = Path:new "not_a_random_filename.lua" + ovr_file:touch() - p:rm() + assert(pcall(test_file.rename, test_file, { new_name = ovr_file, override = false })) + assert.are.same(test_file.filename, test_name) + ovr_file:rm() end) - it("can move to parent dir", function() - local p = Path:new "some_random_filename.lua" - assert(pcall(p.touch, p)) - assert(p:exists()) + it("can rename to an existing file if override true", function() + local ovr_file = Path:new "not_a_random_filename.lua" + ovr_file:touch() - assert(pcall(p.rename, p, { new_name = "../some_random_filename.lua" })) - assert.are.same(vim.loop.fs_realpath(Path:new("../some_random_filename.lua"):absolute()), p:absolute()) + assert(pcall(test_file.rename, test_file, { new_name = ovr_file, override = true })) + assert.are.same(test_file.filename, ovr_file.filename) + ovr_file:rm() + end) - p:rm() + it("fails when moving folders non-recursively", function() + local src_dir = Path:new "src" + src_dir:mkdir() + src_dir:joinpath("file1.lua"):touch() + local trg_dir = Path:new "trg" + + -- error out as intended + assert.has.errors(function() + return src_dir:rename { new_name = trg_dir, recursive = false } + end, string.format( + "Warning: %s was not copied as `recursive=false`", + src_dir:absolute() + )) + src_dir:rm { recursive = true } end) - it("cannot rename to an existing filename", function() - local p1 = Path:new "a_random_filename.lua" - local p2 = Path:new "not_a_random_filename.lua" - assert(pcall(p1.touch, p1)) - assert(pcall(p2.touch, p2)) - assert(p1:exists()) - assert(p2:exists()) + describe("recursively", function() + local scan = require "plenary.scandir" + local scan_opts = { + hidden = true, + depth = 3, + add_dirs = true, + } - assert(not pcall(p1.rename, p1, { new_name = "not_a_random_filename.lua" })) - assert.are.same(p1.filename, "a_random_filename.lua") + local src_dir, src_dirs, ovr_dir, ovr_dirs, oth_dir + local file_prefixes = { "file1", "file2", ".file3" } - p1:rm() - p2:rm() + -- vim.tbl_flatten doesn't work here as copy doesn't return a list + local function flatten(ret, t) + for _, v in pairs(t) do + if type(v) == "table" then + flatten(ret, v) + else + table.insert(ret, v) + end + end + end + + local generate_children = function(files, dirs) + for _, file in ipairs(files) do + for level, dir in ipairs(dirs) do + local p = dir:joinpath(file .. "_" .. level .. ".lua") + p:touch { parents = true, exists_ok = true } + end + end + end + + before_each(function() + -- setup directories + src_dir = Path:new "src" + ovr_dir = Path:new "ovr" + oth_dir = Path:new "oth" + src_dir:mkdir() + + -- set up sub directory paths for creation and testing + local sub_dirs = { "sub_dir1", "sub_dir1/sub_dir2" } + src_dirs = { src_dir } + ovr_dirs = { ovr_dir } + -- {src, trg}_dirs is a table with all directory levels by {src, trg} + for _, dir in ipairs(sub_dirs) do + table.insert(src_dirs, src_dir:joinpath(dir)) + table.insert(ovr_dirs, ovr_dir:joinpath(dir)) + end + + -- generate {file}_{level}.lua on every directory level in src + -- src + -- ├── file1_1.lua + -- ├── file2_1.lua + -- ├── .file3_1.lua + -- └── sub_dir1 + -- ├── file1_2.lua + -- ├── file2_2.lua + -- ├── .file3_2.lua + -- └── sub_dir2 + -- ├── file1_3.lua + -- ├── file2_3.lua + -- └── .file3_3.lua + generate_children(file_prefixes, src_dirs) + end) + + after_each(function() + if src_dir:exists() then + src_dir:rm { recursive = true } + end + if ovr_dir:exists() then + ovr_dir:rm { recursive = true } + end + if oth_dir:exists() then + oth_dir:rm { recursive = true } + end + end) + + it("no override needed, hidden = true", function() + local success = src_dir:rename { new_name = oth_dir, recursive = true, hidden = true } + local file_ops = {} + flatten(file_ops, success) + assert.is.equal(#file_ops, 9) + assert.False(src_dir:exists()) + + local data = scan.scan_dir(oth_dir.filename, scan_opts) + assert.is.equal(#data, 11) + end) + + it("no override needed, hidden = false", function() + local success = src_dir:rename { new_name = oth_dir, recursive = true, hidden = false } + local file_ops = {} + flatten(file_ops, success) + assert.is.equal(#file_ops, 6) + assert.True(src_dir:exists()) + + local src_data = scan.scan_dir(src_dir.filename, scan_opts) + local oth_data = scan.scan_dir(oth_dir.filename, scan_opts) + assert.is.equal(#src_data, 5) + assert.is.equal(#oth_data, 8) + end) + + it("full overriding", function() + generate_children(file_prefixes, ovr_dirs) + local success = src_dir:rename { new_name = ovr_dir, recursive = true, override = true } + local file_ops = {} + flatten(file_ops, success) + assert.is.equal(#file_ops, 9) + + local data = scan.scan_dir(ovr_dir.filename, scan_opts) + assert.is.equal(#data, 11) + end) + + it("partial overriding while keeping non overlapping files", function() + table.insert(file_prefixes, 1, "keep") + generate_children(file_prefixes, ovr_dirs) + local success = src_dir:rename { new_name = ovr_dir, recursive = true, override = true } + local file_ops = {} + flatten(file_ops, success) + assert.is.equal(#file_ops, 9) + + local data = scan.scan_dir(ovr_dir.filename, scan_opts) + assert.is.equal(#data, 14) + end) end) end) @@ -568,6 +717,8 @@ describe("Path", function() end) end) + describe(":rename", function() end) + describe("parents", function() it("should extract the ancestors of the path", function() local p = Path:new(vim.loop.cwd())