NORMAL ~/wkd/p/lib/help/lib.nvim-fs :set skin=modern utf-8

lib.nvim-fs.txt

Filesystem helpers: paths, scanning, IO, trash — lib.nvim

doc/lib.nvim-fs.txt — rendered from the plugin's own vimdoc

*lib.nvim-fs.txt*        Filesystem helpers: paths, scanning, IO, trash

lib.nvim.fs                                                    *lib.nvim-fs*

A flat directory of small, independent filesystem helpers — path predicates,
normalization, recursive scanning (cached and uncached), JSON/text IO, safe
entry creation, and cross-platform trash/open. There is no fs/init.lua
aggregator: every submodule is required by its own full path, e.g.
require("lib.nvim.fs.is_dir"). Most submodules return a function directly
rather than a table — check each entry below.

CONTENTS *lib.nvim-fs-contents*

  1. Design ......................................... |lib.nvim-fs-design|
  2. Usage .......................................... |lib.nvim-fs-usage|
  3. Predicates ..................................... |lib.nvim-fs-predicates|
       is_dir .................................... |lib.nvim-fs-is_dir|
       is_readable_file ........................ |lib.nvim-fs-is_readable_file|
       is_valid_filename ....................... |lib.nvim-fs-is_valid_filename|
       is_subpath .............................. |lib.nvim-fs-is_subpath|
  4. Path utilities ................................. |lib.nvim-fs-path-utils|
       path ....................................... |lib.nvim-fs-path|
       path_shorten ............................ |lib.nvim-fs-path_shorten|
       relpath .................................... |lib.nvim-fs-relpath|
       normkey .................................... |lib.nvim-fs-normkey|
       project_key .............................. |lib.nvim-fs-project_key|
       globbable .................................. |lib.nvim-fs-globbable|
  5. Root discovery .................................. |lib.nvim-fs-roots|
       find_root ................................ |lib.nvim-fs-find_root|
       find_upward_dir ....................... |lib.nvim-fs-find_upward_dir|
       stdpath_config_root ............. |lib.nvim-fs-stdpath_config_root|
       polymorphic_rootresolver .. |lib.nvim-fs-polymorphic_rootresolver|
  6. Scanning ........................................ |lib.nvim-fs-scanning|
       collect_recursive .................... |lib.nvim-fs-collect_recursive|
       collect_async ............................ |lib.nvim-fs-collect_async|
       scan_cached ............................. |lib.nvim-fs-scan_cached|
       scan_roots ................................ |lib.nvim-fs-scan_roots|
  7. IO ............................................... |lib.nvim-fs-io|
       read ........................................ |lib.nvim-fs-read|
       write.to_file ...................... |lib.nvim-fs-write.to_file|
       write.append ......................... |lib.nvim-fs-write.append|
       write.async ............................ |lib.nvim-fs-write.async|
       write.batch ............................. |lib.nvim-fs-write.batch|
       json ........................................ |lib.nvim-fs-json|
       mkdirp .................................... |lib.nvim-fs-mkdirp|
       create_entry ........................ |lib.nvim-fs-create_entry|
  8. Ignore patterns ................................. |lib.nvim-fs-ignore|
       ignore.list ................................ |lib.nvim-fs-ignore.list|
  9. System ........................................... |lib.nvim-fs-system|
       trash ...................................... |lib.nvim-fs-trash|
  10. Watching ........................................ |lib.nvim-fs-watching|
       watch ...................................... |lib.nvim-fs-watch|
  11. Working directory ............................... |lib.nvim-fs-cwd|
       chdir ....................................... |lib.nvim-fs-chdir|
       dir_guard ................................ |lib.nvim-fs-dir_guard|

1. DESIGN *lib.nvim-fs-design*

* One responsibility per submodule; no shared aggregator table to keep
  requires tree-shake friendly.
* Blocking IO uses binary mode ("rb"/"wb") so content round-trips
  byte-exact across platforms; async IO (write.async, write.batch,
  trash.trash) is libuv-based and safe to call from a fast-event context.
* Errors are returned (ok, err), never thrown — callers decide whether a
  failure is worth surfacing.
