Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for trashing files #165

Merged
merged 32 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
03c600a
wip: skeleton code for trash adapter
stevearc Aug 21, 2023
906c656
refactor: split trash implementation for mac and linux
stevearc Aug 23, 2023
4edcaf6
fix: ensure we create the .Trash/$uid dir
stevearc Aug 23, 2023
b29e11b
feat: code complete linux trash implementation
stevearc Aug 26, 2023
9aeb0dd
doc: write up trash features
stevearc Aug 26, 2023
cdbc791
feat: code complete mac trash implementation
stevearc Aug 27, 2023
e13a31f
cleanup: remove previous, terrible, undocumented trash feature
stevearc Aug 27, 2023
43aa851
fix: always disabled trash
stevearc Aug 27, 2023
2b97e82
feat: show original path of trashed files
stevearc Aug 27, 2023
7ae2437
doc: add a note about calling actions directly
stevearc Aug 27, 2023
24958a8
fix: bugs in trash implementation
stevearc Aug 27, 2023
9f415bc
fix: schedule_wrap in mac trash
stevearc Aug 28, 2023
ab0b1f5
doc: fix typo and line wrapping
stevearc Sep 6, 2023
a22f448
fix: parsing of arguments to :Oil command
stevearc Sep 11, 2023
f7a0a90
doc: small documentation tweaks
stevearc Sep 12, 2023
44727a1
doc: fix awkward wording in the toggle_trash action
stevearc Sep 17, 2023
60a3f56
fix: warning on Windows when delete_to_trash = true
stevearc Sep 17, 2023
35cea94
feat: :Oil --trash can open specific trash directories
stevearc Sep 19, 2023
f08e26b
fix: show all trash files in device root
stevearc Oct 12, 2023
ea88400
fix: trash mtime should be sortable
stevearc Oct 12, 2023
0002f90
fix: shorten_path handles optional trailing slash
stevearc Oct 17, 2023
6beec59
refactor: overhaul the UI
stevearc Oct 17, 2023
cdf2d71
fix: keep trash original path vtext from stacking
stevearc Oct 18, 2023
4db7b78
refactor: replace disable_changes with an error filter
stevearc Oct 21, 2023
5267e9e
fix: shorten path names in home directory relative to root
stevearc Oct 21, 2023
f6d9c02
doc: small README format changes
stevearc Oct 22, 2023
f0f86d8
cleanup: remove unnecessary preserve_undo logic
stevearc Oct 22, 2023
13ecc84
test: add a functional test for the freedesktop trash adapter
stevearc Oct 23, 2023
0ebcbf6
test: more functional tests for trash
stevearc Oct 24, 2023
2e70af6
fix: schedule a callback to avoid main loop error
stevearc Oct 24, 2023
97cd378
refactor: clean up mutator logic
stevearc Nov 5, 2023
fc731fe
doc: some comments and type annotations
stevearc Nov 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: overhaul the UI
  • Loading branch information
stevearc committed Nov 5, 2023
commit 6beec596ebe0817c3a4d7de3158964abbf4bfd73
90 changes: 64 additions & 26 deletions lua/oil/adapters/trash/freedesktop.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
-- Based on the FreeDesktop.org trash specification
-- https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html
-- TODO make sure that the subdirs for trash use the same entry as the root
local cache = require("oil.cache")
local config = require("oil.config")
local constants = require("oil.constants")
Expand Down Expand Up @@ -103,21 +102,6 @@ local function get_write_trash_dir(path)
return top_trash
end

---@param path string
---@return boolean
local function is_dev_root(path)
if path == "/" then
return true
end
local stat = uv.fs_stat(path)
if not stat then
return false
end
local dev = stat.dev
local parent = vim.fs.dirname(path)
return uv.fs_stat(parent).dev ~= dev
end

---@param path string
---@return string[]
local function get_read_trash_dirs(path)
Expand All @@ -141,20 +125,24 @@ M.normalize_url = function(url, callback)
)
end

---@param _url string
---@param url string
---@param entry oil.Entry
---@param cb fun(path: string)
M.get_entry_path = function(_url, entry, cb)
M.get_entry_path = function(url, entry, cb)
local internal_entry = assert(cache.get_entry_by_id(entry.id))
local meta = internal_entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = meta.trash_info
if not trash_info then
-- This is a subpath in the trash
M.normalize_url(url, cb)
return
end
local path = fs.os_to_posix_path(trash_info.trash_file)
if meta.stat.type == "directory" then
path = util.addslash(path)
end
local url = "oil://" .. path
cb(url)
cb("oil://" .. path)
end

---@class oil.TrashInfo
Expand Down Expand Up @@ -237,7 +225,6 @@ M.list = function(url, column_defs, cb)
cb = vim.schedule_wrap(cb)
local _, path = util.parse_url(url)
assert(path)
local is_in_dev_root = is_dev_root(path)
local trash_dirs = get_read_trash_dirs(path)
local trash_idx = 0

