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.lsp

btv.lsp.find_root(bufnr, markers, name)

btv.lsp.find_root(bufnr, markers) -> promise of the workspace root: the nearest ancestor directory of the buffer’s file holding one of markers, or nil.

The upward search behind the declarative root_markers config key, public because a config whose root depends on something the declarative form can’t express — “the lockfile root, unless a Deno config is nearer” — needs to run the same search by hand inside its root_dir. It walks the project tree through the async btv.fs seam (local on native-bare, the daemon’s luafs_op over the wire otherwise), so it works on every front end with NO editor-thread block: the btv.async body runs as a coroutine and the caller :nexts the result (or btv.awaits it). Each btv.fs.readdir that rejects — an unreadable or non-directory ancestor — is treated as “no markers here” and the walk continues upward.

markers may carry PRIORITY TIERS — a list of lists rather than a flat list of names. A tier is exhausted over the WHOLE tree before the next one is tried anywhere, which is the entire point of the form: with { { "package-lock.json" }, { ".git" } } a lockfile six directories up beats a nested .git one directory up, so a package inside a monorepo attaches at the monorepo root rather than at its own sub-repo. A flat list is one tier, where the nearest directory holding any marker wins.

root_dir = function(bufnr, on_dir)
  btv.lsp.find_root(bufnr, { { "package-lock.json" }, { ".git" } }):next(on_dir)
end

Each level’s listing is read once and memoized, so a second tier re-walks the cached entries instead of re-reading the tree — N directory listings for any number of tiers, not N per tier.

Defined in lsp.lua.

btv.lsp.get_config(name)

btv.lsp.get_config(name) -> the fully-resolved config for name — the "*" base, the runtimepath lsp/<name>.lua preset, and every btv.lsp.config(name, …) override so far, deep-merged. This is exactly what a server would start with right now, so it is the honest answer to “what is this server configured as?” (neovim’s indexable vim.lsp.config[name], as a function — bemtvi’s btv.lsp.config is call-only).

Reads through the same cached preset loader the dispatcher uses, so there is one copy of the “source a preset off the runtimepath” logic and a caller can’t observe a config the dispatcher wouldn’t use. Returns a fresh table each call: mutating it changes nothing, which is what btv.lsp.config is for.

local cfg = btv.lsp.get_config("gopls")
if cfg.filetypes and btv.tbl.contains(cfg.filetypes, "go") then … end

A name with no preset and no override resolves to an empty table ({}), not nil — “configured as nothing” rather than “unknown”, since a config can be built up purely from btv.lsp.config calls with no bundled preset behind it at all.

Defined in lsp.lua.

btv.lsp.config(name, opts)

btv.lsp.config(name, opts): accumulate opts into name’s override layer (deep-merged over any prior call — configs compose across files and plugins). "*" is the all-clients base inherited by every server. Function-call form only: there is no btv.lsp.config[name] = {…} table-assignment sugar.

Two keys are easy to confuse, because they are the same word in different worlds:

root_dir / root_markers  -- the WORKSPACE root, sent as `rootUri`
cmd_cwd                  -- the directory the server PROCESS is launched in

They are unrelated, exactly as in vim. The root is a protocol fact: it tells the server which project to index, and resolving it is what root_markers does. The spawn directory defaults to the editor’s own working directory (:cd-aware) and only matters to a cmd that cares where it was invoked from — a launcher resolving a virtualenv, or uvx, which refuses to run at all when its cwd sits inside its own cache. Set cmd_cwd to pin it; a relative value resolves against the editor’s cwd.

btv.lsp.config("pylsp", {
  cmd = { "uvx", "--from", "python-lsp-server", "pylsp" },
  cmd_cwd = btv.workspace.dir(),   -- launch it here, whatever buffer attached
})

opts.priority (an integer, default 0) is the routing rank among the servers attached to one buffer, higher first. A buffer carries every server enabled for its filetype, and when several can answer the same request the default order is the config name alphabetically — stable, but an arbitrary preference. priority states the real one:

btv.lsp.config("pyright", { priority = 10, … })   -- the type-checker leads
btv.lsp.config("ruff",    { priority = 5,  … })   -- the linter follows

