btv.complete
btv.complete.setup(opts)
btv.complete.setup { sources = { { "buffer", min_chars = 3 } }, auto = true,
keys = { next = "<C-n>", prev = "<C-p>", confirm = { "<C-y>", "<CR>" }, abort = "<C-e>" } }
(confirm accepts the highlighted row; <CR> only accepts once you’ve navigated
to one — an unnavigated popup is noselect, so Enter still inserts a newline.)
Enables the engine and captures its options. sources is a list of { name, opts... }
entries — a built-in (buffer / lsp / snippets) or a registered plugin source.
Listing a plugin source is optional: every source registered with
btv.complete.source{} is active by default, so a plugin adds completions just by
registering (even after this call). List it only to override its options (min_chars
/ priority / debounce) or to name it under exclusive mode. exclusive = true
restricts the active set to only the sources named here (opt out of auto-join).
min_chars gates how many prefix chars a source needs before it contributes, and is
honored per source: { "buffer", min_chars = 3 }, { "btvsnip", min_chars = 2 }
shows snippets from 2 chars while buffer words wait for 3. A top-level min_chars
sets the default for sources that don’t override it; the popup opens at the minimum
across all sources (each then filters by its own). A trigger-char source and a manual
trigger (btv.complete.trigger()) bypass the gate. auto (default true) completes as
you type. With auto = false nothing opens on its own, but a manual trigger still
follows the prefix: the popup it opens narrows as you type and widens as you
backspace, until you accept, abort, or type a prefix nothing matches — so on-demand
completion is a session, not a one-shot snapshot. keys overrides any of
the four control actions. accept (default "replace") decides what the confirm
keys do when the caret is in the middle of a word: "replace" swaps the whole
word, "insert" keeps the suffix past the cursor. btv.complete.accept{ behavior }
overrides it for a specific key (bind one key to each behavior).
confirm (default "selected") decides what a confirm key does when nothing is
selected — the popup is noselect until you navigate. "selected" keeps confirm
inert (a mapped <CR> still inserts a newline until you pick a row); "first" accepts
the top row un-navigated (Enter-to-accept). An explicit selection confirms either way.
Defined in complete.lua.
btv.complete.trigger()
btv.complete.trigger(): manually open (or refresh) the completion popup at the
cursor, ignoring auto / min_chars (an explicit request always offers what’s
there). A no-op outside insert mode or before btv.complete.setup{}.
The popup it opens is a session, not a snapshot: it keeps following the prefix
through the edits that follow — narrowing as you type, widening as you backspace —
even with auto = false, and ends when it closes (accept, abort, <Esc>, or a
prefix nothing matches). The whole session keeps the manual contract: min_chars
stays bypassed, and the top row stays preselected so a confirm key
(<C-y> / <CR>) accepts without a separate navigation step.
Defined in complete.lua.
btv.complete.accept(behavior)
btv.complete.accept(behavior): accept the highlighted completion row under an
explicit accept behavior, ignoring the engine’s configured default. behavior is
"insert" (replace only the typed prefix, keeping any word suffix past the cursor)
or "replace" (swap the whole word the caret sits inside); nil / omitted uses the
configured default. Passed a table, it reads behavior from it (so
btv.complete.accept{ behavior = "replace" } works too). Bind two keys to the two
behaviors for insert-vs-replace on demand, e.g.
btv.keymap.set("i", "<C-y>", function() btv.complete.accept("insert") end)
btv.keymap.set("i", "<C-l>", function() btv.complete.accept("replace") end)
A no-op when the popup is closed or nothing is highlighted (like the confirm key).
Defined in complete.lua.
btv.complete.choice(items, opts)
btv.complete.choice(items, opts) — open a non-grabbing dropdown at the cursor
listing items (strings), the completion-popup widget rather than the grabbing
btv.ui.select: <C-n>/<C-p> move, <C-y>/<CR> pick, and typing / <Esc>
dismiss it (input keeps flowing to the buffer). Accepting a row replaces
opts.range — { start_row, start_col, end_row, end_col }, 0-based byte cols — with
the pick; the row already sitting in that range is preselected. There is no callback:
the splice is a normal buffer edit, so a caller watching the buffer (btv.buf.attach
on_bytes) reacts to it — how a plugin snippet engine drives a ${1|a,b,c|} choice
tabstop and syncs its mirrors. Defaults the range to the empty span at the cursor
(inserting the pick) when opts.range is omitted.
Defined in complete.lua.
btv.complete.source(spec)
btv.complete.source { name, complete = function(ctx)[, debounce] }: register an
async completion source. complete streams candidates for the prefix in
ctx ({ prefix, buf, row, col }): it calls ctx.push(item) per result — a
string (used as both the menu label and the inserted text) or a table
{ text = <label>, insert = <applied on accept>[, kind = <label>] } — where an
optional kind is the short category shown right-aligned on the row ("Function",
"Module", …), like an LSP item’s kind — and signals completion by
returning (an btv.async source returns its promise; a synchronous one just
returns — btv is promise-only, so there is no done callback).
The source runs off the input path (debounced by debounce ms, default
btv.complete.debounce), and its results are generation-gated: a reply for a
prefix the user has typed past is dropped. Register a ctx.on_cancel(fn) reaper
to kill an in-flight job when the next prefix supersedes this one.
Registering activates the source: it joins the live engine immediately (or when
btv.complete.setup{} next runs, if it hasn’t yet) — a plugin adds completions by
calling this, with no need for the user to list it in setup{ sources }. The spec’s
own priority (merge rank, default 0), min_chars (its prefix gate), and debounce
become the source’s defaults; a setup{ sources } entry for the same name overrides
any of them, and enabled = false (here or on that entry) opts it out. A setup{ exclusive = true } engine ignores unlisted sources.
trigger = { chars = { ":" } } (optional) gates the source: the engine wakes it
only when the completion prefix leads with one of those chars (the emoji shape),
folding the char into the prefix so the source matches :smi and accept replaces
from the :. resolve = function(item) (optional) supplies docs lazily: push an
item with no doc, and when the user selects it the engine calls resolve(item),
which returns a PROMISE of the docs — a doc string, or an item whose .doc is
used. Use it when computing docs up front for every candidate is wasteful.
A pushed item may carry on_accept = function(item, ctx) — a per-item callback that
OWNS the edit when that row is accepted, run INSTEAD of core splicing the item’s
insert. ctx is { buf, start_row, start_col, end_row, end_col } — the trigger
word RANGE under the cursor (0-based, byte columns, end-exclusive), ready to hand to
btv.buf.set_text. This is the seam a snippet engine uses: match a trigger, then on
accept btv.buf.set_text(ctx.buf, ctx.start_row, ctx.start_col, ctx.end_row, ctx.end_col, {""}) and btv.snippet.expand(body) (or drive its own tabstop session).
Not a snippet-only hook — additionalTextEdits, post-accept commands, and any
non-literal expander use it too.
Defined in complete.lua.
btv.complete.scorer(src)
btv.complete.scorer(src): install a re-ranker over the completion popup’s
rows, or clear it with nil. The sibling of btv.picker.scorer.
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.
Four names are in scope, and the expression returns a number — the new sort key, higher first:
label the candidate's text
query the word prefix being completed
score the native key this row already earned (fuzzy score + source bias)
kind the row's kind label: "Snippet", an LSP kind name, or "" (a buffer word)
score is the blended key the popup already sorted on — the fuzzy score
plus the source’s own bias (lsp 8 > snippets 5 > buffer 0) — so nudging it
composes with source order instead of fighting it:
-- keep snippets available but below real code completions
btv.complete.scorer([[ score - (kind == "Snippet" and 20 or 0) ]])
-- prefer shorter candidates among equally-good matches
btv.complete.scorer([[ score - #label / 10 ]])
btv.complete.scorer(nil) -- back to the native order
It re-ranks the filtered rows only, at most the top 1000 of them, and at most once per repaint rather than once per streamed batch — an LSP server can answer with thousands of candidates. The caret follows the row it was standing on, so a re-rank never accepts a candidate you did not choose.
The sandbox is stateless: nothing carries from one call to the next, and assigning a global raises. A scorer that errors, exceeds its deadline, or returns a non-number reports once and is then uninstalled, rather than repeating the error on every keystroke.
Defined in complete.lua.