local helpers = require('test.functional.helpers')(after_each) local clear = helpers.clear local command = helpers.command local eq = helpers.eq local neq = helpers.neq local exec_lua = helpers.exec_lua local matches = helpers.matches local meths = helpers.meths local source = helpers.source before_each(clear) describe('autocmd api', function() describe('nvim_create_autocmd', function() it('does not allow "command" and "callback" in the same autocmd', function() local ok, _ = pcall(meths.create_autocmd, { event = "BufReadPost", pattern = "*.py,*.pyi", command = "echo 'Should Have Errored", callback = "not allowed", }) eq(false, ok) end) it('doesnt leak when you use ++once', function() eq(1, exec_lua([[ local count = 0 vim.api.nvim_create_autocmd { event = "FileType", pattern = "*", callback = function() count = count + 1 end, once = true } vim.cmd "set filetype=txt" vim.cmd "set filetype=python" return count ]], {})) end) it('allows passing buffer by key', function() meths.set_var('called', 0) meths.create_autocmd { event = "Filetype", command = "let g:called = g:called + 1", buffer = 0, } meths.command "set filetype=txt" eq(1, meths.get_var('called')) -- switch to a new buffer meths.command "new" meths.command "set filetype=python" eq(1, meths.get_var('called')) end) it('does not allow passing buffer and patterns', function() local ok = pcall(meths.create_autocmd, { event = "Filetype", command = "let g:called = g:called + 1", buffer = 0, pattern = "*.py", }) eq(false, ok) end) it('does not allow passing invalid buffers', function() local ok, msg = pcall(meths.create_autocmd, { event = "Filetype", command = "let g:called = g:called + 1", buffer = -1, }) eq(false, ok) matches('Invalid buffer id', msg) end) it('errors on non-functions for cb', function() eq(false, pcall(exec_lua, [[ vim.api.nvim_create_autocmd { event = "BufReadPost", pattern = "*.py,*.pyi", callback = 5, } ]])) end) it('allow passing pattern and in same pattern', function() local ok = pcall(meths.create_autocmd, { event = "BufReadPost", pattern = "*.py,", command = "echo 'Should Not Error'" }) eq(true, ok) end) it('should handle multiple values as comma separated list', function() meths.create_autocmd { event = "BufReadPost", pattern = "*.py,*.pyi", command = "echo 'Should Not Have Errored'" } -- We should have one autocmd for *.py and one for *.pyi eq(2, #meths.get_autocmds { event = "BufReadPost" }) end) it('should handle multiple values as array', function() meths.create_autocmd { event = "BufReadPost", pattern = { "*.py", "*.pyi", }, command = "echo 'Should Not Have Errored'" } -- We should have one autocmd for *.py and one for *.pyi eq(2, #meths.get_autocmds { event = "BufReadPost" }) end) describe('desc', function() it('can add description to one autocmd', function() meths.create_autocmd { event = "BufReadPost", pattern = "*.py", command = "echo 'Should Not Have Errored'", desc = "Can show description", } eq("Can show description", meths.get_autocmds { event = "BufReadPost" }[1].desc) end) it('can add description to multiple autocmd', function() meths.create_autocmd { event = "BufReadPost", pattern = {"*.py", "*.pyi"}, command = "echo 'Should Not Have Errored'", desc = "Can show description", } local aus = meths.get_autocmds { event = "BufReadPost" } eq(2, #aus) eq("Can show description", aus[1].desc) eq("Can show description", aus[2].desc) end) end) pending('script and verbose settings', function() it('marks API client', function() meths.create_autocmd { event = "BufReadPost", pattern = "*.py", command = "echo 'Should Not Have Errored'", desc = "Can show description", } local aus = meths.get_autocmds { event = "BufReadPost" } eq(1, #aus, aus) end) end) end) describe('nvim_get_autocmds', function() describe('events', function() it('should return one autocmd when there is only one for an event', function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "1"]] local aus = meths.get_autocmds { event = "InsertEnter" } eq(1, #aus) end) it('should return two autocmds when there are two for an event', function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "1"]] command [[au InsertEnter * :echo "2"]] local aus = meths.get_autocmds { event = "InsertEnter" } eq(2, #aus) end) it('should return the same thing if you use string or list', function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "1"]] command [[au InsertEnter * :echo "2"]] local string_aus = meths.get_autocmds { event = "InsertEnter" } local array_aus = meths.get_autocmds { event = { "InsertEnter" } } eq(string_aus, array_aus) end) it('should return two autocmds when there are two for an event', function() command [[au! InsertEnter]] command [[au! InsertLeave]] command [[au InsertEnter * :echo "1"]] command [[au InsertEnter * :echo "2"]] local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } eq(2, #aus) end) it('should return different IDs for different autocmds', function() command [[au! InsertEnter]] command [[au! InsertLeave]] command [[au InsertEnter * :echo "1"]] source [[ call nvim_create_autocmd(#{ \ event: "InsertLeave", \ command: ":echo 2", \ }) ]] local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } local first = aus[1] eq(first.id, nil) -- TODO: Maybe don't have this number, just assert it's not nil local second = aus[2] neq(second.id, nil) meths.del_autocmd(second.id) local new_aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } eq(1, #new_aus) eq(first, new_aus[1]) end) end) describe('groups', function() before_each(function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "No Group"]] command [[augroup GroupOne]] command [[ au InsertEnter * :echo "GroupOne:1"]] command [[augroup END]] command [[augroup GroupTwo]] command [[ au InsertEnter * :echo "GroupTwo:2"]] command [[ au InsertEnter * :echo "GroupTwo:3"]] command [[augroup END]] end) it('should return all groups if no group is specified', function() local aus = meths.get_autocmds { event = "InsertEnter" } if #aus ~= 4 then eq({}, aus) end eq(4, #aus) end) it('should return only the group specified', function() local aus = meths.get_autocmds { event = "InsertEnter", group = "GroupOne", } eq(1, #aus) eq([[:echo "GroupOne:1"]], aus[1].command) end) it('should return only the group specified, multiple values', function() local aus = meths.get_autocmds { event = "InsertEnter", group = "GroupTwo", } eq(2, #aus) eq([[:echo "GroupTwo:2"]], aus[1].command) eq([[:echo "GroupTwo:3"]], aus[2].command) end) end) describe('groups: 2', function() it('raises error for undefined augroup', function() local success, code = unpack(meths.exec_lua([[ return {pcall(function() vim.api.nvim_create_autocmd { event = "FileType", pattern = "*", group = "NotDefined", command = "echo 'hello'", } end)} ]], {})) eq(false, success) matches('invalid augroup: NotDefined', code) end) end) describe('patterns', function() before_each(function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "No Group"]] command [[au InsertEnter *.one :echo "GroupOne:1"]] command [[au InsertEnter *.two :echo "GroupTwo:2"]] command [[au InsertEnter *.two :echo "GroupTwo:3"]] command [[au InsertEnter :echo "Buffer"]] end) it('should should return for literal match', function() local aus = meths.get_autocmds { event = "InsertEnter", pattern = "*" } eq(1, #aus) eq([[:echo "No Group"]], aus[1].command) end) it('should return for multiple matches', function() -- vim.api.nvim_get_autocmds local aus = meths.get_autocmds { event = "InsertEnter", pattern = { "*.one", "*.two" }, } eq(3, #aus) eq([[:echo "GroupOne:1"]], aus[1].command) eq([[:echo "GroupTwo:2"]], aus[2].command) eq([[:echo "GroupTwo:3"]], aus[3].command) end) it('should work for buffer autocmds', function() local normalized_aus = meths.get_autocmds { event = "InsertEnter", pattern = "", } local raw_aus = meths.get_autocmds { event = "InsertEnter", pattern = "", } local zero_aus = meths.get_autocmds { event = "InsertEnter", pattern = "", } eq(normalized_aus, raw_aus) eq(normalized_aus, zero_aus) eq([[:echo "Buffer"]], normalized_aus[1].command) end) end) end) describe('nvim_do_autocmd', function() it("can trigger builtin autocmds", function() meths.set_var("autocmd_executed", false) meths.create_autocmd { event = "BufReadPost", pattern = "*", command = "let g:autocmd_executed = v:true", } eq(false, meths.get_var("autocmd_executed")) meths.do_autocmd { event = "BufReadPost" } eq(true, meths.get_var("autocmd_executed")) end) it("can pass the buffer", function() meths.set_var("buffer_executed", -1) eq(-1, meths.get_var("buffer_executed")) meths.create_autocmd { event = "BufLeave", pattern = "*", command = 'let g:buffer_executed = +expand("")', } -- Doesn't execute for other non-matching events meths.do_autocmd { event = "CursorHold", buffer = 1 } eq(-1, meths.get_var("buffer_executed")) meths.do_autocmd { event = "BufLeave", buffer = 1 } eq(1, meths.get_var("buffer_executed")) end) it("can pass the filename, pattern match", function() meths.set_var("filename_executed", 'none') eq('none', meths.get_var("filename_executed")) meths.create_autocmd { event = "BufEnter", pattern = "*.py", command = 'let g:filename_executed = expand("")', } -- Doesn't execute for other non-matching events meths.do_autocmd { event = "CursorHold", buffer = 1 } eq('none', meths.get_var("filename_executed")) meths.command('edit __init__.py') eq('__init__.py', meths.get_var("filename_executed")) end) it('cannot pass buf and fname', function() local ok = pcall(meths.do_autocmd, { pattern = "literally_cannot_error.rs", buffer = 1 }) eq(false, ok) end) it("can pass the filename, exact match", function() meths.set_var("filename_executed", 'none') eq('none', meths.get_var("filename_executed")) meths.command('edit other_file.txt') meths.command('edit __init__.py') eq('none', meths.get_var("filename_executed")) meths.create_autocmd { event = "CursorHoldI", pattern = "__init__.py", command = 'let g:filename_executed = expand("")', } -- Doesn't execute for other non-matching events meths.do_autocmd { event = "CursorHoldI", buffer = 1 } eq('none', meths.get_var("filename_executed")) meths.do_autocmd { event = "CursorHoldI", buffer = tonumber(meths.get_current_buf()) } eq('__init__.py', meths.get_var("filename_executed")) -- Reset filename meths.set_var("filename_executed", 'none') meths.do_autocmd { event = "CursorHoldI", pattern = '__init__.py' } eq('__init__.py', meths.get_var("filename_executed")) end) it("works with user autocmds", function() meths.set_var("matched", 'none') meths.create_autocmd { event = "User", pattern = "TestCommand", command = 'let g:matched = "matched"' } meths.do_autocmd { event = "User", pattern = "OtherCommand" } eq('none', meths.get_var('matched')) meths.do_autocmd { event = "User", pattern = "TestCommand" } eq('matched', meths.get_var('matched')) end) end) describe('nvim_create_augroup', function() before_each(function() clear() meths.set_var('executed', 0) end) local make_counting_autocmd = function(opts) opts = opts or {} local resulting = { event = "FileType", pattern = "*", command = "let g:executed = g:executed + 1", } resulting.group = opts.group resulting.once = opts.once meths.create_autocmd(resulting) end local set_ft = function(ft) ft = ft or "txt" source(string.format("set filetype=%s", ft)) end local get_executed_count = function() return meths.get_var('executed') end it('can be added in a group', function() local augroup = "TestGroup" meths.create_augroup({ name = augroup, clear = true }) make_counting_autocmd { group = augroup } set_ft("txt") set_ft("python") eq(get_executed_count(), 2) end) it('works getting called multiple times', function() make_counting_autocmd() set_ft() set_ft() set_ft() eq(get_executed_count(), 3) end) it('handles ++once', function() make_counting_autocmd {once = true} set_ft('txt') set_ft('help') set_ft('txt') set_ft('help') eq(get_executed_count(), 1) end) it('errors on unexpected keys', function() local success, code = pcall(meths.create_autocmd, { event = "FileType", pattern = "*", not_a_valid_key = "NotDefined", }) eq(false, success) matches('not_a_valid_key', code) end) it('can execute simple callback', function() exec_lua([[ vim.g.executed = false vim.api.nvim_create_autocmd { event = "FileType", pattern = "*", callback = function() vim.g.executed = true end, } ]], {}) eq(true, exec_lua([[ vim.cmd "set filetype=txt" return vim.g.executed ]], {})) end) it('calls multiple lua callbacks for the same autocmd execution', function() eq(4, exec_lua([[ local count = 0 local counter = function() count = count + 1 end vim.api.nvim_create_autocmd { event = "FileType", pattern = "*", callback = counter, } vim.api.nvim_create_autocmd { event = "FileType", pattern = "*", callback = counter, } vim.cmd "set filetype=txt" vim.cmd "set filetype=txt" return count ]], {})) end) it('properly releases functions with ++once', function() exec_lua([[ WeakTable = setmetatable({}, { __mode = "k" }) OnceCount = 0 MyVal = {} WeakTable[MyVal] = true vim.api.nvim_create_autocmd { event = "FileType", pattern = "*", callback = function() OnceCount = OnceCount + 1 MyVal = {} end, once = true } ]]) command [[set filetype=txt]] eq(1, exec_lua([[return OnceCount]], {})) exec_lua([[collectgarbage()]], {}) command [[set filetype=txt]] eq(1, exec_lua([[return OnceCount]], {})) eq(0, exec_lua([[ local count = 0 for _ in pairs(WeakTable) do count = count + 1 end return count ]]), "Should have no keys remaining") end) it('groups can be cleared', function() local augroup = "TestGroup" meths.create_augroup({ name = augroup, clear = true }) meths.create_autocmd({ group = augroup, event = "FileType", command = "let g:executed = g:executed + 1" }) set_ft("txt") set_ft("txt") eq(2, get_executed_count(), "should only count twice") meths.create_augroup({ name = augroup, clear = true }) eq({}, meths.get_autocmds { group = augroup }) set_ft("txt") set_ft("txt") eq(2, get_executed_count(), "No additional counts") end) it('groups work with once', function() local augroup = "TestGroup" meths.create_augroup({ name = augroup, clear = true }) make_counting_autocmd { group = augroup, once = true } set_ft("txt") set_ft("python") eq(get_executed_count(), 1) end) it('autocmds can be registered multiple times.', function() local augroup = "TestGroup" meths.create_augroup({ name = augroup, clear = true }) make_counting_autocmd { group = augroup, once = false } make_counting_autocmd { group = augroup, once = false } make_counting_autocmd { group = augroup, once = false } set_ft("txt") set_ft("python") eq(get_executed_count(), 3 * 2) end) it('can be deleted', function() local augroup = "WillBeDeleted" meths.create_augroup({ name = augroup, clear = true }) meths.create_autocmd { event = {"Filetype"}, pattern = "*", command = "echo 'does not matter'", } -- Clears the augroup from before, which erases the autocmd meths.create_augroup({ name = augroup, clear = true }) local result = #meths.get_autocmds { group = augroup } eq(0, result) end) it('can be used for buffer local autocmds', function() local augroup = "WillBeDeleted" meths.set_var("value_set", false) meths.create_augroup({ name = augroup, clear = true }) meths.create_autocmd { event = "Filetype", pattern = "", command = "let g:value_set = v:true", } command "new" command "set filetype=python" eq(false, meths.get_var("value_set")) end) it('can accept vimscript functions', function() source [[ let g:vimscript_executed = 0 function! MyVimscriptFunction() abort let g:vimscript_executed = g:vimscript_executed + 1 endfunction call nvim_create_autocmd(#{ \ event: "Filetype", \ pattern: ["python", "javascript"], \ callback: "MyVimscriptFunction", \ }) set filetype=txt set filetype=python set filetype=txt set filetype=javascript set filetype=txt ]] eq(2, meths.get_var("vimscript_executed")) end) end) describe('augroup!', function() it('legacy: should clear and not return any autocmds for delete groups', function() command('augroup TEMP_A') command(' autocmd! BufReadPost *.py :echo "Hello"') command('augroup END') command('augroup! TEMP_A') eq(false, pcall(meths.get_autocmds, { group = 'TEMP_A' })) -- For some reason, augroup! doesn't clear the autocmds themselves, which is just wild -- but we managed to keep this behavior. eq(1, #meths.get_autocmds { event = 'BufReadPost' }) end) it('legacy: remove augroups that have no autocmds', function() command('augroup TEMP_AB') command('augroup END') command('augroup! TEMP_AB') eq(false, pcall(meths.get_autocmds, { group = 'TEMP_AB' })) eq(0, #meths.get_autocmds { event = 'BufReadPost' }) end) it('legacy: multiple remove and add augroup', function() command('augroup TEMP_ABC') command(' au!') command(' autocmd BufReadPost *.py echo "Hello"') command('augroup END') command('augroup! TEMP_ABC') -- Should still have one autocmd :'( local aus = meths.get_autocmds { event = 'BufReadPost' } eq(1, #aus, aus) command('augroup TEMP_ABC') command(' au!') command(' autocmd BufReadPost *.py echo "Hello"') command('augroup END') -- Should now have two autocmds :'( aus = meths.get_autocmds { event = 'BufReadPost' } eq(2, #aus, aus) command('augroup! TEMP_ABC') eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABC' })) eq(2, #meths.get_autocmds { event = 'BufReadPost' }) end) it('api: should clear and not return any autocmds for delete groups by id', function() command('augroup TEMP_ABCD') command('autocmd! BufReadPost *.py :echo "Hello"') command('augroup END') local augroup_id = meths.create_augroup { name = "TEMP_ABCD", clear = false } meths.del_augroup_by_id(augroup_id) -- For good reason, we kill all the autocmds from del_augroup, -- so now this works as expected eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCD' })) eq(0, #meths.get_autocmds { event = 'BufReadPost' }) end) it('api: should clear and not return any autocmds for delete groups by name', function() command('augroup TEMP_ABCDE') command('autocmd! BufReadPost *.py :echo "Hello"') command('augroup END') meths.del_augroup_by_name("TEMP_ABCDE") -- For good reason, we kill all the autocmds from del_augroup, -- so now this works as expected eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCDE' })) eq(0, #meths.get_autocmds { event = 'BufReadPost' }) end) end) end)