fix: run the loading spinner tick on the main loop - #178
Merged
Conversation
`new` started the spinner timer with a plain callback, so `update_spinner` ran in a fast event context and called `vim.notify` from there. Notification backends open and close floating windows in that call - snacks.notifier does it synchronously from `add` - which is not allowed in a fast event, and the window it opens is dropped before the backend registers it. That leaves a `Fetching ...` toast on screen that nothing can close afterwards, since the backend has no record of it: `Snacks.notifier.hide()` does not touch it and the plugin never held a handle for it either. This is a separate bug from the lost handle fixed in vuki656#177 and predates it. It shows up in a pnpm workspace because `show` starts two jobs there, and every buffer switch during loading adds another pair of instances, so the 16 replaces per second per instance get plenty of chances to race. Wrap the tick in `vim.schedule_wrap` so every notification is sent from the main loop, and bail out of a tick scheduled before `reset_state` closed the timer. Measured on a real pnpm workspace, switching package.json buffers every 250ms while the jobs are in flight, 10 runs each, interleaved: 5/10 runs left a stranded toast before, 0/10 after.
Owner
|
Thanks <3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A
Fetching …notification can still stay on the screen after both jobs have finished, also with #177 merged.This is a different bug with the same symptom. #177 fixed a lost handle inside the plugin, and that part works: the queue becomes empty, the spinner timer stops, and every handle gets its final message. The notification that stays behind now is one that the notification backend itself does not know about anymore, so even
:lua Snacks.notifier.hide()does not remove it. The only way to close it isnvim_win_closeon the floating window.How to reproduce
package.jsonin a pnpm workspace, withyqon$PATHand snacks.notifier installed. Fetching …notifications are still spinning, open anotherpackage.jsonand go back, a few times (about every 250 ms).A workspace shows this most often, because
actions/show.luastarts two jobs there, and every buffer change during loading adds two more loading instances. A simple open without buffer changes almost never shows it.Cause
M.newstarts the spinner timer with a plain callback:A libuv timer callback runs in a fast event context, and
update_spinnercallsvim.notifythere. This is not allowed. With the defaultvim.notifyit fails directly:Plugin users do not see that error, because
M.__is_notifying()is only true when a backend is installed, and thenvim.notifyis a Lua function that does not raise. But the backend still does its own work in that call, and window API is not usable there either (nvim_create_bufalready fails in a fast event context). The spinner sends 16 notifications per second per instance, so it hits this very often.The result with snacks.notifier is a floating window that the notifier never puts in its queue. I checked its internal state during a failing run: the window that stays on the screen has no notification id,
hide()is never called for it, and every notification that snacks does know about is closed correctly. So the window has no owner anymore, and nothing can close it.I did not find the exact line inside snacks where the window is lost, and I think the plugin should not depend on that: sending notifications from a fast event context is wrong on our side, whatever the backend does with it.
Fix
Send the notifications from the main loop:
vim.schedule_wrapmoves each tick out of the fast event context. The guard is for a tick that was already scheduled whenreset_state()closed the timer, so a finished spinner does not send one more notification.Nothing else changes:
update_spinneritself is not touched, and the spinner still runs every 60 ms.Measurement
I ran a pnpm workspace with a script that changes the
package.jsonbuffer every 250 ms while the jobs are running, and counted the notification windows that are still open 16 seconds later. The runs with and without the fix were interleaved, so a slow or fast machine affects both the same way.Tests
The
vim.notifystub inloading_status_spec.luanow also recordsvim.in_fast_event()for every notification. The new case starts a loading instance, waits for a real timer tick, and checks that no notification was sent from a fast event context.The case fails on
master(Left: true,Right: false) and passes with the fix.make testpasses (117 cases), andstylua --check .is clean.Neovim version: v0.12.4. Notification backend used for testing: snacks.notifier.