Skip to content

fix(input): resume async vim.ui.input await when the prompt closes without a callback - #1982

Open
dmbhatti wants to merge 2 commits into
NeogitOrg:masterfrom
dmbhatti:fix/popup-action-lock-input-leak
Open

fix(input): resume async vim.ui.input await when the prompt closes without a callback#1982
dmbhatti wants to merge 2 commits into
NeogitOrg:masterfrom
dmbhatti:fix/popup-action-lock-input-leak

Conversation

@dmbhatti

@dmbhatti dmbhatti commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Some vim.ui.input implementations (e.g. snacks.nvim) only invoke on_confirm via their own confirm/cancel actions and skip it entirely when the prompt window is dismissed another way (:q, c, a focus steal, a programmatic close). A dropped callback parked the awaiting coroutine forever -- and because popup actions run under a single shared permit lock (popup M.__lock), one parked action silently turned every subsequent popup action into a no-op until Neovim was restarted.

Harden the async input wrapper to guarantee its callback fires exactly once: the real value if the user responds, or nil (a normal cancel) if the prompt's floating window closes first, via a one-shot WinClosed autocmd (deferred so a genuine value always wins the race). This releases the popup action permit on every path without changing the locking model.

Add a regression spec covering stray-close-as-cancel, value-wins-race, explicit-cancel, and a synchronous cmdline-style implementation.

@dmbhatti

dmbhatti commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

requires more consideration. I experienced a case where issue was encountered even with this fix in place

this unblocked it

:lua local L=require("neogit.lib.popup").__lock; print(("permits=%d waiting=%d"):format(L.permits, #L._waiting)); L.permits=1; L._waiting={}

Danish Bhatti added 2 commits August 13, 2026 08:57
…thout a callback

Some vim.ui.input implementations (e.g. snacks.nvim) only invoke on_confirm
via their own confirm/cancel actions and skip it entirely when the prompt
window is dismissed another way (:q, <C-w>c, a focus steal, a programmatic
close). A dropped callback parked the awaiting coroutine forever -- and because
popup actions run under a single shared permit lock (popup M.__lock), one
parked action silently turned every subsequent popup action into a no-op until
Neovim was restarted.

Harden the async input wrapper to guarantee its callback fires exactly once:
the real value if the user responds, or nil (a normal cancel) if the prompt's
floating window closes first, via a one-shot WinClosed autocmd (deferred so a
genuine value always wins the race). This releases the popup action permit on
every path without changing the locking model.

Add a regression spec covering stray-close-as-cancel, value-wins-race,
explicit-cancel, and a synchronous cmdline-style implementation.
…s its callback

Popup actions run under a single shared permit lock. An action that awaits an
operation (a git process, a finder selection, user input) holds the permit
while its coroutine is suspended and only returns it once the coroutine resumes
and reaches permit:forget(). If that resume callback is ever dropped, the
coroutine parks forever and the permit is never returned -- silently turning
every subsequent popup action into a no-op until Neovim restarts (e.g. `fa`
fetch-all or `la` log-all open their popup, close it, and nothing happens).

The prior input fix closed one leaf (vim.ui.input). Close the remaining common
ones and add a defensive backstop:

- process: on_exit invoked the resume callback only after its presentation and
  console-management logic; a throw there (e.g. auto_close_console closing an
  already-gone window) skipped the callback. Wrap that logic in pcall so
  cb(res) always fires. Also fix the spawn-failure path, where the cb(nil) call
  was dead code after error().

- async: a wrapped leaf that throws synchronously before invoking its callback
  (a failed spawn, a picker that errors on open) left the coroutine suspended
  forever. Resume it with the error instead, so the await re-raises and a
  surrounding pcall can run its cleanup (releasing the permit).

- finder: guarantee find's on_select fires exactly once on every dismissal
  path, and add a close/abort net to each picker branch that lacked one
  (telescope BufWipeout, fzf-lua on_close, mini.pick MiniPickStop,
  vim.ui.select WinClosed).

Add regression specs for each layer.
@dmbhatti
dmbhatti force-pushed the fix/popup-action-lock-input-leak branch from 1744914 to 5e4863b Compare August 13, 2026 07:03
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.

1 participant