Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.