btv.utils
btv.utils.path_style()
btv.utils.path_style(): the path convention the editor’s paths follow — "windows"
or "unix" (the same spellings btv.glob’s style option takes).
Under "windows", / and \ both separate when a path is read, a path can be
rooted at a drive (C:\, C:/) or a UNC share (\\server\share\), and paths the
editor produces use \. Under "unix", / is the only separator and \ is an
ordinary character.
This is the single place the path helpers (btv.utils.path_sep, path_root,
is_absolute, dirname, and btv.fname.modify built on them) decide which
platform’s rules apply. It follows the session’s path syntax, which the server
publishes as btv._path_style ("posix" or "windows") alongside btv._cwd: the
daemon’s in a daemon session — a Windows client editing a unix host still sees unix
paths — and the host’s otherwise. Before the first publish it answers for the host
(btv.utils.is_windows).
Defined in utils.lua.
btv.utils.path_sep()
btv.utils.path_sep(): the separator the editor writes paths with — "\" under the
"windows" path style, "/" otherwise. Use it to build a path that will be shown
or compared against one the editor reports; when reading a path, accept either
separator on Windows (btv.utils.path_root, dirname and basename already do).
Defined in utils.lua.
btv.utils.path_join(...)
btv.utils.path_join(...): join path segments into ONE filesystem path spelled with
the session’s separator (btv.utils.path_sep), so a path built from a directory the
editor reported (C:\Users\me\data) never comes out half \, half /. Under the
"windows" style a / inside a segment is respelled \ too, so
path_join(cfg, "lua/plugins.lua") is one native path; under "unix" a \ is an
ordinary character and left alone. Separators already at a seam are not doubled,
and nil / "" segments are skipped.
btv.utils.path_join("/home/me", "plugins", "foo") -- "/home/me/plugins/foo"
btv.utils.path_join([[C:\data]], "lua/x.lua") -- [[C:\data\lua\x.lua]]
It follows the session’s path style — in a daemon session, the daemon’s — so it is
for paths on the session filesystem (btv.fs, buffer names, getcwd()). A path on
THIS machine (btv.fs_local, stdpath, package.path) is joined with
btv.utils.local_path_join. This is for paths handed to the filesystem or shown as
files: btv.utils.joinpath stays neovim’s vim.fs.joinpath (always /), and a glob
keeps its own /.
Defined in utils.lua.
btv.utils.local_path_join(...)
btv.utils.local_path_join(...): btv.utils.path_join, but by THIS machine’s rules
(btv.utils.is_windows) whatever the session’s — the twin for paths on the local
filesystem (btv.fs_local, stdpath, the plugin manager’s directories, package.path),
as btv.fs_local is the twin of btv.fs. Identical to path_join outside a daemon
session.
Defined in utils.lua.
btv.utils.path_root(path)
btv.utils.path_root(path): the root prefix of path, exactly as spelled in it,
or "" for a relative path. path:sub(#root + 1) is then the part below the root.
unix: "/a/b" -> "/"
windows: "C:\a" / "C:/a" -> "C:\" / "C:/"
"\\srv\share\a" -> "\\srv\share\"
"\a" -> "\" (rooted on the current drive)
both: "a/b", "" -> ""
A drive-relative C:a has no root (it is relative to that drive’s own cwd).
Defined in utils.lua.
btv.utils.is_absolute(path)
btv.utils.is_absolute(path): is path rooted — /… on Unix; a drive (C:\…),
UNC share (\\server\share\…) or bare separator (\…) on Windows? A relative path
is the one a cwd has to be prepended to (see btv.fname.modify(path, ":p")).
Defined in utils.lua.
btv.utils.dirname(path)
btv.utils.dirname(path): the directory part of path — everything before the
last separator ("/a/b/c.txt" → "/a/b"). "" for an entry directly under the
root ("/a" → "", and on Windows "C:\a" → ""); a path with no separator comes
back unchanged (there is nothing to strip), which callers walking upward detect as
“no parent”. On Windows both / and \ separate.
Defined in utils.lua.
btv.utils.basename(path)
btv.utils.basename(path): the last /- or \-separated component of path,
ignoring trailing separators ("/a/b/" → "b"). nil when nothing remains
(the root "/", an empty string).
Defined in utils.lua.
btv.utils.expanduser(path)
btv.utils.expanduser(path): expand a leading ~ / ~/ to $HOME, so a config
value can point at a home-relative path ("~/work/foo"). Only the leading tilde
is touched — a mid-path ~ is a literal path component, and the ~user form is
not resolved (returned unchanged, like vim with an unknown user). With no $HOME
in the environment the path is returned unchanged rather than mangled.
Defined in utils.lua.
btv.utils.ancestors(path)
btv.utils.ancestors(path): iterate the ancestor directories of path, nearest
first — dirname(path), then its parent, and so on. Stops before the empty
string, so the filesystem root itself is never produced. A relative path is
first resolved against the editor’s working directory (btv.fname.modify(path, ":p"),
which also collapses . / ..) — the ancestry of a relative path is only
meaningful against the cwd, and the paths callers walk are buffer names
(btv.buf.name, an autocmd’s ev.file), which are whatever the user typed and so
routinely relative (bemtvi src/main.rs). Without that, the walk would stop at the
first typed component and never reach the project root. The upward-walk loop behind
.editorconfig discovery and LSP root-marker search:
for dir in btv.utils.ancestors("/a/b/c.txt") do
-- "/a/b", then "/a"
end
Defined in utils.lua.
btv.utils.joinpath(...)
btv.utils.joinpath(...): join path components with a single /, collapsing the
separators at each seam so joinpath("/a/", "/b/", "c") is "/a/b/c". An empty
or nil component is skipped rather than producing a doubled slash, so a
conditionally-absent middle segment doesn’t corrupt the result. A leading / on
the FIRST component is preserved (the only place an absolute path is decided);
everything after it is treated as relative, so a later "/b" appends rather than
restarting at the root. Pure string math — nothing is resolved against the
filesystem or the cwd.
btv.utils.joinpath(root, "node_modules/.bin", cmd)
Defined in utils.lua.
btv.utils.normalize(path)
btv.utils.normalize(path): canonicalize a path as pure string math — expand a
leading ~, fold \ to /, collapse repeated separators, drop . components,
and resolve .. against the component before it. Trailing separators are stripped
(except on the root itself). Nothing touches the filesystem, so this does NOT
resolve symlinks — a .. after a symlinked directory resolves lexically, which is
what a config comparing two configured paths wants (for the real thing, btv.fs.realpath
is the async op).
A .. that would escape a relative path is KEPT ("a/../../b" → "../b"), since
there is no cwd here to resolve it against; on an absolute path it is dropped at
the root ("/../a" → "/a"), matching every OS.
Defined in utils.lua.
btv.utils.relpath(base, target)
btv.utils.relpath(base, target): target expressed relative to base, or nil
when target is not inside base. Both are normalized first, and the comparison
is on whole path COMPONENTS — so relpath("/a/b", "/a/bc") is nil, not "c",
which a plain prefix-match would get wrong. target == base yields ".".
The “is this file under that directory?” test, which is how a config decides a buffer belongs to a dependency tree (a cargo registry checkout, a Go module cache) rather than the project:
if btv.utils.relpath(cargo_registry, file) then --[[ it's a library file ]] end
Defined in utils.lua.
btv.utils.is_windows()
btv.utils.is_windows(): is this a Windows host? Answered from package.config’s
directory separator, which Lua fills in at build time — so it is correct in every
build (native, daemon-side, wasm) without a host round-trip or an env-var guess.
The check behind “which name does this program have here?” — a tool installed as
foo on Unix is foo.exe or foo.bat on Windows, and a plugin resolving an
executable has to know which to look for. Prefer btv.fs.which, which searches for
you; reach for this when the name itself differs.
Defined in utils.lua.
btv.utils.uri_from_path(path)
btv.utils.uri_from_path(path): the file:// URI naming path.
Percent-encodes everything outside the URI unreserved set, / excepted — a path
holding a space, a # or a non-ASCII character is otherwise a malformed URI the
server silently misreads (it truncates at the #, or fails to match the document
it already has open under the encoded spelling). The path is normalized first, so
the same file always produces the same URI and a server’s document map hits, and a
RELATIVE path (a buffer name as typed) is resolved against the editor’s cwd, since a
file:// URI can only name an absolute path.
A Windows path takes the shape every LSP server (and the url crate’s
Url::from_file_path) expects: C:\Users\x\a.py → file:///C:/Users/x/a.py, the
drive’s : left unescaped; a UNC path names its server as the URI’s host
(\\srv\share\a → file://srv/share/a).
Defined in utils.lua.
btv.utils.uri_from_buf(bufnr)
btv.utils.uri_from_buf(bufnr): buffer bufnr’s file:// URI (0/nil = the
current buffer), or "" for a buffer with no file — which is what a server should
be told, rather than a file:// naming nothing.
Defined in utils.lua.
btv.utils.uri_to_path(uri)
btv.utils.uri_to_path(uri): the filesystem path a file:// URI names, with its
percent-escapes decoded, or nil for any other scheme.
nil rather than the raw string: a deno: / jdt: / untitled: URI names a
document that has no path at all, and treating one as a path creates a buffer for a
file that will never exist. Callers branch on the nil.
Under the Windows path style (btv.utils.path_style) the result is a native path:
file:///C:/x/a.py → C:\x\a.py, and a URI with a host is a UNC path
(file://srv/share/a → \\srv\share\a).
Defined in utils.lua.
btv.utils.argv(spec)
btv.utils.argv(spec): build a flat argv list from a run-family spec — spec.cmd
is the program (a string, or an argv list whose first element is the program),
spec.args an optional list appended after it, so { cmd = "git", args = { "log" } }
and { cmd = { "git", "log" } } produce the same argv. The normalizer behind
btv.run / btv.run_stream / btv.run_local, public so a plugin composing specs
for those APIs (a task runner, a DAP adapter) shares the exact same shape.
Defined in utils.lua.
btv.utils.str_list(spec, what)
btv.utils.str_list(spec, what): normalize a config value that may be one string
or a list of strings — a bare string becomes a one-element list, a list of
strings passes through, nil becomes {} (the caller’s default applies).
Anything else raises, with what naming the option in the message (e.g.
"btv.snippet.setup: jump_next"). The shared normalizer behind key-spec options
(btv.complete.setup / btv.snippet.setup), public for any config surface that
accepts string | string[].
Defined in utils.lua.
btv.utils.caller_source()
btv.utils.caller_source(): the source path of the file whose code called into
the current function — walks up the Lua stack to the nearest real-file frame.
bemtvi sources every config / plugin / required file with an @<path> chunk
name, while the embedded prelude chunks are named bemtvi:prelude/* (no @) and
C frames carry =[C] — so the first @-prefixed source above the caller is the
file whose code is running. nil when nothing on the stack is @-named.
Behind vim’s <sfile> / <script> (btv.expand) and the plugin-persistence
namespace attribution (btv._resolve_namespace).
Defined in utils.lua.
btv.utils.debounce(fn, ms)
btv.utils.debounce(fn, ms): wrap fn into a trailing-edge debounce over
btv.timer — the returned value runs fn once, ms after the LAST call, so a
burst of rapid calls collapses to a single invocation with the most recent
arguments. A timing/control-flow helper (which-key’s show-delay, on-change
handlers, resize / scroll reactions); it runs nothing on the input path.
It is callback-shaped, NOT promise-shaped: debounce coalesces a stream of many
calls, whereas a promise models one eventual value — different jobs. They
compose, though: pass an btv.async function as fn to kick awaitable work after
the quiet period, and reach for btv.promise.delay when you want an await-able
one-shot sleep instead.
The result is callable AND carries:
:cancel() drop a pending invocation (the next call re-arms)
:flush() run a pending invocation now (no-op when idle)
Each call (re)arms the timer; nothing fires until the calls stop for ms.
Defined in utils.lua.
debounced:cancel()
No documentation comment in the prelude.
Defined in utils.lua.
debounced:flush()
No documentation comment in the prelude.
Defined in utils.lua.