btv.picker
btv.picker.disabled(name)
btv.picker.disabled(name) -> whether the picker name (a source name: "files",
"live_grep") is disabled in this editor. An app embedding the editor can take
pickers away; btv.picker.open then raises E5803 for one, and the commands and
pickers lists leave it out.
Defined in picker.lua.
btv.picker.patterns(line)
btv.picker.patterns(line) -> the glob patterns one filter box’s text expands to.
line is the raw comma-separated text (btv.glob.split splits it, so a {a,b}
alternation stays one pattern). A list is accepted too, so a caller passing
exclude = { "target/", "*.lock" } need not join it first. Returns an empty list
for empty text — i.e. “no filter”.
Each entry is expanded so that one typed pattern means the same set to btv.glob
and to ripgrep’s -g, which read a bare name differently:
*.lock -> **/*.lock no `/` ⇒ matches at any depth
target -> **/target, **/target/** a plain name is also a directory
vendor/ -> **/vendor, **/vendor/** a trailing `/` only meant "directory"
*.egg-info/ -> **/*.egg-info, **/*.egg-info/** …glob or not
src/** -> src/** already a glob ⇒ taken as written
btv.picker.patterns("target/, *.lock")
--> { "**/target", "**/target/**", "**/*.lock" }
Defined in picker.lua.
btv.picker.setup(opts)
btv.picker.setup(opts): cross-picker defaults for the include/exclude boxes.
include/exclude— the line every filterable picker opens with, as a comma-separated string or a list of globs. This is the “stop showing metarget/” knob: set it once and everyfiles/live_grepopens narrowed.history— how many past lines each box keeps for<C-Up>/<C-Down>(default 20).0disables the history and its persistence.deprioritize— the paths filterable pickers list after every other row, in the include/exclude glob syntax. It REPLACES the shipped list (btv.picker.defaults.deprioritize);{}turns deprioritizing off. Matching paths are not hidden, only pushed down — seebtv.picker.sourcefor how a source searches them second.
These are the LOW end of the precedence ladder — a line you actually typed
(recalled from history) and an explicit btv.picker.open{ include = … } both win,
so a default never overrides an intent expressed later. Calling it again replaces
only the keys given.
btv.picker.setup({
exclude = { "target/", "*.min.js" },
-- the shipped list, plus test files
deprioritize = btv.list.extend(
btv.tbl.deepcopy(btv.picker.defaults.deprioritize),
{ "*test*" }
),
})
Defined in picker.lua.
btv.picker.history(field)
btv.picker.history(field) -> the recallable lines for "include" / "exclude",
most recent first. The read side of what <C-Up> / <C-Down> cycle; useful for a
config that wants to seed a picker from a recent filter, or just to see what is
stored.
local recent = btv.picker.history("exclude")[1] -- the last exclude line used
Defined in picker.lua.
btv.picker.forget_history()
btv.picker.forget_history(): drop every stored filter line (both boxes). The
escape hatch for a history that has accumulated patterns you don’t want back.
Defined in picker.lua.
btv.picker.source(spec)
btv.picker.source { name, items = function(ctx), dynamic, confirm, preview }:
register a source. items(ctx) streams candidates: it calls ctx.push(item) per
result (an item is a table with a text display field, plus any data confirm or
the preview needs — e.g. path / row / col) and signals completion by
returning — a synchronous source just returns when
its loop ends, an asynchronous one is wrapped in btv.async and returns the
promise (the engine awaits it; btv is promise-only, so there is no done
callback). A streaming source consumes a btv.run_stream with btv.await_each,
and reaps its job on close via ctx.on_cancel. dynamic = true re-runs items
on every prompt edit (live grep — the matcher is bypassed), reading the live
prompt from ctx.query and the working directory from ctx.cwd; the default is a
static source matched locally in Rust as you type. confirm(item) acts on the
chosen item. Optional preview adds a side pane for the highlighted item:
"file" shows the head of item.path, "location" shows item.path positioned
at item.row / item.col (1-based). Omitted ⇒ no preview pane; per-open
overridable via btv.picker.open(name, { preview = … }). Optional width /
height fix the box size — a cell count
(number) or a CSS-style viewport fraction string ("80vw" / "60vh" / "50%");
omitted ⇒ the default (~80vw x 60vh). The picker is never content-sized.
Optional align ("top-left"…"center"…"bottom-right", default centered) +
margin (a gap from the editor edges: a number — the vertical gap, horizontal
sides 2x to look even — or {vertical, horizontal} / {top, right, bottom, left} /
{top=, …}) place the box like a float. Optional
prompt_pos = "top" (default) or "bottom" places the input above or below the
results list.
For a dynamic source (which spawns per query), debounce (ms) sets the
trailing delay before a query edit re-runs the source — so a fast typist spawns
one search per pause, not one per keystroke, and a new keystroke cancels the
in-flight search. It defaults to the global btv.picker.debounce (250), and is
also overridable per open via btv.picker.open(name, { debounce = N }); 0
disables it. While that search runs, the PREVIOUS results stay
on screen (the list never flashes empty); they are swapped out only when the new
search’s first result arrives, or cleared if it matched nothing. The widget
windows its rendering and matches incrementally, so a source can stream 100k+
candidates and stay fast; max_results (default 100000) is only a runaway-source
safety bound.
An item may also be a two-column row — a location column plus content, as
live_grep’s path:line:col: <the matched line> is:
ctx.push({
head = "src/main.rs:12:5: ", -- the location column
text = "let x = compute()", -- the body (the row's label is `head .. text`)
match = { 9, 15 }, -- 1-based INCLUSIVE char range of the hit in `text`
})
The widget then fits the two columns separately: the head keeps at least 40% of the
row (a long line can never squeeze the file name off), and the body is windowed
around match so the hit stays on screen instead of scrolling off the right edge.
match also highlights, which is what a dynamic source wants — it bypasses the
fuzzy matcher, so its own match is the only one to show. Every field is optional:
a plain text-only item is a single-column row that truncates path-tail-first.
A row may also lead with a short tag — a classification the head must never lose — and declare its own color, the highlight group it is painted with:
ctx.push({
tag = "E", -- pinned: the elision happens after it
head = "src/main.rs:12 ", -- the location column
text = "unused variable", -- the body
hl = "DiagnosticError", -- the group the row paints with
})
The tag is prepended to the head ("E src/main.rs:12 ") and pinned: a head too
narrow for its path elides around the tag rather than dropping it, so the letter that
says what the row IS survives at any width. Without it the head elides tail-first,
which is right for a pure location (live_grep keeps file:line) and wrong for a
classified one.
hl is an ordinary highlight-group name, resolved against the live colorscheme each
frame (a group the scheme leaves undefined simply doesn’t paint — never an error, so
a source may name groups a minimal theme lacks). It colors the row’s head column
when the row declares one, and the whole label when it doesn’t: the head is the part
that classifies the row, and leaving the body alone keeps the fuzzy-match highlight
readable over it. The selected row keeps its selection background either way.
A row pushed with low = true is deprioritized: the list shows every normal row
before every low one, each group in its own order (stream order with no query, match
order under one), so a source can keep vendored or generated hits findable without
burying the rest. A btv.picker.scorer reorders rows within each group, never across.
A low row still counts toward max_results.
filter = true gives the picker the include/exclude glob boxes (<C-g>), and is
what every candidate’s path is then tested against — declare it on any source
whose items are paths. A source that shells out to rg should also splice
ctx.rg_globs into its argv so the tool prunes the tree instead of streaming paths
the sink is about to drop. Without it the filter keys are an echoed no-op, since
there would be nothing to filter on.
A filterable source’s paths are also tested against the deprioritize globs
(btv.picker.setup): a match is pushed as a low row. To keep those from filling
max_results before the rest are found, a source can search in passes —
ctx.passes lists them in order, each a { tier, rg_globs } table:
for _, pass in ipairs(ctx.passes) do
ctx.begin_pass(pass) -- the sink now keeps only this pass's tier
run_rg(pass.rg_globs) -- the box globs plus this pass's own
end
The first pass is tier = "normal", with the deprioritize globs negated so rg
skips those trees entirely; the second is tier = "low", limited to them. With an
empty list there is one tier = "all" pass whose rg_globs equal ctx.rg_globs.
A tool that honours ignore files must not be handed rg_globs (or ctx.rg_globs):
rg lets a positive -g override .gitignore, so the low pass’s includes would
bring an ignored node_modules/ back. Such a source uses pass.rg_excludes — the
pass’s negations alone — and lets the sink apply the include box and the tier;
pass.keeps(path) says whether a path belongs to the pass, for a source that lists
candidates first and searches only its own.
During a pass the sink drops a row of the other tier — the glob set, not the tool,
decides a path’s tier — so a source can also push one in-hand list once per pass.
ctx.begin_pass(nil) keeps every row again. A source that ignores ctx.passes
still gets its low rows listed last; it just searches them in whatever order it
finds them.
resumable = false opts the source out of btv.picker.resume() (<leader>fr):
opening it never overwrites the resume slot, so a transient internal picker (the
cmdline file completer) can’t shadow the last user-facing one. Defaults to true.
Defined in picker.lua.
btv.picker.open(name, opts)
btv.picker.open(name[, opts]): open the picker for the registered source name.
Each opts field overrides the matching field on the source (which in turn
overrides the picker default):
width/height— a FIXED box size: a cell count (e.g. 100) or a CSS-style viewport fraction string ("80vw"/"60vh"/"50%"). The picker is never content-sized (a content-hugging box looks ragged).align+margin— placement, like a float (seebtv.picker.source).preview—"file"/"location"/ nil (no pane).prompt_pos—"top"(default) /"bottom".query— initial prompt text: the picker opens already filtered against it (the gen-0 run uses it instead of""), caret at its end. Default"".title— a title centered on the box’s top border (e.g."Find Files"); nil ⇒ no title. The shipped sources set their own ("Find Files"/"Live Grep"/…).multiselect— whether<Tab>marks rows for a batch action (default true);falseis a single-choice picker (no marking).debounce— ms before adynamicsource re-runs on a query edit;0off.include/exclude— pre-fill the glob filter boxes, so a keymap can open a picker already scoped (btv.picker.open("files", { include = "src/**" })). Each takes a comma-separated string or a list of patterns, and is a seed: the boxes stay editable. Only meaningful on a source withfilter = true.filters—"open"reveals the filter rows immediately (what a pre-filtered picker usually wants),"collapsed"(the default) keeps the picker’s ordinary single-line shape with the active patterns shown as a badge.layer— where a confirmed item opens:"main"crosses back to the main editor area first (so a file picked while focused in a dock lands in the editor, not the sidebar),"active"opens in the focused layer. Defaults to"active"; the shippedfiles/live_grepsources set"main",buffersstays"active".
Defined in picker.lua.
btv.picker.resume()
btv.picker.resume(): reopen the most-recently-closed picker (telescope’s resume),
restored to exactly where the user left off — the displayed rows, prompt text,
highlighted row, and multi-select marks. The server replays a frozen snapshot it
captured at close (bounded to a window around the cursor), so a live-grep picker
comes back with its actual previous results, not a fresh (differently-ordered)
search. Re-installs btv._picker so confirm works and a later query edit re-runs
the source. No-op (a gentle notice) before any resumable picker has closed.
Defined in picker.lua.
btv.picker.edit(item, mode, layer)
btv.picker.edit(item): the common confirm action — open item.path, and if the
item carries a 1-based row (and optional 1-based col, as live_grep / LSP
location items do), jump the cursor there.
A located jump (item.row set) goes through the btv._jump_to bridge, NOT
:edit: a jump must navigate, never reload. :edit-ing the location would (a)
error with E37 when the target is the current modified buffer (the LSP hands
back an absolute path, but the open buffer may be relatively named) and (b)
strand a duplicate buffer when that absolute path doesn’t string-match the
relative one. btv._jump_to reuses the open buffer cwd-aware and skips the
modified guard, so selecting a symbol in the file you’re editing just moves the
cursor. A location-less item (the files source) is a plain open instead.
mode is the confirm gesture: "tab"/"split"/"vsplit" (<C-t>/<C-x>/<C-v>)
open in a NEW tab / split regardless of 'switchbuf' (an explicit gesture);
"current" (or nil) honors 'switchbuf'.
layer is the confirm target the picker resolved ("main"/"active"), forwarded to
confirm and on to here. "main" crosses back to the main editor layer before
opening, so a file picked while focused in a dock lands in the editor rather than
the sidebar; "active" (or nil) opens in the focused layer.
Defined in picker.lua.
btv.picker.scorer(src)
btv.picker.scorer(src): install a re-ranker over a picker’s surviving
rows, or clear it with nil.
src is a string of Lua source — an expression, not a function value —
because the re-ranker runs in the bounded compute sandbox: a second, pure VM
with a wall-clock deadline, no editor state and no btv.*. A closure cannot
cross between VMs, so the source crosses instead and is compiled there.
Three names are in scope, and the expression returns a number — the new sort key, higher first:
label the row's text
query the active query
score the native fuzzy score this row already earned
The sandbox is stateless: nothing carries from one call to the next, and
assigning a global raises. That is deliberate — no call shape is a clean
once-per-item traversal (:s re-runs on every keystroke of the live preview,
a foldexpr sees only the rows an edit touched, the picker scorer only the top
survivors, foldtext is memoized), so an accumulator would be quietly wrong.
Because score is handed in, a scorer nudges the native order rather than
reinventing matching:
-- push test files down, keep everything else as the matcher ranked it
btv.picker.scorer([[ score - (label:find("/test") and 50 or 0) ]])
btv.picker.scorer(nil) -- back to pure fuzzy order
It is applied to the filtered rows only, never to every candidate, and to at most the top 1000 of them — a picker streams 100k+ candidates and scoring them all would freeze the editor. Rows past that keep native order. The scorer also runs at most once per repaint, not once per streamed batch.
A scorer that errors, exceeds its deadline, or returns a non-number reports once and is then uninstalled, rather than repeating the error every frame.
Defined in picker.lua.