filetree.nvim · Files & navigation · vimdoc
:help filetree
Adapter-agnostic filetree features for Neovim
doc/filetree.txt — rendered from the plugin's own vimdoc
*filetree.txt* Adapter-agnostic filetree features for Neovim *filetree.nvim* Author: Stefan Bartl Version: 0.1.0
CONTENTS
1. Introduction ........................... |filetree-intro| 2. Requirements ........................... |filetree-requirements| 3. Quick start ............................ |filetree-quickstart| 4. Configuration .......................... |filetree-config| 4.1 Default-disabled features ......... |filetree-default-disabled| 4.2 Menu integration .................. |filetree-menu| 4.3 pickers.nvim integration ......... |filetree-pickers-integration| 4.4 Keys claimed twice ................ |filetree-key-conflicts| 5. Features ............................... |filetree-features| 5.1 Picker ............................ |filetree-picker| 5.2 Layout Guard ...................... |filetree-layout-guard| 5.3 CWD Sync ......................... |filetree-cwd-sync| 5.4 CWD Modes ......................... |filetree-cwd-mode| 5.5 Current Highlight ................ |filetree-current-hl| 5.6 Safety / Backup .................. |filetree-safety| 5.7 Auto Reveal ....................... |filetree-auto-reveal| 5.8 Create From Template ............. |filetree-create-from-template| 5.9 Open In File Manager ............. |filetree-open-in-fm| 5.10 No Name Guard ................... |filetree-no-name-guard| 5.11 References ....................... |filetree-refs| 5.12 Move ............................. |filetree-move| 5.13 Feature reference ......................|filetree-feature-reference| 6. Adapters ............................... |filetree-adapters| 7. Public API ............................. |filetree-api| 8. Health check ........................... |filetree-health| 9. Custom adapters ........................ |filetree-custom-adapters|
1. INTRODUCTION
filetree.nvim provides a large set of file-tree features that work with any supported filetree plugin via an adapter pattern — picker, marks, preview, path tools, search/grep, git decorations, batch rename, archives, and many more. filetree.nvim is OPT-OUT: every feature is enabled by default. You do not writeenabled = trueto get a feature — you only writeenabled = falseto turn one off. A short, deliberately-argued list stays off until you ask for it (see |filetree-default-disabled|). Minimal setup — the full feature set, nothing to wire up:
require("filetree").setup({ adapter = "neotree" })
Supported backends: neo-tree.nvim, nvim-tree.lua
2. REQUIREMENTS
• Neovim >= 0.10 — vim.system() and vim.uv are used unguarded, and lib.nvim
itself requires 0.10.
• lib.nvim (StefanBartl/lib.nvim) — shared helpers. REQUIRED for the
:Filetree/:Ft command layer (lib.nvim.bindings.usercmd.composer); most other
integrations still have local fallbacks if it's absent, but the
commands themselves won't register without it.
• One of: neo-tree.nvim, nvim-tree.lua
• pickers.nvim (optional) — f / gr run through it when installed
(|filetree-pickers-integration|); otherwise telescope, fzf-lua, mini.pick or
a built-in fallback.
• ripgrep (optional, recommended) — pre-filters the reference scan
(|filetree-refs|). Without it the scan falls back to a capped
directory walk: slower, same result.
3. QUICK START
Minimal setup with neo-tree (all features on by default):
require("filetree").setup({ adapter = "neotree" })
For nvim-tree:
require("filetree").setup({ adapter = "nvimtree" })
Turn things off (or override defaults) only where you want to:
require("filetree").setup({
adapter = "neotree",
features = {
shell_run = { enabled = false }, -- disable a default-on feature
size_info = { enabled = true }, -- enable a default-off feature
marks = { keymap = "M" }, -- keep on, remap its key
},
})
4. CONFIGURATION
setup({})accepts a|FiletreeConfig|table. All fields are optional. Every feature is ON by default; omitting a feature leaves it enabled with its default options. You only ever setenabled = falseto turn a default-on feature off, orenabled = trueto turn on one of the opt-in few (|filetree-default-disabled|). Representative options with their defaults (theenabledline is shown only to make the default state explicit — you can drop it):
require("filetree").setup({
-- "auto" picks the first available adapter (neotree → nvimtree)
adapter = "auto",
-- Confirmation prompts for destructive/bulk actions (paste, delete,
-- rename_batch). Shipped defaults: paste/rename_batch = no prompt,
-- delete = PROMPTS (trashing is harder to notice/undo than a move or
-- rename). nil (default) leaves each feature's own default alone;
-- true/false applies to all three at once; a table applies per action.
-- A feature's own features.<name>.confirm, if set explicitly, always
-- wins over this switch.
-- confirmations = false -- no prompts at all
-- confirmations = { delete = false } -- opt out of the delete prompt only
confirmations = nil,
features = {
-- Quick two-digit picker
picker = {
enabled = true, -- default: on
index_width = 2, -- digits per index label
timeout_ms = 3000, -- auto-exit after inactivity
keymaps = {
trigger_reveal = "<leader>ftp",
trigger_cwd = "<leader>ftc",
},
},
-- Ensure an editor window always exists
layout_guard = {
enabled = true, -- default: on
delay_ms = 50,
},
-- Silently chdir to the project root + root the tree there + reveal (opt-in)
cwd_sync = {
enabled = false, -- default: off
debounce_ms = 150,
parent_levels = 0, -- fallback: parent dirs to ascend when no root resolved
keep_focus = true, -- keep focus in editor after reveal
change_dir = true, -- actually chdir; never prompts
reveal = true, -- set false if the tree already follows the cwd
-- (neo-tree bind_to_cwd + follow_current_file)
use_project_root = true, -- fallback: broad project_root marker set
root_markers = { ".git" }, -- anchor cwd + tree root to nearest ancestor with
-- one of these (cached); false disables; list widens.
-- Takes priority over use_project_root.
},
-- Highlight the current file and its parent dir (opt-in)
current_hl = {
enabled = false, -- default: off
file_hl = { fg = "#7aa2f7", bold = true },
parent_hl = { fg = "#565f89" },
debounce_ms = 100,
},
-- Backup files before delete/move (opt-in, API only)
safety = {
enabled = false, -- default: off
backup_dir = nil, -- default: stdpath("data")/filetree/backups
max_backups = 5,
dry_run = false,
},
},
})
Highlight specs accept: • A table:{ fg = "#rrggbb", bold = true }(passed to nvim_set_hl) • A hex string:"#7aa2f7"• A link:"link:Comment"• A named color:"red","darkblue"…
DEFAULT-DISABLED FEATURES
These features stay OFF until you set{ enabled = true }, each for a concrete reason: cwd_sync Changes the global cwd automatically on buffer switch; aggressive, and overlaps auto_reveal / tree_traverse. current_hl Purely cosmetic; ships hardcoded colours that only fit some colorschemes. safety A backup API with no keymaps; enabling it has no visible effect unless other code calls into it. auto_resize Automatic width management fights the manual window_size_cycler (on by default). handle_guard Patches a neo-tree internal (fs_watch) and closes libuv handles it owns, to fix a sporadic Windows file-lock at the source. neo-tree adapter + Windows/WSL only; a no-op elsewhere. Inspect with:Filetree handlesor |filetree-health|. size_info Purely cosmetic — an eol extmark next to every node — anddir_async = truerunsdu/Get-ChildItemper directory node by default; better opted into than sprung on someone who just wanted a tree. tree_toggle Four global Alt keys; a claim on the keyboard the user makes, not the plugin. Everything else in the feature registry is on by default. For the full keymap reference see docs/BINDINGS/KEYMAPS.md.
4.2 MENU INTEGRATION
filetree.nvim ships context-menu entries for nvzone/menu but does not depend on it. The plugin owns the entries; a host composes them for the tree window:
local ft = require("filetree.integrations.menu")
require("menu").open(ft.items(), { mouse = true }) -- inline entries
-- ft.submenu() → a single " Filetree" fly-out entry, or nil
Entries are self-gating (an action whose feature is disabled is omitted). nvzone
closes the menu before running an entry, so the node under the cursor in the
tree is the active context. Opt out per group via config.menu:
require("filetree").setup({
menu = {
enable = true,
fileops = true, -- create / rename / batch rename / move / template
clipboard = true, -- copy / cut / paste
delete = true, -- trash
open = true, -- vsplit / split / tab / system app / file manager
paths = true, -- copy path / markdown link
search = true, -- find files / grep in dir
info = true, -- node info + Inspect (`:Inspect`)
},
})
4.3 PICKERS.NVIM INTEGRATION
find_files(f,tf) andgrep_in_dir(gr,tg) hand the directory of the node under the cursor to pickers.nvim when it is installed: your engine, yourfindflags, your entry actions. The picked file is revealed in the tree (find_files.reveal_on_open) through pickers.nvim'son_selecthook; a pickers.nvim without the hook just opens it. On by default; it is used only when all three hold, and each is an opt-out:
require("filetree").setup({ integrations = { pickers = false } })
require("pickers").setup({ filetree = { enabled = false } }) -- other end
pickers.nvim installed (and new enough to shippickers.integrations.filetree),integrations.pickersnotfalsehere,filetree.enablednotfalsethere. If any fails,f/grfall back silently to telescope / fzf-lua / mini.pick / the built-in backend;tf/tgsay "pickers.nvim not available", since they asked for it. |filetree-health| reports which condition holds.
4.4 KEYS CLAIMED TWICE
filetree's own features never share a default key (gplocks the cwd,goopens a PDF,<C-c>clears a filter,Xclears the copy/cut clipboard). Keys you set yourself can: Vim does not complain about a second:map, the later one wins silently, and which one is later depends on attach order.
:Filetree keys
lists every key claimed by more than one action (the live one marked), recommends free alternatives (unmapped, no prefix clash, the original's kind of key first), and moves the action you pick for this session; thesetup()fragment that makes it permanent is copied to the clipboard. The same list is the "conflicts" page of the?cheatsheet (<CR> there starts the flow) and a warning in |filetree-health|. An action bound per buffer (preview) cannot be moved while the session runs.
5. FEATURES
5.1 PICKER
The picker overlays numbered labels on every visible tree node. Type a two-digit number to open/toggle the corresponding node immediately. Default keymaps (configurable):
<leader>ftp Enter picker — reveal current buffer's file
<leader>ftc Enter picker — open at cwd
Inside picker mode: 0-9 Enter digits. Complete index opens/toggles the node. e/s/v/t/p Set open mode (edit/split/vsplit/tab/preview) before digits. <Tab> Cycle filter: all → files → folders → all <C-k>/<C-j> Scroll tree up/down <Esc> Exit picker mode
5.2 LAYOUT GUARD
When the user closes all editor windows, this feature opens a new empty window automatically so the user is never trapped inside the tree. Triggers on: BufDelete, BufWipeout, WinClosed
5.3 CWD SYNC
On BufEnter / WinEnter, when the current file is not under Neovim's cwd: silentlychdirto its project root AND root the tree there, then reveal the file. Never prompts. Whenreveal = true(the default), auto-pauses for 2 seconds when the cursor moves inside the tree window — the tree's own <CR>-driven open could otherwise race cwd_sync's own reveal on the file it just opened. Withreveal = false(see below) this pause never triggers, since there is no reveal of ours left to race — so a file opened shortly after leaving the tree (e.g. from a picker invoked with the cursor still in the tree window) still gets itschdir. auto_reveal (|filetree-auto-reveal|, on by default) handles the common case — scrolling/expanding to reveal a file that is already under the current root — on every buffer switch, cheaply and without touching the cwd. It also re-roots the tree itself (not the cwd) when the file is OUTSIDE the current root, by default — see its ownfollow_rootoption. cwd_sync (opt-in) is what also moves Neovim's own cwd there; enable it when you want the cwd itself to follow, not just the tree's display. The project root is resolved in this order: 1.root_markers(default { ".git" }) — nearest ancestor directory holding one of these markers, found via lib.nvim's cachedfind_root(results are cached per directory, so opening many files in a project is cheap). This keeps the cwd/tree anchored to a stable high-level root and avoids the frequent cwd jumps you get when every file's parent becomes the root. Setroot_markers = falseto disable it, or pass a wider list to broaden it. 2.use_project_root— the project_root feature's broader marker set. 3. The file's own parent directory. Both the cwd and the tree root use the same resolved directory, so the tree always shows the project root rather than the file's parent. No full tree refresh/rescan is issued (the reveal re-renders anyway) — a deliberate change to keep opening files fast.
INTERACTION WITH ADAPTER-NATIVE "FOLLOW CWD" FEATURES
Whetherrevealshould be true or false depends entirely on whether your underlying tree PLUGIN (not filetree.nvim) has its own built-in feature that auto-follows the current buffer independently of filetree. When it does, that feature and cwd_sync's own reveal both try to act on the same BufEnter and race each other — the tree can settle on the file's parent instead of the project root. Setreveal = falsethere so cwd_sync only manages the cwd and lets the adapter's own plugin do the rooting/revealing. When the adapter has no such feature, cwd_sync's reveal IS the only thing that does this job — leavereveal = true(the default), or files switched into a different project would never get revealed at all. Adapter Native follow feature reveal ----------- ------------------------------------------ ------ neotree filesystem.follow_current_file.enabled + false filesystem.bind_to_cwd = true nvimtree update_focused_file.enable = true false (leave update_root at its default false — see caveat below) netrw none true (default) oil none true (default) mini_files none true (default) CAVEAT (tested): nvim-tree'supdate_focused_file.update_root.enableis NOT a drop-in equivalent of neo-tree'sbind_to_cwd. neo-tree's bind_to_cwd is reactive — it just keeps the tree synced to whatever cwd already is, so cwd_sync's chdir (to the resolvedroot_markers/project root) is respected. nvim-tree'supdate_root, by contrast, actively DRIVES Neovim's cwd itself: per its own docs it "prefers vim's cwd and root_dirs, falling back to the directory containing the file" — i.e. the file's own parent, NOT a project root. Withupdate_root.enable = true, nvim-tree overwrites cwd_sync's git-root-anchored cwd with the file's own directory on every switch, independent ofreveal. If you want cwd_sync'sroot_markersanchoring to win, leaveupdate_rootat its defaultfalse(onlyupdate_focused_filefollows/expands within whatever root is already set — this combines cleanly withreveal = false, verified against nvim-tree.lua directly). Example for neo-tree with both native options on:
-- neo-tree.nvim setup:
require("neo-tree").setup({
filesystem = {
bind_to_cwd = true,
follow_current_file = { enabled = true },
},
})
-- filetree.nvim setup:
require("filetree").setup({
adapter = "neotree",
features = { cwd_sync = { enabled = true, reveal = false } },
})
Same principle for nvim-tree.lua withupdate_focused_file.enable = true: setfeatures = { cwd_sync = { enabled = true, reveal = false } }there too. For netrw/oil/mini_files, just enable cwd_sync with itsrevealdefault (true) — there is no competing native mechanism to defer to.
NEO-TREE'S "FILE NOT IN CWD" PROMPT IS SUPPRESSED AUTOMATICALLY
neo-tree has its own native confirm prompt ("File not in cwd. Change cwd to
<dir>?") that fires whenever a reveal is requested — explicitly, or implicitly
whenever filesystem.follow_current_file.enabled is on — without an explicit
dir, and the file to reveal isn't under the tree's current root. This can be
triggered by ANY code that calls neo-tree's command API, not just
filetree.nvim itself — including your own custom keymaps that call
require("neo-tree.command").execute() directly.
As soon as require("filetree").setup({ adapter = "neotree" }) runs,
filetree.nvim wraps neo-tree's command.execute once so this prompt can never
fire — any at-risk call (yours or filetree's own) gets neo-tree's
reveal_force_cwd = true applied automatically. A call that already sets
dir, reveal_force_cwd, or an explicit reveal = false is left completely
alone. No configuration needed; this protects every entry point uniformly,
including ones filetree.nvim has no other way to know about.
cwd_sync.pause(ms) pauses auto-reveal programmatically:
require("filetree").feature("cwd_sync").pause(5000)
5.4 CWD MODES
cwd_sync is stateless: on every buffer switch it re-resolves a root from the
file. A mode is state — "keep the cwd here regardless of which buffer is
focused" — so it lives in its own feature, cwd_mode, which cwd_sync asks
before it changes anything.
MODES
follow No policy. cwd_sync's own resolution applies unchanged
(root_markers → project_root → the file's parent). This is the
default, and it makes the feature completely inert.
project The cwd stays at the current project root as long as the focused
file lives inside it, and moves only when a file outside it is
opened. With project.sticky (default) a file with no root of
its own — a scratch note, something in /tmp — does not drag the
cwd along either.
NEEDS cwd_sync enabled: following the focused file as it moves
between projects runs through cwd_sync's BufEnter hook. Without
it, switching into project only seeds the initial pin and then
does nothing on the next buffer switch — filetree warns once, on
the switch, if cwd_sync is not active.
nearest Like project, but the root is the nearest PACKAGE boundary
(package.json, Cargo.toml, go.mod, …) rather than the VCS root.
In a monorepo .git is too coarse: you want the package you are
editing. Everything else — sticky, skip_dirs — is shared with
project, and .git is the last-resort marker so a file outside
any package lands on the repository instead of walking to /.
Same cwd_sync dependency and warning as project.
lock The cwd is pinned to one directory. Buffer switches never move
it, and with lock.enforce (default) neither does foreign code:
a :cd from a picker, a session restore or another plugin is
reverted (via lib.nvim.fs.dir_guard).
manual Nothing automatic. The cwd and the tree root change only through
explicit action (+/-, :Filetree cwd …).
tree_leads Direction reversal: the TREE is the authority and the cwd
follows it. Buffer switches move nothing at all; re-rooting the
tree with +/- is what moves the cwd. A file already under the
tree root is still revealed — showing anything else would mean
re-rooting, which is what this mode refuses.
Re-rooting the tree by hand is never fought: pressing + in lock mode moves
the pin instead of being reverted (lock.follow_manual_root).
The badge shows PROJECT, PKG, LOCK, MANUAL, TREE — and nothing in
follow mode (this is indicator.style = "text"; see LABEL STYLE below for
the other label styles). cycle defaults to `{ "follow", "project", "lock"
}; add the others if you want :Filetree cwd toggle (and L`) to reach
them.
SCOPE
Orthogonal to the mode: how far a directory change reaches. global:cd— the global cwd (default) tab:tcd— one project per tab win:lcd— window-local The scope applies to everything the policy does — the mode's own changes, the lock's enforcement, and cwd_sync's per-buffer change. Switching scope re-anchors a held root in the new one. NOTE: leaving "tab"/"win" cannot undo a:tcd/:lcdthat was set in some OTHER tab or window — Vim has no "clear everywhere" for those. The root is re-applied in the new scope; stale local directories elsewhere stay until something else changes them.
COMMANDS
:Filetree cwd mode <name> follow | project | nearest | lock | manual |
tree_leads (<Tab> completes)
:Filetree cwd scope <name> global | tab | win
:Filetree cwd lock [dir] pin to dir (default: the current cwd)
:Filetree cwd here pin to the node under the cursor
:Filetree cwd unlock back to the previous mode
:Filetree cwd toggle cycle through cycle
:Filetree cwd status active mode, its scope, root, and the cwd
:Filetree cwd forget drop the policy saved for this project
PERSISTENCE
Withpersist = truethe mode, the scope and a lock's pinned directory are remembered per project (lib.nvim.store.project, understdpath("cache")) and restored on the next start. Off by default: it writes to disk and makes a mode outlive the session that set it, which should be asked for. The entry is keyed by the project of the directory NEOVIM WAS STARTED IN, captured before anything can move the cwd. Keying by the current cwd would make the key change with the very state being saved — lock onto another project and the entry lands under that project, so the session you were actually in never finds it again. Only explicit actions write.projectmode re-pins itself on every buffer switch, and persisting those would mean a disk write per|BufEnter|for a value that is re-derived at startup anyway — so only a lock's pin is stored. A restored policy outranks the configuredmode: it is what you chose last, in this project, at runtime. When there is nothing saved — or the stored lock points at a directory that no longer exists — the configured mode applies as usual; a deleted directory can never take the session hostage. Tree-buffer keymaps:Lcycles modes,gplocks onto the node under the cursor.
INDICATOR
The active mode is shown bottom-left in the tree window: "PROJECT", "LOCK", "MANUAL" — and nothing at all in follow mode. In lock mode the pinned path is appended, elided to the tree's width. The badge uses the window's own statusline, and falls back to a one-line float over the tree's last row whenlaststatus = 3removes per-window statuslines (indicator.mode = "auto"; force one with "statusline"/"float").
LABEL STYLE
indicator.stylepicks which label table the badge text comes from: text (default) PROJECT PKG LOCK MANUAL TREE (nothing in follow) short P N L M T (nothing in follow) numeric 1 2 3 4 5 (0 in follow) icon (nothing in follow)numericis the only style that shows anything forfollow("0") — its whole point is "which of the N states am I in", and 0 answers that; the other styles keep follow as the inert, nothing-to-report default they always were. Each style reads its own table —labels(text),labels_short,labels_numeric,icons— so overriding one mode's text in one style never touches the others:
indicator = {
style = "short",
labels_short = { lock = "🔒" }, -- only `lock` changes; P/N/M/T stay default
},
iconsholds Nerd Font glyphs and is the only table with no built-in text fallback if your font is missing a glyph — swap the entry for that mode in your own config.hl(the highlight group per mode) is shared across all four styles: switchingstyleonly changes the text, never the color. Getting the filled, capsule-like look of a vim-mode indicator (background color instead of just colored text) is a rendering choice for the host statusline, not something this plugin'stext/hlpair does on its own —badge()gives you the raw text and highlight group; wrapping them in a bg-filled highlight group with a fading separator is the same recipe you'd use formode()itself. See EXTERNAL STATUSLINE below.
EXTERNAL STATUSLINE
To render the mode yourself — in lualine, heirline, or a hand-rolled
statusline — set indicator.enabled = false first. Skipping this shows the
mode TWICE: once in your own statusline, once bottom-left in the tree window.
Two ways to read it:
local cwd_mode = require("filetree").feature("cwd_mode")
cwd_mode.component() --> "LOCK …/Notes" (plain text, e.g. lualine)
cwd_mode.badge() --> { text = "LOCK …/Notes", hl = "DiagnosticWarn",
mode = "lock", root = "/home/you/Notes" }
badge()is for a component that wants to style itself (heirline) or gate on the raw mode/root rather than parsing the text. AUser FiletreeCwdModeChangedautocmd fires — with a scheduledredrawstatus— whenever the text or highlightcomponent()/badge()would return actually changes. Most statusline plugins already redraw on common events (CursorMoved,ModeChanged, …) and pick the new value up on their own; the event exists for the gap a:Filetree cwd lockfrom a command line or a script would otherwise leave until the next unrelated redraw. Hook it if your plugin needs an explicit nudge:
-- lualine
vim.api.nvim_create_autocmd("User", {
pattern = "FiletreeCwdModeChanged",
callback = function() require("lualine").refresh() end,
})
Or, in a heirline component:
{
provider = function() return require("filetree").feature("cwd_mode").badge().text end,
hl = function() return require("filetree").feature("cwd_mode").badge().hl end,
}
CONFIGURATION
features = {
cwd_mode = {
mode = "follow", -- follow | project | nearest | lock | manual | tree_leads
scope = "global", -- global | tab | win
project = {
markers = { ".git", ".hg", ".svn" },
skip_dirs = { "node_modules", ".venv", "vendor" },
sticky = true,
},
nearest = { -- markers only; skip_dirs/sticky come from `project`
markers = { "package.json", "Cargo.toml", "go.mod", "pyproject.toml",
"setup.py", "*.rockspec", "mix.exs", "build.zig",
"CMakeLists.txt", ".git" },
},
lock = { enforce = true, follow_manual_root = true },
reveal_outside = "skip", -- or "reveal"
persist = false, -- remember mode/scope/pin per project
indicator = { enabled = true, mode = "auto", show_path = "lock",
style = "text" }, -- text | short | numeric | icon
cycle = { "follow", "project", "lock" },
},
}
project.markersis VCS-only on purpose:projectmode answers "which repository am I in". Addingpackage.json/Cargo.tomlturns it into nearest-package (monorepo) behaviour — a deliberate choice, not the default.skip_dirsis the counterpart: a file undernode_modules/pkg/resolves to the project ABOVE the vendor directory, never to the vendored package.reveal_outsidedecides what happens when the focused file is outside the held root: "skip" (default) leaves the tree where it is — re-rooting it would break the very thing the mode holds — while "reveal" reveals the file anyway.
ONE ROOT WALK
cwd_mode owns the plugin's marker-based root walk. cwd_sync'sroot_markersand theproject_rootfeature are the fallback for a setup where cwd_mode is disabled — configurecwd_mode.project.markersinstead. Before this there were three walkers with three marker sets, and they disagreed: cwd_sync anchored the cwd to the git root while find_files scoped itself to the nearestpackage.jsonunder it, with nothing to say which was right. One walk now answers for both. Asking for the package rather than the repository is whatnearestmode is for — a deliberate choice, not an accident of which feature happened to answer.
OTHER FEATURES
A held root is not just cosmetic — the project-scoped features honour it: find_files searches the held root when no tree node is selected grep_in_dir greps the held root when no tree node is selected git_status decorates the tree from the held root's repository breadcrumbs count relative to the held root They all resolve throughfiletree.util.root.find([path]), which asks cwd_mode first, then project_root, then the cwd. Before that, each resolved from the CURRENT BUFFER — so opening a file from another project silently moved them there while the tree, the cwd and the badge still said otherwise (git_status would decorate a tree rooted at /notes with /repos/foo's status). In follow mode nothing changes: no root is held, and the buffer decides as before.cwd_mode.root()is the same answer for your own code, with the cwd as its fallback. NOTE: cwd_mode needs cwd_sync enabled to influence buffer-switch behaviour — cwd_sync is the executor and is default-disabled (|filetree-default-disabled|). Lock enforcement and the indicator work on their own regardless.
5.5 CURRENT HIGHLIGHT
Highlights the current file's line and its parent directory's line in the tree buffer using extmarks (priority 150). Re-applies on BufEnter, WinEnter, BufWritePost, and ColorScheme. Highlight groups created: FiletreeCurrentFile — current file node FiletreeCurrentParent — parent directory node
5.6 SAFETY / BACKUP
Provides a backup API for use before destructive operations:
local safety = require("filetree").feature("safety")
local backup_path = safety.before_delete("/path/to/file")
-- now safe to delete
Methods:before_delete(path)→ string? Create backup, return backup path.before_move(src, dst)→ string? Create backup of src.list_backups()→ string[] List all existing backups.toggle_dry_run()Toggle dry-run mode at runtime.
5.7 AUTO REVEAL
On BufEnter, scrolls or expands the tree to reveal the current file — and NEVER changes the cwd itself (that is cwd_sync's job; see |filetree-cwd-sync|). Debounced; auto-pauses for 500ms after the cursor enters the tree window so it doesn't fight manual navigation. Reveal per buffer switch: 1. If the file is already rendered (its parent dirs are expanded), just scroll the tree cursor to it — cheap, uses the adapter's cached path→line map. 2. Otherwise, if the file IS under the tree's CURRENT root, expand collapsed parent directories to reveal it (adapter.open_revealis called with that same root pinned) — the root itself stays put. 3. If the file lives OUTSIDE the current root: re-roots the tree to wherever it resolves via the same directory logic cwd_sync uses (see |filetree-cwd-sync|'s "project root is resolved in this order" list), unlessfollow_root = false. Skipped when cwd_sync is already doing its own reveal for this switch — the two resolve the identical directory, so either or both running is redundant work at worst, never a conflict. This step never touches the cwd itself; getting Neovim's own cwd there too is still cwd_sync's job (or the tree plugin's native cwd-follow, e.g. neo-tree'sbind_to_cwd+follow_current_file).follow_rootdefaults to true, so the tree follows the current buffer on every switch out of the box — regardless of what triggered it (a keymap from another plugin, a picker,:edit, …) — even into a different project, without needing cwd_sync enabled at all. Set it to false to restore the old behaviour of silently doing nothing for a file outside the current root. Entering the tree window (|CTRL-W_w|, a<C-h>mapping, a mouse click) puts the tree cursor on the current file's node as a separate step. It runs outside the debounce on purpose: otherwise where the cursor landed depended on whether the debounced BufEnter reveal had fired yet, and a quick window switch lost the race — the reveal arrived to find the cursor already in the tree and dropped itself. The file is taken from the window you came from, falling back to the last real file an editor window showed. Config:enabledbooleandebounce_msinteger Delay after BufEnter (default 150ms).ignore_ftstring[] Filetypes that never trigger reveal.only_if_openboolean Only reveal when tree window is visible (default true).sync_on_enterboolean Move the tree cursor onto the current file's node when the tree window is entered (default true).follow_rootboolean Re-root the tree for a file outside its current root instead of doing nothing (default true). Commands:
:FiletreeAutoRevealPause [ms] Pause for N ms (default 2000).
:FiletreeAutoRevealResume Resume immediately.
:FiletreeRevealCurrent Force reveal now.
5.8 CREATE FROM TEMPLATE
Create files from user-defined templates with variable substitution. Each
file directly inside the template directory (default
stdpath("data")/filetree/templates/) is a template; its filename is the name
shown in the picker. Templates shipped with filetree.nvim itself (several per
common language — module/class/test variants for Lua, TypeScript, JavaScript,
Python, Go, Rust, C#, C, C++, Zig, plus JSON, Markdown, YAML, TOML, shell,
PowerShell, HTML and CSS; see docs/features.md for the full table) are merged
in underneath — a same-named file in your own template directory shadows a
built-in one entirely.
Workflow: press "A" in the tree (the smart_create "a" counterpart) — or
:Filetree template — pick a template FIRST, then enter the new filename,
pre-filled with the picked template's own filename (extension included).
Fixes the old name-first flow's actual bug: typing the name first and only
filtering the picker by its extension didn't stop a fallback pick of a
template whose real extension didn't match (e.g. "check.md" with a C++
template's content) — the file's extension and its content could disagree,
opening the buffer with the wrong filetype. Surrounding whitespace in the
name is trimmed, a name with a subdirectory ("sub/x.lua") creates the
missing parent directories, and a name ending in "/" (no filename) is
refused. ${module} and every other variable below resolves once both the
template and the destination path are known.
DISPLAY GROUPING
Once you've added your own templates, the builtin picker (see PICKER BACKEND below) groups the list under a "[custom]" and a "[builtin]" header instead of tagging every single built-in entry — repeating a "[builtin]" marker on every row not authored by you was pure noise once the list mixes both. Headers only appear when both kinds exist and are never selectable (<Up>/<Down>/<C-n>/<C-p> step over them; a header that still gets submitted re-opens the picker rather than closing it); a directory with just built-ins (the default) stays a plain list. The pickers.nvim path (prefer = "telescope"/"fzf"/"snacks") shows a flat, ungrouped list instead — a fuzzy-match item list has no room for non-selectable header rows. Variables (substituted on creation):
${filename} Basename of the new file (without extension)
${ext} Extension of the new file (without dot)
${date} Current date, YYYY-MM-DD
${year} ${month} ${day} ${time}
${author} config.author, or $USER/$USERNAME
${module} Dotted Lua module path for a destination under lua/ (e.g.
lua/plugins/test.lua -> "plugins.test"); a generic dotted
path from the project root for anything else.
PICKER BACKEND
preferpicks what renders the template list: auto (default) Real fuzzy search + a native content preview of the highlighted template, via pickers.nvim (github.com/ StefanBartl/pickers.nvim) — its own telescope > fzf > snacks priority. Falls back to "builtin" when pickers.nvim isn't installed. telescope Force pickers.nvim's telescope engine. fzf Force pickers.nvim's fzf-lua engine. snacks Force pickers.nvim's snacks engine. builtin The original picker: a hand-rolled prompt+results float (plain substring match, no preview), or plainvim.ui.selectwhen even that is unavailable. pickers.nvim is a soft dependency (like lib.nvim's ui kit) — nothing to install unless you want it. Trade-off: only "builtin" supports reordering (below). pickers.nvim'spick_item()has no concept of custom in-picker keymaps, so picking "auto"/"telescope"/"fzf"/"snacks" loses <M-j>/<M-k> — setprefer = "builtin"to keep reordering instead of fuzzy search + preview. REORDERING ~ (builtin picker only) While the picker is open and the filter is empty, <M-j>/<M-k> move the highlighted template down/up. The order persists to a.order.jsonsidecar in the template directory, so it survives restarts; a never-reordered or newly-added template is appended alphabetically after the ones with an explicit position. A move never crosses the [custom]/[builtin] boundary — the persisted order is itself kept grouped custom-then-builtin, in step with the display.
CONFIGURATION
features = {
create_from_template = {
enabled = true,
keymap = "A",
template_dir = nil, -- defaults to stdpath("data")/filetree/templates/
author = nil, -- defaults to $USER/$USERNAME
open_after = true, -- open the created file in the editor
prefer = "auto", -- auto | telescope | fzf | snacks | builtin
},
}
PUBLIC API
local cft = require("filetree").feature("create_from_template")
cft.list() --> {name:string, path:string, builtin:boolean?}[]
cft.add_template(name, content)
cft.move(name, -1) --> boolean moved (up); +1 moves down
5.9 OPEN IN FILE MANAGER
Shows the node under the cursor in the platform's file manager (default keymap "<leader>fm"): Windows -> Explorer, macOS -> Finder (viaopen), Linux -> the first manager found on PATH. A file node is selected inside its parent directory unlessreveal = false; a directory node is navigated into. The platform dispatch itself lives inlib.nvim.cross.reveal_in_fm, shared with open.nvim's:Open filemanager, so a fix there lands in both plugins. On Windows the launch also brings the Explorer window to the FRONT, which takes a short PowerShell helper. Spawning explorer.exe directly does create the window, but Windows only grants SetForegroundWindow to the process owning the foreground window — inside a terminal that is the terminal host, not nvim.exe — so the window was created behind everything and the feature looked dead. It appeared to work under a GUI Neovim (Neovide, nvim-qt), where nvim.exe IS the foreground process; that is what made this failure look intermittent for so long. See lib.nvim'sreveal_in_fm/README.md. A launch is otherwise fire-and-forget: nothing waits on the file manager. If one silently opens nothing, turn ondebugto see what happened:
features = {
open_in_fm = { enabled = true, debug = true },
}
This logs the resolved target, the options handed to the shared dispatcher, and any error it reports back.
REUSE AN EXISTING WINDOW (Windows only)
reuse_existing = true Navigates an already-open Explorer window (found via aShell.ApplicationCOM query) to the target instead of spawning another. This is NOT the same as opening a new tab in that window — Explorer's own tab feature (Windows 11 22H2+) has no public scripting hook to add a tab to an existing window from outside the process, so this reuses/replaces that window's current view instead. A file node cannot be selected this way, since Navigate2 takes a folder; it reuses the window on the file's parent directory. Forwarded tolib.nvim.cross.reveal_in_fmasreuse, which does the reuse and the raise in the same asynchronous step — it is not a separate blocking pre-check, and it costs no extra round-trip over a normal Windows launch. It defaults to off because it replaces whatever the reused window was showing. If your default folder handler is something other than Explorer (some third-party file managers register themselves as theDirectoryshell-open handler the same way a browser registers itself for http(s) links), a normal launch still works — Windows dispatches to whatever's registered — but this COM query is Explorer-specific and will simply find nothing to reuse.
5.10 NO NAME GUARD
Redirects a stray [No Name] editor window to a real, open buffer and wipes the stray one — Neovim falls back to showing [No Name] in any window left with nothing to display (a:bd/:bwipeoutwith no alternate,:enew, …). Left alone when no real buffer exists to redirect to (e.g. the last file buffer just closed) — that IS the legitimate case. Two passes, both always skipping the tree's own window: - ABufWinEnterhandler tied to the window actually being focused — fires deterministically the moment a stray buffer becomes the focused one. - ABufAdd/BufDelete/BufWipeoutsweep across every window, all tabs — catches a stray buffer sitting in some OTHER window that never itself refires BufWinEnter, which the first pass alone would never revisit. Both defer one tick and re-validate before acting (BufDelete/BufWipeout fire mid-transition, before Neovim has settled the window's replacement buffer), so this never races a user who deliberately switched into a [No Name] window. Config:enabledboolean (default true)
5.11 REFERENCES
Moving a file breaks everything that pointed at it. The reference engine is the one place that fixes that, and every mutating feature routes through it: smart rename, batch rename, move (|filetree-move|), cut+paste, and trash. The scan starts the moment the key is pressed — while the file is still at its old path — and the mutation runs strictly after it finished, so a reference can never be missed because the file moved out from under the scanner. Each hit is then re-expressed for the new location in the style it was written in (an absolute link stays absolute,./xkeeps its./, an aliased import stays aliased), and one chooser covers the whole operation:
7 reference(s) in 4 file(s) (5 markdown, 2 lua)
Update all | Select… | Show diff | Leave as-is
Every rewrite is content-verified at the byte range the scan recorded, so a line that changed in the meantime is skipped rather than corrupted. A file open in a buffer is patched in that buffer (and written back only when it had no unsaved changes).:Filetree refs undoreverts the last batch. Every path the engine carries is normalized to forward slashes the moment the candidate list is built, so a file is spelled the same way whether ripgrep or the built-in walk found it, and the file list above readslua/proj/a.luaon every OS. Providers (opt out per language): markdown [text](./path), images, [id]: definitions, HTML src=/href=, optionally [[wiki]] links (default on) lua require("a.b") / require "a.b", incl. the submodule cascade when a directory moves (default on) python import a.b, from a.b import x, relative from .x (default on) ts_js import/export … from, dynamic import(), CJS require(), relative specifiers plus tsconfigpathsaliases (default OFF — tsserver does this better via willRenameFiles when it is running) A markdown file can link to any file type, so the markdown provider runs for every move: renaming foo.lua fixes the docs that link to it too. Matching resolves each target against the file it appears in and compares absolute paths — never text. That is why../Test.mdfrom a subdirectory matches while a same-named file elsewhere does not. Config (one central block, not per feature):
require("filetree").setup({
refs = {
enabled = true,
providers = { markdown = true, lua = true, python = true, ts_js = false },
on_rename = "ask", -- "ask" | "auto" | "off"
on_move = "ask",
on_delete = "ask", -- trash: blank dangling links to REF!
copy = false, -- a copy breaks nothing
picker = "auto", -- auto | telescope | fzf-lua | quickfix
prefer_lsp = true,
wiki_links = false,
scan = {
root = "project", respect_gitignore = true,
max_files = 5000, timeout_ms = 3000,
},
undo = true,
},
})
The per-feature options this replaces (check_markdown_refs,refs_picker_prefer,smart_rename.update_references) are migrated automatically, with a one-time notice. Without ripgrep, the fallback walk reads extension-matching files in chunks across event-loop ticks once the candidate set passes ~20 files, with a[filetree.refs]progress indicator rather than freezing the editor for the whole scan. Commands: :Filetree refs undo Revert the last reference rewrite :Filetree refs status Modes, providers, ripgrep, pending undo Third-party providers register through the same contract the built-ins use:require("filetree.refs").register({ name = …, plan = … }). See lua/filetree/@types/refs.lua.
5.12 MOVE
Mmoves the node under the cursor — or every marked node — to a destination typed into one prompt, instead of the cut / navigate / paste round trip. <Tab> completes directories; the destination may be cwd-relative, absolute, or ~-prefixed. • several nodes, or a destination that already is a directory → the items move INTO it under their own names; • a single node and a destination that does not exist → that becomes the node's new full path, soMdoubles as move-and-rename. A destination directory that does not exist is offered for creation rather than silently created; a collision asks Overwrite / Keep both / Cancel. References are updated through |filetree-refs|, whose scan starts on the keypress and therefore runs while the destination is being typed. Config:enabledboolean (default true)keymapstring (default "M")use_safetyboolean Backup before moving (default true)dry_runboolean Log the plan without executing (default false) Commands: :Filetree move [destination]
5.13 FEATURE REFERENCE
Every feature that has no section of its own above. Generated from docs/FEATURES/*.md and the binding catalog by scripts/gen_vimdoc_reference.lua -- edit those, not this section. The markdown originals carry the tables, worked examples and cross-links that do not survive the trip into help format; reach for them when an entry here is thinner than the question you arrived with. COMPARE Diff ........................................................|filetree-diff| FILEOPS Buffer Save ..........................................|filetree-buffer-save| Copy / Move ............................................|filetree-copy-move| Link Create ..........................................|filetree-link-create| Open Replace ........................................|filetree-open-replace| Open Variants ......................................|filetree-open-variants| Batch Rename ........................................|filetree-rename-batch| Smart Create ........................................|filetree-smart-create| Smart Rename ........................................|filetree-smart-rename| Trash ......................................................|filetree-trash| GIT Git Status ............................................|filetree-git-status| INFRA File Watcher ........................................|filetree-file-watcher| Handle Guard ........................................|filetree-handle-guard| Hooks API ..............................................|filetree-hooks-api| Ignore List ..........................................|filetree-ignore-list| Project Root ........................................|filetree-project-root| Tree Integrity ....................................|filetree-tree-integrity| Watcher Quarantine ............................|filetree-watcher-quarantine| LSP LSP Diagnostics ..................................|filetree-lsp-diagnostics| NAV Auto Resize (opt-in) .................................|filetree-auto-resize| Buffer Cycle ........................................|filetree-buffer-cycle| Reveal Alt ............................................|filetree-reveal-alt| Sidebar Guard ......................................|filetree-sidebar-guard| Source Switcher (neo-tree) .......................|filetree-source-switcher| Tree Toggle (opt-in) .................................|filetree-tree-toggle| Tree Traverse ......................................|filetree-tree-traverse| ORG Marks ......................................................|filetree-marks| Session ..................................................|filetree-session| PATHS Copy File List ....................................|filetree-copy-file-list| Lua Require Copy ................................|filetree-lua-require-copy| Markdown Links ....................................|filetree-markdown-links| Path Copy ..............................................|filetree-path-copy| SEARCH Filter ....................................................|filetree-filter| Find Files ............................................|filetree-find-files| Grep In Directory ....................................|filetree-grep-in-dir| Live Search ..........................................|filetree-live-search| SYSTEM File Clipboard ....................................|filetree-file-clipboard| Open With ..............................................|filetree-open-with| PDF Create ............................................|filetree-pdf-create| PDF bridge (pdfport.nvim) ...............................|filetree-pdf-open| Shell Run ..............................................|filetree-shell-run| UI Breadcrumbs ..........................................|filetree-breadcrumbs| Broken Link Notify ............................|filetree-broken-link-notify| Cheatsheet ............................................|filetree-cheatsheet| Context Menu ........................................|filetree-context-menu| Cursor Hide ..........................................|filetree-cursor-hide| Link Marker ..........................................|filetree-link-marker| Node Info ..............................................|filetree-node-info| Opened-buffer Sync ...................................|filetree-opened-sync| Preview ..................................................|filetree-preview| Size Info ..............................................|filetree-size-info| Tree Reset ............................................|filetree-tree-reset| Window Size Cycler ............................|filetree-window-size-cycler| Window Style ........................................|filetree-window-style|
DIFF
D diffs the node under the cursor — against the working tree, a git
revision, or another node, depending on how it's invoked. Native diffmode
under the hood, nothing custom rendered.
Keymaps: D
Module: lua/filetree/features/compare/diff/
BUFFER SAVE
Force-save the adjacent editor's buffer (<C-s>) or the node's own buffer if it's open elsewhere (<M-s>), without leaving the tree window. Keymaps: <C-s> <M-s> Module: lua/filetree/features/fileops/buffer_save/
COPY MOVE
Stage one or more nodes withc(copy) orx(cut), thenpto paste them under the cursor's directory — the same stage-then-paste model as a system file manager, works across multiple marked nodes at once. If any staged item's name already exists at the paste target, a prompt appears before anything is touched: Overwrite (replaces the existing item — backed up first whenuse_safetyis on), Keep both (pastes alongside it as `name (2).ext`), Skip (leaves that item out of this paste; a skipped cut stays staged so you can resolve it and paste again instead of it silently vanishing), or Cancel (aborts the whole paste, nothing is touched). No conflicts means no prompt — pasting into an empty or non-colliding directory behaves exactly as before. A multi-item paste shows a progress indicator (current item, N/M, final summary) vialib.nvim.progress— see Progress indicators'sprogress_styleoption (top-levelrequire("filetree").setup({...})config, not per-feature). A cut+paste is a move, so it runs the reference engine too — the scan starts when you pressx, and overlaps with you navigating to the paste target. A copy never breaks a reference (the original stays put), so copies are not scanned. Staged nodes are marked in the tree with aC/Xoverlay. That marker is drawn on the node's own line, so it needs the adapter to resolve a line to a node (see Backends): it shows on neo-tree and nvim-tree, and is absent on netrw, oil and mini.files. The staging and the paste itself work on all five either way. A cut or copy item that cannot actually be transferred is reported, not silently dropped. A Windows sharing lock (a file-explorer window, a search indexer or an AV scan still holding the item open) can outlast the automatic retry — when it does, the paste reports exactly which item failed and why (Failed: <src> → <dst> (<reason>)), instead of just showing a bare `Pasted 0/1 item(s)` with no indication that anything went wrong. A failed cut item stays staged so you can resolve the lock and paste it again. Copying a symlink copies the link, not its target. A staged symlink — file or directory — pastes as a new symlink pointing at the same target, instead of being silently dereferenced into an independent copy of whatever it points at. This matters most for a symlinked *directory*: without it, pasting one would deep-copy everything behind the link (potentially huge, and unboundedly recursive through a symlink cycle, since a real directory tree cannot have one but a naive walk following links does not know that). An ordinary (non-link) directory or file still copies its actual content, as always. Keymaps: c x p P X Module: lua/filetree/features/fileops/copy_move/
LINK CREATE
:Filetree symlinkcreates a symlink or hardlink inside the current tree directory, pointing at a path you type into a prompt. The link is named after the target's basename, and lands in the node under the cursor — its own directory if it is one, otherwise its parent, the same resolution Smart Create uses. A directory target only ever gets a symlink: neither Windows nor POSIX lets an unprivileged process hard-link a directory. A file target is offered the Symlink / Hardlink choice.:Filetree symlink mark [path]/:Filetree symlink pasteare a faster mark-once, paste-many pair for the same job. Marking with no path uses the node under the cursor when the tree is focused, else the focused editor buffer's file; an explicit path (relative or absolute) always wins. Pasting inserts the marked source into the node under the cursor and picks the link kind itself instead of asking: directories always get a symlink; files get a hardlink on Windows (needs no elevation or Developer Mode, unlike a Windows symlink) and a symlink elsewhere. Marking again replaces the previous source; pasting does not clear it, so one marked source can be linked into several places in a row. Env roots and portable links. The target may be written with a root —$REPOS_DIR/x,$NVIM_CONFIG_DIR/x(no environment variable needed for that one) or a root of your own (see Env roots) — and the messages show targets in that form. What the link itself stores is decided byfeatures.link_create.relative: •"auto"(default): when the link and its target live under the same root, the target is stored relative (../../proj/dir_a). Such a link keeps working on another machine where that root is on another drive or under another home — an absoluteE:/repos/…would not. (On Windows the relative text is written with backslashes: Windows does not resolve a relative symlink target written with/.) The created link is checked: a relative target is resolved against the link's physical directory, so a link inside a symlinked folder would dangle — such a link is stored absolute instead. • Across two roots (link under$REPOS_DIR, target under$NVIM_CONFIG_DIR) there is no portable form: a symlink cannot carry an environment variable, and the two roots have no fixed layout between them. The target stays absolute and the message says so — seerepairbelow for getting it back on another machine. •"always": relative whenever a relative form exists (same drive)."never": always absolute. Hardlinks are never relative. With `env_roots = { enable = false },"auto"` stores absolute targets. Usercmd-first, like Path Copy's format picker: no key is bound by default, so setfeatures.link_create.keymap(or.keymap_mark/.keymap_paste) if you want one. A link created this way is not just another entry in the listing — see Link Marker for the⇢sign that sets it apart in the tree, and Node Info'sIwindow for theLink to:/ hard-link detail.:Filetree symlink check [path]reports whether the node under the cursor (or an explicit path, or the focused editor buffer's file) is a symlink at all, and if so whether it still resolves —:Filetree symlink checkallruns the same check over every marked node (else the node under the cursor), skipping non-symlinks and summarizing the result.:Filetree symlink repair [path]targets a broken symlink: it reads the link's own recorded target and searches the filesystem for a file with a matching name via gopath.nvim — an optional soft dependency, same style as PDF Open's pdfport.nvim bridge. Found candidates are offered in a picker (plus "delete instead" / "keep broken"); with gopath.nvim not installed, or nothing found, it falls back to the delete-or-keep choice directly. `:Filetree symlink repairall` runs this over every broken symlink among the marked nodes (else the node under the cursor), one at a time. Before any search,repairtries the cheapest candidate first: the same path under this machine's roots. A link recorded on another machine asE:/repos/casedesk.nvim/x.mdis re-anchored by its root's folder name — everything after arepossegment is looked up under this machine's$REPOS_DIR(likewisenvimfor$NVIM_CONFIG_DIR, or the folder name of a root of your own). This is what makes a cross-root link — which has to be stored absolute — usable on a second device::Filetree symlink repairallon the marked links and pick the candidate. When it finds one the disk search is skipped. (Switched off with the rest ofenv_rootsbyenable = false.) The search never widens into Neovim's own cache/data/state directories (that's where the undo/swap/shada directories live — a stray file there could otherwise spuriously match a broken link's basename and get offered as a bogus "candidate"), no matter what the two settings below are set to. It runs in two passes, cheapest first: gopath's own cache and fast default roots (buffer dir/cwd/git root) first, then — only if that comes up empty —features.link_create.repair_roots(default{ "$REPOS_DIR" },$VAR-expanded) and.repair_nvim_config_root(defaulttrue, also searchesstdpath("config")), for a target that moved to a sibling project entirely outside gopath's own search. Measured at ~0.1s even against a 5.8k-file/737MB directory, so both are on by default; if that second pass is ever genuinely slow on a particular machine (e.g. a network drive),repair_search_slow_hint_ms(default2000) notifies with a one-time hint on how to narrow or disable it, andrepair_search_progress(default"auto") shows a progress indicator for it —"statusline"feedsui.nvim's statusline segment,falseshows nothing. Neither pass blocks the tree or any other window.:Filetree symlink deleteremoves every symlink among the marked nodes (else the node under the cursor) through the trash feature — a non-symlink caught up in the same batch is skipped, never deleted. Trashing a symlink (viad, or this command) only ever removes the link itself; the confirm prompt says so explicitly rather than reading like an ordinary file delete. Config:features.link_create.keymap/.keymap_mark/.keymap_paste(all unset by default),.relative(default"auto"),.repair_roots(default{ "$REPOS_DIR" }),.repair_nvim_config_root(defaulttrue),.repair_search_progress(default"auto"),.repair_search_slow_hint_ms(default2000) Commands::Filetree symlink,:Filetree symlink mark [path], `:Filetree symlink paste,:Filetree symlink check [path],:Filetree symlink checkall,:Filetree symlink repair [path],:Filetree symlink repairall,:Filetree symlink delete` Module: lua/filetree/features/fileops/link_create/
OPEN REPLACE
Opens the node under the cursor into an existing editor window — never into the tree's own — in two shapes that differ in what becomes of the buffer already sitting there.Oreplaces::editover the editor window. The previous buffer stays in the buffer list, it just isn't on screen any more.<M-CR>(and<C-CR>) swaps: the previous buffer is closed as well, and the new file takes over the slot it held in the bufferline. Reach for it when the buffer list is a working set rather than a history — opening five files to find the one you wanted otherwise leaves four behind to close by hand. A swap refuses to run when the focused buffer has unsaved changes: it says so and does nothing at all, rather than opening the file and quietly leaving the old buffer behind (which would just beO). Write it first, or useO. Keeping the slot needs a buffer list that has slots. Neovim's own order is the buffer numbers, and those only ever increase — a file opened now can never sort ahead of one opened earlier, and no API moves it, because there is nothing to move: the order isn't stored, it's derived. Tabline plugins in the NvChad lineage keepvim.t.bufs, a per-tabpage list they order themselves, and that one can be rewritten — so the new buffer is put back at the index the replaced one held. Without such a list the swap still swaps; the new file simply lands where its buffer number puts it, which is last. Nothing errors, and there is nothing to configure for it — the list is either there or it isn't. Two keys for one action because which of them the terminal delivers isn't ours to decide: many terminals send plain<CR>for Ctrl+Enter, in which case<C-CR>never fires and the tree's own<CR>behaves as always. Alt+Enter travels further, so it's the primary.
open_replace = {
keymap = "O", -- replace; previous buffer stays listed
keymap_swap = "<M-CR>", -- swap; previous buffer closed
keymap_swap_alt = "<C-CR>", -- same, where the terminal distinguishes it
close_tree = true, -- close the tree after keymap
swap_close_tree = false, -- ... and after a swap
keep_position = true, -- new buffer takes the replaced one's slot
},
<
Keymaps: O <M-CR> <C-CR>
Module: lua/filetree/features/fileops/open_replace/
OPEN VARIANTS
Open a node in a split (sg), vsplit (sv), tab (st), or add it to the buffer list without switching focus (gb/<S-CR>) — every common "open, but not by replacing my current window" shape in one feature. On a directory,<S-CR>does something else entirely: it collapses the node viaadapter.collapse_node(), since the adapter's own<CR>only ever expands (there's no toggle to undo it — see the note below).gbstays file-only; only<S-CR>picks up the directory case. Collapsing a drilled-into directory (neo-tree)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
neo-tree'sfilesystem.group_empty_dirsmerges a chain of directories that hold nothing but another single directory into one display line —personalcontaining onlyAllcontaining onlyFinishrenders aspersonal/All/Finish. Each<CR>lazily loads one more level and rebuilds that merged node from scratch, which can leave it reporting `is_expanded() == falseright after the very expand that just opened it — so<CR>` never recognizes it as open, and every further press just drills one level deeper with no way back topersonal.<S-CR>collapses the node when it is expanded, and falls back to its nearest collapsible ancestor when it isn't (the same fallback neo-tree's ownclose_nodecommand — bound toCby default — uses). Each merge step re-parents the replacement node onto the grandparent of whatever it just absorbed, so a chain merged all the way from a top-level directory ends up parented directly on the tree root — nothing left to structurally collapse without hiding the whole tree. For that case<S-CR>falls back once more, to a plain refresh: a re-scan rebuilds the top level fresh from disk, which is unmerged and collapsed because the node was never actually marked expanded to begin with. • Module (adapter side):M.collapse_node()inlua/filetree/adapter/neotree.luaKeymaps: sg sv st gb <S-CR> Module: lua/filetree/features/fileops/open_variants/
RENAME BATCH
<leader>rb opens an edit-buffer listing every node in view; editing a line
and saving renames the corresponding file — a bulk rename express lane for
renaming several files at once without one prompt per file.
Keymaps: <leader>rb
Module: lua/filetree/features/fileops/rename_batch/
SMART CREATE
acreates a file or directory under the cursor, template-aware — typing a trailing/creates a directory, anything else a file, with parent directories created as needed. Keymaps: a Module: lua/filetree/features/fileops/smart_create/
SMART RENAME
rrenames the node under the cursor and updates every LSP reference to it project-wide, the same guarantee an IDE's "rename symbol" gives you, applied to a file/module rename instead of a variable. Whatever the language server does not rewrite — markdown links always, plusrequire()/importstatements when no server handled the rename (for Lua that is always, since lua_ls never implementsworkspace/willRenameFiles) — is picked up by the reference engine. Keymaps: r Module: lua/filetree/features/fileops/smart_rename/
TRASH
Cross-platform trash with undo —dto trash,Uto undo,<leader>thfor trash history. How far back that reaches isfeatures.trash.max_history(default 50,0= unlimited): a preference, not a limit protecting anything, since the history is a small JSON file. Marking multiple nodes and trashing them opens one batch confirmation instead of one prompt per file, and force-closes any open buffers backed by the deleted paths so they don't linger as edits-to-nowhere. Same progress indicator as Copy / Move above, for both the "delete all at once" and "confirm each individually" batch paths.features.trash.modeis"trash"by default — Windows Recycle Bin (viaMicrosoft.VisualBasic.FileIO.FileSystem, not the Explorer delete verb, which triggers a confirmation dialog no headless PowerShell script can answer), macOS Finder/Trash, orgio trash/trash-puton Linux (XDG-trash fallback when neither is installed). Set it to"permanent"to skip the OS trash and delete for good instead — no shell, same libuv/Vim-builtin primitive the "Overwrite" paste resolution uses. Opt-in on purpose: a permanent delete has noU/history entry to fall back on, so the confirm dialog's wording changes to make that explicit ("Permanently delete (cannot be undone)?") rather than sharing the "Send to trash?" phrasing. On Windows, "delete all at once" for several marked nodes runs the whole batch through a single PowerShell process instead of spawning one per file — spawningpowershell.exe(plus its .NET startup) per file is what made trashing a couple dozen marked files visibly slow. macOS and Linux batch the same way, onetrash/AppleScript/gio trash/trash-put/mvcall for the whole batch instead of one per file — cheaper to begin with there (no COM/.NET startup), but no reason to pay per-process overhead N times over when the backend already accepts a list of paths in one call. When something links to the file being deleted, the plain yes/no becomes a chooser — Delete + remove refs (blanks the dangling links toREF!), Inspect first (pick which ones), Delete, keep refs, Cancel. Undoing such a delete withUrestores theREF!markers along with the file. See References. For a large batch, the reference rewrite can still be in flight whenUfires — undo does not wait for it.Ustill restores the file immediately; if the rewrite finishes after that, a warning names how many references it touched and points at:Filetree refs undoto revert those separately.:Filetree trash dry-runcovers that reference rewrite too, not just the delete: it reports what would be marked and writes nothing. It is the one part of a delete that touches files the user did not select, so a dry-run has even less business making it than the delete itself — and every line of a dry-run, down to the closing summary, reports in the conditional, so nothing in the output reads as though it had happened. A dangling symlink (its target no longer exists) can still be trashed — the existence check that gatesdreads the dirent itself (filetree.util.conflict.exists,fs_lstat-aware), not justfilereadable/isdirectory, both of which follow the link and see nothing through a broken one. Keymaps: d U <leader>th Config:features.trash.mode("trash"|"permanent", default"trash") Module: lua/filetree/features/fileops/trash/
GIT STATUS
Decorates tree nodes with git status (modified/staged/untracked/…), resolved per adapter — each backend surfaces this through its own native mechanism rather than filetree.nvim reimplementing git status parsing. Backend support. Drawn as extmarks on the node's own line, which needs the adapter to say which node a given line holds (get_node_at_line). The neo-tree and nvim-tree adapters implement it; netrw, oil and mini.files do not, so this renders nothing there rather than misplacing anything. Refreshes on:BufEnter(tree buffer),BufWritePost(any buffer),FocusGained, and — optionally, no dependency either way — gitsuite.nvim'sUser GitsuiteBranchSwitched/GitsuiteConflictsResolvedevents, so a branch switch or clearing the last conflict marker in a buffer updates the decorations immediately instead of waiting for the next write or focus change. Module: lua/filetree/features/git/git_status/
FILE WATCHER
Watches the tree root directory for filesystem changes viavim.uv.fs_event(libuv) and auto-refreshes the tree —ReadDirectoryChangesWon Windows, inotify/kqueue on POSIX. Debounces bursts of events before callingadapter.refresh(), and re-arms whenever the tree root changes. Config:opts.features.file_watcher—enabled(default false, opt-in),debounce_ms(500),watch_recursive(true),ignore_events({}) Usercmds::Filetree watcher enter [ms],:Filetree watcher exitModule: lua/filetree/features/infra/file_watcher/
HANDLE GUARD
Fixes the same Windows/WSL file-lock at its source instead of hiding it: neo-tree's own directory watchers (withuse_libuv_file_watcher = true) keep an OS handle open per expanded directory and never close it, so renaming/deleting a watched directory can intermittently fail withEPERM/ERROR_SHARING_VIOLATIONbecause filetree's own watcher is still holding it. Once enabled it wires automatically into the fileops that move/ rename/trash a watched path (vialib.nvim.cross.fs.mutate'son_retryhook callingM.release(path)), closing the offending libuv handle so the retry succeeds. neo-tree adapter + Windows/WSL only — a safe no-op everywhere else, so the fileops' hook can always be passed unconditionally. Config:opts.features.handle_guard.enabled(default false, opt-in — patches a neo-tree internal and closes libuv handles it owns) Usercmds::Filetree handles— lists tracked handles, flags any pointing at a path that no longer exists (the leak signature) Module: lua/filetree/features/infra/handle_guard/
HOOKS API
Programmatic hook registration so other code (or a user's own config) can react to tree events without patching filetree itself. Usercmds::Filetree hooks events,:Filetree hooks clear [event]Module: lua/filetree/features/infra/hooks_api/
IGNORE LIST
Hides common filesystem clutter from the tree by default —.git,.github,node_modules,.venv,__pycache__, build/dist/targetdirectories,.DS_Store, editor dirs, and similar. Toggle at runtime with the adapter's own native "show hidden" key (Hon neo-tree/nvim-tree). Config: top-levelopts.ignore_list—true(default, built-in list, also readslib.nvimconfig if present),false(show everything), or astring[]that fully replaces the built-in list Module: lua/filetree/features/infra/ignore_list/
PROJECT ROOT
Shared, cached project-root detection used bycwd_syncand other features that need "which project is this file in" without a policy attached. Walks up from a file/directory for any ofmarkers(.git,package.json,Cargo.toml,go.mod, ... — a broad default list), returns the deepest directory holding one, and falls back to the file's own parent (or cwd, withfallback = "cwd") when nothing is found. Every directory resolved — not just the query directory, every intermediate directory walked past en route — is cached for the session.cwd_mode.resolve()is the plugin's one canonical marker-based walk whencwd_modeis enabled;project_root(andcwd_sync.root_markers) is the fallback path for acwd_mode-less setup, so a repo running both doesn't end up with the cwd anchored to the git root while a search scopes itself to the nearestpackage.json. Config:opts.features.project_root—enabled(defaulttrue),markers,fallback("parent"|"cwd"),cache(defaulttrue) Module: lua/filetree/features/infra/project_root/
TREE INTEGRITY
Fixes an upstream crash that otherwise breaks a neo-tree session until it is closed and re-opened:
[Neo-tree ERROR] Error setting nodes: .../nui/tree/init.lua:494: attempt to index local'node'(a nil value) < — followed by a dump of the entire tree, on every render from then on. nui's node initialization is not idempotent: it consumes a node's__children, keeping only their ids in_child_ids.Tree:set_nodes()first deletes the parent's whole subtree from itsby_idindex and then re-initializes whatever it was handed, so handing it live nodes re-registers those nodes but not their children —by_idloses them while_child_idsstill lists them. The nextset_nodes()over that subtree indexes a nil node and throws, and because it throws before_child_idsis reset, the inconsistency is permanent. neo-tree reaches that call from one place: thegroup_empty_dirsbranch for a lazily loaded single sub-folder (ui/renderer.lua, with the defaultscan_mode = "shallow"), which re-exports a whole level withstate.tree:get_nodes(parentId)and passes those live nodes straight back. Expanding a directory next to a one-child chain is enough. This feature wrapsNuiTree.set_nodeswith a pre-pass that (a) hands every live node its children back as__children, so the re-initialization rebuilds the subtree instead of orphaning it — nothing is lost and expanded directories stay expanded — and (b) drops ids already missing fromby_id, so an *already*-corrupted tree repairs itself on the next render rather than throwing. Fresh nodes (the normalcreate_nodes()path) are not touched at all, so nui behaves exactly as before wherever it was already correct. Left on by default, unlike the two features above: it changes nothing on a healthy tree, costs one pass over the subtree being replaced, and the crash it prevents is not recoverable without re-opening the tree. Disabling it is a one-liner if a future nui release fixes this upstream. Config:opts.features.tree_integrity—enabled(default true),silent(true — set false to get a debug note whenever a corrupt subtree is healed) Module: lua/filetree/features/infra/tree_integrity/
WATCHER QUARANTINE
On Windows, libuv file watchers can emit spuriousEPERMerrors when a file or directory is deleted/moved while watched. This feature suspends watching for a configurable window and suppresses the resulting error notifications — it hides the symptom rather than fixing the cause (seehandle_guardbelow for the fix). Complementary, not a replacement: the two can run together. Config:opts.features.watcher_quarantine—enabled(default false),duration_ms(500),silent(true),patch_neotree_watch(true — wraps neo-tree'sfs_watchcallbacks to swallow EPERM) Usercmds::Filetree watcher enter [ms],:Filetree watcher exit(shared dispatcher withfile_watcher) Module: lua/filetree/features/infra/watcher_quarantine/
LSP DIAGNOSTICS
Decorates tree nodes with LSP diagnostic severity (error/warn/…) rolled up
from the files under them, so a directory with a broken file inside it is
visible without expanding into it first.
Backend support. Drawn as extmarks on the node's own line, which needs the
adapter to say which node a given line holds (get_node_at_line). The
neo-tree and nvim-tree adapters implement it; netrw, oil and mini.files do
not, so this renders nothing there rather than misplacing anything.
Module: lua/filetree/features/lsp/lsp_diagnostics/
AUTO RESIZE
Responsive tree sidebar width driven byVimResized: breakpoints map editor column count to a tree width, defaulting to<100 cols → 25,<140 → 30,≥140 → 35(the largest breakpoint ≤ current columns wins). Off by default because it fights the manually-drivenwindow_size_cycler(on by default) — enabling both means every manual resize gets silently reverted on the nextVimResized. Config:opts.features.auto_resize.enabled(default false),breakpoints,min_width(20),max_width(60) Usercmds::Filetree resize [width]Module: lua/filetree/features/nav/auto_resize/
BUFFER CYCLE
<C-n>/<C-p>cycle the buffer shown in the adjacent editor window (like:bnext/:bprevious) while focus stays in the tree — verified against a live neo-tree buffer to not collide with neo-tree's own defaultwindow.mappings(unlike<C-f>/<C-b>, which neo-tree claims natively forscroll_preview). Keymaps: <C-n> <C-p> Module: lua/filetree/features/nav/buffer_cycle/
REVEAL ALT
Bresolves the alternate buffer (#) and callsadapter.open_reveal()on it, adjusting the tree root if the file lives outside the current one — the tree-buffer analogue of:e #. Keymaps: B Module: lua/filetree/features/nav/reveal_alt/
SIDEBAR GUARD
Keeps the tree in its sidebar. A buffer that lands in the tree window — a stray:buffer N, a mouse click on a tabline buffer (NvChad's tabufline, bufferline, …) while the cursor is inside the tree, or another plugin's:edit— is moved to a real editor window, and the tree is put back where it was. Without that, the swap displaces the tree, and neo-tree's own recovery (buffer_enter_event) reopens the sidebar through a bare:vsplit: with the defaultsplitright = falsethe new file window lands to the left of the tree and shoves the sidebar to the right. The repro is exactly "tree open on the left, click a tabline buffer, tree jumps to the right". The redirect serves both cases at once, because they are the same thing at the API level: the only difference is what the caller meant, and nobody ever means "put this file in the sidebar".winfixbuf = true(off by default) swaps the redirect for the older strategy: pin the window so the switch is *refused*. Callers that check the flag (NvChad'sgoto_buf, neo-tree's ownopen_file) then route around it by themselves, with no window shuffling at all. Callers that do not get
E1513: Cannot switch buffer.'winfixbuf'is enabled < — and the file does not open. A plugin opening a README from its own picker has no way to know a tree is focused, so that refusal broke reposcope, lazygit and anything else driving:editfrom a callback. It is kept as an option for anyone who prefers a refusal to a redirect; the two do not combine, since a refused switch never reaches the redirect. Both strategies stand down for the one legitimate in-window buffer swap — switching source via thesource_selectorwinbar — which neo-tree brackets with itsNEO_TREE_WINDOW_BEFORE_OPEN/_AFTER_OPENevents. neo-tree only. The redirect works on any Neovim;winfixbuf = trueadditionally needs 0.10+ (&winfixbuf) and is a no-op below it. Config:opts.features.sidebar_guard.enabled(defaulttrue),opts.features.sidebar_guard.winfixbuf(defaultfalse— the redirect; settrueto pin withwinfixbufand refuse the switch instead) Module: lua/filetree/features/nav/sidebar_guard/
SOURCE SWITCHER
neo-tree renders its filesystem, buffer list, git status, symbol outline, diagnostics and (with neo-tree-tests-source) tests through one window. Its own</>re-open the tree at the configured position — not where it is when you press the key in a float. This feature keeps the position:"/!cycle to the next/previous source in place,:Filetree source(or a global key) opens a floating list with each source's icon and name, the current one marked and a[!]on one that cannot load right now (document_symbolswithout an LSP client, an uninstalled optional source). The pure half needs nosetup():display_name(source, opts)returns the ` <icon> <Name>string neo-tree'ssource_selector` wants, in three icon families (nerd,codicons,commonfor a font without glyphs) and two name lengths — so a host builds neo-tree's own opts from it. Leavingfamilyunset resolves to"nerd"only whenvim.g.have_nerd_font = trueis declared,"common"otherwise; passingfamily = "nerd"explicitly (as below) always renders Nerd Font glyphs, tofu boxes included if the terminal font doesn't have them — the same conventionlib.nvim.ui.nerd_fontuses.
local sw = require("filetree.features.nav.source_switcher")
source_selector = {
winbar = true,
sources = sw.display_names({ "filesystem", "buffers", "git_status" }, { family = "nerd" }),
}
<
A silent no-op on every other adapter: one tree, nothing to switch.
Keymaps: " !
Config: opts.features.source_switcher.sources (override the list), `.icons =
{ family, variant, length }`
Usercmds: :Filetree source [name|pick|next|prev|debug]
Module: lua/filetree/features/nav/source_switcher/
TREE TOGGLE
Four global keys that open (or close) the tree at a chosen position and reveal the current file on the way in, re-rooting to the cwd when the file lies outside the tree:<M-l>left,<M-r>right,<M-f>float,<M-c>in the current window. The:Neotree toggle position=… reveal reveal_force_cwdmost configs write four times, throughadapter.toggle_at()instead — so it works on every backend that can place its tree, and refuses with a reason on one that cannot. On neo-tree the adapter also heals one race: toggling again before the previous toggle's debounced scan has settled makesnvim_buf_set_namecollide (E95) and leaves a blank, unfocusable tree window that re-errors on every redraw. The adapter closes any never-rendered neo-tree window and retries once, which is what pressing the key again used to do by hand. Off by default: four global Alt keys are a claim on the keyboard the user makes, not the plugin. Keymaps: <M-c> <M-f> <M-l> <M-r> Config:opts.features.tree_toggle.enabled(default false),reveal(true),reveal_force_cwd(true) Usercmds::Filetree toggle [left|right|float|current]Module: lua/filetree/features/nav/tree_toggle/
TREE TRAVERSE
-navigates up to the parent directory,+sets the directory under the cursor as the new tree root — viaadapter.set_root()where supported, falling back to:cd+adapter.open_cwd()otherwise. A manual re-root here is reported tocwd_mode.notify_manual_root()when that feature is active, so alock/project/tree_leadspolicy moves its pin instead of fighting the user's own+/-press. Keymaps: - + Usercmds::Filetree traverse up,:Filetree traverse downModule: lua/filetree/features/nav/tree_traverse/
MARKS
Toggle a mark on the node under the cursor (m), batch mark/unmark (]m/[m), clear every mark (<leader>mc) and list them (<leader>ms) — the selection mechanism several fileops features (trash, copy/move, copy_file_list) build on for "act on more than one node at once". Navigating and selecting marks (2026-08-24)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Marks were set one line at a time and, once set, had no way back to them. Two additions close the flag/option audit's entries: Jumping.Ngmgoes to the Nth marked node in render order,]M/[Mcycle to the next/previous one, wrapping. Navigation follows the tree *as rendered*, notget_marked()'s alphabetical order — that is the right answer for "what is marked" and the wrong one for moving around, and a marked node inside a collapsed directory has no line to jump to at all. An out-of-range count clamps to the last mark rather than erroring, the wayGtreats one. Visual-mode marking.mover a selection marks every node in it,[munmarks. These are the only Visual-mode keymaps filetree binds, and the audit's entry about there being none was really about this: a line range over a rendered tree is exactly a set of nodes, which is the one thing a tree buffer's Visual mode is good for. Marking a run of files no longer means pressingmonce per line. Diffing two marked files against each other was listed as missing, butdiff_marked()has always done exactly that (it requires exactly two marks and diffs them against one another, not against the current buffer) — nothing to add. Auto-clear on idle (2026-09-25)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Marks used to live forever until an explicit<leader>mc, or until a batch op that consumed them cleared them itself (trash/move/copy_move/diff do this; the read-only consumers — PDF-from-marks, markdown_links, path_copy, copy_file_list — deliberately don't, so the same marks can feed several of those in a row). That is a footgun on its own: mark a batch, run one of the read-only actions, get pulled away, come back much later and run an unrelated single-node action expecting it to act on just the node under the cursor — trash/move/copy_move all prefer the marked set over the cursor node whenever anything is marked, so it silently acts on the long-stale marks instead, with no visible reason why.marks.auto_clear_ms(default60000) clears every mark after that many ms without genuine mark activity — toggling, mark/unmark all, a Visual-mode mark, jumping between marks, or opening the list — with a notify so the "why did that just act on everything" moment has an obvious cause. Closing and reopening the tree does not count as activity in either direction: it neither resets the idle clock nor clears marks by itself, only elapsed idle time does. Set to0to disable the timeout and go back to the old "forever until cleared" behavior.goto_adjacent_mark,mark_visual) plusm/[min Visual mode (]M),marks.keymap_prev([M),marks.auto_clear_ms(default60000,0disables) Keymaps: m ]m [m <leader>mc <leader>ms gm ]M [M Config:marks.keymap_goto(defaultgm),marks.keymap_nextModule: lua/filetree/features/org/marks/
SESSION
Persists and restores tree state (root, expanded nodes, marks) across Neovim sessions, so reopening a project brings the tree back roughly where you left it rather than starting fresh every time. Module: lua/filetree/features/org/session/
COPY FILE LIST
Copies a recursive listing of files and/or directories under the node —[f/]ffor files,[F/]Ffor directories — useful for pasting a directory's contents into an issue, a prompt, or a script. Mark-aware: with nodes marked (see Marks), the listing is collected from every marked node (deduplicated) instead of just the one under the cursor — mark three directories, then]fcopies the combined relative file listing across all three. Keymaps: [f ]f [F ]F Module: lua/filetree/features/paths/copy_file_list/
LUA REQUIRE COPY
rqcopies the node as arequire("…")string, resolved the same way FILEOPS.md'screate_from_templateresolves its${module}template variable — for a Lua file under a reallua/directory, the canonical dotted module path. Keymaps: rq Module: lua/filetree/features/paths/lua_require_copy/
MARKDOWN LINKS
Copies the current node, a recursive listing, or every marked node as Markdown links (ML/MR/MM) — for dropping references into a README or a design doc directly from the tree, no manual path-to-link formatting.MLis mark-aware: with nodes marked it links all of them (one per line), same asMM;MMstays as an explicit marks-only keymap.MIinserts instead of copying: marked nodes if any, else the current node, go as[name](path)into the window you came from (one link inline, several on lines of their own below the cursor line). The cursor lands in the first link — in its empty title, or in the path when the title is filled — in insert mode (cursor = { enable, startinsert, path_cursor }, seelib.nvim.markdown.link_cursor). The path is spelled byinsert_path:"buffer"(default; relative to the target buffer,./x/../x),"cwd","absolute", or"env"($REPOS_DIR/…,$NVIM_CONFIG_DIR/…, else"buffer";env_rootsadds variables). Keymaps: ML MR MM MI Module: lua/filetree/features/paths/markdown_links/
PATH COPY
Copies the node's absolute path or its parent directory's path ([a/]a), or the path relative to the project root ([R/]R) — keys covering the "I need this path somewhere else" cases without a prompt.absolute,dirnameandproject_rootwrite an absolute path, so they go through the top-levelenv_rootsoption: a path under$REPOS_DIR,$NVIM_CONFIG_DIRor a root of your own is copied as$NAME/rest(default on).:Filetree copy absolute_rawis the plain absolute path, `env_roots = { enable = false }turns the folding off for good, anduri` always keeps the real path. Every format copies with forward slashes, on every OS —sub/b.lua, neversub\b.lua. That is the same separator the tree, its prompts and its notifications use, and the one this feature's own examples have always shown;fnamemodifywas quietly handing back native ones on Windows forabsolute,relative,dirname,lineandproject_root. Forward slashes work in Neovim, in libuv and in every shell the clipboard is likely to land in — only a literalexplorer.exe/cmd /cinvocation needs native ones, and those are converted where they are invoked, not here. On a symlink node, every format reads the link's own path — never resolved to whatever it points at. Consistent with the rest of the plugin's symlink handling (Copy/Move's paste creates a new symlink rather than a dereferenced copy; Node Info shows the link's own path with the target on a separate `Link to:line, never substituted in).:Filetree symlink checkor Node Info'sI` is the way to see what a symlink resolves to. Mark-aware: with nodes marked (see Marks), everypath_copykeymap — includingpick— copies one line per marked node instead of just the node under the cursor. Same rule ascopy_move/trash: marks win when any are set, otherwise it's the cursor's node. Two more answer the cases where neither the cwd nor the project root is the right frame of reference:]bcopies the path relative to the buffer open in the editor, in the./x/../xform a Markdown link target needs. This is the one that makes a pasted link actually resolve: with the cwd at the repo root,docs/ROADMAP/ROADMAP.mdis correct in the root README and wrong indocs/ROADMAP/Notes.md, where the same file is./ROADMAP.md.]breads the open buffer's directory instead of the cwd, so it is right in both. The base is the editor window's file, then the alternate file (#), then the cwd — so it still answers something when the tree is the only window.[ecopies the absolute path, but folds a configured environment variable back into its root:$REPOS_DIR/filetree.nvim/lua/x.luarather thanE:/repos/filetree.nvim/lua/x.lua. Written into a note, that path still means the same file on a machine where the checkout lives on another drive. The variables to try areenv_roots(default{ "REPOS_DIR" }, written without the$); the longest match wins, so a$REPOS_DIRinside$HOMEbeats$HOME.nvim_config_root(defaulttrue) additionally tries$NVIM_CONFIG_DIR, backed byvim.fn.stdpath("config")— no environment variable of that name has to actually be set. Without it, a node inside your own Neovim config falls all the way throughenv_roots(it typically lives nowhere near$REPOS_DIR) to the plain absolute path; with it,[eon~/.config/nvim/lua/plugins/foo.luacopies$NVIM_CONFIG_DIR/lua/plugins/foo.lua. Set itfalseto turn this off. With no variable or$NVIM_CONFIG_DIRmatching, the plain absolute path comes back.
path_copy = {
enabled = true,
keymap_buffer_rel = "]b",
keymap_env_root = "[e",
-- Longest match wins; each name is written without the $.
env_roots = { "REPOS_DIR", "XDG_CONFIG_HOME", "HOME" },
nvim_config_root = true, -- also try $NVIM_CONFIG_DIR (stdpath("config"))
}
<
Keymaps: [a ]a ]b [e
Module: lua/filetree/features/paths/path_copy/
FILTER
Live-filters the tree listing as you type (/) — narrows what's shown without leaving the tree, unlike find/grep below which open a separate picker. Backend support. neo-tree and nvim-tree narrow the listing for real, each through its own filter: neo-tree'sstate.search_patternplus a refresh, nvim-tree'sExplorer.live_filter. netrw, oil and mini.files have no filter of their own, and the intended fallback — dimming the non-matching lines instead of hiding them — needs the adapter to resolve a line to a node (get_node_at_line, see Backends), which none of the three implements. So/still does nothing there; implementingget_node_at_linefor them is what would fix it. Both native branches were broken until 2026-09-17, and broken in a way worth remembering: each wrapped its call in a barepcalland reported success regardless. neo-tree'smanager.filter_allhad been removed upstream, and nvim-tree'sapi.tree.search_nodetakes no argument at all (it opens its ownSearch:prompt and reveals a single file). So/raised, the error was swallowed, the branch claimed to have handled it, and the dim fallback was never reached —/silently did nothing on the two backends that had a filter.try_native_filternow reports failure honestly, so a native path that breaks again degrades to dimming instead of to nothing. Keymaps: / <C-c> Module: lua/filetree/features/search/filter/
FIND FILES
ffinds files via whichever picker is available — pickers.nvim first, then telescope, fzf-lua, mini.pick, or a built-in fallback — auto-detected.tfforces pickers.nvim specifically. A telescope-only key is available throughkeymap_telescope(off by default). The pickers.nvim path is opt-out on both sides:integrations.pickers = falsehere,filetree = { enabled = false }in pickers.nvim; see pickers.nvim integration. Withreveal_on_open(default on) the picked file is revealed in the tree, also when the pick went through pickers.nvim (needs a pickers.nvim that ships theon_selecthook; an older one just opens the file). Keymaps: f tf Module: lua/filetree/features/search/find_files/
GREP IN DIR
grgreps inside the node's directory using the same auto-detected picker asfind_files;tgforces pickers.nvim specifically. Keymaps: gr tg Module: lua/filetree/features/search/grep_in_dir/
LIVE SEARCH
Incremental search inside the tree (gs) — jumps between matches without filtering the listing down, complementingfilterrather than duplicating it. Keymaps: gs Module: lua/filetree/features/search/live_search/
FILE CLIPBOARD
gyputs the files themselves on the operating system's clipboard, so Ctrl+V pastes them into a chat window, a mail, an upload dialog or a file manager — without opening the OS file manager first. Mark three screenshots withm, pressgy, switch to the chat, Ctrl+V: three images. It copies what Ctrl+C does in Explorer or Finder: a *file list*. That is neither the text of the paths ([a,[f— see SEARCH_AND_PATHS.md) nor filetree's own copy/cut staging (c/x/p, which onlypinside the tree can paste). Targets: every marked node when any is marked, else the node under the cursor. Directories go as directories; entries that no longer exist are skipped and reported. The marks are left alone, so the same selection can feedpath_copyormarkdown_linksnext. Platform · Tool · Status Windows ·powershell.exe(started by absolute path under%SystemRoot%) →Set-Clipboard -LiteralPath· tested, incl. umlauts,&,', `[ ],$` macOS ·osascript(set the clipboard to {POSIX file …}) · implemented, not yet run on a Mac Linux ·wl-copy(Wayland) orxclip(X11), MIMEtext/uri-list· implemented, not yet run on Linux; GNOME Files may want its ownx-special/gnome-copied-filesformat WSL · — · refused with a message No path is ever spliced into a script or a shell string; the tools receive them as argv, stdin or an environment variable. A tool that does not finish within 15 seconds is stopped and reported, and a failure shows the tool's first error line (the full text goes to the debug log,debug = true). The Windows round trip can be re-run withFILETREE_TEST_REAL_CLIPBOARD=1(seeTESTS/file_clipboard.lua; it overwrites the clipboard). Keymaps: gy Config:opts.features.file_clipboard.keymap,.preview_limit(names listed in the notification, default 5) Commands::Filetree clipfilesModule: lua/filetree/features/system/file_clipboard/
OPEN WITH
Opens the node with a configured external application (<leader>sm) — for file types you want handled by a specific program rather than Neovim's ownpreview/openpath. Keymaps: <leader>sm Config: external-app mapping — see configuration.md Module: lua/filetree/features/system/open_with/
PDF CREATE
The write direction of the same bridge:gPturns the image, markdown, text, HTML or office file(s) under the cursor into PDF(s). It always confirms first — unlike PDF open, which only reads, this writes new files to disk. Targets are gathered in the same order as Trash and Copy / Move: marked nodes if any are marked, where a marked directory expands to its own creatable direct children; otherwise the node under the cursor — a file means that one file, a directory means every direct child file pdfport can convert. Non-recursive on purpose: "all files in the folder" is this folder's own files, not a project-wide sweep. One PDF is produced per input file, next to its source (`on_conflict = "suffix"` by default) — a multi-file selection does not merge into one document. Files pdfport has no producer for are skipped and reported in the summary rather than treated as an error. Off by default, and pdfport.nvim is a soft dependency: without it the keymap warns and does nothing, rather than filetree requiring pdfport just to load.on_conflict(default"suffix"),confirm(defaulttrue) Keymaps: gP Config:features.pdf_create.enabled(defaultfalse), Module: lua/filetree/features/system/pdf_create/
PDF OPEN
When pdfport.nvim is installed,preview(UI.md) or opened directly can route through pdfport's own backend fallback chain (render into a buffer) instead of always shelling out to the system PDF reader. Soft dependency — without pdfport.nvim installed,pdf_open's default keymap (go) opens directly indefault_mode(default "buffer"), no prompt. Setdefault_mode = "picker"(or bindkeymap_picker) instead to get pdfport's own "open PDF as…" chooser — every backend/mode pdfport knows about, plus "system application", which is always offered even without pdfport.nvim installed (falls back to the system reader directly, no empty prompt). wired intopreview— UI.md); see pdfport.nvim for the render side Keymaps: go Module: lua/filetree/features/system/pdf_open/
SHELL RUN
iprompts for a shell command and runs it in the node's directory — for one-off commands (npm install,go build) scoped to wherever the cursor happens to be in the tree, no manualcdfirst. Keymaps: i Module: lua/filetree/features/system/shell_run/
BREADCRUMBS
Shows the path from the tree root down to the current node, so a deeply nested file's location is legible without scrolling up through every parent directory. Sharing the winbar. In"winbar"mode (the default) the trail is written to every non-tree, non-floating window — andvim.wo.winbaris a surface with no notion of an owner.my.nvim's breadcrumbs put a symbol trail in the same place, and ui.nvim'sui.winbar.set()exists to arbitrate exactly that. This feature always goes through it, so the two no longer overwrite each other. Usemode = "float"ormode = "statusline"to stay off the surface entirely. Module: lua/filetree/features/ui/breadcrumbs/
BROKEN LINK NOTIFY
Link Marker's⇢!sign only exists in the tree — once a dangling symlink's target is actually open in an editor window, that context is gone. Opening one reads exactly like opening any other nonexistent path to Neovim: a silent, empty[New]buffer, no error, no hint why it's empty. Warns instead, once, the moment such a buffer is created:BufNewFilefires exactly when Neovim could not read the path it was asked to open — precisely the dangling-symlink case, since Neovim never resolves through the link itself, so the buffer name IS the symlink's own path. Backend-agnostic by construction. A single global autocmd, not tied to the tree's own<CR>or any particular adapter — it catches neo-tree's native open,open_variants' split/vsplit/tabnew,open_replace's edit/swap,gf, a plain:edit, all the same way, without any of them needing to know this feature exists. Config:opts.features.broken_link_notify—enabled(default true) Module: lua/filetree/features/ui/broken_link_notify/
CHEATSHEET
A paged float built from what is actually bound on the tree buffer, not from a table of defaults — so a key filetree rebinds (Dfor diff, say) is listed as what it does now. It works on every adapter, neo-tree included (it replaces neo-tree's native?, whose list was built from a hand-kept table that lagged the features).<Tab>/<S-Tab>(or1..4) turn the pages: Page · Content 1filetree· filetree.nvim's own keymaps, grouped by category, plus the global ones 2other keys· every other buffer-local key: the adapter's native ones and those other plugins attach (pickers.nvim entry actions, pdfport, ...) 3commands· the:Filetreesub-commands 4conflicts· only when two actions claim one key: which one is live, free alternatives;<CR>moves one (:Filetree keys) Design presets (style). The float's border and highlighting come fromui.nvim's ownui.kit.themepreset system (the same one behindsessions.nvim's andcasedesk.nvim's chips) — setfeatures.cheatsheet.styleto one of this ecosystem's three shared names:style· Look"classic"· borderless, plain text"chip"· flat, square-cornered border"rounded_chip"(or leavestyleunset) · rounded border — today's default look Any otherui.kit.themepreset name also works as-is — the built-ins are"solid","double","ascii","hacker","menu"(seeui.nvim's own docs), or one you registered yourself via `ui.kit.theme.setup({presets = {...}})`. The active page's tab in the tab strip is always highlighted with the resolved theme's "selection" colour, regardless ofstyle.
require("filetree").setup({
features = { cheatsheet = { style = "chip" } },
})
<
Keymaps: ?
Module: lua/filetree/features/ui/cheatsheet/
CONTEXT MENU
Right-click (<RightMouse>) opens a context menu throughui.contextmenu, which draws with nvzone/menu if it is installed, or its own themedui.kit.menu(no third-party plugin needed) otherwise — either way, right-click works out of the box. With the kit renderer, the clicked node's line is highlighted for as long as the menu stays open, and with the tree docked left/right the menu opens beside it rather than on top of it. Only a click on a node opens it: a right-click on the empty area below the last node, on the tree's statusline or separator, or in another window does nothing — no menu, and the cursor stays where it was. See docs/menu.md for the entries offered and the full detail on both. Keymaps: <RightMouse> Module: lua/filetree/features/ui/context_menu/
CURSOR HIDE
Hides the block cursor inside the tree window, resolved adapter-agnostic via each adapter's ownfiletypeslist rather than a single hardcoded filetype check. Hiding the real cursor only makes sense as long as something else marks the current line — normally'cursorline'.force_cursorline(defaulttrue) force-enables it on the tree window for exactly as long as the cursor stays hidden, and restores whatever it was on leave, so the tree can never end up with no visible position indicator at all (a plugin's own cursorline-management autocmd racing this one, a colorscheme reset, …) — that combination looks exactly like a lost cursor: movement, opening nodes and closing the window all keep working, there is just nothing on screen marking where you are. Setforce_cursorline = falseto go back to leaving'cursorline'alone. Config:enabled(defaulttrue),force_cursorline(defaulttrue) Module: lua/filetree/features/ui/cursor_hide/
LINK MARKER
Marks a symlinked node in the tree listing so it reads differently from an ordinary file/directory at a glance: a small⇢sign right before the node's own name (⇢!for a symlink whose target could not be resolved), optionally followed by its target at the end of the line when `show_target = true`. The sign's position is found fresh per render, as the byte offset of the node's own name on the line — past whatever indent, tree-guide characters and icon the backend drew, not a fixed offset — so it lines up correctly regardless of nesting depth or indent width, and never splices into the middle of a guide line. On by default, unlike most decorators here — it costs nothing extra per render: it readsis_link/link_to/link_brokenstraight off data the neo-tree and nvim-tree adapters already hold (neo-tree's own scan already callsuv.fs_readlink()for every link it finds), not a filesystemstatper node. Hard links are not decorated here. Telling a file with more than one name apart from an ordinary one needs an actualstat, and every one of its names is an equal hard link — there is no single dirent to flag as the hard link. See Node Info'sIwindow for that instead, on demand rather than on every rendered line. Backend support. Same line-resolved-decoration contract assize_infobelow: neo-tree and nvim-tree draw it, netrw/oil.nvim/mini.files don't. Whether a dangling symlink gets its ownbrokensign also depends on the backend: neo-tree already knows (it tried to resolve the link during its own scan); nvim-tree does not expose that, so there a symlink always gets the plain sign, working or not. Config:opts.features.link_marker—show_target(default false),target_hl("Comment"),signs.symlink(`{text="⇢", hl="Special"}),signs.broken({text="⇢!", hl="DiagnosticError"}`) Module: lua/filetree/features/ui/link_marker/
NODE INFO
A float (I) reporting path, type, size, permission mode, and mtime for the node under the cursor. For a file: line count. For a directory: recursive item count plus aggregate size — computed on demand, not kept live, so it reflects the tree at the moment you press the key. A symlink's Type line says so (file (symlink)) and gets its ownLink to:line naming the target — flagged(broken — target missing)when the target does not resolve, rather than the whole window falling back to "No stat info" the way it used to (a plainstatalone sees nothing at all through a dangling link). A file with more than one hard-linked name gets a `(hardlink, N names)` note on its Type line instead: every one of its names is an equal hard link, so this reads as "shares its data with N-1 other name(s)", not "this one IS the hard link" — there is no single dirent to single out that way. Keymaps: I Module: lua/filetree/features/ui/node_info/
OPENED SYNC
Re-renders the tree whenever a buffer opens or closes, so the tree plugin's own "this file is open" highlight stays in sync with reality instead of only updating on the tree's own redraw triggers. Module: lua/filetree/features/ui/opened_sync/
PREVIEW
Toggles a live preview of the node under the cursor in the adjacent editor window, or a floating window — updates as the cursor moves, no separate open action needed.<Tab>/<CR>dispatch images/PDFs to their own viewer instead of rendering raw bytes as text;<PageUp>/<PageDown>page a long preview without leaving the tree. (scroll) — see BINDINGS/KEYMAPS.md Keymaps: <Tab> <CR> <C-b> <C-f> <PageUp> <PageDown> Module: lua/filetree/features/ui/preview/
SIZE INFO
Shows file/directory sizes inline in the tree listing. Off by default, opt-in — purely cosmetic, anddir_asyncrunsdu/Get-ChildItemper directory node once it renders, which is not something to spring on someone who just wanted a tree. Backend support. Drawn as extmarks on the node's own line, which needs the adapter to say which node a given line holds (get_node_at_line). The neo-tree and nvim-tree adapters implement it; netrw, oil and mini.files do not, so this renders nothing there rather than misplacing anything. Config:opts.features.size_info—enabled(default false, opt-in),show_files(true),show_dirs(true),dir_async(true — asyncdu -sb/Get-ChildItemfor directories;falseskips directory sizes entirely rather than blocking),hl_group("Comment") Module: lua/filetree/features/ui/size_info/
TREE RESET
<Esc> in one keystroke clears the active preview, any live filter, and an
in-progress live search — the "get back to a plain tree" key.
Keymaps: <Esc>
Module: lua/filetree/features/ui/tree_reset/
WINDOW SIZE CYCLER
Cycles the tree window's width through a configured set of presets (w) — a fixed, deliberate step instead of manual<C-w>resizing. Keymaps: w Module: lua/filetree/features/ui/window_size_cycler/
WINDOW STYLE
A blank statusline for the tree window (adapter-agnostic, on by default) plus optional isolated tree highlight groups (opt-in), so the tree reads as a distinct UI region rather than another ordinary buffer. configuration.md Config: isolated highlights are opt-in — see Module: lua/filetree/features/ui/window_style/
6. ADAPTERS
"neotree" neo-tree.nvim (requires: folke/neo-tree.nvim) "nvimtree" nvim-tree.lua (requires: nvim-tree/nvim-tree.lua) "auto" Try neotree first, then nvimtree.
7. PUBLIC API
require("filetree").setup(config)*filetree.setup()* Initialize the plugin. Must be called once at startup.require("filetree").adapter()→ FiletreeAdapter? *filetree.adapter()* Return the active adapter, or nil.require("filetree").config()→ FiletreeConfig *filetree.config()* Return the current configuration.require("filetree").feature(name)→ table? *filetree.feature()* Return a loaded feature module by name for direct access.require("filetree").register_adapter(a)*filetree.register_adapter()* Register a custom FiletreeAdapter before setup().require("filetree").is_initialized()→ boolean *filetree.is_initialized()* True when setup() completed without errors.
8. HEALTH CHECK
Run :checkhealth filetree to verify: • Neovim version • Configuration validity • Adapter availability • Feature status • Optional dependencies • The pickers.nvim integration (which of its three conditions holds) • Keys claimed by more than one action
9. CUSTOM ADAPTERS
Implement the FiletreeAdapter interface and register before setup():
local my_adapter = {
name = "my_tree",
is_available = function() return true end,
is_open = function() return false, nil end,
get_winid = function() return nil end,
-- … all other methods …
}
require("filetree").register_adapter(my_adapter)
require("filetree").setup({ adapter = "my_tree" })
See lua/filetree/@types/adapter.lua for the full interface definition.