* scan_cached (in-memory, session-lifetime) and scan_roots (optional
  on-disk cache) both build on collect_recursive, the one actual directory
  walker in this namespace — nothing else re-implements the walk.

2. USAGE *lib.nvim-fs-usage*

Every submodule is required individually:

    local is_dir = require("lib.nvim.fs.is_dir")
    local collect_recursive = require("lib.nvim.fs.collect_recursive")
    local json = require("lib.nvim.fs.json")
Submodules that return a single function are called directly; submodules
that return a table expose one or more named functions on it (each entry
below states which).

3. PREDICATES *lib.nvim-fs-predicates*


is_dir({p}) *lib.nvim-fs-is_dir*

Function. Returns true iff p stats as a directory; false otherwise
(including on stat failure).

    local is_dir = require("lib.nvim.fs.is_dir")
    if is_dir("/repo/src") then ... end

is_readable_file({filepath}) *lib.nvim-fs-is_readable_file*

Function. true if filepath is a readable file OR a directory; false
otherwise. Despite the name, directories pass too.

    local is_readable_file = require("lib.nvim.fs.is_readable_file")
    if is_readable_file("/repo/README.md") then ... end

is_valid_filename({name}) *lib.nvim-fs-is_valid_filename*

Function. Validates a bare filename (not a full path): rejects characters
illegal on Windows (\ / : * ? " < > |), an embedded NUL, an empty or
whitespace-only string. Returns ok, err.

    local is_valid_filename = require("lib.nvim.fs.is_valid_filename")
    local ok, err = is_valid_filename("new_file.lua")

is_subpath({path}, {base}) *lib.nvim-fs-is_subpath*

Function. true if path == base or path starts with base .. "/",
after vim.fs.normalize-ing both (always forward slashes).

    local is_subpath = require("lib.nvim.fs.is_subpath")
    is_subpath("/a/b/c", "/a/b") -- true
    is_subpath("/a/bc", "/a/b")  -- false

4. PATH UTILITIES *lib.nvim-fs-path-utils*


path *lib.nvim-fs-path*

Table M:

    local path = require("lib.nvim.fs.path")

    local abs = path.from_repo_relative("lua/init.lua")
    -- absolute path: tries <git repo root>/raw, then cwd-relative,
    -- returns the first filereadable candidate (else the last one tried)

    local joined = path.joinpath({ "a", "b", "c.lua" })
    -- vim.fs.joinpath if available, else table.concat with the OS separator

    local ok, err = path.ensure_dir("/tmp/new/deep/file.txt")
    -- creates the PARENT directory of a file path; fast-event safe

path_shorten({path}, {max_len}, {opts}) *lib.nvim-fs-path_shorten*

Function. Shorten a path for display.

    local path_shorten = require("lib.nvim.fs.path_shorten")
    local short = path_shorten("/home/user/very/long/project/file.lua", 30)
    local label = path_shorten("/home/user/project/src/file.lua", nil, { style = "label" })

Options

    style      "fit"|"label"   "fit": preserve root + filename, collapse the
                                middle to fit max_len (default). "label":
                                always render <root>/<ellipsis>/<parent>/<file>,
                                ignoring max_len. Ported from Harpoon's
                                menu-label formatter.
    ellipsis   string          collapsed-segment marker ("…" for fit,
                                "...." for label)

relpath({path}, {base}) *lib.nvim-fs-relpath*

Function. path relative to base (both made absolute, forward-slashed
first). .. segments climb to the nearest common ancestor when path is
not under base; cross-drive Windows paths return path unchanged;
path == base yields ".".

    local relpath = require("lib.nvim.fs.relpath")
    local rel = relpath("/repo/src/foo.lua", "/repo") -- "src/foo.lua"

normkey({p}, {opts}) *lib.nvim-fs-normkey*

Function. Canonical, cross-platform cache/dedup key for a path: expands ~,
optionally resolves symlinks, forces forward slashes, uppercases a Windows
drive letter, collapses duplicate separators (UNC-safe).

    local normkey = require("lib.nvim.fs.normkey")
    local key = normkey("~/project", { realpath = false })

Options

    realpath   boolean   resolve symlinks via uv.fs_realpath (default true)

project_key({path}) *lib.nvim-fs-project_key*

Function. Stable per-project cache key: the Git root of path (default
cwd) if inside a work-tree, else path/cwd itself — run through normkey.
Uses the cached, marker-based find_root rather than shelling out to git
on every call.

    local project_key = require("lib.nvim.fs.project_key")
    local key = project_key()          -- current project
    local key2 = project_key("/repo")  -- explicit path

globbable({root}) *lib.nvim-fs-globbable*

Function. Returns root spelled so vim.fn.glob/globpath reads it as a
path, not a pattern. On Windows, a ~ in a glob pattern is a
home-directory reference — an 8.3 short name like C:/Users/STEFAN~1/...
(what %TEMP%/vim.fn.tempname() expand to for long profile names) makes
glob silently return an empty list. Resolves via uv.fs_realpath (skipped
when there is no ~ to resolve, and when root does not exist). Other
glob metacharacters ([, ], *, ?) in a directory name are not
handled -- not portably escapable.

    local globbable = require("lib.nvim.fs.globbable")
    local safe_root = globbable(vim.fn.tempname())
    vim.fn.glob(safe_root .. "/*")

5. ROOT DISCOVERY *lib.nvim-fs-roots*


find_root({opts}) *lib.nvim-fs-find_root*

Factory. Given a file/directory path, walks upward to the nearest ancestor
holding any of the configured markers. Results are LRU-cached per
directory.

    local find_root = require("lib.nvim.fs.find_root")
    local finder = find_root({ markers = { ".git", "*.rockspec" } })
    local root = finder.find("/repo/src/a.lua")  -- "/repo"
    finder.clear()                                -- drop the cache

Options

    markers        string[]   marker basenames or globs (default {".git"})
    cache          boolean    enable the LRU cache (default true)
    cache_chain    boolean    cache every ancestor visited, not just the
                               queried directory (default false)
    capacity       integer    LRU capacity (default 256, or 512 when
                               cache_chain is true)

find_upward_dir({names}, {from}) *lib.nvim-fs-find_upward_dir*

Function. Walk upward from from, return the nearest ancestor directory
holding one of names (basenames, or */? globs).

    local find_upward_dir = require("lib.nvim.fs.find_upward_dir")
    local root = find_upward_dir({ ".git", "*.rockspec" }, vim.fn.expand("%:p:h"))

stdpath_config_root({dir}) *lib.nvim-fs-stdpath_config_root*

Function. The Neovim config directory if {dir} lies inside it, else nil.
The returned path is always a prefix of {dir}.

Both the normalized stdpath("config") and its uv.fs_realpath form are
tried, so a ~/.config/nvim that is a symlink into a dotfiles repo still
matches a directory taken from a buffer name — which Neovim canonicalizes on
Unix. Never stdpath("config") verbatim, on either branch: a plain match
returns the normalized form, the symlink branch the realpath-canonicalized one
-- on Windows the raw value carries native separators on every call (measured),
and returning it as-is would break the "genuine prefix of {dir}" promise above
for anyone on a plain match, not only the symlinked case. The realpath is
resolved once per stdpath("config") value, not per call -- keyed on that
string, so a symlink re-pointed at a different target while Neovim keeps
running is not seen until restart.

    local stdpath_config_root = require("lib.nvim.fs.stdpath_config_root")
    stdpath_config_root("/home/me/.config/nvim/lua")  -- "/home/me/.config/nvim"
    stdpath_config_root("/home/me/work/proj/src")     -- nil
Used by |lib.nvim-fs-polymorphic_rootresolver| for cfg.include_stdpath_config.

polymorphic_rootresolver({cfg}) *lib.nvim-fs-polymorphic_rootresolver*

Factory. Returns a resolver function for LSP-style root detection, accepting
either a buffer number or a filename.

    local make_resolver = require("lib.nvim.fs.polymorphic_rootresolver")
    local resolve_root = make_resolver({ markers = { ".git", "pyproject.toml" } })

    local root = resolve_root("/home/user/project/src/main.lua")
    local root_buf = resolve_root(0, function(res) print("root:", res) end)

Options

    markers                   string[]   root markers passed to vim.fs.root
                                          (default {".git", ".hg", ".svn"})
    resolve                   fun(dir, cfg): string|nil
                                          replaces the marker search entirely,
                                          for servers whose root is more than
                                          "nearest marker"; pcall'd, and a nil/
                                          error result falls back to dir
                                          itself. Takes precedence over
                                          markers when set.
    include_stdpath_config    boolean    snap the root to stdpath("config")
                                          when it falls under it (default true)

The returned resolver takes (arg, cb?): arg is a buffer number or
filename (empty falls back to cwd); cb, if given, is invoked with the
resolved root in addition to it being returned synchronously.

6. SCANNING *lib.nvim-fs-scanning*


collect_recursive *lib.nvim-fs-collect_recursive*

Table M. Recursive directory walker built on fs_scandir; returns a flat
array of absolute paths. M.collect/M.files/M.dirs.

    local collect_recursive = require("lib.nvim.fs.collect_recursive")
    local all = collect_recursive.collect("/repo", { kind = "files" })
    local files = collect_recursive.files("/repo", {
      ignore = function(p) return p:match("/%.git$") ~= nil end,
    })
    local dirs = collect_recursive.dirs("/repo")

Options

    kind      "all"|"files"|"dirs"                      what to collect
    ignore    fun(abs_path: string, is_dir: boolean): boolean
                                        prune a path (and, for a dir, its
                                        whole subtree) when it returns true

collect_async({root}, {opts}, {on_done})     *lib.nvim-fs-collect_async*

Non-blocking counterpart to collect, same result and same opts. The
synchronous walk above blocks the caller until each fs_scandir/fs_stat
syscall returns — fine for a handful of directories, but stalls Neovim's
main loop for the whole walk on a large tree (a node_modules, a
monorepo). collect_async uses the async form of those same libuv calls,
one directory at a time, driven by a small internal coroutine so the walk
still reads like a plain recursive function — no callback pyramid — while
control genuinely returns to the event loop between calls. files_async/
dirs_async mirror files/dirs.

    local cancel = collect_recursive.collect_async("/repo", { kind = "files" }, function(paths)
      -- called once, vim.schedule-dispatched
    end)
    -- cancel()  -- stop early; on_done will not fire at all
Not a parallel scan and not necessarily a wall-clock speedup — the fix is
for main-loop responsiveness during a large tree, not raw throughput;
siblings are still visited sequentially, deliberately, to keep the
coroutine driver simple. Returns a cancel() function: calling it stops
the walk after its current in-flight libuv call settles, and on_done is
not called at all afterward.

scan_cached({root}, {opts}) *lib.nvim-fs-scan_cached*

Function M.scan. In-memory, TTL-cached wrapper around collect_recursive
for one root — the session-lifetime counterpart to scan_roots.

    local scan_cached = require("lib.nvim.fs.scan_cached")
    local files = scan_cached.scan("/repo/lua", { ttl_seconds = 5 })
    local fresh = scan_cached.scan("/repo/lua", { ttl_seconds = 5, refresh = true })

Options

    kind          "files"|"dirs"|"all"   default "files"
    ignore        function                forwarded to collect_recursive
    ttl_seconds   integer                 cache freshness window (default 5)
    refresh       boolean                 force a rescan, refreshing the cache

Cache key is root .. ":" .. kind; only the walk is cached, not any
downstream per-path work.

M.scan_async(root, opts, on_done) is the non-blocking counterpart: a
cache miss walks via collect_recursive.collect_async instead of
blocking; a cache hit still calls on_done (vim.schedule-dispatched
either way, so both branches behave the same to a caller).

scan_roots({roots}, {opts}) *lib.nvim-fs-scan_roots*

Function M.scan. Scan multiple root directories sequentially, merged into
one flat array, with an optional TTL-based **on-disk** JSON cache.

    local scan_roots = require("lib.nvim.fs.scan_roots")
    local files = scan_roots.scan({ "/repo/src", "/repo/lua" }, {
      ignore_dirs = { "node_modules", ".git" },
      cache_path = vim.fn.stdpath("cache") .. "/my_plugin/scan.json",
      ttl_seconds = 60,
    })

Options

    ignore_dirs    string[]              directory names to skip (default {})
    kind           "files"|"dirs"|"all"  default "files"
    cache_path     string                when set, results persist to this
                                          JSON file
    ttl_seconds    integer               cache lifetime; omit for "never
                                          expires until manually invalidated"

M.scan_async(roots, opts, on_done) is the non-blocking counterpart: same
options and cache semantics (the JSON cache file itself is still read/
written synchronously — one small file, not the part that scales badly),
roots still walked sequentially, one collect_async call chained into the
next.

7. IO *lib.nvim-fs-io*


read({path}) *lib.nvim-fs-read*

Function. Read a whole file into a string (binary mode, byte-exact).
Returns content, err.

    local read = require("lib.nvim.fs.read")
    local content, err = read("/repo/README.md")

write.to_file({path}, {content}) *lib.nvim-fs-write.to_file*

Function. Truncate-write content to path, creating parent directories.
Adds a trailing newline when missing. Returns ok, err.

    local write_to_file = require("lib.nvim.fs.write.to_file")
    local ok, err = write_to_file("/tmp/out.txt", "hello world")

write.append({path}, {content}) *lib.nvim-fs-write.append*

Function. Append content to path (creating parent directories), adding a
trailing newline when missing. Returns ok, err.

    local write_append = require("lib.nvim.fs.write.append")
    local ok, err = write_append("/tmp/log.txt", "new line")

write.async({path}, {content}, {cb}) *lib.nvim-fs-write.async*

Function. Non-blocking counterpart to write.to_file, libuv-based. cb runs
on the main loop via vim.schedule.

    local write_async = require("lib.nvim.fs.write.async")
    write_async("/tmp/out.txt", "hello", function(ok, err)
      if not ok then vim.notify("write failed: " .. tostring(err)) end
    end)

write.batch({entries}, {cb}) *lib.nvim-fs-write.batch*

Function. Write many files asynchronously (built on write.async), one
callback when all have finished. results is index-aligned with
entries.

    local write_batch = require("lib.nvim.fs.write.batch")
    write_batch({
      { path = "/tmp/a.txt", content = "a" },
      { path = "/tmp/b.txt", content = "b" },
    }, function(all_ok, results) end)

json *lib.nvim-fs-json*

Table M. Read/write JSON files; writes are atomic (path .. ".tmp" then
rename).

    local json = require("lib.nvim.fs.json")
    local ok, err = json.write("/tmp/state.json", { count = 1 })
    local tbl, err2 = json.read("/tmp/state.json")

mkdirp({path}) *lib.nvim-fs-mkdirp*

Function. Recursive mkdir -p built purely on libuv — safe to call from a
fast-event context (uv timer/fs_event/subprocess callback), unlike
vim.fn.mkdir. Returns ok, err.

    local mkdirp = require("lib.nvim.fs.mkdirp")
    local ok, err = mkdirp("/tmp/a/b/c")

create_entry({parent_dir}, {name}) *lib.nvim-fs-create_entry*

Function. Create a file or directory relative to parent_dir — the shared
core behind "create file/folder" picker actions. A trailing separator on
name creates a directory (mkdir -p); otherwise an empty file is created.
Returns ok, kind, path_or_err.

    local create_entry = require("lib.nvim.fs.create_entry")
    local ok, kind, path = create_entry("/repo/src", "new_file.lua")
    local ok2, kind2, dir = create_entry("/repo/src", "sub/dir/")

8. IGNORE PATTERNS *lib.nvim-fs-ignore*


ignore.list *lib.nvim-fs-ignore.list*

Table M. Canonical, centralized filesystem ignore definitions shared
across dev tooling (LSP indexing, pickers, trees, search). Heuristic and
conservative — not a .gitignore replacement.

    local ignore = require("lib.nvim.fs.ignore.list")
    require("telescope").setup({
      defaults = { file_ignore_patterns = ignore.as_telescope_patterns() },
    })

Fields / functions

    basenames                string[]   exact ignored basenames (.git, node_modules, ...)
    patterns                 string[]   Lua-pattern ignores (%.pyc, %.log, ...)
    normalize(s)              string    normalized basename for comparison
    as_set()                  table     basenames as a lookup set
    as_luals_patterns()       string[]  Lua.workspace.ignoreDir-compatible globs
    as_telescope_patterns()   string[]  basenames + patterns combined
    as_neotree_names()        string[]  basenames only

9. SYSTEM *lib.nvim-fs-system*


trash({path}, {cb}) *lib.nvim-fs-trash*

Table M. Cross-platform "send to trash" (not a permanent delete):
PowerShell FileSystem API on Windows, Finder via osascript on macOS,
gio trash/trash-put on Linux/WSL, with an XDG-trash-directory
fs_rename fallback when no trash tool is present.

    local trash = require("lib.nvim.fs.trash")

    trash.trash("/tmp/some_file.txt", function(ok, err)
      if not ok then vim.notify(err, vim.log.levels.ERROR) end
    end)

    local ok, err = trash.trash_blocking("/tmp/some_dir")
M.trash is async (result via cb); M.trash_blocking blocks and returns
ok, err directly. The fallback path does not write .trashinfo
metadata, so "restore from trash" UIs may not show the original location.

10. WATCHING *lib.nvim-fs-watching*


watch.start({path}, {on_change}, {opts}) *lib.nvim-fs-watch*

Function. Watch a file or directory for filesystem changes via
uv.new_fs_event(), debounced through lib.nvim.debounce so one change
(a save-via-rename, a build tool touching several files) does not fire
on_change several times in quick succession.

    local watch = require("lib.nvim.fs.watch")
    local handle, err = watch.start("/repo/lua", function(path, filename, events)
      vim.notify(filename .. " changed under " .. path)
    end, { debounce_ms = 200 })

    handle.stop()  -- safe to call more than once

Options

    debounce_ms   integer   debounce window in ms (default 200)

11. WORKING DIRECTORY *lib.nvim-fs-cwd*


chdir({path}, {opts}) *lib.nvim-fs-chdir*

Function. Scope-aware working-directory change: global (:cd), tab-local
(:tcd) or window-local (:lcd) -- explicit where vim.fn.chdir() is
implicit (its scope depends on what the current window happens to have).
path is canonicalized through |lib.nvim-fs-normkey|; a path that is
missing or not a directory is rejected before the command runs. Never
throws.

    local chdir = require("lib.nvim.fs.chdir")
    chdir("~/projects/app")                        -- global
    chdir("/repo", { scope = "tab" })               -- :tcd in the current tab
    chdir("/repo", { scope = "win", win = winid })  -- :lcd in that window
Returns ok, err; the resolved path is not returned -- read it back with
vim.fn.getcwd().

Options

    scope   "global"|"tab"|"win"   default "global"
    win     integer   window to run the change in (scope "win"), default
                       the current window
    tab     integer   tabpage whose current window is borrowed (scope
                       "tab"); ignored when win is set

dir_guard.hold({path}, {opts}) *lib.nvim-fs-dir_guard*

Function. Hold the working directory on path until released, undoing any
foreign change via DirChanged (a picker :lcd-ing into a result, a
session restore, an LSP root-dir hook). Built on |lib.nvim-fs-chdir|; a
hold watches exactly the scope it holds. Releasing does NOT restore the
previous directory -- the guard holds a position, it does not own a
stack.

    local dir_guard = require("lib.nvim.fs.dir_guard")

    local held = dir_guard.hold("/repo")
    vim.cmd.cd("/tmp")                                -- undone: cwd is /repo again
    held.bypass(function() vim.cmd.cd("/tmp") end)    -- allowed, then restored
    held.update("/other")                             -- move the pin
    held.release()
Returns handle, err (handle is nil when the initial change failed).
handle exposes path(), is_held(), bypass(fn), update(new_path),
and release(). opts also takes an on_violation(new_cwd, held) hook
(return false to accept a foreign change and release the guard instead
of undoing it) and on_error(err) for a failed restore, plus every
|lib.nvim-fs-chdir| scope option (scope, win, tab).

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