261019.1 · 348df165f1773dd08c5d710add3fd0db6d421e6a
fix(buffers): assertion failed when buflist is nil
Problem: the `contents` callback calls `utils.CTX()` without `includeBuflist`, so if the context is recreated between picker setup (which passes `includeBuflist=true`) and the async `contents` callback, `buflist` is nil and the assertion crashes. This happens with `reload` actions such as `actions.buf_del` when the picker is closed while the reload is still in flight.
Solution: pass `{ includeBuflist = true }` explicitly, ensuring the context always has `buflist` populated when accessed inside the callback.
Add a regression test that opens the buffer picker, captures its reload command, closes the picker (clearing the context), and then runs the stale reload. Without the fix this reproduces the exact assertion failure from #2736.
Closes #2736
reading declarations
lua/fzf-lua/providers/buffers.lua
@@ -216,7 +216,12 @@ M.buffers = function(opts) if not opts then return end local contents = function(cb)- local buflist = assert(utils.CTX().buflist)+ -- `contents` can run asynchronously via fzf's `reload` action (#2736),+ -- possibly after the picker was closed or its context was re-created+ -- without `buflist` (e.g. another picker started). Request the buffer+ -- list explicitly instead of relying on the context created when the+ -- picker was opened.+ local buflist = assert(utils.CTX({ includeBuflist = true }).buflist) local filtered, _, max_bufnr = filter_buffers(opts, buflist) if next(filtered) then
@@ -0,0 +1,57 @@+---@diagnostic disable: unused-local, unused-function, unused+local MiniTest = require("mini.test")+local helpers = require("fzf-lua.test.helpers")+local child = helpers.new_child_neovim()+local eq = helpers.expect.equality+local new_set = MiniTest.new_set+local exec_lua = child.lua++local T = helpers.new_set_with_child(child)++T["buffers"] = new_set({ n_retry = not helpers.IS_LINUX() and 5 or nil })++-- Regression for #2736: `actions.buf_del` uses `reload=true`, which runs the+-- contents callback asynchronously via a separate RPC process. If the picker+-- is closed (or its context is otherwise re-created without `buflist`) before+-- that reload is processed, the old code asserted on `ctx.buflist`.+T["buffers"]["reload after picker close"] = function()+ exec_lua([[+ for i = 1, 3 do+ vim.cmd.edit(vim.fn.tempname())+ vim.api.nvim_buf_set_lines(0, 0, -1, false, { "buffer " .. i })+ end+ vim.cmd.edit(vim.fn.tempname())+ ]])++ child.FzfLua.buffers({ previewer = false })+ child.wait_until(function() return child.lua_get([[_G._fzf_lua_on_create]]) == true end)+ child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)+ local reload_cmd = exec_lua([[return FzfLua.config.__resume_data.contents]])+ eq(type(reload_cmd), "string")++ -- closing the picker clears the global context+ child.type_keys("<c-c>")+ child.wait_until(function() return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL end)+ child.wait_until(function() return exec_lua([[return FzfLua.utils.__CTX() == nil]]) end)++ -- deliver the pending reload, as if it was still in flight when the picker+ -- was closed+ local job = exec_lua([[+ local cmd = ...+ _G._fzf_lua_reload_exit = nil+ return vim.fn.jobstart(cmd, {+ env = { FZF_LUA_SERVER = vim.v.servername },+ on_exit = function(_, code) _G._fzf_lua_reload_exit = code end,+ })+ ]], { reload_cmd })+ eq(job > 0, true, { fail_reason = "jobstart failed: " .. tostring(job) })+ child.wait_until(function()+ return exec_lua([[return _G._fzf_lua_reload_exit ~= nil]]) == true+ end)+ eq(exec_lua([[return _G._fzf_lua_reload_exit]]), 0)++ local msgs = child.cmd_capture("messages")+ eq(msgs:match("assertion failed") == nil, true, msgs)+end++return T