Expand All @@ -248,6 +235,16 @@ M.list = function(url, column_defs, cb)
if not trash_dir then
return cb()
end

-- Show all files from the trash directory if we are in the root of the device, which we can
-- tell if the trash dir is a subpath of our current path
local show_all_files = fs.is_subpath(path, trash_dir)
-- The first trash dir is a special case; it is in the home directory and we should only show
-- all entries if we are in the top root path "/"
if trash_idx == 1 then
show_all_files = path == "/"
end

local info_dir = fs.join(trash_dir, "info")
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_opendir(info_dir, function(open_err, fd)
Expand Down Expand Up @@ -288,7 +285,7 @@ M.list = function(url, column_defs, cb)
poll()
else
local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ":h"))
if path == parent or is_in_dev_root then
if path == parent or show_all_files then
local name = vim.fn.fnamemodify(info.trash_file, ":t")
---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, info.stat.type)
Expand All @@ -300,6 +297,21 @@ M.list = function(url, column_defs, cb)
}
table.insert(internal_entries, cache_entry)
end
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name)
while next_par ~= "." do
name = next_par
next_par = vim.fs.dirname(name)
end
---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, "directory")

cache_entry[FIELD_META] = {
stat = info.stat,
}
table.insert(internal_entries, cache_entry)
end
poll()
end
end)
Expand Down Expand Up @@ -338,16 +350,20 @@ file_columns.mtime = {
local meta = entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = meta.trash_info
local time = trash_info and trash_info.deletion_date or meta.stat and meta.stat.mtime.sec
if not time then
return nil
end
local fmt = conf and conf.format
local ret
if fmt then
ret = vim.fn.strftime(fmt, trash_info.deletion_date)
ret = vim.fn.strftime(fmt, time)
else
local year = vim.fn.strftime("%Y", trash_info.deletion_date)
local year = vim.fn.strftime("%Y", time)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", trash_info.deletion_date)
ret = vim.fn.strftime("%b %d %Y", time)
else
ret = vim.fn.strftime("%b %d %H:%M", trash_info.deletion_date)
ret = vim.fn.strftime("%b %d %H:%M", time)
end
end
return ret
Expand Down Expand Up @@ -384,6 +400,28 @@ end

M.supported_cross_adapter_actions = { files = "move" }

---@param action oil.Action
---@return boolean
M.filter_action = function(action)
if action.type == "create" then
return false
elseif action.type == "delete" then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
return meta.trash_info ~= nil
elseif action.type == "move" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
return src_adapter.name == "files" or dest_adapter.name == "files"
elseif action.type == "copy" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
return src_adapter.name == "files" or dest_adapter.name == "files"
else
error(string.format("Bad action type '%s'", action.type))
end
end

---@param action oil.Action
---@return string
M.render_action = function(action)
Expand Down
1 change: 1 addition & 0 deletions lua/oil/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ local M = {}
---@field write_file? fun(bufnr: integer) Used for adapters that deal with remote/virtual files. Write the contents of a buffer to the destination.
---@field supported_cross_adapter_actions? table<string, oil.CrossAdapterAction> Mapping of adapter name to enum for all other adapters that can be used as a src or dest for move/copy actions.
---@field disable_changes? boolean When true, adapter will not support creating new entries or changing (e.g. renaming) existing entries
---@field filter_action? fun(action: oil.Action): boolean When present, filter out actions as they are created

-- TODO remove after https://github.com/folke/neodev.nvim/pull/163 lands
---@diagnostic disable: undefined-field
Expand Down
23 changes: 17 additions & 6 deletions lua/oil/mutator/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ M.create_actions_from_diffs = function(all_diffs)
if not adapter then
error("Missing adapter")
end
local function add_action(action)
if not adapter.filter_action or adapter.filter_action(action) then
table.insert(actions, action)
end
end
local parent_url = vim.api.nvim_buf_get_name(bufnr)
for _, diff in ipairs(diffs) do
if diff.type == "new" then
Expand All @@ -86,7 +91,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- Parse alternations like foo.{js,test.js}
for _, alt in ipairs(vim.split(alternation, ",")) do
local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt)
table.insert(actions, {
add_action({
type = "create",
url = alt_url,
entry_type = entry_type,
Expand All @@ -95,7 +100,7 @@ M.create_actions_from_diffs = function(all_diffs)
end
else
url = url .. "/" .. v
table.insert(actions, {
add_action({
type = "create",
url = url,
entry_type = entry_type,
Expand All @@ -105,7 +110,7 @@ M.create_actions_from_diffs = function(all_diffs)
end
end
elseif diff.type == "change" then
table.insert(actions, {
add_action({
type = "change",
url = parent_url .. diff.name,
entry_type = diff.entry_type,
Expand All @@ -121,6 +126,12 @@ M.create_actions_from_diffs = function(all_diffs)
end
end

local function add_action(action)
local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url))
if not adapter.filter_action or adapter.filter_action(action) then
table.insert(actions, action)
end
end
for id, diffs in pairs(diff_by_id) do
local entry = cache.get_entry_by_id(id)
if not entry then
Expand All @@ -131,7 +142,7 @@ M.create_actions_from_diffs = function(all_diffs)
if has_create then
-- MOVE (+ optional copies) when has both creates and delete
for i, diff in ipairs(diffs) do
table.insert(actions, {
add_action({
type = i == #diffs and "move" or "copy",
entry_type = entry[FIELD_TYPE],
dest_url = diff.dest,
Expand All @@ -140,7 +151,7 @@ M.create_actions_from_diffs = function(all_diffs)
end
else
-- DELETE when no create
table.insert(actions, {
add_action({
type = "delete",
entry_type = entry[FIELD_TYPE],
url = cache.get_parent_url(id) .. entry[FIELD_NAME],
Expand All @@ -149,7 +160,7 @@ M.create_actions_from_diffs = function(all_diffs)
else
-- COPY when create but no delete
for _, diff in ipairs(diffs) do
table.insert(actions, {
add_action({
type = "copy",
entry_type = entry[FIELD_TYPE],
src_url = cache.get_parent_url(id) .. entry[FIELD_NAME],
Expand Down
10 changes: 6 additions & 4 deletions lua/oil/view.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local cache = require("oil.cache")
local columns = require("oil.columns")
local config = require("oil.config")
local constants = require("oil.constants")
local fs = require("oil.fs")
local keymap_util = require("oil.keymap_util")
local loading = require("oil.loading")
local util = require("oil.util")
Expand Down Expand Up @@ -427,7 +428,7 @@ local function render_buffer(bufnr, opts)
jump = false,
jump_first = false,
})
local scheme = util.parse_url(bufname)
local scheme, buf_path = util.parse_url(bufname)
local adapter = util.get_adapter(bufnr)
if not scheme or not adapter then
return false
Expand Down Expand Up @@ -480,17 +481,18 @@ local function render_buffer(bufnr, opts)
util.set_highlights(bufnr, highlights)

-- Show original location of trash file as virtual text
if adapter.name == "trash" and bufname == "oil-trash:///" then
if adapter.name == "trash" then
local ns = vim.api.nvim_create_namespace("OilVtext")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this should show the original path of the thrashed file, but I see nothing.

nvim_oil_trash_scheme_no_virtual_text

This shows Oil in floating mode, but same in normal mode.
The file gamble.xml is in trash.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional, but perhaps not intuitive. You're using the Linux version which has support for listing the trash files per-directory. Since you're looking at the trashed files from a specific directory, there's no point to displaying the original path. If you open the root :Oil --trash / it shows all trashed files, and the original path for each.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, virtual text shows up fine in root trash.
To be honest, it is pretty unintuitive (for me).
What I thought would happen is:

  • virtual text is always visible
  • listing of thrash files is per-directory recursively downwards

Now that I know how it works, it is definitely usable though.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain more what you mean by "per-directory recursively downwards"?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, given the following directory state (trashed files are marked with *):

- project
  - fileA
  - fileB
  - fileC*
  - dirA
    - fileA.A
    - fileA.B*
    - fileA.C
    - dirA.D
      - fileA.D.A*
      - fileA.D.B
  - dirB
    - fileB.A
    - fileB.B*
    - fileB.C*

Oil --trash project shows

- fileC -> **virtual text...**
- fileA.B -> **...**
- fileA.D.A -> **...**
- fileB.B -> **...**
- fileB.c -> **...**

Oil --trash dirA shows

- fileA.B -> **...**
- fileA.D.A -> **...**

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reworked the display logic. The trash directory will now show files that were trashed from that directory, and also directories that lead to trashed files from subdirs. When you are at the root of a trash scope (the root of a device, or the filesystem root), all trashed files will be visible.
I've also changed it so that trashed files will always have virtual text that displays their original path.

vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
local os_path = fs.posix_to_os_path(assert(buf_path))
for i, entry in ipairs(entry_list) do
local meta = entry[FIELD_META]
---@type nil|oil.TrashInfo
local trash_info = meta and meta.trash_info
if trash_info then
vim.api.nvim_buf_set_extmark(0, ns, i - 1, 0, {
vim.api.nvim_buf_set_extmark(bufnr, ns, i - 1, 0, {
virt_text = {
{ "➜ " .. trash_info.original_path, "OilTrashSourcePath" },
{ "➜ " .. fs.shorten_path(trash_info.original_path, os_path), "OilTrashSourcePath" },
},
})
end
Expand Down