Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
776ff4a
add trash
pynappo Oct 26, 2025
330888c
update comments
pynappo Oct 26, 2025
714a6f2
healthcheck
pynappo Oct 27, 2025
8ff1044
fix func
pynappo Oct 27, 2025
6bffcfb
better windows recommendations
pynappo Oct 27, 2025
1ee05c1
cleanup
pynappo Oct 27, 2025
d161086
update
pynappo Oct 28, 2025
1a3214e
update docs
pynappo Oct 28, 2025
7ae21b6
update comments
pynappo Oct 28, 2025
aefa7c5
reverts
pynappo Oct 28, 2025
b4bcebf
update
pynappo Mar 22, 2026
959498d
sync
pynappo Mar 22, 2026
1da8421
update
pynappo Mar 24, 2026
c4d16c3
wip
pynappo Apr 4, 2026
adfe5d5
remove some builtins
pynappo Apr 7, 2026
005655b
cleanup
pynappo Apr 20, 2026
c16eb44
docs update
pynappo Apr 21, 2026
199f14b
add undo functionality
pynappo Apr 21, 2026
0a9468a
get undo working
pynappo Apr 22, 2026
19c3952
better info/warnings
pynappo Apr 23, 2026
3def196
fix log message
pynappo Apr 26, 2026
807638f
fix tests/defaults
pynappo Apr 28, 2026
d824ed1
add restore bind
pynappo May 1, 2026
c83406b
stop file watch when trashed folders error on windows
pynappo May 5, 2026
384c374
fixup docs, fix some errors
pynappo May 6, 2026
65d9b49
docs
pynappo May 6, 2026
88b23ed
fix test
pynappo May 6, 2026
2d610d8
fix default bind
pynappo May 7, 2026
5e1197c
Fix more logging/docs
pynappo May 7, 2026
13b4e13
Update tests
pynappo May 9, 2026
d83f94c
wip, TODO: need _visual commands to fire events
pynappo May 9, 2026
b82b874
Hook up deletion events to trashing (for LSP and such)
pynappo May 14, 2026
68b9705
update docs
pynappo May 15, 2026
f8d4355
Apply suggestions from Copilot review
pynappo May 15, 2026
fa71b8e
more review fixes
pynappo May 15, 2026
1d8c143
last typo fix
pynappo May 16, 2026
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
147 changes: 143 additions & 4 deletions doc/neo-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -307,9 +308,20 @@ 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.
Supports undo, depending on command used.

u = undo: Reverts the most recent undoable action.

U = restore_from_trash: Restores selected file or directories when used on
items inside the trash folder. See |neo-tree-trash|
for supported OSs.

i = show_file_details Show file details in a popup window, such as size
i = show_file_details: Show file details in a popup window, such as size
and last modified date.

This command supports two optional options to
Expand Down Expand Up @@ -798,7 +810,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
Expand Down Expand Up @@ -975,8 +987,135 @@ 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 trash files with the `trash` command (`T` by default).

Support for undoable trash operations and the ability to restore any file from a
trash bin depends on OS (see below). The default binds for these, are `u` for
the `undo` command, and `U` for the `restore_from_trash` command.

Neo-tree has a variety of built-in methods it tries to use, depending on OS. In
order, these are:

MacOS:
- `trash`,
- `osascript` commands.

Linux:
- `gio trash` (supports undo, and restoration)
- Neo-tree's native implementation of the FreeDesktop XDG trash spec at
`require('neo-tree.trash.freedesktop')`. (supports undo, and restoration)

Windows:
- `PowerShell` commands (supports restoration).

You can configure the command used by specifying `trash.command` in `setup()`:

>lua
require("neo-tree").setup({
trash = {
-- the default - uses built-ins.
command = nil,
-- Use Neo-tree's built-in freedesktop trasher over `gio trash`
-- command = function(...) require('neo-tree.trash.freedesktop').generate_trashfunc(...) end
}
})
<

This option will be attempted first, before falling back to the built-ins listed
above. The accepted formats for this option are:

1. An `rm`-like trash command to use. Neo-tree will append all the filepaths
being trashed after the last argument given.

>lua
require("neo-tree").setup({
trash = {
command = { "your-trash-cli", "--put", "--" }
}
})
<

2. A function that returns a list of commands to execute:

>lua
require("neo-tree").setup({
trash = {
---@type neotree.trash.CommandGenerator
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
}
})
<

(Neo-tree will attempt to run every command in-order, and log output for each
command that returns with a non-zero error code).

3. Or a function that, itself, returns an internal function that does the
deletion, which will be executed immediately.

