feat: trash support for linux and mac (#165)
* wip: skeleton code for trash adapter * refactor: split trash implementation for mac and linux * fix: ensure we create the .Trash/$uid dir * feat: code complete linux trash implementation * doc: write up trash features * feat: code complete mac trash implementation * cleanup: remove previous, terrible, undocumented trash feature * fix: always disabled trash * feat: show original path of trashed files * doc: add a note about calling actions directly * fix: bugs in trash implementation * fix: schedule_wrap in mac trash * doc: fix typo and line wrapping * fix: parsing of arguments to :Oil command * doc: small documentation tweaks * doc: fix awkward wording in the toggle_trash action * fix: warning on Windows when delete_to_trash = true * feat: :Oil --trash can open specific trash directories * fix: show all trash files in device root * fix: trash mtime should be sortable * fix: shorten_path handles optional trailing slash * refactor: overhaul the UI * fix: keep trash original path vtext from stacking * refactor: replace disable_changes with an error filter * fix: shorten path names in home directory relative to root * doc: small README format changes * cleanup: remove unnecessary preserve_undo logic * test: add a functional test for the freedesktop trash adapter * test: more functional tests for trash * fix: schedule a callback to avoid main loop error * refactor: clean up mutator logic * doc: some comments and type annotations
This commit is contained in:
parent
d8f0d91b10
commit
6175bd6462
27 changed files with 1580 additions and 229 deletions
|
|
@ -11,8 +11,6 @@ a.describe("files adapter", function()
|
|||
a.after_each(function()
|
||||
if tmpdir then
|
||||
tmpdir:dispose()
|
||||
a.util.scheduler()
|
||||
tmpdir = nil
|
||||
end
|
||||
test_util.reset_editor()
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
vim.cmd([[set runtimepath+=.]])
|
||||
vim.opt.runtimepath:append(".")
|
||||
|
||||
vim.o.swapfile = false
|
||||
vim.bo.swapfile = false
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ describe("parser", function()
|
|||
after_each(function()
|
||||
test_util.reset_editor()
|
||||
end)
|
||||
|
||||
it("detects new files", function()
|
||||
vim.cmd.edit({ args = { "oil-test:///foo/" } })
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@ M.reset_editor = function()
|
|||
test_adapter.test_clear()
|
||||
end
|
||||
|
||||
local function throwiferr(err, ...)
|
||||
if err then
|
||||
error(err)
|
||||
else
|
||||
return ...
|
||||
end
|
||||
end
|
||||
|
||||
M.await = function(fn, nargs, ...)
|
||||
return throwiferr(a.wrap(fn, nargs)(...))
|
||||
end
|
||||
|
||||
M.wait_for_autocmd = a.wrap(function(autocmd, cb)
|
||||
local opts = {
|
||||
pattern = "*",
|
||||
|
|
@ -58,4 +70,48 @@ M.feedkeys = function(actions, timestep)
|
|||
a.util.sleep(timestep)
|
||||
end
|
||||
|
||||
M.actions = {
|
||||
---Open oil and wait for it to finish rendering
|
||||
---@param args string[]
|
||||
open = function(args)
|
||||
vim.schedule(function()
|
||||
vim.cmd.Oil({ args = args })
|
||||
-- If this buffer was already open, manually dispatch the autocmd to finish the wait
|
||||
if vim.b.oil_ready then
|
||||
vim.api.nvim_exec_autocmds("User", {
|
||||
pattern = "OilEnter",
|
||||
modeline = false,
|
||||
data = { buf = vim.api.nvim_get_current_buf() },
|
||||
})
|
||||
end
|
||||
end)
|
||||
M.wait_for_autocmd({ "User", pattern = "OilEnter" })
|
||||
end,
|
||||
|
||||
---Save all changes and wait for operation to complete
|
||||
save = function()
|
||||
vim.schedule_wrap(require("oil").save)({ confirm = false })
|
||||
M.wait_for_autocmd({ "User", pattern = "OilMutationComplete" })
|
||||
end,
|
||||
|
||||
---@param bufnr? integer
|
||||
reload = function(bufnr)
|
||||
M.await(require("oil.view").render_buffer_async, 3, bufnr or 0)
|
||||
end,
|
||||
|
||||
---Move cursor to a file or directory in an oil buffer
|
||||
---@param filename string
|
||||
focus = function(filename)
|
||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true)
|
||||
local search = " " .. filename .. "$"
|
||||
for i, line in ipairs(lines) do
|
||||
if line:match(search) then
|
||||
vim.api.nvim_win_set_cursor(0, { i, 0 })
|
||||
return
|
||||
end
|
||||
end
|
||||
error("Could not find file " .. filename)
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
local fs = require("oil.fs")
|
||||
local test_util = require("tests.test_util")
|
||||
|
||||
local function throwiferr(err, ...)
|
||||
if err then
|
||||
error(err)
|
||||
else
|
||||
return ...
|
||||
end
|
||||
end
|
||||
|
||||
local function await(fn, nargs, ...)
|
||||
return throwiferr(a.wrap(fn, nargs)(...))
|
||||
end
|
||||
local await = test_util.await
|
||||
|
||||
---@param path string
|
||||
---@param cb fun(err: nil|string)
|
||||
|
|
@ -41,6 +32,7 @@ local TmpDir = {}
|
|||
|
||||
TmpDir.new = function()
|
||||
local path = await(vim.loop.fs_mkdtemp, 2, "oil_test_XXXXXXXXX")
|
||||
a.util.scheduler()
|
||||
return setmetatable({ path = path }, {
|
||||
__index = TmpDir,
|
||||
})
|
||||
|
|
@ -60,6 +52,7 @@ function TmpDir:create(paths)
|
|||
end
|
||||
end
|
||||
end
|
||||
a.util.scheduler()
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
|
|
@ -72,6 +65,7 @@ local read_file = function(filepath)
|
|||
local stat = vim.loop.fs_fstat(fd)
|
||||
local content = vim.loop.fs_read(fd, stat.size)
|
||||
vim.loop.fs_close(fd)
|
||||
a.util.scheduler()
|
||||
return content
|
||||
end
|
||||
|
||||
|
|
@ -99,9 +93,9 @@ local assert_fs = function(root, paths)
|
|||
local pieces = vim.split(k, "/")
|
||||
local partial_path = ""
|
||||
for i, piece in ipairs(pieces) do
|
||||
partial_path = fs.join(partial_path, piece) .. "/"
|
||||
partial_path = partial_path .. piece .. "/"
|
||||
if i ~= #pieces then
|
||||
unlisted_dirs[partial_path:sub(2)] = true
|
||||
unlisted_dirs[partial_path] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -152,8 +146,23 @@ function TmpDir:assert_fs(paths)
|
|||
assert_fs(self.path, paths)
|
||||
end
|
||||
|
||||
function TmpDir:assert_exists(path)
|
||||
a.util.scheduler()
|
||||
path = fs.join(self.path, path)
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
assert.truthy(stat, string.format("Expected path '%s' to exist", path))
|
||||
end
|
||||
|
||||
function TmpDir:assert_not_exists(path)
|
||||
a.util.scheduler()
|
||||
path = fs.join(self.path, path)
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
assert.falsy(stat, string.format("Expected path '%s' to not exist", path))
|
||||
end
|
||||
|
||||
function TmpDir:dispose()
|
||||
await(fs.recursive_delete, 3, "directory", self.path)
|
||||
a.util.scheduler()
|
||||
end
|
||||
|
||||
return TmpDir
|
||||
|
|
|
|||
164
tests/trash_spec.lua
Normal file
164
tests/trash_spec.lua
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
local uv = vim.uv or vim.loop
|
||||
require("plenary.async").tests.add_to_env()
|
||||
local TmpDir = require("tests.tmpdir")
|
||||
local fs = require("oil.fs")
|
||||
local test_util = require("tests.test_util")
|
||||
|
||||
---Get the raw list of filenames from an unmodified oil buffer
|
||||
---@param bufnr? integer
|
||||
---@return string[]
|
||||
local function parse_entries(bufnr)
|
||||
bufnr = bufnr or 0
|
||||
if vim.bo[bufnr].modified then
|
||||
error("parse_entries doesn't work on a modified oil buffer")
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||
return vim.tbl_map(function(line)
|
||||
return line:match("^/%d+ +(.+)$")
|
||||
end, lines)
|
||||
end
|
||||
|
||||
a.describe("freedesktop", function()
|
||||
local tmpdir
|
||||
a.before_each(function()
|
||||
require("oil.config").delete_to_trash = true
|
||||
tmpdir = TmpDir.new()
|
||||
package.loaded["oil.adapters.trash"] = require("oil.adapters.trash.freedesktop")
|
||||
local trash_dir = string.format(".Trash-%d", uv.getuid())
|
||||
tmpdir:create({ fs.join(trash_dir, "__dummy__") })
|
||||
end)
|
||||
a.after_each(function()
|
||||
if tmpdir then
|
||||
tmpdir:dispose()
|
||||
end
|
||||
test_util.reset_editor()
|
||||
package.loaded["oil.adapters.trash"] = nil
|
||||
end)
|
||||
|
||||
a.it("files can be moved to the trash", function()
|
||||
tmpdir:create({ "a.txt", "foo/b.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
vim.api.nvim_feedkeys("p", "x", true)
|
||||
test_util.actions.save()
|
||||
tmpdir:assert_not_exists("a.txt")
|
||||
tmpdir:assert_exists("foo/b.txt")
|
||||
test_util.actions.reload()
|
||||
assert.are.same({ "a.txt" }, parse_entries(0))
|
||||
end)
|
||||
|
||||
a.it("deleting a file moves it to trash", function()
|
||||
tmpdir:create({ "a.txt", "foo/b.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
tmpdir:assert_not_exists("a.txt")
|
||||
tmpdir:assert_exists("foo/b.txt")
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
assert.are.same({ "a.txt" }, parse_entries(0))
|
||||
end)
|
||||
|
||||
a.it("deleting a directory moves it to trash", function()
|
||||
tmpdir:create({ "a.txt", "foo/b.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("foo/")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
tmpdir:assert_not_exists("foo")
|
||||
tmpdir:assert_exists("a.txt")
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
assert.are.same({ "foo/" }, parse_entries(0))
|
||||
end)
|
||||
|
||||
a.it("deleting a file from trash deletes it permanently", function()
|
||||
tmpdir:create({ "a.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.reload()
|
||||
tmpdir:assert_not_exists("a.txt")
|
||||
assert.are.same({}, parse_entries(0))
|
||||
end)
|
||||
|
||||
a.it("cannot create files in the trash", function()
|
||||
tmpdir:create({ "a.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
vim.api.nvim_feedkeys("onew_file.txt", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.reload()
|
||||
assert.are.same({ "a.txt" }, parse_entries(0))
|
||||
end)
|
||||
|
||||
a.it("cannot rename files in the trash", function()
|
||||
tmpdir:create({ "a.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
vim.api.nvim_feedkeys("0facwnew_name", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.reload()
|
||||
assert.are.same({ "a.txt" }, parse_entries(0))
|
||||
end)
|
||||
|
||||
a.it("cannot copy files in the trash", function()
|
||||
tmpdir:create({ "a.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
vim.api.nvim_feedkeys("yypp", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.reload()
|
||||
assert.are.same({ "a.txt" }, parse_entries(0))
|
||||
end)
|
||||
|
||||
a.it("can restore files from trash", function()
|
||||
tmpdir:create({ "a.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
test_util.actions.focus("a.txt")
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
vim.api.nvim_feedkeys("p", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.reload()
|
||||
assert.are.same({ "a.txt" }, parse_entries(0))
|
||||
local uid = uv.getuid()
|
||||
tmpdir:assert_fs({
|
||||
["a.txt"] = "a.txt",
|
||||
[".Trash-" .. uid .. "/__dummy__"] = ".Trash-" .. uid .. "/__dummy__",
|
||||
[".Trash-" .. uid .. "/files/"] = true,
|
||||
[".Trash-" .. uid .. "/info/"] = true,
|
||||
})
|
||||
end)
|
||||
|
||||
a.it("can have multiple files with the same name in trash", function()
|
||||
tmpdir:create({ "a.txt" })
|
||||
test_util.actions.open({ tmpdir.path })
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
tmpdir:create({ "a.txt" })
|
||||
test_util.actions.reload()
|
||||
vim.api.nvim_feedkeys("dd", "x", true)
|
||||
test_util.actions.save()
|
||||
test_util.actions.open({ "--trash", tmpdir.path })
|
||||
assert.are.same({ "a.txt", "a.txt" }, parse_entries(0))
|
||||
end)
|
||||
end)
|
||||
|
|
@ -13,7 +13,7 @@ describe("url", function()
|
|||
}
|
||||
for _, case in ipairs(cases) do
|
||||
local input, expected, expected_basename = unpack(case)
|
||||
local output, basename = oil.get_buffer_parent_url(input)
|
||||
local output, basename = oil.get_buffer_parent_url(input, true)
|
||||
assert.equals(expected, output, string.format('Parent url for path "%s" failed', input))
|
||||
assert.equals(
|
||||
expected_basename,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue