From 2d4759f61fd399db0986260c084ace656acfad1e Mon Sep 17 00:00:00 2001 From: pynappo Date: Sun, 26 Oct 2025 01:28:21 -0700 Subject: [PATCH 01/10] add trash --- lua/neo-tree/defaults.lua | 5 + lua/neo-tree/health/init.lua | 13 ++ lua/neo-tree/log.lua | 13 +- lua/neo-tree/sources/common/commands.lua | 40 ++++- lua/neo-tree/sources/common/file-items.lua | 2 +- .../sources/filesystem/lib/fs_actions.lua | 160 ++++++++++++++---- lua/neo-tree/sources/filesystem/lib/trash.lua | 133 +++++++++++++++ lua/neo-tree/types/config.lua | 4 + lua/neo-tree/utils/init.lua | 12 +- 9 files changed, 342 insertions(+), 40 deletions(-) create mode 100644 lua/neo-tree/sources/filesystem/lib/trash.lua diff --git a/lua/neo-tree/defaults.lua b/lua/neo-tree/defaults.lua index 711051a3e..922cbba71 100644 --- a/lua/neo-tree/defaults.lua +++ b/lua/neo-tree/defaults.lua @@ -60,6 +60,9 @@ local config = { sort_function = nil , -- uses a custom function for sorting files and directories in the tree use_popups_for_input = true, -- If false, inputs will use vim.ui.input() instead of custom floats. use_default_mappings = true, + trash = { + cmd = nil -- by default: powershell script on windows, `trash` or `osascript` on macOS, and `gio trash` or `trash` (like trash-cli) on other Unixes + }, -- source_selector provides clickable tabs to switch between sources. source_selector = { winbar = false, -- toggle to show selector on winbar @@ -451,6 +454,8 @@ local config = { }, ["A"] = "add_directory", -- also accepts the config.show_path and config.insert_as options. ["d"] = "delete", + -- ["d"] = "trash", + ["T"] = "trash", ["r"] = "rename", ["y"] = "copy_to_clipboard", ["x"] = "cut_to_clipboard", diff --git a/lua/neo-tree/health/init.lua b/lua/neo-tree/health/init.lua index 7ea49c847..e4c082120 100644 --- a/lua/neo-tree/health/init.lua +++ b/lua/neo-tree/health/init.lua @@ -59,6 +59,9 @@ function M.check_config(config) function(cfg) ---@class neotree.health.Validator.Generators local v = { + ---@generic T + ---@param validator neotree.health.Validator + ---@return fun(arr: T[]) array = function(validator) ---@generic T ---@param arr T[] @@ -177,6 +180,16 @@ function M.check_config(config) validate("sort_function", cfg.sort_function, "function", true) validate("use_popups_for_input", cfg.use_popups_for_input, "boolean") validate("use_default_mappings", cfg.use_default_mappings, "boolean") + validate("trash", cfg.trash, function(trash) + validate("cmd", trash.cmd, function(cmd) + if type(cmd) == "function" then + local cmd_output = cmd({ "neotree/test/update" }) + validate("", cmd_output, v.array("string"), true) + elseif type(cmd) == "table" then + v.array("string")(cmd) + end + end, true) + end) validate("source_selector", cfg.source_selector, function(ss) validate("winbar", ss.winbar, "boolean") validate("statusline", ss.statusline, "boolean") diff --git a/lua/neo-tree/log.lua b/lua/neo-tree/log.lua index 047a6d635..251d9911d 100644 --- a/lua/neo-tree/log.lua +++ b/lua/neo-tree/log.lua @@ -100,6 +100,7 @@ log_maker.new = function(config) or table.concat({ config.plugin_short, prefix }, " ") local title_opts = { title = config.plugin_short } + ---@param message string ---@param level vim.log.levels local notify = vim.schedule_wrap(function(message, level) @@ -205,9 +206,7 @@ log_maker.new = function(config) -- Output to console if config.use_console and can_log_to_console then - vim.schedule(function() - notify(msg, log_level) - end) + notify(msg, log_level) end end end @@ -262,6 +261,9 @@ log_maker.new = function(config) log.error = logfunc(Levels.ERROR, make_string) ---Unused, kept around for compatibility at the moment. Remove in v4.0. log.fatal = logfunc(Levels.FATAL, make_string) + + log.notify = notify + log.levels = Levels -- tree-sitter queries recognize any .format and highlight it w/ string.format highlights ---@type table log.at = { @@ -374,8 +376,11 @@ log_maker.new = function(config) else errmsg = "assertion failed!" end + local old = config.use_console + config.use_console = false log.error(errmsg) - return assert(v, errmsg) + config.use_console = old + error(errmsg, 2) end ---@param context string diff --git a/lua/neo-tree/sources/common/commands.lua b/lua/neo-tree/sources/common/commands.lua index ac6b1d198..ae63012b4 100644 --- a/lua/neo-tree/sources/common/commands.lua +++ b/lua/neo-tree/sources/common/commands.lua @@ -704,7 +704,7 @@ M.delete = function(state, callback) fs_actions.delete_node(node.path, callback) end ----@param callback function +---@param callback fun(path: string) ---@type neotree.TreeCommandVisual M.delete_visual = function(state, selected_nodes, callback) local paths_to_delete = {} @@ -725,6 +725,44 @@ M.delete_visual = function(state, selected_nodes, callback) fs_actions.delete_nodes(paths_to_delete, callback) end +M.trash = function(state) + local node = assert(state.tree:get_node()) + if node.type ~= "file" and node.type ~= "directory" then + log.warn("The `trash` command can only be used on files and directories") + return + end + if node:get_depth() == 1 then + log.error( + "Will not trash root node " + .. node.path + .. ", please back out of the current directory if you want to trash the root node." + ) + return + end + fs_actions.trash_node(node.path) +end + +---@param callback fun(path: string) +---@type neotree.TreeCommandVisual +M.trash_visual = function(state, selected_nodes, callback) + local paths_to_trash = {} + for _, node_to_trash in pairs(selected_nodes) do + if node_to_trash:get_depth() == 1 then + log.error( + "Will not trash root node " + .. node_to_trash.path + .. ", please back out of the current directory if you want to trash the root node." + ) + return + end + + if node_to_trash.type == "file" or node_to_trash.type == "directory" then + table.insert(paths_to_trash, node_to_trash.path) + end + end + fs_actions.trash_nodes(paths_to_trash, callback) +end + M.preview = function(state) Preview.show(state) end diff --git a/lua/neo-tree/sources/common/file-items.lua b/lua/neo-tree/sources/common/file-items.lua index 7b7cce44b..2e114284f 100644 --- a/lua/neo-tree/sources/common/file-items.lua +++ b/lua/neo-tree/sources/common/file-items.lua @@ -199,7 +199,7 @@ local function create_item(context, path, _type, bufnr) if item.type == "link" then ---@cast item neotree.FileItem.Link item.is_link = true - item.link_to = uv.fs_readlink(path) + item.link_to = uv.fs_realpath(path) if item.link_to then local link_to_stat = uv.fs_stat(item.path) if link_to_stat then diff --git a/lua/neo-tree/sources/filesystem/lib/fs_actions.lua b/lua/neo-tree/sources/filesystem/lib/fs_actions.lua index a97da67e2..aaaf32450 100644 --- a/lua/neo-tree/sources/filesystem/lib/fs_actions.lua +++ b/lua/neo-tree/sources/filesystem/lib/fs_actions.lua @@ -4,11 +4,11 @@ -- https://github.com/mhartington/dotfiles -- and modified to fit neo-tree's api. -- Permalink: https://github.com/mhartington/dotfiles/blob/7560986378753e0c047d940452cb03a3b6439b11/config/nvim/lua/mh/filetree/init.lua -local api = vim.api local uv = vim.uv or vim.loop local scan = require("plenary.scandir") local utils = require("neo-tree.utils") local inputs = require("neo-tree.ui.inputs") +local trash = require("neo-tree.sources.filesystem.lib.trash") local events = require("neo-tree.events") local log = require("neo-tree.log") local Path = require("plenary.path") @@ -327,7 +327,7 @@ M.copy_node = function(source, destination, callback, using_root_directory) local source_stat = log.assert(uv.fs_lstat(source)) if source_stat.type == "link" then - local target = log.assert(uv.fs_readlink(source)) + local target = log.assert(uv.fs_realpath(source)) local symlink_ok, err = uv.fs_symlink(target, destination) log.assert(symlink_ok, "Could not copy symlink ", source, "to", destination, ":", err) else @@ -542,17 +542,28 @@ local function delete_dir(dir_path) end -- Delete Node +---@param path string +---@param callback fun(string)? +---@param noconfirm boolean? M.delete_node = function(path, callback, noconfirm) local _, name = utils.split_path(path) - local msg = string.format("Are you sure you want to delete '%s'?", name) log.trace("Deleting node:", path) local _type = "unknown" - local stat = uv.fs_stat(path) - if stat then + local stat = uv.fs_lstat(path) + local children_count = 0 + if not stat then + log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...") + -- Guess the type by whether it appears to have an extension + if path:match("%.(.+)$") then + _type = "file" + else + _type = "directory" + end + else _type = stat.type if _type == "link" then - local link_to = uv.fs_readlink(path) + local link_to = uv.fs_realpath(path) if not link_to then log.error("Could not read link") return @@ -564,25 +575,13 @@ M.delete_node = function(path, callback, noconfirm) _type = uv.fs_stat(link_to).type end if _type == "directory" then - local children = scan.scan_dir(path, { + children_count = #scan.scan_dir(path, { hidden = true, respect_gitignore = false, add_dirs = true, depth = 1, }) - if #children > 0 then - msg = "WARNING: Dir not empty! " .. msg - end end - else - log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...") - -- Guess the type by whether it appears to have an extension - if path:match("%.(.+)$") then - _type = "file" - else - _type = "directory" - end - return end local do_delete = function() @@ -599,29 +598,34 @@ M.delete_node = function(path, callback, noconfirm) return end - if _type == "directory" then + if _type ~= "directory" then + local success = uv.fs_unlink(path) + if not success then + return log.error("Could not remove file: " .. path) + end + clear_buffer(path) + else -- first try using native system commands, which are recursive local success = false if utils.is_windows then - local result = - vim.fn.system({ "cmd.exe", "/c", "rmdir", "/s", "/q", vim.fn.shellescape(path) }) - local error = vim.v.shell_error - if error ~= 0 then + local delete_ok, result = + utils.execute_command({ "cmd.exe", "/c", "rmdir", "/s", "/q", vim.fn.shellescape(path) }) + if not delete_ok then log.debug("Could not delete directory '", path, "' with rmdir: ", result) else log.info("Deleted directory ", path) success = true end else - local result = vim.fn.system({ "rm", "-Rf", path }) - local error = vim.v.shell_error - if error ~= 0 then + local delete_ok, result = utils.execute_command({ "rm", "-Rf", path }) + if not delete_ok then log.debug("Could not delete directory '", path, "' with rm: ", result) else log.info("Deleted directory ", path) success = true end end + -- Fallback to using libuv if native commands fail if not success then success = delete_dir(path) @@ -629,12 +633,6 @@ M.delete_node = function(path, callback, noconfirm) return log.error("Could not remove directory: " .. path) end end - else - local success = uv.fs_unlink(path) - if not success then - return log.error("Could not remove file: " .. path) - end - clear_buffer(path) end complete() end @@ -642,6 +640,14 @@ M.delete_node = function(path, callback, noconfirm) if noconfirm then do_delete() else + local msg = string.format("Are you sure you want to delete '%s'?", name) + if children_count > 0 then + msg = ("WARNING: Dir has %s %s! %s"):format( + children_count, + children_count == 1 and "child" or "children", + msg + ) + end inputs.confirm(msg, function(confirmed) if confirmed then do_delete() @@ -650,6 +656,8 @@ M.delete_node = function(path, callback, noconfirm) end end +---@param paths_to_delete string[] +---@param callback fun(path)? M.delete_nodes = function(paths_to_delete, callback) local msg = "Are you sure you want to delete " .. #paths_to_delete .. " items?" inputs.confirm(msg, function(confirmed) @@ -669,6 +677,92 @@ M.delete_nodes = function(paths_to_delete, callback) end) end +-- Trash Node +---@param path string +---@param callback fun(string)? +---@param noconfirm boolean? +M.trash_node = function(path, callback, noconfirm) + local _, name = utils.split_path(path) + + log.trace("Trashing node:", path) + local _type = "unknown" + local stat = uv.fs_lstat(path) + local children_count = 0 + if not stat then + log.error("Could not read", path) + return + end + + _type = stat.type + if _type == "directory" then + children_count = #scan.scan_dir(path, { + hidden = true, + respect_gitignore = false, + add_dirs = true, + depth = 1, + }) + end + + local do_trash = function() + local complete = vim.schedule_wrap(function() + events.fire_event(events.FILE_DELETED, path) + if callback then + callback(path) + end + end) + + local event_result = events.fire_event(events.BEFORE_FILE_DELETE, path) or {} + if event_result.handled then + complete() + return + end + + log.assert(trash.trash({ path })) + log.info("Trashed", path) + complete() + end + + if noconfirm then + do_trash() + else + local msg = string.format("Are you sure you want to trash '%s'?", name) + if children_count > 0 then + msg = ("WARNING: Dir has %s %s! %s"):format( + children_count, + children_count == 1 and "child" or "children", + msg + ) + end + inputs.confirm(msg, function(confirmed) + if confirmed then + do_trash() + end + end) + end +end + +---@param paths_to_trash string[] +---@param callback fun(path)? +M.trash_nodes = function(paths_to_trash, callback) + local msg = "Are you sure you want to trash " .. #paths_to_trash .. " items?" + inputs.confirm(msg, function(confirmed) + if not confirmed then + return + end + + for _, path in ipairs(paths_to_trash) do + M.trash_node(path, nil, true) + end + + log.info("Trashed", #paths_to_trash, #paths_to_trash == 1 and "file" or "files") + if callback then + vim.schedule(function() + callback(paths_to_trash[#paths_to_trash]) + end) + end + end) +end + local rename_node = function(msg, name, get_destination, path, callback) inputs.input(msg, name, function(new_name) -- If cancelled diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua new file mode 100644 index 000000000..d3a1c0620 --- /dev/null +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -0,0 +1,133 @@ +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") +local M = {} + +---Either rm-like, or a custom list of commands +---@alias neotree.trash.Command string[]|fun(paths: string[]):string[][]? + +---Returns a list of possible commands for a platform. +---@param paths string[] +---@return (neotree.trash.Command)[] possible_commands +M.generate_commands = function(paths) + log.assert(#paths > 0) + local commands = { + require("neo-tree").config.trash.cmd, + } + + -- Using code from https://github.com/folke/snacks.nvim/blob/ed08ef1a630508ebab098aa6e8814b89084f8c03/lua/snacks/explorer/actions.lua + if utils.is_macos then + vim.list_extend(commands, { + { "trash" }, -- trash-cli (Python or Node.js) + function(p) + local cmds = {} + for i, path in ipairs(p) do + cmds[i] = { + "osascript", + "-e", + ('tell application "Finder" to delete POSIX file "%s"'):format( + path:gsub("\\", "\\\\"):gsub('"', '\\"') + ), + } + end + return cmds + end, + }) + elseif utils.is_windows then + vim.list_extend(commands, { + { "trash" }, -- trash-cli (Python or Node.js) + function(p) + local powershell = utils.executable("pwsh") or utils.executable("powershell") + if not powershell then + return nil + end + + local cmd = { + powershell, + "-NoProfile", + "-Command", + } + + local pwsh_cmds = { + "Add-Type -AssemblyName Microsoft.VisualBasic;", + } + for _, path in ipairs(p) do + local escaped = path:gsub("\\", "\\\\"):gsub("'", "''") + pwsh_cmds[#pwsh_cmds + 1] = ("[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('%s','OnlyErrorDialogs', 'SendToRecycleBin');"):format( + escaped + ) + end + cmd[#cmd + 1] = table.concat(pwsh_cmds, " ") + return { + cmd, + } + end, + }) + else + vim.list_extend(commands, { + { "gio", "trash" }, -- Most universally available on modern Linux + { "trash" }, -- trash-cli (Python or Node.js) + function(p) + local kioclient = utils.executable("kioclient5") or utils.executable("kioclient") + if not kioclient then + return nil + end + local kioclient_cmds = {} + for _, path in ipairs(p) do + kioclient_cmds[#kioclient_cmds + 1] = { kioclient, "move", path, "trash:/" } + end + return kioclient_cmds + end, + }) + end + return commands +end + +---@param paths string[] +---@return boolean success +---@return string? err +M.trash = function(paths) + local cmds = M.generate_commands(paths) + for _, command in ipairs(cmds) do + repeat + if type(command) == "table" then + if not utils.executable(command[1]) then + log.debug("Trash command", command, "not executable") + break -- try next command + end + + local full_command = vim.list_extend({ + unpack(command), + }, paths) + log.debug("Running trash command", full_command) + local trash_ok, output = utils.execute_command(full_command) + if not trash_ok then + return false, "Could not trash with " .. full_command .. ":" .. output + end + log.debug("Trashed", paths, "using", full_command) + return true + end + + if type(command) == "function" then + local commands = command(paths) + if not commands then + break -- try next command + end + + for _, cmd in ipairs(commands) do + -- assume it's already executable + local trash_ok = utils.execute_command(cmd) + if not trash_ok then + return false, + "Error executing trash command " .. table.concat(cmd, " ") .. ", aborting operation." + end + end + return true + end + + return false, "Invalid trash command:" .. command + until true + end + return false +end + +return M diff --git a/lua/neo-tree/types/config.lua b/lua/neo-tree/types/config.lua index 309334f34..f9d10990d 100644 --- a/lua/neo-tree/types/config.lua +++ b/lua/neo-tree/types/config.lua @@ -103,6 +103,9 @@ ---@field created neotree.Component.Common.Created? ---@field symlink_target neotree.Component.Common.SymlinkTarget? +---@class neotree.Config.Trash +---@field cmd neotree.trash.Command? + ---@alias neotree.Config.BorderStyle "NC"|"rounded"|"single"|"solid"|"double"|"" ---@class (exact) neotree.Config.Base @@ -141,6 +144,7 @@ ---@field nesting_rules neotree.filenesting.Rule[] ---@field commands table ---@field window neotree.Config.Window +---@field trash neotree.Config.Trash --- ---@field filesystem neotree.Config.Filesystem ---@field buffers neotree.Config.Buffers diff --git a/lua/neo-tree/utils/init.lua b/lua/neo-tree/utils/init.lua index f89b43c7f..fb921255f 100644 --- a/lua/neo-tree/utils/init.lua +++ b/lua/neo-tree/utils/init.lua @@ -191,12 +191,22 @@ M.tbl_equals = function(table1, table2) return true end +---@generic P : string +---@param path P +---@return P? path_if_executable +M.executable = function(path) + return vim.fn.executable(path) == 1 and path or nil +end + +---@param cmd string|string[] +---@return boolean success +---@return string[] result M.execute_command = function(cmd) local result = vim.fn.systemlist(cmd) -- An empty result is ok if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then - return false, {} + return false, result or {} else return true, result end From 68db98263e8df186e00ba57620e048271c5c297f Mon Sep 17 00:00:00 2001 From: pynappo Date: Sun, 26 Oct 2025 14:54:58 -0700 Subject: [PATCH 02/10] update comments --- lua/neo-tree/sources/filesystem/lib/trash.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua index d3a1c0620..00914aeae 100644 --- a/lua/neo-tree/sources/filesystem/lib/trash.lua +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -5,7 +5,9 @@ local M = {} ---Either rm-like, or a custom list of commands ---@alias neotree.trash.Command string[]|fun(paths: string[]):string[][]? ----Returns a list of possible commands for a platform. +---Returns a list of possible trash commands for the current platform. +---The commands will either be raw string[] form (possibly executable) or a function that returns a list of those same raw commands. +---It is on the function to determine whether or not its commands are already executable. ---@param paths string[] ---@return (neotree.trash.Command)[] possible_commands M.generate_commands = function(paths) @@ -64,8 +66,8 @@ M.generate_commands = function(paths) }) else vim.list_extend(commands, { - { "gio", "trash" }, -- Most universally available on modern Linux - { "trash" }, -- trash-cli (Python or Node.js) + { "gio", "trash" }, + { "trash" }, -- trash-cli, usually function(p) local kioclient = utils.executable("kioclient5") or utils.executable("kioclient") if not kioclient then From a7cf8c0a15b41236ae79e756c98d0ff844ca737a Mon Sep 17 00:00:00 2001 From: pynappo Date: Mon, 27 Oct 2025 04:42:18 -0700 Subject: [PATCH 03/10] healthcheck --- lua/neo-tree/health/init.lua | 46 ++++- lua/neo-tree/sources/filesystem/lib/trash.lua | 180 ++++++++++-------- 2 files changed, 150 insertions(+), 76 deletions(-) diff --git a/lua/neo-tree/health/init.lua b/lua/neo-tree/health/init.lua index e4c082120..10b65a116 100644 --- a/lua/neo-tree/health/init.lua +++ b/lua/neo-tree/health/init.lua @@ -1,4 +1,5 @@ local typecheck = require("neo-tree.health.typecheck") +local utils = require("neo-tree.utils") local health = vim.health local M = {} @@ -21,8 +22,24 @@ local check_dependency = function(modname, repo, optional) health.ok(repo .. " is installed") end +---@param path string +---@param desc string? +---@return boolean success +local check_executable = function(path, desc) + if utils.executable(path) then + health.ok(("`%s` is executable"):format(path)) + return true + end + local warning = ("`%s` not found"):format(path) + if desc then + warning = table.concat({ warning, desc }, " ") + end + health.warn(warning) + return false +end + function M.check() - health.start("Dependencies") + health.start("Required dependencies") check_dependency("plenary", "nvim-lua/plenary.nvim") check_dependency("nui.tree", "MunifTanjim/nui.nvim") @@ -42,6 +59,33 @@ function M.check() health.start("Configuration") local config = require("neo-tree").ensure_config() M.check_config(config) + + health.start("Trash executables (prioritized in descending order)") + if utils.is_windows then + check_executable( + "trash", + "(usually from https://github.com/andreafrancia/trash-cli or similar)" + ) + if not check_executable("pwsh", "(builtin)") then + check_executable("powershell", "(builtin)") + end + elseif utils.is_macos then + check_executable("trash", "(builtin)") + check_executable("osascript", "(builtin)") + else + if check_executable("gio", "(from glib2)") then + if not utils.execute_command({ "gio", "trash", "--list" }) then + health.warn("gio trash --list failed, maybe you need `gvfs` installed?") + end + end + check_executable( + "trash", + "(usually from https://github.com/andreafrancia/trash-cli or similar)" + ) + if not check_executable("kioclient") then + check_executable("kioclient5") + end + end end local validate = typecheck.validate diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua index 00914aeae..83d5d82af 100644 --- a/lua/neo-tree/sources/filesystem/lib/trash.lua +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -2,14 +2,91 @@ local utils = require("neo-tree.utils") local log = require("neo-tree.log") local M = {} ----Either rm-like, or a custom list of commands ----@alias neotree.trash.Command string[]|fun(paths: string[]):string[][]? +---Either rm-like, or a function that will do the trashing for you and return true/false. +---@alias neotree.trash.CommandOrFunction neotree.trash.Command|neotree.trash.Function + +---@class neotree.trash.Command +---@field healthcheck fun(paths: string[]):boolean,string? + +---@alias neotree.trash.Function fun(paths: string[]):string[][]|boolean,string? + +---@param cmds string[][] +local function run_cmds(cmds) end + +local builtins = { + macos = { + { "trash" }, -- trash-cli, usually + function(p) + local cmds = {} + for i, path in ipairs(p) do + cmds[i] = { + "osascript", + "-e", + ('tell application "Finder" to delete POSIX file "%s"'):format( + path:gsub("\\", "\\\\"):gsub('"', '\\"') + ), + } + end + return cmds + end, + }, + linux = { + { + "gio", + "trash", + healthcheck = function() + return utils.executable("gio") and utils.execute_command({ "gio", "trash", "--list" }) + end, + }, + { "trash" }, -- trash-cli, usually + function(p) + local kioclient = utils.executable("kioclient") or utils.executable("kioclient5") + if not kioclient then + return nil + end + local kioclient_cmds = {} + for _, path in ipairs(p) do + kioclient_cmds[#kioclient_cmds + 1] = { kioclient, "move", path, "trash:/" } + end + return kioclient_cmds + end, + }, + windows = { + { "trash" }, -- trash-cli, usually + function(p) + local powershell = utils.executable("pwsh") or utils.executable("powershell") + if not powershell then + return nil + end + + local cmd = { + powershell, + "-NoProfile", + "-Command", + } + + local pwsh_cmds = { + "Add-Type -AssemblyName Microsoft.VisualBasic;", + } + for _, path in ipairs(p) do + local escaped = path:gsub("\\", "\\\\"):gsub("'", "''") + pwsh_cmds[#pwsh_cmds + 1] = ("[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('%s','OnlyErrorDialogs', 'SendToRecycleBin');"):format( + escaped + ) + end + cmd[#cmd + 1] = table.concat(pwsh_cmds, " ") + return { + cmd, + } + end, + }, +} ---Returns a list of possible trash commands for the current platform. ---The commands will either be raw string[] form (possibly executable) or a function that returns a list of those same raw commands. ---It is on the function to determine whether or not its commands are already executable. ---@param paths string[] ----@return (neotree.trash.Command)[] possible_commands +---@return (neotree.trash.CommandOrFunction)[] possible_commands M.generate_commands = function(paths) log.assert(#paths > 0) local commands = { @@ -18,68 +95,11 @@ M.generate_commands = function(paths) -- Using code from https://github.com/folke/snacks.nvim/blob/ed08ef1a630508ebab098aa6e8814b89084f8c03/lua/snacks/explorer/actions.lua if utils.is_macos then - vim.list_extend(commands, { - { "trash" }, -- trash-cli (Python or Node.js) - function(p) - local cmds = {} - for i, path in ipairs(p) do - cmds[i] = { - "osascript", - "-e", - ('tell application "Finder" to delete POSIX file "%s"'):format( - path:gsub("\\", "\\\\"):gsub('"', '\\"') - ), - } - end - return cmds - end, - }) + vim.list_extend(commands, builtins.macos) elseif utils.is_windows then - vim.list_extend(commands, { - { "trash" }, -- trash-cli (Python or Node.js) - function(p) - local powershell = utils.executable("pwsh") or utils.executable("powershell") - if not powershell then - return nil - end - - local cmd = { - powershell, - "-NoProfile", - "-Command", - } - - local pwsh_cmds = { - "Add-Type -AssemblyName Microsoft.VisualBasic;", - } - for _, path in ipairs(p) do - local escaped = path:gsub("\\", "\\\\"):gsub("'", "''") - pwsh_cmds[#pwsh_cmds + 1] = ("[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('%s','OnlyErrorDialogs', 'SendToRecycleBin');"):format( - escaped - ) - end - cmd[#cmd + 1] = table.concat(pwsh_cmds, " ") - return { - cmd, - } - end, - }) + vim.list_extend(commands, builtins.windows) else - vim.list_extend(commands, { - { "gio", "trash" }, - { "trash" }, -- trash-cli, usually - function(p) - local kioclient = utils.executable("kioclient5") or utils.executable("kioclient") - if not kioclient then - return nil - end - local kioclient_cmds = {} - for _, path in ipairs(p) do - kioclient_cmds[#kioclient_cmds + 1] = { kioclient, "move", path, "trash:/" } - end - return kioclient_cmds - end, - }) + vim.list_extend(commands, builtins.linux) end return commands end @@ -92,7 +112,13 @@ M.trash = function(paths) for _, command in ipairs(cmds) do repeat if type(command) == "table" then - if not utils.executable(command[1]) then + if command.healthcheck then + local command_ok, err = command.healthcheck(paths) + if not command_ok then + log.debug("Trash command", command, "failed healthcheck:", err) + break -- try next command + end + elseif not utils.executable(command[1]) then log.debug("Trash command", command, "not executable") break -- try next command end @@ -110,26 +136,30 @@ M.trash = function(paths) end if type(command) == "function" then - local commands = command(paths) - if not commands then - break -- try next command + local command_ok, success, err = pcall(command, paths) + log.debug("Trash function result:", command_ok, success, err) + if not command_ok then + return false, table.concat({ "Invalid trash function: ", success, err }) end - for _, cmd in ipairs(commands) do - -- assume it's already executable - local trash_ok = utils.execute_command(cmd) - if not trash_ok then - return false, - "Error executing trash command " .. table.concat(cmd, " ") .. ", aborting operation." + if success then + if type(success) == "table" then + for _, cmd in ipairs(success) do + local trash_ok, output = utils.execute_command(cmd) + if not trash_ok then + return false, "Could not trash with " .. cmd .. ":" .. output + end + log.debug("Trashed", paths, "using", cmd) + end end + break -- try next command end - return true end return false, "Invalid trash command:" .. command until true end - return false + return false, "No trash commands or functions worked." end return M From 1994f0e6944b32d4f73290349d8d366d8f0ea165 Mon Sep 17 00:00:00 2001 From: pynappo Date: Mon, 27 Oct 2025 13:34:16 -0700 Subject: [PATCH 04/10] fix func --- lua/neo-tree/sources/filesystem/lib/trash.lua | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua index 83d5d82af..8a96ce9a2 100644 --- a/lua/neo-tree/sources/filesystem/lib/trash.lua +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -142,18 +142,20 @@ M.trash = function(paths) return false, table.concat({ "Invalid trash function: ", success, err }) end - if success then - if type(success) == "table" then - for _, cmd in ipairs(success) do - local trash_ok, output = utils.execute_command(cmd) - if not trash_ok then - return false, "Could not trash with " .. cmd .. ":" .. output - end - log.debug("Trashed", paths, "using", cmd) + if not success then + break -- try next cmd + end + + if type(success) == "table" then + for _, cmd in ipairs(success) do + local trash_ok, output = utils.execute_command(cmd) + if not trash_ok then + return false, "Could not trash with " .. cmd .. ":" .. output end + log.debug("Trashed", paths, "using", cmd) end - break -- try next command end + return true end return false, "Invalid trash command:" .. command From 056ac9e08a5698217c4fd7694ce71f20d5c1ff2c Mon Sep 17 00:00:00 2001 From: pynappo Date: Mon, 27 Oct 2025 13:51:49 -0700 Subject: [PATCH 05/10] better windows recommendations --- lua/neo-tree/health/init.lua | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lua/neo-tree/health/init.lua b/lua/neo-tree/health/init.lua index 10b65a116..04fa1bb0b 100644 --- a/lua/neo-tree/health/init.lua +++ b/lua/neo-tree/health/init.lua @@ -62,12 +62,9 @@ function M.check() health.start("Trash executables (prioritized in descending order)") if utils.is_windows then - check_executable( - "trash", - "(usually from https://github.com/andreafrancia/trash-cli or similar)" - ) - if not check_executable("pwsh", "(builtin)") then - check_executable("powershell", "(builtin)") + check_executable("trash", "(from https://github.com/sindresorhus/trash#cli or similar)") + if not check_executable("pwsh", "(https://github.com/PowerShell/PowerShell)") then + check_executable("powershell", "(builtin Windows PowerShell)") end elseif utils.is_macos then check_executable("trash", "(builtin)") @@ -75,13 +72,10 @@ function M.check() else if check_executable("gio", "(from glib2)") then if not utils.execute_command({ "gio", "trash", "--list" }) then - health.warn("gio trash --list failed, maybe you need `gvfs` installed?") + health.warn("`gio trash` --list failed, maybe you need `gvfs` installed?") end end - check_executable( - "trash", - "(usually from https://github.com/andreafrancia/trash-cli or similar)" - ) + check_executable("trash", "(from https://github.com/andreafrancia/trash-cli or similar)") if not check_executable("kioclient") then check_executable("kioclient5") end From 530671cc72b3efa58ed53908b8e6cb6052235e7c Mon Sep 17 00:00:00 2001 From: pynappo Date: Mon, 27 Oct 2025 14:29:14 -0700 Subject: [PATCH 06/10] cleanup --- lua/neo-tree/sources/filesystem/lib/trash.lua | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua index 8a96ce9a2..1b56c0c76 100644 --- a/lua/neo-tree/sources/filesystem/lib/trash.lua +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -2,17 +2,18 @@ local utils = require("neo-tree.utils") local log = require("neo-tree.log") local M = {} ----Either rm-like, or a function that will do the trashing for you and return true/false. +---Either rm-like command, or a function that either returns commands, or a true/false, err result. +---Functions must not return commands when they are not executable. ---@alias neotree.trash.CommandOrFunction neotree.trash.Command|neotree.trash.Function ---@class neotree.trash.Command ---@field healthcheck fun(paths: string[]):boolean,string? +---@field [integer] string ---@alias neotree.trash.Function fun(paths: string[]):string[][]|boolean,string? ----@param cmds string[][] -local function run_cmds(cmds) end - +-- Using programs mentioned by +-- https://github.com/folke/snacks.nvim/blob/ed08ef1a630508ebab098aa6e8814b89084f8c03/lua/snacks/explorer/actions.lua local builtins = { macos = { { "trash" }, -- trash-cli, usually @@ -82,18 +83,28 @@ local builtins = { }, } ----Returns a list of possible trash commands for the current platform. ----The commands will either be raw string[] form (possibly executable) or a function that returns a list of those same raw commands. ----It is on the function to determine whether or not its commands are already executable. +---@param cmds string[][] +---@return boolean success +---@return string[]? err +---@return integer? failed_at +local execute_trash_commands = function(cmds) + for i, cmd in ipairs(cmds) do + local success, err = utils.execute_command(cmd) + if not success then + return false, err, i + end + end + return true +end + ---@param paths string[] ----@return (neotree.trash.CommandOrFunction)[] possible_commands -M.generate_commands = function(paths) +---@return boolean success +---@return string? err +M.trash = function(paths) log.assert(#paths > 0) local commands = { - require("neo-tree").config.trash.cmd, + require("neo-tree").ensure_config().trash.cmd, } - - -- Using code from https://github.com/folke/snacks.nvim/blob/ed08ef1a630508ebab098aa6e8814b89084f8c03/lua/snacks/explorer/actions.lua if utils.is_macos then vim.list_extend(commands, builtins.macos) elseif utils.is_windows then @@ -101,15 +112,8 @@ M.generate_commands = function(paths) else vim.list_extend(commands, builtins.linux) end - return commands -end ----@param paths string[] ----@return boolean success ----@return string? err -M.trash = function(paths) - local cmds = M.generate_commands(paths) - for _, command in ipairs(cmds) do + for _, command in ipairs(commands) do repeat if type(command) == "table" then if command.healthcheck then @@ -127,10 +131,11 @@ M.trash = function(paths) unpack(command), }, paths) log.debug("Running trash command", full_command) - local trash_ok, output = utils.execute_command(full_command) + local trash_ok, output = execute_trash_commands({ full_command }) if not trash_ok then return false, "Could not trash with " .. full_command .. ":" .. output end + log.debug("Trashed", paths, "using", full_command) return true end @@ -147,13 +152,16 @@ M.trash = function(paths) end if type(success) == "table" then - for _, cmd in ipairs(success) do - local trash_ok, output = utils.execute_command(cmd) - if not trash_ok then - return false, "Could not trash with " .. cmd .. ":" .. output - end - log.debug("Trashed", paths, "using", cmd) + local cmds = success + local trash_ok, output, failed_at = execute_trash_commands(cmds) + if not trash_ok then + return false, + "Could not trash with " .. table.concat(cmds[failed_at], " ") .. ":" .. output end + + log.debug("Trashed", paths, "using", cmds) + else + log.debug("Trashed", paths, "using function") end return true end From b59587459925b6480ed5614f2aa971c967be80ac Mon Sep 17 00:00:00 2001 From: pynappo Date: Mon, 27 Oct 2025 19:58:51 -0700 Subject: [PATCH 07/10] update --- lua/neo-tree/health/init.lua | 3 +-- .../sources/filesystem/lib/fs_actions.lua | 19 ++++++++----------- lua/neo-tree/types/config.lua | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/lua/neo-tree/health/init.lua b/lua/neo-tree/health/init.lua index 04fa1bb0b..d8c8f361d 100644 --- a/lua/neo-tree/health/init.lua +++ b/lua/neo-tree/health/init.lua @@ -221,8 +221,7 @@ function M.check_config(config) validate("trash", cfg.trash, function(trash) validate("cmd", trash.cmd, function(cmd) if type(cmd) == "function" then - local cmd_output = cmd({ "neotree/test/update" }) - validate("", cmd_output, v.array("string"), true) + return true -- TODO: maybe better validation here elseif type(cmd) == "table" then v.array("string")(cmd) end diff --git a/lua/neo-tree/sources/filesystem/lib/fs_actions.lua b/lua/neo-tree/sources/filesystem/lib/fs_actions.lua index aaaf32450..af467ca6a 100644 --- a/lua/neo-tree/sources/filesystem/lib/fs_actions.lua +++ b/lua/neo-tree/sources/filesystem/lib/fs_actions.lua @@ -704,22 +704,19 @@ M.trash_node = function(path, callback, noconfirm) end local do_trash = function() - local complete = vim.schedule_wrap(function() + local event_result = events.fire_event(events.BEFORE_FILE_DELETE, path) or {} + if not event_result.handled then + log.assert(trash.trash({ path })) + log.info("Trashed", path) + return + end + + vim.schedule(function() events.fire_event(events.FILE_DELETED, path) if callback then callback(path) end end) - - local event_result = events.fire_event(events.BEFORE_FILE_DELETE, path) or {} - if event_result.handled then - complete() - return - end - - log.assert(trash.trash({ path })) - log.info("Trashed", path) - complete() end if noconfirm then diff --git a/lua/neo-tree/types/config.lua b/lua/neo-tree/types/config.lua index f9d10990d..542d49d10 100644 --- a/lua/neo-tree/types/config.lua +++ b/lua/neo-tree/types/config.lua @@ -104,7 +104,7 @@ ---@field symlink_target neotree.Component.Common.SymlinkTarget? ---@class neotree.Config.Trash ----@field cmd neotree.trash.Command? +---@field cmd neotree.trash.CommandOrFunction? ---@alias neotree.Config.BorderStyle "NC"|"rounded"|"single"|"solid"|"double"|"" From f7b491534af061d01bf2a99321dae0b4bd46c14b Mon Sep 17 00:00:00 2001 From: pynappo Date: Tue, 28 Oct 2025 02:55:59 -0700 Subject: [PATCH 08/10] update docs --- doc/neo-tree.txt | 98 ++++++++++++++++++- lua/neo-tree/sources/filesystem/lib/trash.lua | 73 +++++++------- lua/neo-tree/types/config.lua | 2 +- tests/mininit.lua | 20 ++++ 4 files changed, 155 insertions(+), 38 deletions(-) diff --git a/doc/neo-tree.txt b/doc/neo-tree.txt index af41d1118..abe39073d 100644 --- a/doc/neo-tree.txt +++ b/doc/neo-tree.txt @@ -15,6 +15,7 @@ Configuration ............... |neo-tree-configuration| Setup ..................... |neo-tree-setup| Source Selector ........... |neo-tree-source-selector| Filtered Items ............ |neo-tree-filtered-items| + Trash ..................... |neo-tree-trash| Preview Mode .............. |neo-tree-preview-mode| Hijack Netrw Behavior ..... |neo-tree-netrw-hijack| Component Configs ......... |neo-tree-component-configs| @@ -307,7 +308,11 @@ A = add_directory: Create a new directory, in this mode it does not command. Also accepts `config.show_path` options d = delete: Delete the selected file or directory. - Supports visual selection.~ + Supports visual selection. + +T = trash: Trash the selected file or directory. + See |neo-tree-trash| to configure. + Supports visual selection. i = show_file_details Show file details in a popup window, such as size and last modified date. @@ -798,7 +803,7 @@ wish to remove a default mapping without overriding it with your own function, assign it the string "none". This will cause it to be skipped and allow any existing global mappings to work. -NOTE: SOME OPTIONS ARE ONLY DOCUMENTED IN THE DEFAULT CONFIG!~ +NOTE: SOME OPTIONS ARE ONLY DOCUMENTED IN THE DEFAULT CONFIG! ~ Run `:lua require("neo-tree").paste_default_config()` to dump the fully commented default config in your current file. Even if you don't want to use that config as your starting point, you still may want to dump it to a blank @@ -975,8 +980,95 @@ In addition to `"tab"` and `"window"`, you can also set the target to `"global"` for either option, which is the same as using the |cd| command. Setting the target to `"none"` will prevent neo-tree from setting vim's cwd for that position. +TRASH *neo-tree-trash* + +Neo-tree can use existing commands on your system to trash files. By default, +this is bound to `T`. + +The default commands that neo-tree tries to use, in order, are: + +MacOS: `trash`, or `osascript` commands. + +Linux: `gio trash`, `trash`, and `kioclient/kioclient5`. + +Windows: `trash`, or `PowerShell` commands. + +You can configure the command used by specifying `trash.command` in `setup()`: + +>lua + require("neo-tree").setup({ + trash = { + command = nil -- the default - uses built-ins. + } + }) +< + +This option takes three types of commands: + +1. A string array listing an `rm`-like trash command to use. In otherwords, a + CLI tool where the invocation of the command looks like `trash ...FILES`. + +>lua + require("neo-tree").setup({ + trash = { + command = { "your-trash-cli", "--put" } + } + }) +< + +2. Or a function that either returns a list of commands to execute that will + result in the deletion of the items: + +>lua + require("neo-tree").setup({ + trash = { + ---@param paths string[] + ---@return string[][] commands + command = function(paths) + if not require("neo-tree.utils").executable("mv") then + return nil -- defer to built-ins + end + local cmds = {} + for i, p in ipairs(paths) do + cmds[#cmds+1] = { "mv", p, ("%s/trash"):format(vim.env.HOME) } + end + return cmds + end + } + }) +< + +(if any of these commands fail, the rest are aborted). + +3. Or a function that is entirely responsible for deletion, and returns a + (true/false, string) result tuple. Return false to defer to builtin handlers, + return true to succeed, error to stop. + +>lua + require("neo-tree").setup({ + trash = { + ---@type neotree.trash.FunctionGenerator fun(paths: string[]):(fun():boolean,string?)? + command = function(paths) + if not setup then + return nil -- defer to built-ins + end + return function() + for i, p in ipairs(paths) do + -- ... logic to trash the given paths + if something_failed then + return false, err + end + end + + return true + end + end + } + }) +< + -FILTERED ITEMS *neo-tree-filtered-items* +FILTERED ITEMS *neo-tree-filtered-items* The `filesystem` source has a `filtered_items` section in it's config that allows you to specify what files and folders should be hidden. By default, any diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua index 1b56c0c76..efe841491 100644 --- a/lua/neo-tree/sources/filesystem/lib/trash.lua +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -1,16 +1,19 @@ local utils = require("neo-tree.utils") local log = require("neo-tree.log") +local validate = require("neo-tree.health.typecheck").validate local M = {} ----Either rm-like command, or a function that either returns commands, or a true/false, err result. ----Functions must not return commands when they are not executable. ----@alias neotree.trash.CommandOrFunction neotree.trash.Command|neotree.trash.Function +---@alias neotree.trash.Command neotree.trash.PureCommand|neotree.trash.FunctionGenerator|neotree.trash.CommandGenerator ----@class neotree.trash.Command ----@field healthcheck fun(paths: string[]):boolean,string? +---@class neotree.trash.PureCommand +---@field healthcheck? fun(paths: string[]):boolean,string? ---@field [integer] string ----@alias neotree.trash.Function fun(paths: string[]):string[][]|boolean,string? +---A function that may return commands to execute, in order. +---@alias neotree.trash.CommandGenerator fun(paths: string[]):neotree.trash.PureCommand[]? + +---A function that may return a function that will do the trashing. +---@alias neotree.trash.FunctionGenerator fun(paths: string[]):((fun():success: boolean, err: string?)?) -- Using programs mentioned by -- https://github.com/folke/snacks.nvim/blob/ed08ef1a630508ebab098aa6e8814b89084f8c03/lua/snacks/explorer/actions.lua @@ -67,11 +70,12 @@ local builtins = { } local pwsh_cmds = { - "Add-Type -AssemblyName Microsoft.VisualBasic;", + "$shell = New-Object -ComObject 'Shell.Application';", + "$folder = $shell.NameSpace(0);", } for _, path in ipairs(p) do local escaped = path:gsub("\\", "\\\\"):gsub("'", "''") - pwsh_cmds[#pwsh_cmds + 1] = ("[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('%s','OnlyErrorDialogs', 'SendToRecycleBin');"):format( + pwsh_cmds[#pwsh_cmds + 1] = ([[$path = Get-Item '%s'; $folder.ParseName($path.FullName).InvokeVerb('delete');]]):format( escaped ) end @@ -113,8 +117,35 @@ M.trash = function(paths) vim.list_extend(commands, builtins.linux) end + ---@type string[][] for _, command in ipairs(commands) do repeat + if type(command) == "function" then + local res, err = command(paths) + log.debug("Trash function result:", res, err) + if not res then + break -- try next command + end + + if type(res) == "table" then + local cmds = res + local trash_ok, output, failed_at = execute_trash_commands(cmds) + if not trash_ok then + return false, + "Trash commands failed at " .. table.concat(cmds[failed_at], " ") .. ":" .. output + end + + log.debug("Trashed", paths, "using", cmds) + elseif type(res) == "function" then + local trash_ok, trashfunc_err = res() + if not trash_ok then + return false, "Trash function failed: " .. (trashfunc_err or "") + end + log.debug("Trashed", paths, "using function") + end + return true + end + if type(command) == "table" then if command.healthcheck then local command_ok, err = command.healthcheck(paths) @@ -140,32 +171,6 @@ M.trash = function(paths) return true end - if type(command) == "function" then - local command_ok, success, err = pcall(command, paths) - log.debug("Trash function result:", command_ok, success, err) - if not command_ok then - return false, table.concat({ "Invalid trash function: ", success, err }) - end - - if not success then - break -- try next cmd - end - - if type(success) == "table" then - local cmds = success - local trash_ok, output, failed_at = execute_trash_commands(cmds) - if not trash_ok then - return false, - "Could not trash with " .. table.concat(cmds[failed_at], " ") .. ":" .. output - end - - log.debug("Trashed", paths, "using", cmds) - else - log.debug("Trashed", paths, "using function") - end - return true - end - return false, "Invalid trash command:" .. command until true end diff --git a/lua/neo-tree/types/config.lua b/lua/neo-tree/types/config.lua index 542d49d10..f9d10990d 100644 --- a/lua/neo-tree/types/config.lua +++ b/lua/neo-tree/types/config.lua @@ -104,7 +104,7 @@ ---@field symlink_target neotree.Component.Common.SymlinkTarget? ---@class neotree.Config.Trash ----@field cmd neotree.trash.CommandOrFunction? +---@field cmd neotree.trash.Command? ---@alias neotree.Config.BorderStyle "NC"|"rounded"|"single"|"solid"|"double"|"" diff --git a/tests/mininit.lua b/tests/mininit.lua index a9d19798f..b975c1b40 100644 --- a/tests/mininit.lua +++ b/tests/mininit.lua @@ -16,3 +16,23 @@ vim.cmd.source(root_dir .. "/plugin/neo-tree.lua") vim.g.mapleader = " " vim.keymap.set("n", "e", "Neotree") +require("neo-tree").setup({ + trash = { + ---@type neotree.trash.FunctionGenerator + command = function(paths) + if not setup then + return nil -- defer to built-ins + end + return function() + for i, p in ipairs(paths) do + -- ... logic to trash the given paths + if something_failed then + return false, err + end + end + + return true + end + end, + }, +}) From 54344024600b99ff13939f414c1ad7910884443d Mon Sep 17 00:00:00 2001 From: pynappo Date: Tue, 28 Oct 2025 03:14:51 -0700 Subject: [PATCH 09/10] update comments --- lua/neo-tree/sources/filesystem/lib/trash.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua index efe841491..d4039d460 100644 --- a/lua/neo-tree/sources/filesystem/lib/trash.lua +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -7,6 +7,7 @@ local M = {} ---@class neotree.trash.PureCommand ---@field healthcheck? fun(paths: string[]):boolean,string? +---@field list? fun():boolean,string? ---@field [integer] string ---A function that may return commands to execute, in order. @@ -17,9 +18,9 @@ local M = {} -- Using programs mentioned by -- https://github.com/folke/snacks.nvim/blob/ed08ef1a630508ebab098aa6e8814b89084f8c03/lua/snacks/explorer/actions.lua -local builtins = { +local trash_builtins = { macos = { - { "trash" }, -- trash-cli, usually + { "trash" }, -- trash-cli, usually https://github.com/andreafrancia/trash-cli function(p) local cmds = {} for i, path in ipairs(p) do @@ -42,7 +43,7 @@ local builtins = { return utils.executable("gio") and utils.execute_command({ "gio", "trash", "--list" }) end, }, - { "trash" }, -- trash-cli, usually + { "trash" }, -- trash-cli, usually https://github.com/andreafrancia/trash-cli function(p) local kioclient = utils.executable("kioclient") or utils.executable("kioclient5") if not kioclient then @@ -56,7 +57,7 @@ local builtins = { end, }, windows = { - { "trash" }, -- trash-cli, usually + { "trash" }, -- trash-cli, usually https://github.com/sindresorhus/trash#cli function(p) local powershell = utils.executable("pwsh") or utils.executable("powershell") if not powershell then @@ -110,11 +111,11 @@ M.trash = function(paths) require("neo-tree").ensure_config().trash.cmd, } if utils.is_macos then - vim.list_extend(commands, builtins.macos) + vim.list_extend(commands, trash_builtins.macos) elseif utils.is_windows then - vim.list_extend(commands, builtins.windows) + vim.list_extend(commands, trash_builtins.windows) else - vim.list_extend(commands, builtins.linux) + vim.list_extend(commands, trash_builtins.linux) end ---@type string[][] From be72e4c5ae1b92ad31c7da27c8eb311c96732fc3 Mon Sep 17 00:00:00 2001 From: pynappo Date: Tue, 28 Oct 2025 03:17:26 -0700 Subject: [PATCH 10/10] reverts --- lua/neo-tree/sources/common/file-items.lua | 2 +- tests/mininit.lua | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/lua/neo-tree/sources/common/file-items.lua b/lua/neo-tree/sources/common/file-items.lua index 2e114284f..7b7cce44b 100644 --- a/lua/neo-tree/sources/common/file-items.lua +++ b/lua/neo-tree/sources/common/file-items.lua @@ -199,7 +199,7 @@ local function create_item(context, path, _type, bufnr) if item.type == "link" then ---@cast item neotree.FileItem.Link item.is_link = true - item.link_to = uv.fs_realpath(path) + item.link_to = uv.fs_readlink(path) if item.link_to then local link_to_stat = uv.fs_stat(item.path) if link_to_stat then diff --git a/tests/mininit.lua b/tests/mininit.lua index b975c1b40..a9d19798f 100644 --- a/tests/mininit.lua +++ b/tests/mininit.lua @@ -16,23 +16,3 @@ vim.cmd.source(root_dir .. "/plugin/neo-tree.lua") vim.g.mapleader = " " vim.keymap.set("n", "e", "Neotree") -require("neo-tree").setup({ - trash = { - ---@type neotree.trash.FunctionGenerator - command = function(paths) - if not setup then - return nil -- defer to built-ins - end - return function() - for i, p in ipairs(paths) do - -- ... logic to trash the given paths - if something_failed then - return false, err - end - end - - return true - end - end, - }, -})