It decides the order the merging verbs present in — the hover float’s sections, the signature lines, the code-action chooser’s rows, the reference, symbol and goto lists, and the completion popup’s rows (plus the docs float’s sections for a candidate several servers offer) — and which server the verbs that ACT (format, rename) pick, since two servers’ edits cannot both be applied. It does not decide whether a server is asked: that is capability, and a server that doesn’t advertise the feature is skipped whatever its rank. btv.lsp.hover{ name = … } / :LspHover <server> override the rank outright for one call. A change takes effect on the next start, so btv.lsp.restart(name) applies it to a server already running.

opts.docs_format ("markdown" | "plaintext" | "rst") says how to render what this server sends as plaintext — the hover float and the completion docs float.

It exists because the protocol cannot say. LSP’s MarkupKind is a closed two-value set, plaintext or markdown; a server whose docstrings are reStructuredText has no value to name that with, so it declares plaintext, and nothing in the reply distinguishes rst from genuinely-plain text. Guessing from the content (“it starts with :param”) would silently reinterpret a plain docstring that happens to contain a *, so bemtvi does not guess — you say it:

btv.lsp.config("pylsp", { docs_format = "rst" })

Only blocks the server declared plaintext are affected: one it declared markdown is markdown whatever this says, because there the server told us. Note that python-lsp-server and jedi-language-server convert rst to markdown themselves when the docstring-to-markdown package is installed alongside them, and bemtvi asks for markdown first — so install that and you need none of this.

Defined in lsp.lua.

btv.lsp.disabled()

btv.lsp.disabled() -> whether LSP is off in this editor: an app embedding it turned it off, so no language server starts here. btv.lsp.enable, start and restart then raise E5804, so a config meant for both checks first:

if not btv.lsp.disabled() then
  btv.lsp.enable({ "lua_ls" })
end

Defined in lsp.lua.

btv.lsp.enable(names)

btv.lsp.enable(names): mark configs for auto-activation on current and future buffers and install the dispatcher. names is a string or a list. "*" is the base layer, not an activatable server, so it is rejected here.

Defined in lsp.lua.

btv.lsp.disable(names)

btv.lsp.disable(names): the inverse of enable — future buffers won’t start the named servers (already-running servers keep serving until their buffers close).

To shut a server down now, use btv.lsp.stop — on its own, disable closes the gate without touching what is already through it.

Defined in lsp.lua.

btv.lsp.stop(name, opts)

btv.lsp.stop(name[, opts]): shut down every running server with config name, detaching it from the buffers it was serving. Returns a promise that resolves with the NUMBER of servers stopped (0 when nothing by that name was running) — so a caller can report “no server named X is running” rather than a silent success.

btv.lsp.stop("gopls"):next(function(n)
  btv.notify(n > 0 and ("stopped " .. n) or "gopls was not running")
end)

A stopped server can come back: the config stays enabled, so the next buffer whose filetype matches starts a fresh one. Pass opts.disable = true to stop it and close the gate (btv.lsp.disable), which is what :LspStop does — otherwise the very next matching buffer would silently restart what you just stopped.

Defined in lsp.lua.

btv.lsp.restart(name)

btv.lsp.restart(name): tear down and respawn every running server with config name, re-starting it from the config in force NOW. A server that reads its whole configuration only at startup (efm-langserver’s languages map is the canonical case) does not see a btv.lsp.config change applied to an already-running instance; restarting it makes the grown config take effect. Each buffer bound to the server re-attaches to the fresh process. A no-op when nothing named name is running.

Defined in lsp.lua.

btv.lsp.start(cfg, opts)

btv.lsp.start(cfg, opts): the low-level, un-merged direct start (the raw LspOp::Start) for advanced/manual use — bypasses the registry. cfg is a resolved config ({ name, cmd, root_dir, filetypes, settings, … }); opts.bufnr is the buffer to attach (default the current one), opts.filetype its languageId.

Defined in lsp.lua.

btv.lsp.definition(opts)

btv.lsp.definition([opts]): jump to the definition of the symbol under the cursor, resolving with the { text, path, row, col } item list.

Asks every capable server and merges: a definition can genuinely live in two places to two servers (a generated stub and its source, a .d.ts and its implementation). Duplicates collapse, so the merged list holding ONE place still jumps — the one-server case is unchanged — and only a real disagreement opens btv.picker. opts.name narrows the round to that client.

Defined in lsp.lua.

btv.lsp.declaration(opts)

btv.lsp.declaration([opts]): the definition twin for textDocument/declaration (C headers, extern declarations). Merges across servers and opts.name routes it, exactly as btv.lsp.definition does.

Defined in lsp.lua.

btv.lsp.type_definition(opts)

btv.lsp.type_definition([opts]): jump to the definition of the type of the symbol under the cursor. Merges across servers and opts.name routes it, exactly as btv.lsp.definition does.

Defined in lsp.lua.

btv.lsp.implementation(opts)

btv.lsp.implementation([opts]): the implementations of the interface / trait method under the cursor. Merges across servers and opts.name routes it, exactly as btv.lsp.definition does.

Defined in lsp.lua.

btv.lsp.references(opts)

btv.lsp.references([opts]): the references to the symbol under the cursor, opened in btv.picker. Fans out to every capable server and merges (two servers indexing one project each know references the other does not); opts.name narrows the round to that one client’s list.

Defined in lsp.lua.

btv.lsp.hover(opts)

btv.lsp.hover([opts]): the hover documentation for the symbol under the cursor, in a float, resolving with the shown text.

Asks every attached server advertising hoverProvider and composes one float: on a pyright + ruff buffer each knows something the other doesn’t (a type, a lint rationale), and answering from one silently hides the other. With more than one contributor each section is headed # <client name>, in priority order; with one it renders bare. opts.name narrows the round to that client alone.

Defined in lsp.lua.

btv.lsp.signature_help(opts)

btv.lsp.signature_help([opts]): the signature of the call under the cursor, in a float. opts.name routes it; see btv.lsp.signature_help_autotrigger for showing it as you type instead of on demand.

A call with more than one parameter is laid out one parameter per line, with a ▸ marking the parameter the cursor is inside — drawn in the foreground of the LspSignatureActiveParameter highlight group, since a theme that spells that group as a background band would otherwise paint a lone block into the popup:

def connect(
    host: str,
  ▸ port: int = 5432,
    timeout: float = 30.0,
) -> Connection

The split reads the server’s own parameter ranges, not commas, so f(items: dict[str, int]) stays one parameter. A one-parameter call — or a server whose parameters can’t be located in the signature it sent — stays on a single line with the active parameter named in brackets.

Asks every capable server, like btv.lsp.hover. With more than one answering they are shown together in priority order, each labelled with its client — where neovim shows one at a time and binds <C-s> to cycle. bemtvi’s float is passive (the next keystroke dismisses it), so there is nothing to cycle through and no mode to leave.

Defined in lsp.lua.

btv.lsp.signature_help_autotrigger(enable)

btv.lsp.signature_help_autotrigger(enable): opt into auto-showing signature help as you type a call (after (, refreshed at each ,), instead of only on <C-k>. It is driven by the server’s advertised signatureHelpProvider.triggerCharacters, so it only fires for servers that offer signature help. enable defaults to true; pass false to turn it back off. Off unless a config opts in.

Defined in lsp.lua.

btv.lsp.format(opts)

btv.lsp.format([opts]): format the buffer, resolving nil once the edits apply.

opts.name   format with the attached server of this config name, instead of the
            first one advertising `documentFormatting`. Meaningful when a buffer
            carries several servers — `pyright` + `ruff`, `ts_ls` + `eslint` —
            where the default pick is not necessarily the formatter you want.

A name that is not attached to the buffer reports so on the message line and resolves nil; it never falls back to formatting with a different server, since silently using the wrong formatter is the failure the option exists to prevent. Any other key is rejected loudly (as in btv.lsp.code_action).

Defined in lsp.lua.

btv.lsp.code_action(opts)

btv.lsp.code_action(opts): run a code action at the cursor. Bare, it is interactive — the reply only opens the chooser menu, and the returned promise resolves once you pick an action and its edit applies (through a codeAction/resolve round-trip if the action is lazy), or nil if you cancel the chooser (Esc) — so you can, e.g., organize imports then format:

btv.lsp.code_action():next(function() return btv.lsp.format() end)

opts narrows it to a specific action, which is what makes it usable non-interactively (a format-on-save chain, say):

btv.lsp.code_action({ context = { only = { "source.fixAll" } }, apply = true })

A code action is asked about a range, and the refactor kinds (refactor.extract, refactor.inline) are exactly the ones a server offers only over a non-empty one. Called with a live Visual / Select selection — from a "v"-mode keymap, say — the selection is the range (and is consumed, like any : command acting on a selection); opts.range states one explicitly; with neither, the request is a point at the cursor. :'<,'>LspCodeAction is the ex-command form, scoped to the addressed whole lines.

btv.keymap.set({ "n", "v" }, "<leader>ca", function() btv.lsp.code_action() end)
context.only   list of code-action kinds to ask for; sent as the request's
               `context.only` AND re-applied to the reply (honoring it is a
               protocol "should", so a server that ignores it can't turn a
               one-shot into a chooser). Matching follows the LSP kind
               hierarchy: `"source.fixAll"` matches the kind `source.fixAll`
               and `source.fixAll.ruff`. An action with no kind never matches.
apply          when exactly ONE action survives the filter, apply it directly
               with no chooser. Two or more still open the chooser (there is a
               real choice to make); none echoes "No code actions available"
               and the promise resolves `nil`.
range          the range to ask about, stated outright:
               { start_row = 0, start_col = 0, end_row = 2, end_col = 0 }
               0-based rows, 0-based BYTE columns, end-EXCLUSIVE (the
               `btv.win.select_range` convention). All four fields are
               required, and it wins over both a live selection and the
               cursor — the non-interactive way to act on a computed span.
name           ask only the attached client of this config name, instead of
               merging every capable server's actions into one chooser — "run
               eslint's fixes", not ts_ls's refactors as well.

Anything else in opts (neovim’s filter, context.diagnostics, context.triggerKind) is rejected loudly rather than silently ignored — bemtvi doesn’t model it yet, and a quietly-dropped filter would silently do the wrong thing.

Defined in lsp.lua.

btv.lsp.document_symbol(opts)

btv.lsp.document_symbol([opts]): the symbols defined in the current document, opened in btv.picker (kind 16 mirrors LspReqKind::DocumentSymbol::as_u16). Resolves with the symbol { text, path, row, col } item list. opts.name lists only that client’s symbols instead of merging every capable server’s.

Defined in lsp.lua.

btv.lsp.workspace_symbol(query, opts)

btv.lsp.workspace_symbol([query], [opts]): symbols across the workspace matching query, opened in btv.picker. With no query, prompt for one via btv.ui.input (non-blocking) — an empty/cancelled prompt resolves nil. Returns a promise of the matched symbol item list. opts.name searches only that client’s index instead of merging every capable server’s; since a query is always a string, the options table may take the first argument’s place when you want the prompt and a route (btv.lsp.workspace_symbol({ name = "ts_ls" })).

Defined in lsp.lua.

btv.lsp.rename(new_name, opts)

btv.lsp.rename([new_name], [opts]): rename the symbol under the cursor. With a name, request it straight away; with none (the bare btv.keymap.set("n", "<leader>rn", btv.lsp.rename) case), prompt for it via btv.ui.input (non-blocking promise), prefilled with the symbol under the cursor, and rename on confirm. Returns a promise that resolves nil once the rename applies (or immediately, nil, on an empty / cancelled prompt).

opts.name routes the request to one attached client. A new name is always a string, so the options table may take the first argument’s place when you want the prompt and a route (btv.lsp.rename({ name = "ts_ls" })).

Defined in lsp.lua.

client:request(method, params, handler, _bufnr)

client:request(method, params, handler[, bufnr]): issue a generic LSP request; the reply runs handler(err, result) off-tick (err a message string on failure, result the server’s value on success — exactly one set). An unimplemented or uncapable method fails loud through err, never a silent no-op.

bufnr is accepted for source compatibility with neovim’s fourth argument and has no effect: the request is addressed to THIS client, and bemtvi cancels a client’s in-flight requests when the client goes away rather than per buffer.

Defined in lsp.lua.

client:notify(method, params)

client:notify(method, params): fire-and-forget a generic LSP notification.

Defined in lsp.lua.

client:supports_method(method)

client:supports_method(method) -> boolean: does this server answer method?

Read from what the server advertised at initialize (client.server_capabilities), so a config guards its own commands against a server build that lacks the feature instead of firing a request that comes back as an error the user has to interpret. A method bemtvi maps no capability for is supported — see METHOD_CAPABILITY.

Defined in lsp.lua.

client:exec_cmd(command, context, handler)

client:exec_cmd(command[, context[, handler]]): run an LSP Command ({ title, command, arguments }) — the verb behind a code action that carries a command, and the one a per-server :Lsp… command uses to drive its server’s own vocabulary (deno.cache, texlab.cleanArtifacts, _typescript.goToSourceDefinition).

Resolved in precedence order, which is the same order an applied code action takes:

1. the OFFERING config's own `commands` table   (this client's config)
2. the global `btv.lsp.commands` registry
3. `workspace/executeCommand` on this client

A client-side handler (1 or 2) is called as handler(command, ctx) and the round trip never happens — that is the whole point of registering one. Otherwise the command goes to the server and handler(err, result, ctx) receives the reply. ctx carries { client_id, bufnr, params }, so a failing handler can report which arguments failed.

context.bufnr names the buffer the command acts on (0/nil = current). Unlike neovim, a command the server did not list in executeCommandProvider is still sent: bemtvi reports whatever the server answers rather than refusing locally, since servers under-advertise this list routinely and a local refusal reads as “the command did nothing”.

Defined in lsp.lua.

btv.lsp.client_by_id(id)

btv.lsp.client_by_id(id): the handle for client id, or nil once its server has exited. The lookup behind a handler that is handed a client_id (a code-action ctx, an LspAttach autocmd’s args.data.client_id) rather than a handle.

Defined in lsp.lua.

btv.lsp.clients(filter)

btv.lsp.clients(filter): a snapshot list of active clients, narrowable by filter.bufnr (the clients attached to that buffer; 0/nil = current) and/or filter.name (the config name). Reads the mirror — no request is issued.

A buffer can have several clients attached (pyright + ruff, ts_ls + eslint): every server enabled for its filetype attaches, so a bufnr filter may return more than one. Don’t index [1] expecting “the” server — filter by name, or by what the client advertises in server_capabilities. The list is in routing order: priority descending, then config name, then client id.

Defined in lsp.lua.

btv.lsp.progress(filter)

btv.lsp.progress(filter): the $/progress work language servers are running right now — what a statusline renders as “lua_ls: Indexing 43%”.

Servers report long tasks (indexing, loading a workspace, building a crate graph) as a begin → report* → end sequence sharing a token; bemtvi folds that stream into settled records, so this returns a flat list of what is in flight, newest task last:

for _, p in ipairs(btv.lsp.progress()) do
  print(("%s: %s %s%s"):format(
    p.client_name, p.title, p.message or "", p.percentage and (" " .. p.percentage .. "%%") or ""))
end

Each item is:

client_id    the reporting client (resolve with `btv.lsp.client_by_id`)
client_name  its config name (`"lua_ls"`)
token        the task's `$/progress` token, unique within the client
title        the `begin` title (`"Indexing"`); `""` if the server never began
message      the detail line (`"3/25 files"`), or nil
percentage   0-100, or nil for an indeterminate task (show a spinner, not a bar)
cancellable  whether the server would honor a cancel for this token

filter.client_id narrows to one client; filter.bufnr (0 = current) narrows to the clients attached to that buffer, which is what a per-window statusline wants — a busy server serving some other buffer isn’t this buffer’s status.

