local util = require('canola.util') local uv = vim.uv or vim.loop local cache = require('canola.cache') local config = require('canola.config') local constants = require('canola.constants') local files = require('canola.adapters.files') local fs = require('canola.fs') local powershell_trash = require('canola.adapters.trash.windows.powershell-trash') local FIELD_META = constants.FIELD_META local FIELD_TYPE = constants.FIELD_TYPE local M = {} ---@return string local function get_trash_dir() local cwd = assert(vim.fn.getcwd()) local trash_dir = cwd:sub(1, 3) .. '$Recycle.Bin' if vim.fn.isdirectory(trash_dir) == 1 then return trash_dir end trash_dir = 'C:\\$Recycle.Bin' if vim.fn.isdirectory(trash_dir) == 1 then return trash_dir end error('No trash found') end ---@param path string ---@return string local win_addslash = function(path) if not vim.endswith(path, '\\') then return path .. '\\' else return path end end ---@class canola.WindowsTrashInfo ---@field trash_file string ---@field original_path string ---@field deletion_date integer ---@field info_file? string ---@param url string ---@param column_defs string[] ---@param cb fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun()) M.list = function(url, column_defs, cb) local _, path = util.parse_url(url) path = fs.posix_to_os_path(assert(path)) local trash_dir = get_trash_dir() local show_all_files = fs.is_subpath(path, trash_dir) powershell_trash.list_raw_entries(function(err, raw_entries) if err then cb(err) return end local raw_displayed_entries = vim.tbl_filter( ---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string} function(entry) local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ':h'))) local is_in_path = path == parent local is_subpath = fs.is_subpath(path, parent) return is_in_path or is_subpath or show_all_files end, raw_entries ) local displayed_entries = vim.tbl_map( ---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string} ---@return {[1]:nil, [2]:string, [3]:string, [4]:{stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}} function(entry) local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ':h'))) --- @type canola.InternalEntry local cache_entry if path == parent or show_all_files then local deleted_file_tail = assert(vim.fn.fnamemodify(entry.Path, ':t')) local deleted_file_head = assert(vim.fn.fnamemodify(entry.Path, ':h')) local info_file_head = deleted_file_head --- @type string? local info_file cache_entry = cache.create_entry(url, deleted_file_tail, entry.IsFolder and 'directory' or 'file') -- info_file on windows has the following format: $I<6 char hash>. -- the hash is the same for the deleted file and the info file -- so, we take the hash (and extension) from the deleted file -- -- see https://superuser.com/questions/368890/how-does-the-recycle-bin-in-windows-work/1736690#1736690 local info_file_tail = deleted_file_tail:match('^%$R(.*)$') --[[@as string?]] if info_file_tail then info_file_tail = '$I' .. info_file_tail info_file = info_file_head .. '\\' .. info_file_tail end cache_entry[FIELD_META] = { stat = nil, ---@type canola.WindowsTrashInfo trash_info = { trash_file = entry.Path, original_path = entry.OriginalPath, deletion_date = entry.DeletionDate, info_file = info_file, }, display_name = entry.Name, } 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) cache_entry = cache.create_entry(url, name, 'directory') cache_entry[FIELD_META] = {} end end return cache_entry end, raw_displayed_entries ) cb(nil, displayed_entries) end) end M.is_modifiable = function(_bufnr) return true end local current_year -- Make sure we run this import-time effect in the main loop (mostly for tests) vim.schedule(function() current_year = vim.fn.strftime('%Y') end) local file_columns = {} file_columns.mtime = { render = function(entry, conf) local meta = entry[FIELD_META] if not meta then return nil end ---@type canola.WindowsTrashInfo local trash_info = meta.trash_info local time = trash_info and trash_info.deletion_date if not time then return nil end local fmt = conf and conf.format local ret if fmt then ret = vim.fn.strftime(fmt, time) else local year = vim.fn.strftime('%Y', time) if year ~= current_year then ret = vim.fn.strftime('%b %d %Y', time) else ret = vim.fn.strftime('%b %d %H:%M', time) end end return ret end, get_sort_value = function(entry) local meta = entry[FIELD_META] ---@type nil|canola.WindowsTrashInfo local trash_info = meta and meta.trash_info if trash_info and trash_info.deletion_date then return trash_info.deletion_date else return 0 end end, parse = function(line, conf) local fmt = conf and conf.format local pattern if fmt then pattern = fmt:gsub('%%.', '%%S+') else pattern = '%S+%s+%d+%s+%d%d:?%d%d' end return line:match('^(' .. pattern .. ')%s+(.+)$') end, } ---@param name string ---@return nil|canola.ColumnDefinition M.get_column = function(name) return file_columns[name] end ---@param action canola.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 ~= nil and 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' -- selene: allow(if_same_then_else) 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 url string ---@param callback fun(url: string) M.normalize_url = function(url, callback) local scheme, path = util.parse_url(url) assert(path) local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p') assert(os_path) uv.fs_realpath( os_path, vim.schedule_wrap(function(_err, new_os_path) local realpath = new_os_path or os_path callback(scheme .. util.addslash(fs.os_to_posix_path(realpath))) end) ) end ---@param url string ---@param entry canola.Entry ---@param cb fun(path: string) 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] --[[@as {stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}]] local trash_info = meta and 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 entry.type == 'directory' then path = win_addslash(path) end cb('canola://' .. path) end ---@param err canola.ParseError ---@return boolean M.filter_error = function(err) if err.message == 'Duplicate filename' then return false end return true end ---@param action canola.Action ---@return string M.render_action = function(action) if action.type == 'delete' then local entry = assert(cache.get_entry_by_url(action.url)) local meta = entry[FIELD_META] ---@type canola.WindowsTrashInfo local trash_info = assert(meta).trash_info local short_path = fs.shorten_path(trash_info.original_path) return string.format(' PURGE %s', short_path) 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)) if src_adapter.name == 'files' then local _, path = util.parse_url(action.src_url) assert(path) local short_path = files.to_short_os_path(path, action.entry_type) return string.format(' TRASH %s', short_path) elseif dest_adapter.name == 'files' then local _, path = util.parse_url(action.dest_url) assert(path) local short_path = files.to_short_os_path(path, action.entry_type) return string.format('RESTORE %s', short_path) else error('Must be moving files into or out of trash') end 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)) if src_adapter.name == 'files' then local _, path = util.parse_url(action.src_url) assert(path) local short_path = files.to_short_os_path(path, action.entry_type) return string.format(' COPY %s -> TRASH', short_path) elseif dest_adapter.name == 'files' then local _, path = util.parse_url(action.dest_url) assert(path) local short_path = files.to_short_os_path(path, action.entry_type) return string.format('RESTORE %s', short_path) else error('Must be copying files into or out of trash') end else error(string.format("Bad action type '%s'", action.type)) end end ---@param trash_info canola.WindowsTrashInfo ---@param cb fun(err?: string, raw_entries: canola.WindowsRawEntry[]?) local purge = function(trash_info, cb) fs.recursive_delete('file', trash_info.info_file, function(err) if err then return cb(err) end fs.recursive_delete('file', trash_info.trash_file, cb) end) end ---@param path string ---@param type string ---@param cb fun(err?: string, trash_info?: canola.TrashInfo) local function create_trash_info_and_copy(path, type, cb) local temp_path = path .. 'temp' -- create a temporary copy on the same location fs.recursive_copy( type, path, temp_path, vim.schedule_wrap(function(err) if err then return cb(err) end -- delete original file M.delete_to_trash(path, function(err2) if err2 then return cb(err2) end -- rename temporary copy to the original file name fs.recursive_move(type, temp_path, path, cb) end) end) ) end ---@param action canola.Action ---@param cb fun(err: nil|string) M.perform_action = function(action, cb) if action.type == 'delete' then local entry = assert(cache.get_entry_by_url(action.url)) local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}]] local trash_info = meta and meta.trash_info purge(trash_info, cb) 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)) if src_adapter.name == 'files' then local _, path = util.parse_url(action.src_url) M.delete_to_trash(assert(path), cb) elseif dest_adapter.name == 'files' then -- Restore local _, dest_path = util.parse_url(action.dest_url) assert(dest_path) dest_path = fs.posix_to_os_path(dest_path) local entry = assert(cache.get_entry_by_url(action.src_url)) local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}]] local trash_info = meta and meta.trash_info fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err) if err then return cb(err) end uv.fs_unlink(trash_info.info_file, cb) end) end 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)) if src_adapter.name == 'files' then local _, path = util.parse_url(action.src_url) assert(path) path = fs.posix_to_os_path(path) local entry = assert(cache.get_entry_by_url(action.src_url)) create_trash_info_and_copy(path, entry[FIELD_TYPE], cb) elseif dest_adapter.name == 'files' then -- Restore local _, dest_path = util.parse_url(action.dest_url) assert(dest_path) dest_path = fs.posix_to_os_path(dest_path) local entry = assert(cache.get_entry_by_url(action.src_url)) local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}]] local trash_info = meta and meta.trash_info fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb) else error('Must be moving files into or out of trash') end else cb(string.format('Bad action type: %s', action.type)) end end M.supported_cross_adapter_actions = { files = 'move' } ---@param path string ---@param cb fun(err?: string) M.delete_to_trash = function(path, cb) powershell_trash.delete_to_trash(path, cb) end return M