From 23519568f1823fcab8c6835b7b7388c1f373fe6e Mon Sep 17 00:00:00 2001 From: zuqini Date: Wed, 8 Apr 2026 23:25:38 -0700 Subject: [PATCH] fix: forward ev.data, validate buffer, chain dependencies, and deduplicate augroups on event re-fire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After lazy-loading a plugin, the re-fired autocmd was missing ev.data (crashing callbacks like LspAttach that read event.data.client_id), could error on wiped buffers, lacked dependency chaining (BufReadPre → BufReadPost → FileType), and double-fired pre-existing handlers. Extract shared refire module with snapshot-based augroup dedup: before loading, snapshot existing augroups per event; after loading, fire only newly-added groups. FileType is exempt from dedup (all handlers re-fire). All nvim_exec_autocmds calls are pcall-wrapped so one plugin's error does not break the chain. Also wrap process_spec in pcall so a failed plugin load doesn't prevent the re-fire chain from executing, and remove vim.schedule deferral in ft.lua to fire synchronously (matching lazy.nvim's behavior). Closes #9 --- lua/zpack/lazy_trigger/event.lua | 12 +- lua/zpack/lazy_trigger/ft.lua | 22 +- lua/zpack/lazy_trigger/refire.lua | 89 ++++++ tests/helpers.lua | 1 + tests/lazy_event_test.lua | 507 ++++++++++++++++++++++++++++++ tests/lazy_ft_test.lua | 226 +++++++++++++ 6 files changed, 843 insertions(+), 14 deletions(-) create mode 100644 lua/zpack/lazy_trigger/refire.lua diff --git a/lua/zpack/lazy_trigger/event.lua b/lua/zpack/lazy_trigger/event.lua index c33c08e..f98551d 100644 --- a/lua/zpack/lazy_trigger/event.lua +++ b/lua/zpack/lazy_trigger/event.lua @@ -1,6 +1,7 @@ local util = require('zpack.utils') local state = require('zpack.state') local loader = require('zpack.plugin_loader') +local refire = require('zpack.lazy_trigger.refire') local M = {} @@ -83,8 +84,15 @@ M.setup = function(pack_spec, spec, event) if #other_events > 0 then util.autocmd(other_events, function(ev) - loader.process_spec(pack_spec) - vim.api.nvim_exec_autocmds(ev.event, { buffer = ev.buf, modeline = false }) + local snap = refire.snapshot(ev.event) + local ok, err = pcall(loader.process_spec, pack_spec) + if not ok then + vim.schedule(function() + vim.notify(("Failed to load plugin: %s"):format(err), vim.log.levels.ERROR) + end) + return + end + refire.exec(ev.event, ev.buf, ev.data, snap) end, { group = state.lazy_group, once = true, pattern = normalized_event.pattern }) end end diff --git a/lua/zpack/lazy_trigger/ft.lua b/lua/zpack/lazy_trigger/ft.lua index 71c0a88..1c5532f 100644 --- a/lua/zpack/lazy_trigger/ft.lua +++ b/lua/zpack/lazy_trigger/ft.lua @@ -1,6 +1,7 @@ local util = require('zpack.utils') local state = require('zpack.state') local loader = require('zpack.plugin_loader') +local refire = require('zpack.lazy_trigger.refire') local M = {} @@ -10,18 +11,15 @@ M.setup = function(pack_spec, ft) local filetypes = util.normalize_string_list(ft) util.autocmd("FileType", function(ev) - loader.process_spec(pack_spec) - - -- Re-trigger events for the buffer that triggered loading to ensure LSP/Treesitter attach - vim.schedule(function() - local bufnr = ev.buf - if not vim.api.nvim_buf_is_valid(bufnr) then - return - end - vim.api.nvim_exec_autocmds("BufReadPre", { buffer = bufnr, modeline = false }) - vim.api.nvim_exec_autocmds("BufReadPost", { buffer = bufnr, modeline = false }) - vim.api.nvim_exec_autocmds("FileType", { buffer = bufnr, modeline = false }) - end) + local snap = refire.snapshot("FileType") + local ok, err = pcall(loader.process_spec, pack_spec) + if not ok then + vim.schedule(function() + vim.notify(("Failed to load plugin: %s"):format(err), vim.log.levels.ERROR) + end) + return + end + refire.exec("FileType", ev.buf, ev.data, snap) end, { group = state.lazy_group, pattern = filetypes, once = true }) end diff --git a/lua/zpack/lazy_trigger/refire.lua b/lua/zpack/lazy_trigger/refire.lua new file mode 100644 index 0000000..0bcc9c5 --- /dev/null +++ b/lua/zpack/lazy_trigger/refire.lua @@ -0,0 +1,89 @@ +local M = {} + +local triggers = { + FileType = "BufReadPost", + BufReadPost = "BufReadPre", +} + +---@param event string +---@return string[] +local function build_chain(event) + local chain = {} + local current = event + while current do + table.insert(chain, 1, current) + current = triggers[current] + end + return chain +end + +---@param event string +---@return table?> +M.snapshot = function(event) + local chain = build_chain(event) + local snap = {} + for _, ev in ipairs(chain) do + if ev == "FileType" then + snap[ev] = nil + else + local existing = {} + for _, au in ipairs(vim.api.nvim_get_autocmds({ event = ev })) do + if au.group then + existing[au.group] = true + end + end + snap[ev] = existing + end + end + return snap +end + +---@param ev string +---@param buf number +---@param ev_data? any +---@param snap table?> +local function fire_new_groups(ev, buf, ev_data, snap) + local pre_existing = snap[ev] + if pre_existing == nil then + pcall(vim.api.nvim_exec_autocmds, ev, { + buffer = buf, + data = ev_data, + modeline = false, + }) + return + end + + local fired = {} + for _, au in ipairs(vim.api.nvim_get_autocmds({ event = ev })) do + if au.group and not pre_existing[au.group] and not fired[au.group] then + fired[au.group] = true + pcall(vim.api.nvim_exec_autocmds, ev, { + buffer = buf, + group = au.group_name, + data = ev_data, + modeline = false, + }) + end + end +end + +---@param event string +---@param buf number +---@param data? any +---@param snapshot table?> +M.exec = function(event, buf, data, snapshot) + if not vim.api.nvim_buf_is_valid(buf) then + return + end + + local chain = build_chain(event) + + for _, ev in ipairs(chain) do + if not vim.api.nvim_buf_is_valid(buf) then + return + end + fire_new_groups(ev, buf, ev == event and data or nil, snapshot) + end +end + +return M diff --git a/tests/helpers.lua b/tests/helpers.lua index 4613fc1..6a4f43a 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -242,6 +242,7 @@ function M.cleanup_test_env() package.loaded['zpack.plugin_loader'] = nil package.loaded['zpack.lazy_trigger.event'] = nil package.loaded['zpack.lazy_trigger.ft'] = nil + package.loaded['zpack.lazy_trigger.refire'] = nil package.loaded['zpack.lazy_trigger.cmd'] = nil package.loaded['zpack.lazy_trigger.keys'] = nil package.loaded['zpack.keymap'] = nil diff --git a/tests/lazy_event_test.lua b/tests/lazy_event_test.lua index 5714b33..de3ec4d 100644 --- a/tests/lazy_event_test.lua +++ b/tests/lazy_event_test.lua @@ -159,6 +159,513 @@ return function() helpers.cleanup_test_env() end) + helpers.test("re-fired event forwards original ev.data", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_data.lua') + local received_data = nil + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPost', { + group = test_group, + pattern = '*', + callback = function(ev) + received_data = ev.data + end, + once = true, + }) + end + + vim.api.nvim_exec_autocmds('BufReadPost', { + buffer = buf, + data = { client_id = 42 }, + }) + + helpers.flush_pending() + + helpers.assert_not_nil(received_data, "Re-fired event should forward data") + helpers.assert_equal(received_data.client_id, 42, "Re-fired event data should contain original fields") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire triggers when buffer is still valid", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_refire.lua') + local refire_count = 0 + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPost', { + group = test_group, + pattern = '*', + callback = function() + refire_count = refire_count + 1 + end, + once = true, + }) + end + + vim.api.nvim_exec_autocmds('BufReadPost', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_equal(refire_count, 1, "Should re-fire event when buffer is valid") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire skips invalid buffer without error", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_invalid.lua') + local refire_count = 0 + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_buf_delete(buf, { force = true }) + vim.api.nvim_create_autocmd('BufReadPost', { + group = test_group, + pattern = '*', + callback = function() + refire_count = refire_count + 1 + end, + once = true, + }) + end + + local ok = pcall(vim.api.nvim_exec_autocmds, 'BufReadPost', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_true(ok, "Should not error when buffer becomes invalid during lazy-load") + helpers.assert_equal(refire_count, 0, "Should skip re-fire when buffer is no longer valid") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire chains BufReadPre before BufReadPost", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_chain.lua') + local fired_events = {} + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPre', { + group = test_group, + pattern = '*', + callback = function() table.insert(fired_events, 'BufReadPre') end, + once = true, + }) + vim.api.nvim_create_autocmd('BufReadPost', { + group = test_group, + pattern = '*', + callback = function() table.insert(fired_events, 'BufReadPost') end, + once = true, + }) + end + + vim.api.nvim_exec_autocmds('BufReadPost', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_equal(#fired_events, 2, "Should fire both BufReadPre and BufReadPost") + helpers.assert_equal(fired_events[1], 'BufReadPre', "BufReadPre should fire first") + helpers.assert_equal(fired_events[2], 'BufReadPost', "BufReadPost should fire second") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire chains BufReadPre and BufReadPost before FileType", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'FileType', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_ft_chain.lua') + local fired_events = {} + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + for _, ev_name in ipairs({ 'BufReadPre', 'BufReadPost', 'FileType' }) do + vim.api.nvim_create_autocmd(ev_name, { + group = test_group, + pattern = '*', + callback = function() table.insert(fired_events, ev_name) end, + once = true, + }) + end + end + + vim.api.nvim_exec_autocmds('FileType', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_equal(#fired_events, 3, "Should fire all three events") + helpers.assert_equal(fired_events[1], 'BufReadPre', "BufReadPre should fire first") + helpers.assert_equal(fired_events[2], 'BufReadPost', "BufReadPost should fire second") + helpers.assert_equal(fired_events[3], 'FileType', "FileType should fire last") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire chain only forwards data to the original event", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_data_chain.lua') + local pre_data = 'not_called' + local post_data = 'not_called' + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPre', { + group = test_group, + pattern = '*', + callback = function(ev) pre_data = ev.data end, + once = true, + }) + vim.api.nvim_create_autocmd('BufReadPost', { + group = test_group, + pattern = '*', + callback = function(ev) post_data = ev.data end, + once = true, + }) + end + + vim.api.nvim_exec_autocmds('BufReadPost', { + buffer = buf, + data = { client_id = 99 }, + }) + + helpers.flush_pending() + + helpers.assert_nil(pre_data, "BufReadPre should not receive data") + helpers.assert_not_nil(post_data, "BufReadPost should receive data") + helpers.assert_equal(post_data.client_id, 99, "BufReadPost data should match original") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("event without chain does not fire dependency events", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'LspAttach', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_no_chain.lua') + local fired_events = {} + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('LspAttach', { + group = test_group, + pattern = '*', + callback = function() table.insert(fired_events, 'LspAttach') end, + once = true, + }) + vim.api.nvim_create_autocmd('BufReadPre', { + group = test_group, + pattern = '*', + callback = function() table.insert(fired_events, 'BufReadPre') end, + once = true, + }) + end + + vim.api.nvim_exec_autocmds('LspAttach', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_equal(#fired_events, 1, "Should only fire the original event") + helpers.assert_equal(fired_events[1], 'LspAttach', "Only LspAttach should fire, no chain") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire does not double-fire pre-existing augroup handlers", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local pre_existing_group = vim.api.nvim_create_augroup('PreExisting', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_dedup.lua') + local pre_existing_count = 0 + + vim.api.nvim_create_autocmd('BufReadPost', { + group = pre_existing_group, + pattern = '*', + callback = function() + pre_existing_count = pre_existing_count + 1 + end, + }) + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + end + + vim.api.nvim_exec_autocmds('BufReadPost', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_equal(pre_existing_count, 1, "Pre-existing handler should fire once (original only), not twice") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(pre_existing_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire fires only newly-added augroup handlers", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local pre_existing_group = vim.api.nvim_create_augroup('PreExisting', { clear = true }) + local new_group = vim.api.nvim_create_augroup('NewPlugin', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_new_only.lua') + local pre_existing_count = 0 + local new_count = 0 + + vim.api.nvim_create_autocmd('BufReadPost', { + group = pre_existing_group, + pattern = '*', + callback = function() + pre_existing_count = pre_existing_count + 1 + end, + }) + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPost', { + group = new_group, + pattern = '*', + callback = function() + new_count = new_count + 1 + end, + }) + end + + vim.api.nvim_exec_autocmds('BufReadPost', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_equal(pre_existing_count, 1, "Pre-existing handler should fire once (original only)") + helpers.assert_equal(new_count, 1, "New handler should fire once (re-fire only)") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(pre_existing_group) + vim.api.nvim_del_augroup_by_id(new_group) + helpers.cleanup_test_env() + end) + + helpers.test("re-fire pcall prevents one group error from breaking the chain", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local error_group = vim.api.nvim_create_augroup('ErrorPlugin', { clear = true }) + local ok_group = vim.api.nvim_create_augroup('OkPlugin', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + event = 'BufReadPost', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_pcall.lua') + local ok_fired = false + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPre', { + group = error_group, + pattern = '*', + callback = function() error("intentional test error") end, + once = true, + }) + vim.api.nvim_create_autocmd('BufReadPost', { + group = ok_group, + pattern = '*', + callback = function() ok_fired = true end, + once = true, + }) + end + + local ok = pcall(vim.api.nvim_exec_autocmds, 'BufReadPost', { buffer = buf }) + + helpers.flush_pending() + + helpers.assert_true(ok_fired, "BufReadPost handler should still fire despite BufReadPre error") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(error_group) + vim.api.nvim_del_augroup_by_id(ok_group) + helpers.cleanup_test_env() + end) + helpers.test("lazy event plugin does not load at startup", function() helpers.setup_test_env() local state = require('zpack.state') diff --git a/tests/lazy_ft_test.lua b/tests/lazy_ft_test.lua index be55a48..dea7bda 100644 --- a/tests/lazy_ft_test.lua +++ b/tests/lazy_ft_test.lua @@ -50,6 +50,232 @@ return function() helpers.cleanup_test_env() end) + helpers.test("ft re-fire chains BufReadPre, BufReadPost, and FileType", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + ft = 'lua', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_ft.lua') + local fired_events = {} + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + for _, ev_name in ipairs({ 'BufReadPre', 'BufReadPost', 'FileType' }) do + vim.api.nvim_create_autocmd(ev_name, { + group = test_group, + pattern = '*', + callback = function() table.insert(fired_events, ev_name) end, + once = true, + }) + end + end + + vim.api.nvim_set_current_buf(buf) + vim.bo[buf].filetype = 'lua' + + helpers.flush_pending() + + helpers.assert_equal(#fired_events, 3, "Should fire all three events in chain") + helpers.assert_equal(fired_events[1], 'BufReadPre', "BufReadPre should fire first") + helpers.assert_equal(fired_events[2], 'BufReadPost', "BufReadPost should fire second") + helpers.assert_equal(fired_events[3], 'FileType', "FileType should fire last") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("ft re-fire forwards data only to FileType", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local test_group = vim.api.nvim_create_augroup('ZpackTest', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + ft = 'lua', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_ft_data.lua') + local pre_data = 'not_called' + local post_data = 'not_called' + local ft_data = 'not_called' + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPre', { + group = test_group, + pattern = '*', + callback = function(ev) pre_data = ev.data end, + once = true, + }) + vim.api.nvim_create_autocmd('BufReadPost', { + group = test_group, + pattern = '*', + callback = function(ev) post_data = ev.data end, + once = true, + }) + vim.api.nvim_create_autocmd('FileType', { + group = test_group, + pattern = '*', + callback = function(ev) ft_data = ev.data end, + once = true, + }) + end + + vim.api.nvim_set_current_buf(buf) + vim.bo[buf].filetype = 'lua' + + helpers.flush_pending() + + helpers.assert_nil(pre_data, "BufReadPre should not receive data") + helpers.assert_nil(post_data, "BufReadPost should not receive data") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(test_group) + helpers.cleanup_test_env() + end) + + helpers.test("ft re-fire fires ALL FileType handlers including pre-existing", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local pre_existing_group = vim.api.nvim_create_augroup('PreExistingFT', { clear = true }) + local new_group = vim.api.nvim_create_augroup('NewPluginFT', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + ft = 'lua', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_ft_all.lua') + local pre_existing_count = 0 + local new_count = 0 + + vim.api.nvim_create_autocmd('FileType', { + group = pre_existing_group, + pattern = '*', + callback = function() + pre_existing_count = pre_existing_count + 1 + end, + }) + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('FileType', { + group = new_group, + pattern = '*', + callback = function() + new_count = new_count + 1 + end, + }) + end + + vim.api.nvim_set_current_buf(buf) + vim.bo[buf].filetype = 'lua' + + helpers.flush_pending() + + helpers.assert_true(pre_existing_count >= 2, "Pre-existing FileType handler should fire from both original and re-fire") + helpers.assert_true(new_count >= 1, "New FileType handler should fire from re-fire") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(pre_existing_group) + vim.api.nvim_del_augroup_by_id(new_group) + helpers.cleanup_test_env() + end) + + helpers.test("ft re-fire deduplicates BufReadPost chain events", function() + helpers.setup_test_env() + + local loader = require('zpack.plugin_loader') + local original_process_spec = loader.process_spec + local pre_existing_group = vim.api.nvim_create_augroup('PreExistingBRP', { clear = true }) + local new_group = vim.api.nvim_create_augroup('NewPluginBRP', { clear = true }) + + require('zpack').setup({ + spec = { + { + 'test/plugin', + ft = 'lua', + }, + }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, vim.fn.tempname() .. '/test_ft_dedup.lua') + local pre_existing_count = 0 + local new_count = 0 + + vim.api.nvim_create_autocmd('BufReadPost', { + group = pre_existing_group, + pattern = '*', + callback = function() + pre_existing_count = pre_existing_count + 1 + end, + }) + + loader.process_spec = function(pack_spec) + original_process_spec(pack_spec) + vim.api.nvim_create_autocmd('BufReadPost', { + group = new_group, + pattern = '*', + callback = function() + new_count = new_count + 1 + end, + }) + end + + vim.api.nvim_set_current_buf(buf) + vim.bo[buf].filetype = 'lua' + + helpers.flush_pending() + + helpers.assert_equal(pre_existing_count, 0, "Pre-existing BufReadPost handler should not fire from re-fire") + helpers.assert_equal(new_count, 1, "New BufReadPost handler should fire once from re-fire") + + loader.process_spec = original_process_spec + vim.api.nvim_del_augroup_by_id(pre_existing_group) + vim.api.nvim_del_augroup_by_id(new_group) + helpers.cleanup_test_env() + end) + helpers.test("lazy ft plugin does not load at startup", function() helpers.setup_test_env() local state = require('zpack.state')