A debugging environment for Neovim, built on top of nvim-dap, targeting
JavaScript/TypeScript first via vscode-js-debug, with optional LLM-assisted
investigation as a layered feature.
Status: Phase 1 in progress — session manager, extmark namespace pool, and main panel frame landed.
- Neovim 0.10+
- nvim-dap ≥ 0.8.0
(minimum version verified against commit
d5b09c2, 2024-01; thevscode-js-debugsmoke test is the canary for upstream drift) - plenary.nvim (test runner)
- vscode-js-debug (smoke tests and JS/TS debugging)
lua/nvim-dapper/session/ is the first shipped capability. It is the
single source of truth for live nvim-dap session state:
- Typed event stream —
session_started,session_terminated,stopped,continued,thread_changed,frame_changed,focus_changed,output,adapter_failed,attach_failed. - Subscriber-keyed subscriptions —
subscribe(id, handler)/unsubscribe(id). Teardown is atomic per subscriber id, making mount/unmount listener-leak-free. - Pull accessors —
current_session(),current_thread(),current_frame(),is_paused(),is_running(). Updated before each dispatch so handlers see the new state synchronously. - Multi-session aware — observes all live nvim-dap sessions, follows
nvim-dap's own focus policy (most recent
stoppedwins), emitsfocus_changedwhen focus shifts. - Infrastructure-failure events —
adapter_failedandattach_failedwith discriminatingreasonfields; no silent failure.
See docs/session-manager.md for the full API,
event payload shape, and the thread focus model.
lua/nvim-dapper/panel/main/ owns the single Neovim window that every
Phase 1 view (variables, stack, breakpoints, watches, REPL, output) will
register into via panel.register_view(spec). Views never create their
own windows or scratch buffers — that constraint is CI-enforced. See
docs/main-panel.md for the registration API, the
mount-on-first-focus lifecycle, the on_session_event re-dispatch
contract, the four user-facing commands (:DebugPanel,
:DebugPanelToggle, :DebugPanelClose, :DebugView), and the winbar
highlight groups.
lua/nvim-dapper/extmark_pool/ is the sole allocator of
vim.api.nvim_create_namespace in the plugin. It distinguishes
concern-scoped (plugin-lifetime) namespaces from instance-scoped
(owner-lifecycle) namespaces, tracks which buffers have received marks
under each namespace, and bulk-clears O(tracked buffers) instead of
scanning every buffer. See docs/extmark-pool.md
for the API and the rules a Phase 1 view must follow.
local session = require("nvim-dapper.session")
-- Mount
session.subscribe("my-view", function(event)
if event.kind == "stopped" then
local frame = session.current_frame() -- pull state inside handler
-- render ...
elseif event.kind == "session_terminated" then
-- clear ...
end
end)
-- Unmount
session.unsubscribe("my-view")Every Phase 1 panel view ships view-local default keymaps on its
scratch buffer (<CR> jumps to source, l/h expand/collapse, r
retries, ]p pages, etc.). The full table — plus the override snippet
for each layer — lives in docs/keymaps.md.
Global debugger keymaps (<F5> continue, <F10> step over,
<leader>de evaluate, …) are opt-in. Enable them with:
require("nvim-dapper").setup({
keymaps = { global = { enabled = true } },
})When using Dapper panels with step/continue commands, enable Dapper's
stopped-frame source targeting so nvim-dap opens the stopped source in
an editing window instead of replacing the focused panel buffer:
require("nvim-dapper").setup({
stopped_frame_source_targeting = true,
keymaps = { global = { enabled = true } },
})This option installs Dapper's managed nvim-dap switchbuf function. It
reuses an eligible editing window for stopped source, including unopened source
buffers, and opens a split only when no editing window is available. When
enabled, Dapper also applies the managed policy to adapter-specific
dap.defaults.<type>.switchbuf values for active sessions and restores the
prior values when disabled. If nvim-dap loads after setup(), Dapper keeps
the opt-in pending and installs the policy once dap is available. Plain
setup({}) leaves any existing nvim-dap switchbuf value intact.
See docs/keymaps.md for the full default table,
override grammar, and the require("nvim-dapper.keymaps").actions()
facade for building your own bindings.
- Never import
dap.listenersor calldap.session()from outsidelua/nvim-dapper/session/. - Never read
event.datafrom adapper.SessionEventoutsidelua/nvim-dapper/session/. - Never call blocking primitives (
vim.fn.system,io.read, …) from any function reachable from a handler.
See docs/contributing.md for details.
# Static lint (dap-isolation, event-data, blocking-handler,
# namespace-isolation, extmark-pool-imports, window-ownership, panel-no-dap)
nvim -l scripts/lint.lua lua
# Unit tests (no adapter required)
nvim --headless \
-u tests/minimal_init.lua \
-c "lua require('plenary.test_harness').test_directory('tests/session/', { sequential = true })" \
-c "qa!"
nvim --headless \
-u tests/minimal_init.lua \
-c "lua require('plenary.test_harness').test_directory('tests/extmark_pool/', { sequential = true })" \
-c "qa!"
nvim --headless \
-u tests/minimal_init.lua \
-c "lua require('plenary.test_harness').test_directory('tests/panel/main/', { sequential = true })" \
-c "qa!"
# Smoke test (requires vscode-js-debug)
VSCODEJSDEBUG=/path/to/ms-vscode.js-debug/extension \
nvim --headless \
-u tests/minimal_init.lua \
-c "lua require('plenary.test_harness').test_directory('tests/session/', { sequential = true, pattern = 'smoke_spec' })" \
-c "qa!"- Phase 1: Session manager ✓ → main panel → variables view → stack view → inline virtual text values
- Phase 2: Agent tool registry,
attach_and_pause_atprimitive, multi-session controller