The order is STABLE: clients ascending by id (so the longest-running server’s work comes first), and within a client the order the tasks began. A renderer showing only the first task therefore keeps showing the same one across updates.

The list is EMPTY when nothing is in flight (a finished task is removed, not left at 100%), and a server that exits mid-task takes its entries with it. Reads the mirror — no request is issued. LspProgress fires on every update, with the kind ("begin" / "report" / "end") as the autocmd pattern, so a renderer invalidates on the event and reads through here rather than polling.

Defined in lsp.lua.

btv.lsp.request(method, params, handler, target)

btv.lsp.request(method, params, handler, target): sugar resolving one of the buffer’s clients and issuing client:request. target is a bufnr (0/nil = current) or { bufnr =, name = }, where name picks the attached client by config name instead of taking the first — the first in btv.lsp.clients order (highest priority, then config name). No matching client fails loud.

Defined in lsp.lua.

btv.lsp.notify(method, params, target)

btv.lsp.notify(method, params, target): the fire-and-forget sibling of request, with the same target routing.

Defined in lsp.lua.

btv.lsp.text_document_params(bufnr)

btv.lsp.text_document_params([bufnr]) -> { uri = … }, the LSP TextDocumentIdentifier for buffer bufnr (0/nil = current) — the params of every request that names a document and nothing else.

Defined in lsp.lua.

btv.lsp.position_params(opts)

btv.lsp.position_params([opts]) -> { textDocument = { uri }, position = { line, character } }, the LSP TextDocumentPositionParams for the cursor — the params shape most requests take.

opts:

  • win — the window whose cursor to read (0/nil = current).
  • bufnr — the document to name (default: win’s buffer).
  • encoding — the position encoding to count character in. Pass the answering client’s offset_encoding; the default is whatever the buffer’s first attached server negotiated, which is only right by luck when two servers with different encodings share a buffer.
local params = btv.lsp.position_params({ encoding = client.offset_encoding })
client:request("textDocument/build", params, on_reply)

Defined in lsp.lua.

btv.lsp.locations_to_items(locations, opts)

btv.lsp.locations_to_items(locations[, opts]) -> a PROMISE of quickfix items, one per location, sorted by file then position — the bridge from a server’s Location[] / LocationLink[] to the shape btv.qf.setqflist takes ({ filename, lnum, col, end_lnum, end_col, text }, all 1-based).

btv.lsp.locations_to_items(refs, { encoding = client.offset_encoding })
  :next(function(items)
    btv.qf.setqflist({}, " ", { title = "References", items = items })
    btv.qf.open()
  end)

A promise, not a value, and that is not incidental: each item’s text is the source line it points at, and a location into a file no buffer holds means reading that file. neovim reads them synchronously; bemtvi does no blocking I/O anywhere, so the conversion is async and the editor keeps running while a 900-reference result resolves.

opts.encoding is the position encoding the server counted its columns in (default "utf-16", the protocol’s) — columns come out as bemtvi’s own bytes. Locations naming a non-file:// document are dropped: a quickfix entry addresses a path, and a deno:/jdt: URI has none.

Defined in lsp.lua.

btv.lsp.semantic_tokens.start(bufnr)

start/stop the per-buffer semantic-token paint (neovim’s start/stop verbs).

Defined in lsp.lua.

btv.lsp.semantic_tokens.stop(bufnr)

No documentation comment in the prelude.

Defined in lsp.lua.

btv.lsp.semantic_tokens.enable(enabled)

bemtvi’s editor-wide gate (default on) — neovim has only the per-buffer verbs. Off ⇒ no semantic paint anywhere; flipping back on re-requests every buffer.

Defined in lsp.lua.

btv.lsp.semantic_tokens.force_refresh(bufnr)

Drop the cached result_id and re-request the whole token set (neovim’s force_refresh) — the one operation with no readable state to model as a noun.

Defined in lsp.lua.

btv.lsp.semantic_tokens.get_at_pos(bufnr, row, col)

get_at_pos(bufnr, row, col): the decoded tokens covering the 0-based (row, col) (neovim’s vim.lsp.semantic_tokens.get_at_pos). col is a 0-based byte column; a token covers [start_col, end_col). Returns a list (possibly empty).

