Skip to content

fix(autocmds): guard debounced cursor action against wiped buffer#519

Merged
OXY2DEV merged 1 commit into
OXY2DEV:mainfrom
ImmanuelHaffner:fix/cursor-action-buffer-validity
Jul 6, 2026
Merged

fix(autocmds): guard debounced cursor action against wiped buffer#519
OXY2DEV merged 1 commit into
OXY2DEV:mainfrom
ImmanuelHaffner:fix/cursor-action-buffer-validity

Conversation

@ImmanuelHaffner

Copy link
Copy Markdown
Contributor

Problem

Moving the cursor in a Markdown/preview buffer can crash with:

Invalid buffer id: N
stack traceback:
	[C]: in function 'nvim_buf_clear_namespace'
	.../markview/renderers/markdown.lua: in function 'clear'
	.../markview/renderer.lua: in function 'clear'
	.../markview/actions.lua: in function 'clear'
	.../markview/actions.lua: in function 'render'
	.../markview/autocmds.lua: in function 'fn'

Root cause

The cursor autocmd (CursorMoved / CursorMovedI) in autocmds.lua defers its work through a debounce timer wrapped in vim.schedule_wrap:

autocmds.cursor_timer:start(delay, 0, vim.schedule_wrap(action));

So the action closure runs on a later event-loop tick than the autocmd that queued it. The buffer id captured in args.buf is validated when the autocmd fires (state.buf_attached(args.buf) etc.), but not when the scheduled callback finally runs.

If that buffer is wiped during the debounce window — e.g. a transient Telescope preview buffer, a scratch buffer, or any :bwipeout — the captured id becomes dangling. The callback then flows through actions.render/actions.clearrenderer.clearmarkdown.clearnvim_buf_clear_namespace with a dead id, and none of those layers re-validate the buffer, so the raw API call throws.

Fix

Re-validate args.buf at the top of the scheduled action closure, so it bails out cleanly when the buffer no longer exists:

local function action ()
	if not args.buf or not vim.api.nvim_buf_is_valid(args.buf) then
		return;
	end
	--
end

This is the tightest single choke point — it protects the splitview_render, render, and clear paths at once, right before any nvim_buf_* call can reach the stale id.

Notes

  • Behaviour is unchanged for live buffers; only the wiped-buffer race is short-circuited.
  • Reproducible reliably by opening a Telescope preview of a Markdown file and moving the cursor as the preview buffer is recycled/wiped.

The cursor autocmd (CursorMoved/CursorMovedI) defers its work through a
debounce timer wrapped in vim.schedule_wrap, so the `action` closure runs on
a later event-loop tick than the autocmd that queued it. The buffer id
captured in `args.buf` is validated when the autocmd fires but not when the
scheduled callback finally runs.

If that buffer is wiped during the debounce window (e.g. a transient Telescope
preview or scratch buffer), the captured id becomes dangling. The callback
then reaches actions.render/clear -> renderer.clear -> markdown.clear ->
nvim_buf_clear_namespace with a dead id, throwing:

    Invalid buffer id: N

Re-validate `args.buf` at the top of the scheduled closure so it bails out
cleanly when the buffer no longer exists. Behaviour is unchanged for live
buffers; only the wiped-buffer race is short-circuited.
@OXY2DEV OXY2DEV merged commit 092c087 into OXY2DEV:main Jul 6, 2026
1 check passed
@ImmanuelHaffner ImmanuelHaffner deleted the fix/cursor-action-buffer-validity branch July 7, 2026 12:55
ImmanuelHaffner added a commit to ImmanuelHaffner/markview.nvim that referenced this pull request Jul 11, 2026
Merge upstream/main (OXY2DEV) up to 41ecde1 into the fork's dev branch.

Incorporated upstream:
- 41ecde1 feat(code_blocks): allow `style` to be dynamically changed (fn)
- e85f4a3 fix(code_blocks): icons not rendering in block style
- 092c087 fix(autocmds): guard debounced cursor action vs wiped buffer (OXY2DEV#519)
  -- identical to our 270a687; merge reconciles it as a no-op.

Conflict: lua/markview/renderers/markdown.lua code-block dispatch.
Resolved by keeping our wrap_forces_simple()/block_on_wrap machinery as the
wrap authority (needed for block_on_wrap = 'adaptive', which upstream's
wrap->"simple" style-fn cannot express). Upstream's dynamic `style` is still
honoured: spec.get(eval_args=...) resolves style to a string before dispatch,
so a user style-fn works and our adaptive gate runs on top.

Net behavioural change vs our dev tip: block-style code blocks now render the
language sign glyph in the gutter (from e85f4a3).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants