Skip to content

Latest commit

 

History

History
154 lines (116 loc) · 5.99 KB

File metadata and controls

154 lines (116 loc) · 5.99 KB

Extmark Namespace Pool

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.

Scope model

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.

Handle shape

---@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 instance

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

API reference

Allocation

local pool = require("nvim-dapper.extmark_pool")

local ns       = pool.concern("inline-values")            -- plugin-lifetime
local claim_ns = pool.instance("claim", "investigation-42") -- owner-scoped

Writing marks (the recommended path)

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

Track discipline (manual path)

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.

Bulk clear (concern scope)

pool.clear("inline-values")                  -- across every tracked buffer
pool.clear_in_buffer("inline-values", buf)   -- targeted to one buffer

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

Retirement (instance scope)

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.

Introspection (leak diagnosis)

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
  -- }
end

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

CI-enforced rules

  • namespace-isolationnvim_create_namespace may only appear in Lua source under lua/nvim-dapper/extmark_pool/.
  • extmark-pool-imports — files under lua/nvim-dapper/extmark_pool/ may not require dap, the session manager, or any AI module.

See scripts/lint.lua and docs/contributing.md.