The internal function returns a boolean indicating whether it succeeded
(usually, whether it trashed every file given) and may return an additional
restore function that, when called (via Neo-tree's `undo` keymap), should undo
the trashing as best it can.

This additional restore function returns a boolean indicating whether it fully
succeeded or not.

>lua
require("neo-tree").setup({
trash = {
---@type neotree.trash.FunctionGenerator
command = function(paths)
if not setup then
return nil -- defer to built-ins
end
---@type neotree.trash.InternalFunction
return function()
local paths_in_trash = {}
for i, p in ipairs(paths) do
local trashed = false
-- ... logic to trash each path
if not trashed then
require("neo-tree.log").warn("couldn't trash", p, "because...")
else
paths_in_trash[#paths_in_trash + 1] = "/some/path/" .. p
end
end

-- Restore functions are optional
---@type neotree.trash.RestoreInternalFunction
local restorefunc = function()
local all_restored = true
for i, p in ipairs(paths_in_trash) do
local restored = false
-- ... logic to restore each path
if restored then
require("neo-tree.log").warn("couldn't restore", p, "because...")
else
all_restored = false
end
end
return all_restored
end

return #paths_in_trash == #paths, restorefunc
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
Expand Down
10 changes: 10 additions & 0 deletions lua/neo-tree/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,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 = {
command = nil -- by default: powershell script on windows, `trash` or `osascript` on macOS, and `gio trash` or Neo-tree's freedesktop trash implementation on other Unixes
},
-- source_selector provides clickable tabs to switch between sources.
source_selector = {
winbar = false, -- toggle to show selector on winbar
Expand Down Expand Up @@ -456,6 +459,9 @@ local config = {
},
["A"] = "add_directory", -- also accepts the config.show_path and config.insert_as options.
["d"] = "delete",
["T"] = "trash",
["u"] = "undo", -- currently only supports trash.
["U"] = "restore_from_trash", -- only works on files that are in the recycle bin
["r"] = "rename",
["y"] = "copy_to_clipboard",
["x"] = "cut_to_clipboard",
Expand Down Expand Up @@ -709,6 +715,10 @@ local config = {
["c"] = "noop",
["m"] = "noop",
["a"] = "noop",
["T"] = "noop",
["<C-r>"] = "noop",
["u"] = "noop",
["U"] = "noop",
["/"] = "filter",
["f"] = "filter_on_submit",
},
Expand Down
2 changes: 2 additions & 0 deletions lua/neo-tree/events/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ local M = {
AFTER_RENDER = "after_render",
BEFORE_FILE_ADD = "before_file_add",
BEFORE_FILE_DELETE = "before_file_delete",
BEFORE_FILE_RESTORE = "before_file_restore",
BEFORE_FILE_MOVE = "before_file_move",
BEFORE_FILE_RENAME = "before_file_rename",
BEFORE_GIT_STATUS = "before_git_status",
Expand All @@ -18,6 +19,7 @@ local M = {
FILE_OPENED = "file_opened",
FILE_OPEN_REQUESTED = "file_open_requested",
FILE_RENAMED = "file_renamed",
FILE_RESTORED = "file_restored",
FS_EVENT = "fs_event",
GIT_EVENT = "git_event",
GIT_STATUS_CHANGED = "git_status_changed",
Expand Down
61 changes: 59 additions & 2 deletions lua/neo-tree/health/init.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
local typecheck = require("neo-tree.health.typecheck")
local utils = require("neo-tree.utils")
local health = vim.health

local M = {}

---@param modname string
---@param repo string
---@param optional boolean?
---@return boolean
local check_dependency = function(modname, repo, optional)
local m = pcall(require, modname)
if not m then
Expand All @@ -15,14 +17,31 @@ local check_dependency = function(modname, repo, optional)
else
health.error(errmsg)
end
return
return false
end

health.ok(repo .. " is installed")
return true
end

---@param path string
---@param desc string?
---@return boolean executable
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")

Expand All @@ -42,6 +61,32 @@ 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, `:h neo-tree-trash`)")
if utils.is_windows then
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", "(built-in Windows PowerShell)")
end
elseif utils.is_macos then
check_executable("trash", "(built-in)")
check_executable("osascript", "(built-in)")
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
if vim.fn.has("nvim-0.10") == 1 then
health.info(
[[(Neo-tree will fall back to its own implementation of the XDG freedesktop trash spec, as needed)]]
)
else
health.warn(
[[Neovim version is below 0.10, cannot fallback to Neo-tree's XDG freedesktop trash implementation.]]
)
end
end
end

local validate = typecheck.validate
Expand All @@ -59,6 +104,9 @@ function M.check_config(config)
function(cfg)
---@class neotree.health.Validator.Generators
local v = {
---@generic T
---@param validator neotree.health.Validator<T>
---@return fun(arr: T[])
array = function(validator)
---@generic T
---@param arr T[]
Expand Down Expand Up @@ -177,6 +225,15 @@ 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.command, function(cmd)
if type(cmd) == "function" then
return true -- TODO: maybe better validation here
elseif type(cmd) == "table" then
v.array("string")(cmd)
end
end, true)
Comment on lines +228 to +235
end)
validate("source_selector", cfg.source_selector, function(ss)
validate("winbar", ss.winbar, "boolean")
validate("statusline", ss.statusline, "boolean")
Expand Down
13 changes: 7 additions & 6 deletions lua/neo-tree/log.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string, { format: fun(fmt: string?, ...: any) }>
log.at = {
Expand Down Expand Up @@ -374,11 +376,10 @@ log_maker.new = function(config)
else
errmsg = "assertion failed!"
end
local temp = config.use_console
local old = config.use_console
config.use_console = false
log.error(errmsg)
config.use_console = temp
-- actually raise the error so execution stops
config.use_console = old
error(errmsg, 2)
end

Expand Down
3 changes: 3 additions & 0 deletions lua/neo-tree/sources/buffers/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ end
M.delete = function(state)
cc.delete(state, refresh)
end
M.trash = function(state)
cc.delete(state, refresh)
end

---Navigate up one level.
M.navigate_up = function(state)
Expand Down
Loading
Loading