lua/nvim-dapper/extmark_pool/ is the sole allocator of
vim.api.nvim_create_namespace in nvim-dapper. Project rule §3.5 mandates
this so cross-concern bulk cleanup is possible without scanning every
buffer; the CI lint namespace-isolation rejects any ad-hoc call from
elsewhere in the codebase.
The pool has zero coupling to nvim-dap, the session manager, or any AI
surface. Its only runtime dependency is vim.api.
Two namespace scopes, expressed as distinct constructors:
| Constructor | Lifetime | Cleared by |
|---|---|---|
pool.concern(name) |
Plugin (memoized by name) | pool.clear(name) / pool.clear_in_buffer(name, buf) |
pool.instance(name, owner_id) |
Owner lifecycle | pool.retire(owner_id) |
The two are separate functions, not a single function with a flag, so misuse is visible at the call site (a concern allocation never needs an owner; an instance allocation always does).
Neither name nor owner_id may contain : — it is the registry
separator that keeps the composed Neovim-level name unambiguous.
Violations raise a clear error.
---@class dapper.Namespace
---@field id integer -- pass to nvim_buf_set_extmark / clear
---@field name string -- logical name (no "nvim-dapper:" prefix)
---@field scope "concern"|"instance"
---@field owner string|nil -- nil for concern, owner_id for instanceThe handle is opaque: do not construct or mutate it. pool.track and
pool.set_extmark reject any handle that is not currently owned by the
pool — this catches both manually constructed tables and stale instance
handles retained across pool.retire.
local pool = require("nvim-dapper.extmark_pool")
local ns = pool.concern("inline-values") -- plugin-lifetime
local claim_ns = pool.instance("claim", "investigation-42") -- owner-scopedlocal id = pool.set_extmark(ns, buf, line, col, opts)set_extmark is a thin discipline enforcer:
pool.track(ns, buf) then vim.api.nvim_buf_set_extmark(buf, ns.id, line, col, opts).
It returns the mark id unchanged and forwards opts by reference —
no field is added, removed, defaulted, or interpreted. If you need
opinionated convenience kwargs, write a higher-level helper; do not
extend this function.
For callers needing fine-grained control (conditional writes, non-standard
extmark APIs, populating from another path) call pool.track explicitly
before the first mark of a given namespace lands in a buffer:
pool.track(ns, buf)
vim.api.nvim_buf_set_extmark(buf, ns.id, line, col, opts)track is idempotent. It silently refuses invalid buffers (closing a
vim.schedule / async-handler race window — see design D3) and raises a
clear programmer error for retired or manually constructed handles.
A BufWipeout autocmd auto-removes wiped buffers from every tracked set,
so callers do not need to untrack manually on the normal wipe path.
pool.clear("inline-values") -- across every tracked buffer
pool.clear_in_buffer("inline-values", buf) -- targeted to one bufferpool.clear(name) is a no-op for unknown concern names. It does
not clear instance namespaces — those are cleared by pool.retire.
Both clear functions skip (and drop) buffers that are no longer valid;
this skip is load-bearing for windows where BufWipeout does not fire
(noautocmd wipes, plugin reload, :bwipe! during reset).
pool.clear_in_buffer(name, buf) only touches buf if the pool is
currently tracking it under name. Unknown names, and buffers the pool
never wrote to under name, are silent no-ops — the function never
issues nvim_buf_clear_namespace on a buffer the pool didn't write to.
pool.retire("investigation-42")Clears every instance-scoped namespace allocated under the owner across
its tracked buffers, then drops the Lua-side state. Previously returned
handles for that owner are no longer live and will be rejected by
track / set_extmark.
A subsequent pool.instance(name, owner_id) returns a fresh handle
table with an empty tracked-buffer set. The integer id field equals
the pre-retire id — Neovim's nvim_create_namespace memoizes by name
string, so re-allocation reuses the integer. The retirement boundary is
enforced by the bulk clear and the dropped Lua-side state, not by id
rotation. See openspec/changes/add-extmark-namespace-pool/design.md D5
for the full rationale and the future { fresh = true } extension path.
pool.retire on an unknown owner_id is a silent no-op.
for _, entry in ipairs(pool.list()) do
-- entry = {
-- name, scope, owner,
-- tracked_buffers, -- shallow copy of the tracked set
-- tracked_buffer_count, -- length of tracked_buffers
-- valid_buffer_count, -- nvim_buf_is_valid() count at snapshot time
-- }
endtracked_buffers is a shallow copy — mutating it does not affect pool
state. A discrepancy between tracked_buffer_count and
valid_buffer_count is the documented leak-diagnosis signal: invalid
buffers accumulating in a tracked set indicates a path where the
BufWipeout autocmd did not run (a noautocmd window, a reload race) —
the next clear will sweep them.
Retired instance namespaces are excluded from the snapshot.
namespace-isolation—nvim_create_namespacemay only appear in Lua source underlua/nvim-dapper/extmark_pool/.extmark-pool-imports— files underlua/nvim-dapper/extmark_pool/may notrequiredap, the session manager, or any AI module.
See scripts/lint.lua and docs/contributing.md.