A buffer served by several language servers carries every capable server’s tokens, each tagged with its client_id, so one column can hold more than one token. Filter on client_id when you mean a specific server’s.

Defined in lsp.lua.

btv.lsp.inlay_hint.enable(enable, filter)

enable(enable?, filter?): flip the per-buffer inlay-hint paint. enable defaults to true; filter.bufnr (0/nil = current) targets the buffer (neovim’s modern vim.lsp.inlay_hint.enable(enable, { bufnr })).

Defined in lsp.lua.

btv.lsp.inlay_hint.is_enabled(filter)

is_enabled(filter?): whether inlay hints are on for the buffer.

Defined in lsp.lua.

btv.lsp.inlay_hint.get(filter)

get(filter?): the decoded inlay hints in a buffer (filter.bufnr, 0/nil = current), each { bufnr, client_id, inlay_hint = <decoded entry> } to match neovim’s shape. filter.range ({ start_line, end_line }, 0-based inclusive) narrows by line. Reads the mirror — no request is issued.

Every capable server’s hints are here, in line-then-column order, each tagged with the client_id that produced it — a buffer served by two servers shows both sets.

Defined in lsp.lua.

btv.lsp.apply_workspace_edit(edit, opts)

Apply an LSP WorkspaceEdit — the same path a rename reply or a server-initiated workspace/applyEdit takes, exposed for a plugin (or an btv.lsp.commands handler) holding an edit a server handed it as command arguments.

edit is the protocol shape, either form:

btv.lsp.apply_workspace_edit({
  changes = { ["file:///p/a.rs"] = { { range = r, newText = "bar" } } },
})
btv.lsp.apply_workspace_edit({
  documentChanges = {
    { kind = "create", uri = "file:///p/new.rs" },
    { textDocument = { uri = "file:///p/new.rs", version = 0 },
      edits = { { range = r, newText = "fn helper() {}\n" } } },
  },
})

documentChanges is applied in the order given, resource operations (create / rename / delete) included: those move real files, so they run off the editor tick and settle a moment later — identically in a local, daemon or browser session. Text edits land in the buffers (left modified, written by :w / :wa) exactly as a rename’s do; a create is written out for you.

opts.encoding is the position encoding the edit’s character columns are counted in ("utf-8" / "utf-16" / "utf-32"), defaulting to the protocol’s "utf-16". Pass the encoding the server that authored the edit negotiated — btv.lsp.clients({ bufnr = 0 })[1].offset_encoding — when it isn’t utf-16, or every column on a line with a multi-byte character lands in the wrong place. Anything that fails is reported loud, never silently skipped.

Defined in lsp.lua.

btv.lsp.show_document(location, opts)

Jump to an LSP Location (or LocationLink), opening its file if it isn’t already in a buffer — for a location a server hands over, e.g. as a command’s arguments:

btv.lsp.commands["rust-analyzer.gotoLocation"] = function(command)
  local loc = command.arguments and command.arguments[1]
  if loc then btv.lsp.show_document(loc) end
end

opts.encoding is the position encoding the location’s columns are in ("utf-8" / "utf-16" / "utf-32"), defaulting to the protocol’s "utf-16". A location with no usable URI is an error, not a silent no-op.

Defined in lsp.lua.

btv.lsp.foldexpr(_lnum)

btv.lsp.foldexpr is the canonical LSP foldexpr, the foldmethod=expr fold source backed by textDocument/foldingRange:

btv.bo.foldmethod = "expr"
btv.bo.foldexpr   = "v:lua.btv.lsp.foldexpr()"

bemtvi recognizes that exact reference and folds the buffer from the language server’s folding ranges (requested on open/change while the buffer wants LSP folds — see crates/bemtvi-core/src/editor/fold.rs and crates/bemtvi-server’s lsp/folding.rs). Like the tree-sitter marker it is never evaluated per line, so calling it directly is a usage error — fail loud rather than return a wrong level. vim.lsp.foldexpr is the muscle-memory alias; bemtvi recognizes both.

Defined in lsp.lua.