NORMAL ~/wkd/p/filetree/help :set skin=modern utf-8

filetree.txt

Adapter-agnostic filetree features for Neovim — filetree.nvim

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 *filetree-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-intro*

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
write enabled = true to get a feature — you only write enabled = false to
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 *filetree-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 *filetree-quickstart*

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 *filetree-config*

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 set enabled = false to turn a default-on feature off,
or enabled = true to turn on one of the opt-in few (|filetree-default-disabled|).

Representative options with their defaults (the enabled line 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 *filetree-default-disabled*

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 handles or |filetree-health|.
  size_info              Purely cosmetic — an eol extmark next to every node
                         — and dir_async = true runs du/Get-ChildItem
                         per 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-menu*

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 *filetree-pickers-integration*

find_files (f, tf) and grep_in_dir (gr, tg) hand the directory of the
node under the cursor to pickers.nvim when it is installed: your engine, your
find flags, your entry actions. The picked file is revealed in the tree
(find_files.reveal_on_open) through pickers.nvim's on_select hook; 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 ship pickers.integrations.filetree),
integrations.pickers not false here, filetree.enabled not false there. If
any fails, f / gr fall back silently to telescope / fzf-lua / mini.pick / the
built-in backend; tf / tg say "pickers.nvim not available", since they asked
for it. |filetree-health| reports which condition holds.

4.4 KEYS CLAIMED TWICE *filetree-key-conflicts*

filetree's own features never share a default key (gp locks the cwd, go opens
a PDF, <C-c> clears a filter, X clears 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; the setup() 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 *filetree-features*


5.1 PICKER *filetree-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 *filetree-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 *filetree-cwd-sync*

On BufEnter / WinEnter, when the current file is not under Neovim's cwd:
silently chdir to its project root AND root the tree there, then reveal the
file. Never prompts.

When reveal = 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. With
reveal = 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 its chdir.

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 own follow_root option. 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 cached find_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. Set
     root_markers = false to 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

Whether reveal should 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. Set reveal = false there 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 — leave
reveal = 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's update_focused_file.update_root.enable is NOT a
drop-in equivalent of neo-tree's bind_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 resolved root_markers/project root) is respected.
nvim-tree's update_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. With update_root.enable = true, nvim-tree overwrites cwd_sync's
git-root-anchored cwd with the file's own directory on every switch,
independent of reveal. If you want cwd_sync's root_markers anchoring to
win, leave update_root at its default false (only update_focused_file
follows/expands within whatever root is already set — this combines cleanly
with reveal = 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 with update_focused_file.enable = true:
set features = { cwd_sync = { enabled = true, reveal = false } } there too.
For netrw/oil/mini_files, just enable cwd_sync with its reveal default
(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 *filetree-cwd-mode*

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/:lcd that 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

With persist = true the mode, the scope and a lock's pinned directory are
remembered per project (lib.nvim.store.project, under stdpath("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. project mode 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 configured mode: 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: L cycles modes, gp locks 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 when laststatus = 3 removes per-window
statuslines (indicator.mode = "auto"; force one with "statusline"/"float").

LABEL STYLE

indicator.style picks 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)

numeric is the only style that shows anything for follow ("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
  },
icons holds 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: switching
style only 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's text/hl pair 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 for mode() 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.

A User FiletreeCwdModeChanged autocmd fires — with a scheduled
redrawstatus — whenever the text or highlight component()/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 lock from 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.markers is VCS-only on purpose: project mode answers "which
repository am I in". Adding package.json/Cargo.toml turns it into
nearest-package (monorepo) behaviour — a deliberate choice, not the default.
skip_dirs is the counterpart: a file under node_modules/pkg/ resolves to
the project ABOVE the vendor directory, never to the vendored package.

reveal_outside decides 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's root_markers
and the project_root feature are the fallback for a setup where cwd_mode is
disabled — configure cwd_mode.project.markers instead.

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 nearest package.json under it, with nothing to say which was
right. One walk now answers for both. Asking for the package rather than the
repository is what nearest mode 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 through filetree.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 *filetree-current-hl*

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 *filetree-safety*

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 *filetree-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_reveal is 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),
     unless follow_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's bind_to_cwd + follow_current_file).

follow_root defaults 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:
  enabled        boolean
  debounce_ms    integer   Delay after BufEnter (default 150ms).
  ignore_ft      string[]  Filetypes that never trigger reveal.
  only_if_open   boolean   Only reveal when tree window is visible
                             (default true).
  sync_on_enter  boolean   Move the tree cursor onto the current file's
                             node when the tree window is entered
                             (default true).
  follow_root    boolean   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 *filetree-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

prefer picks 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 plain
                  vim.ui.select when 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's
pick_item() has no concept of custom in-picker keymaps, so picking
"auto"/"telescope"/"fzf"/"snacks" loses <M-j>/<M-k> — set prefer = "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.json sidecar
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 *filetree-open-in-fm*

Shows the node under the cursor in the platform's file manager (default keymap
"<leader>fm"): Windows -> Explorer, macOS -> Finder (via open), Linux -> the
first manager found on PATH. A file node is selected inside its parent
directory unless reveal = false; a directory node is navigated into.

The platform dispatch itself lives in lib.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's reveal_in_fm/README.md.

A launch is otherwise fire-and-forget: nothing waits on the file manager. If
one silently opens nothing, turn on debug to 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 a Shell.Application COM
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 to lib.nvim.cross.reveal_in_fm as reuse, 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 the Directory shell-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 *filetree-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/:bwipeout with 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:
  - A BufWinEnter handler tied to the window actually being focused —
    fires deterministically the moment a stray buffer becomes the focused one.
  - A BufAdd/BufDelete/BufWipeout sweep 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:
  enabled   boolean   (default true)

5.11 REFERENCES *filetree-refs*

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, ./x keeps 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 undo reverts 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 reads lua/proj/a.lua on
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 tsconfig paths aliases            (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.md from 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 *filetree-move*

M moves 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, so M doubles 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:
  enabled     boolean  (default true)
  keymap      string   (default "M")
  use_safety  boolean  Backup before moving (default true)
  dry_run     boolean  Log the plan without executing (default false)

Commands:
  :Filetree move [destination]

5.13 FEATURE REFERENCE *filetree-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 *filetree-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 *filetree-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 *filetree-copy-move*

Stage one or more nodes with c (copy) or x (cut), then p to 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 when use_safety is 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) via lib.nvim.progress — see Progress indicators's
progress_style option (top-level require("filetree").setup({...}) config,
not per-feature).

A cut+paste is a move, so it runs the reference engine too — the scan starts
when you press x, 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 a  C/ X overlay. 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-link-create*

:Filetree symlink creates 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 paste are 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 by
features.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 absolute E:/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 — see repair below 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
set features.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's I
window for the Link 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 checkall runs 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, repair tries the cheapest candidate first: the same path
under this machine's roots. A link recorded on another machine as
E:/repos/casedesk.nvim/x.md is re-anchored by its root's folder name —
everything after a repos segment is looked up under this machine's
$REPOS_DIR (likewise nvim for $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 repairall
on the marked links and pick the candidate. When it finds one the disk search
is skipped. (Switched off with the rest of env_roots by enable = 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 (default true, also searches
stdpath("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 (default 2000) notifies with a one-time hint on
how to narrow or disable it, and repair_search_progress (default "auto")
shows a progress indicator for it — "statusline" feeds ui.nvim's
statusline segment, false shows nothing. Neither pass blocks the tree or any
other window.

:Filetree symlink delete removes 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 (via d,
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 (default
        true), .repair_search_progress (default "auto"),
        .repair_search_slow_hint_ms (default 2000)

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 *filetree-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.

O replaces: :edit over 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 be O). Write it first, or use O.

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 keep vim.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 *filetree-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
via adapter.collapse_node(), since the adapter's own <CR> only ever
expands (there's no toggle to undo it — see the note below). gb stays
file-only; only <S-CR> picks up the directory case.

Collapsing a drilled-into directory (neo-tree)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

neo-tree's filesystem.group_empty_dirs merges a chain of directories that
hold nothing but another single directory into one display line — personal
containing only All containing only Finish renders as
personal/All/Finish. Each <CR> lazily loads one more level and rebuilds
that merged node from scratch, which can leave it reporting `is_expanded() ==
false right 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 to personal.

<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 own
close_node command — bound to C by 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() in
lua/filetree/adapter/neotree.lua

Keymaps: sg sv st gb <S-CR>

Module: lua/filetree/features/fileops/open_variants/

RENAME BATCH *filetree-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 *filetree-smart-create*

a creates 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 *filetree-smart-rename*

r renames 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, plus
require()/import statements when no server handled the rename (for Lua
that is always, since lua_ls never implements workspace/willRenameFiles) —
is picked up by the reference engine.

Keymaps: r

Module: lua/filetree/features/fileops/smart_rename/

TRASH *filetree-trash*

Cross-platform trash with undo — d to trash, U to undo, <leader>th for
trash history. How far back that reaches is features.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.mode is "trash" by default — Windows Recycle Bin (via
Microsoft.VisualBasic.FileIO.FileSystem, not the Explorer delete verb, which
triggers a confirmation dialog no headless PowerShell script can answer),
macOS Finder/Trash, or gio trash/trash-put on 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
no U/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 —
spawning powershell.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, one trash/AppleScript/gio trash/trash-put/mv call 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 to REF!),
Inspect first (pick which ones), Delete, keep refs, Cancel. Undoing such a
delete with U restores the REF! markers along with the file. See
References. For a large batch, the reference rewrite can still be in flight
when U fires — undo does not wait for it. U still restores the file
immediately; if the rewrite finishes after that, a warning names how many
references it touched and points at :Filetree refs undo to revert those
separately.

:Filetree trash dry-run covers 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 gates d reads the dirent itself
(filetree.util.conflict.exists, fs_lstat-aware), not just
filereadable/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 *filetree-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's User GitsuiteBranchSwitched/GitsuiteConflictsResolved
events, 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 *filetree-file-watcher*

Watches the tree root directory for filesystem changes via vim.uv.fs_event
(libuv) and auto-refreshes the tree — ReadDirectoryChangesW on Windows,
inotify/kqueue on POSIX. Debounces bursts of events before calling
adapter.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 exit

Module: lua/filetree/features/infra/file_watcher/

HANDLE GUARD *filetree-handle-guard*

Fixes the same Windows/WSL file-lock at its source instead of hiding it:
neo-tree's own directory watchers (with use_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 with
EPERM/ERROR_SHARING_VIOLATION because filetree's own watcher is still
holding it. Once enabled it wires automatically into the fileops that move/
rename/trash a watched path (via lib.nvim.cross.fs.mutate's on_retry hook
calling M.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 *filetree-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 *filetree-ignore-list*

Hides common filesystem clutter from the tree by default — .git,
.github, node_modules, .venv, __pycache__, build/dist/target
directories, .DS_Store, editor dirs, and similar. Toggle at runtime with the
adapter's own native "show hidden" key (H on neo-tree/nvim-tree).

Config: top-level opts.ignore_list — true (default, built-in list, also
        reads lib.nvim config if present), false (show everything), or a
        string[] that fully replaces the built-in list

Module: lua/filetree/features/infra/ignore_list/

PROJECT ROOT *filetree-project-root*

Shared, cached project-root detection used by cwd_sync and other features
that need "which project is this file in" without a policy attached. Walks up
from a file/directory for any of markers (.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, with
fallback = "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 when
cwd_mode is enabled; project_root (and cwd_sync.root_markers) is the
fallback path for a cwd_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
nearest package.json.

Config: opts.features.project_root — enabled (default true),
        markers, fallback ("parent"|"cwd"), cache (default true)

Module: lua/filetree/features/infra/project_root/

TREE INTEGRITY *filetree-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 its by_id index and then
re-initializes whatever it was handed, so handing it live nodes re-registers
those nodes but not their children — by_id loses them while _child_ids
still lists them. The next set_nodes() over that subtree indexes a nil node
and throws, and because it throws before _child_ids is reset, the
inconsistency is permanent.

neo-tree reaches that call from one place: the group_empty_dirs branch for a
lazily loaded single sub-folder (ui/renderer.lua, with the default
scan_mode = "shallow"), which re-exports a whole level with
state.tree:get_nodes(parentId) and passes those live nodes straight back.
Expanding a directory next to a one-child chain is enough.

This feature wraps NuiTree.set_nodes with 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 from by_id,
so an *already*-corrupted tree repairs itself on the next render rather than
throwing. Fresh nodes (the normal create_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 *filetree-watcher-quarantine*

On Windows, libuv file watchers can emit spurious EPERM errors 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 (see handle_guard below
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's fs_watch callbacks to swallow EPERM)

Usercmds: :Filetree watcher enter [ms], :Filetree watcher exit (shared
          dispatcher with file_watcher)

Module: lua/filetree/features/infra/watcher_quarantine/

LSP DIAGNOSTICS *filetree-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 *filetree-auto-resize*

Responsive tree sidebar width driven by VimResized: 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-driven window_size_cycler (on by
default) — enabling both means every manual resize gets silently reverted on
the next VimResized.

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 *filetree-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 default
window.mappings (unlike <C-f>/<C-b>, which neo-tree claims natively for
scroll_preview).

Keymaps: <C-n> <C-p>

Module: lua/filetree/features/nav/buffer_cycle/

REVEAL ALT *filetree-reveal-alt*

B resolves the alternate buffer (#) and calls adapter.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 *filetree-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
default splitright = false the 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's goto_buf, neo-tree's own open_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 :edit from 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 the source_selector winbar — which neo-tree brackets
with its NEO_TREE_WINDOW_BEFORE_OPEN / _AFTER_OPEN events.

neo-tree only. The redirect works on any Neovim; winfixbuf = true
additionally needs 0.10+ (&winfixbuf) and is a no-op below it.

Config: opts.features.sidebar_guard.enabled (default true),
        opts.features.sidebar_guard.winfixbuf (default false — the
        redirect; set true to pin with winfixbuf and refuse the switch
        instead)

Module: lua/filetree/features/nav/sidebar_guard/

SOURCE SWITCHER *filetree-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_symbols without an
LSP client, an uninstalled optional source).

The pure half needs no setup(): display_name(source, opts) returns the `
<icon> <Name> string neo-tree's source_selector` wants, in three icon
families (nerd, codicons, common for a font without glyphs) and two name
lengths — so a host builds neo-tree's own opts from it. Leaving family
unset resolves to "nerd" only when vim.g.have_nerd_font = true is
declared, "common" otherwise; passing family = "nerd" explicitly (as
below) always renders Nerd Font glyphs, tofu boxes included if the terminal
font doesn't have them — the same convention lib.nvim.ui.nerd_font uses.
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 *filetree-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_cwd
most configs write four times, through adapter.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 makes nvim_buf_set_name collide
(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 *filetree-tree-traverse*

- navigates up to the parent directory, + sets the directory under the
cursor as the new tree root — via adapter.set_root() where supported,
falling back to :cd + adapter.open_cwd() otherwise. A manual re-root here
is reported to cwd_mode.notify_manual_root() when that feature is active, so
a lock/project/tree_leads policy moves its pin instead of fighting the
user's own +/- press.

Keymaps: - +

Usercmds: :Filetree traverse up, :Filetree traverse down

Module: lua/filetree/features/nav/tree_traverse/

MARKS *filetree-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. Ngm goes to the Nth marked node in render order, ]M/[M cycle to
the next/previous one, wrapping. Navigation follows the tree *as rendered*,
not get_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 way G treats one.

Visual-mode marking. m over a selection marks every node in it, [m
unmarks. 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
pressing m once per line.

Diffing two marked files against each other was listed as missing, but
diff_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 (default 60000) 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 to 0 to disable the timeout and go back to the old "forever until
cleared" behavior.

goto_adjacent_mark, mark_visual) plus m/[m in Visual mode (]M),
marks.keymap_prev ([M), marks.auto_clear_ms (default 60000, 0
disables)

Keymaps: m ]m [m <leader>mc <leader>ms gm ]M [M

Config: marks.keymap_goto (default gm), marks.keymap_next

Module: lua/filetree/features/org/marks/

SESSION *filetree-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 *filetree-copy-file-list*

Copies a recursive listing of files and/or directories under the node —
[f/]f for files, [F/]F for 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 ]f copies the combined relative file listing across
all three.

Keymaps: [f ]f [F ]F

Module: lua/filetree/features/paths/copy_file_list/

LUA REQUIRE COPY *filetree-lua-require-copy*

rq copies the node as a require("…") string, resolved the same way
FILEOPS.md's create_from_template resolves its ${module} template variable
— for a Lua file under a real lua/ directory, the canonical dotted module
path.

Keymaps: rq

Module: lua/filetree/features/paths/lua_require_copy/

MARKDOWN LINKS *filetree-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. ML is
mark-aware: with nodes marked it links all of them (one per line), same as
MM; MM stays as an explicit marks-only keymap.

MI inserts 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 }, see
lib.nvim.markdown.link_cursor). The path is spelled by insert_path:
"buffer" (default; relative to the target buffer, ./x / ../x), "cwd",
"absolute", or "env" ($REPOS_DIR/…, $NVIM_CONFIG_DIR/…, else
"buffer"; env_roots adds variables).

Keymaps: ML MR MM MI

Module: lua/filetree/features/paths/markdown_links/

PATH COPY *filetree-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, dirname and project_root write an absolute path, so they go
through the top-level env_roots option: a path under $REPOS_DIR,
$NVIM_CONFIG_DIR or a root of your own is copied as $NAME/rest (default
on). :Filetree copy absolute_raw is the plain absolute path, `env_roots = {
enable = false } turns the folding off for good, and uri` always keeps the
real path.

Every format copies with forward slashes, on every OS — sub/b.lua, never
sub\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;
fnamemodify was quietly handing back native ones on Windows for absolute,
relative, dirname, line and project_root. Forward slashes work in
Neovim, in libuv and in every shell the clipboard is likely to land in —
only a literal explorer.exe/cmd /c invocation 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 check or Node Info's I`
is the way to see what a symlink resolves to.

Mark-aware: with nodes marked (see Marks), every path_copy keymap —
including pick — copies one line per marked node instead of just the node
under the cursor. Same rule as copy_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:

]b copies the path relative to the buffer open in the editor, in the ./x /
../x form 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.md is correct in the root README and wrong in
docs/ROADMAP/Notes.md, where the same file is ./ROADMAP.md. ]b reads 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.

[e copies the absolute path, but folds a configured environment variable
back into its root: $REPOS_DIR/filetree.nvim/lua/x.lua rather than
E:/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 are env_roots (default { "REPOS_DIR" }, written without
the $); the longest match wins, so a $REPOS_DIR inside $HOME beats
$HOME.

nvim_config_root (default true) additionally tries $NVIM_CONFIG_DIR,
backed by vim.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 through env_roots (it typically lives nowhere near $REPOS_DIR)
to the plain absolute path; with it, [e on
~/.config/nvim/lua/plugins/foo.lua copies
$NVIM_CONFIG_DIR/lua/plugins/foo.lua. Set it false to turn this off. With
no variable or $NVIM_CONFIG_DIR matching, 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 *filetree-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's state.search_pattern plus a refresh,
nvim-tree's Explorer.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; implementing get_node_at_line for 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 bare pcall and reported success
regardless. neo-tree's manager.filter_all had been removed upstream, and
nvim-tree's api.tree.search_node takes no argument at all (it opens its own
Search: 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_filter now 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 *filetree-find-files*

f finds files via whichever picker is available — pickers.nvim first, then
telescope, fzf-lua, mini.pick, or a built-in fallback — auto-detected. tf
forces pickers.nvim specifically. A telescope-only key is available through
keymap_telescope (off by default). The pickers.nvim path is opt-out on both
sides: integrations.pickers = false here, filetree = { enabled = false }
in pickers.nvim; see pickers.nvim integration.

With reveal_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
the on_select hook; an older one just opens the file).

Keymaps: f tf

Module: lua/filetree/features/search/find_files/

GREP IN DIR *filetree-grep-in-dir*

gr greps inside the node's directory using the same auto-detected picker as
find_files; tg forces pickers.nvim specifically.

Keymaps: gr tg

Module: lua/filetree/features/search/grep_in_dir/

LIVE SEARCH *filetree-live-search*

Incremental search inside the tree (gs) — jumps between matches without
filtering the listing down, complementing filter rather than duplicating it.

Keymaps: gs

Module: lua/filetree/features/search/live_search/

FILE CLIPBOARD *filetree-file-clipboard*

gy puts 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 with m,
press gy, 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 only p inside 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 feed path_copy
or markdown_links next.

  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) or xclip (X11), MIME text/uri-list ·
     implemented, not yet run on Linux; GNOME Files may want its own
     x-special/gnome-copied-files format
  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 with FILETREE_TEST_REAL_CLIPBOARD=1 (see
TESTS/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 clipfiles

Module: lua/filetree/features/system/file_clipboard/

OPEN WITH *filetree-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 own
preview/open path.

Keymaps: <leader>sm

Config: external-app mapping — see configuration.md

Module: lua/filetree/features/system/open_with/

PDF CREATE *filetree-pdf-create*

The write direction of the same bridge: gP turns 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 (default true)

Keymaps: gP

Config: features.pdf_create.enabled (default false),

Module: lua/filetree/features/system/pdf_create/

PDF OPEN *filetree-pdf-open*

When pdfport.nvim is installed, .pdf nodes dispatched through 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 nodes just
open with the system reader, no prompt.

pdf_open's default keymap (go) opens directly in default_mode (default
"buffer"), no prompt. Set default_mode = "picker" (or bind keymap_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 into preview — UI.md); see pdfport.nvim for the render side

Keymaps: go

Module: lua/filetree/features/system/pdf_open/

SHELL RUN *filetree-shell-run*

i prompts 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 manual cd first.

Keymaps: i

Module: lua/filetree/features/system/shell_run/

BREADCRUMBS *filetree-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 — and vim.wo.winbar is a surface with
no notion of an owner. my.nvim's breadcrumbs put a symbol trail in the same
place, and ui.nvim's ui.winbar.set() exists to arbitrate exactly that. This
feature always goes through it, so the two no longer overwrite each other. Use
mode = "float" or mode = "statusline" to stay off the surface entirely.

Module: lua/filetree/features/ui/breadcrumbs/

BROKEN LINK NOTIFY *filetree-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: BufNewFile fires
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 *filetree-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 (D for 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> (or 1..4) turn the pages:

  Page · Content
  1 filetree · filetree.nvim's own keymaps, grouped by category, plus the
     global ones
  2 other keys · every other buffer-local key: the adapter's native ones
     and those other plugins attach (pickers.nvim entry actions, pdfport, ...)
  3 commands · the :Filetree sub-commands
  4 conflicts · 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 from
ui.nvim's own ui.kit.theme preset system (the same one behind
sessions.nvim's and casedesk.nvim's chips) — set
features.cheatsheet.style to one of this ecosystem's three shared names:

  style · Look
  "classic" · borderless, plain text
  "chip" · flat, square-cornered border
  "rounded_chip" (or leave style unset) · rounded border — today's
     default look

Any other ui.kit.theme preset name also works as-is — the built-ins are
"solid", "double", "ascii", "hacker", "menu" (see ui.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 of style.
require("filetree").setup({
  features = { cheatsheet = { style = "chip" } },
})
<

Keymaps: ?

Module: lua/filetree/features/ui/cheatsheet/

CONTEXT MENU *filetree-context-menu*

Right-click (<RightMouse>) opens a context menu through ui.contextmenu,
which draws with nvzone/menu if it is installed, or its own themed
ui.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 *filetree-cursor-hide*

Hides the block cursor inside the tree window, resolved adapter-agnostic via
each adapter's own filetypes list 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 (default true)
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. Set force_cursorline = false to go back to leaving
'cursorline' alone.

Config: enabled (default true), force_cursorline (default true)

Module: lua/filetree/features/ui/cursor_hide/

LINK MARKER *filetree-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 reads is_link/link_to/link_broken straight off data the
neo-tree and nvim-tree adapters already hold (neo-tree's own scan already
calls uv.fs_readlink() for every link it finds), not a filesystem stat per
node.

Hard links are not decorated here. Telling a file with more than one name
apart from an ordinary one needs an actual stat, 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's I window for that instead, on demand rather than on every
rendered line.

Backend support. Same line-resolved-decoration contract as size_info below:
neo-tree and nvim-tree draw it, netrw/oil.nvim/mini.files don't. Whether a
dangling symlink gets its own broken sign 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 *filetree-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 own Link 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 plain stat alone 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 *filetree-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 *filetree-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 *filetree-size-info*

Shows file/directory sizes inline in the tree listing.

Off by default, opt-in — purely cosmetic, and dir_async runs
du/Get-ChildItem per 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 — async
        du -sb/Get-ChildItem for directories; false skips directory
        sizes entirely rather than blocking), hl_group ("Comment")

Module: lua/filetree/features/ui/size_info/

TREE RESET *filetree-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 *filetree-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 *filetree-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 *filetree-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 *filetree-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 *filetree-health*

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 *filetree-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.

keys

j / k
next / previous line
gg / G
first / last line
⏎
open the line under the cursor
/
search the plugins
:
command line — Tab completes
:help x
vimdoc of a plugin (:e x = plugin page)
:ls · :log · :stack
plugin list · activity stream · dependency graph
:colo x
colorscheme
:set skin=
modern | tui
?
this help
esc
close