diff --git a/runtime/lua/vim/_auto.lua b/runtime/lua/vim/_auto.lua new file mode 100644 index 0000000000..847dfa41b0 --- /dev/null +++ b/runtime/lua/vim/_auto.lua @@ -0,0 +1,243 @@ +---@class EventContext +---@field _group string|boolean +---@field _buffer integer|boolean +local EventContext = {} + +---@class Event +---@field _ctx EventContext +---@field _event string|string[] +---@field _pattern? string|string[] +local Event = {} + +---@class Augroup +---@field _ctx EventContext +---@field _group string +local Augroup = {} + +local a, validate = vim.api, vim.validate + +---@param group? string|boolean +---@param buffer? integer|boolean +---@return EventContext +---@private +function EventContext._new(group, buffer) + -- use non-nil values to avoid triggering the `__index` metamethod when we access fields on self. + local self = { + _group = group or false, + _buffer = buffer or false, + } + return setmetatable(self, EventContext) +end + +---@param opts? table API options +---@return table opts +function EventContext:_apply(opts) + opts = opts or {} + local g, b = self._group, self._buffer + b = b == true and 0 or b + opts.buffer = b or opts.buffer + opts.group = g or opts.group + return opts +end + +---@param self EventContext +---@param opts? table +---@return table +---@see |nvim_get_autocmds()| +function EventContext:get(opts) + return a.nvim_get_autocmds(self:_apply(opts)) +end + +---@param self EventContext +---@param opts? table +---@see |nvim_clear_autocmds()| +function EventContext:clear(opts) + a.nvim_clear_autocmds(self:_apply(opts)) +end + +---@param self EventContext +---@private +function EventContext:__index(k) + -- first, check methods + if EventContext[k] then + return EventContext[k] + end + -- then, check if we're trying to specify a buffer + if k == "buf" then + return self._buffer == false and EventContext._new(self._group, true) or nil + elseif type(k) == "number" then + return self._buffer == true and EventContext._new(self._group, k) or nil + end + -- nothing else to check; use k as event name + return Event._new(self, k) +end + +---@param ctx EventContext +---@param event string|string[] +---@param pattern? string|string[] +---@return Event +---@private +function Event._new(ctx, event, pattern) + local self = { + _ctx = ctx, + _event = event, + _pattern = pattern or false, + } + return setmetatable(self, Event) +end + +---@param self Event +---@param opts? table +---@return table[] +function Event:get(opts) + opts = self._ctx:_apply(opts) + opts.event = self._event + opts.pattern = self._pattern or opts.pattern + return a.nvim_get_autocmds(opts) +end + +---@param self Event +---@param opts? table +function Event:exec(opts) + opts = self._ctx:_apply(opts) + opts.pattern = self._pattern or opts.pattern + a.nvim_exec_autocmds(self._event, opts) +end + +---@param self Event +---@param opts? table +function Event:clear(opts) + opts = self._ctx:_apply(opts) + opts.event = self._event + opts.pattern = self._pattern or opts.pattern + a.nvim_clear_autocmds(opts) +end + +--- Create an autocommand for this event +---@param self Event +---@param handler string|function +---@param opts? table +---@return integer +function Event:__call(handler, opts) + validate { + handler = { handler, {"s", "f"} }, + opts = { opts, "t", true }, + } + opts = self._ctx:_apply(opts) + opts.pattern = self._pattern or opts.pattern + if type(handler) == "string" and handler:sub(1, 1) == ":" then + opts.command = handler:sub(2) + else + opts.callback = handler + end + return a.nvim_create_autocmd(self._event, opts) +end + +---@param self Event +---@return function|Event|nil +function Event:__index(k) + if Event[k] then + return Event[k] + elseif not self._pattern and not self._ctx._buffer then + return Event._new(self._ctx, self._event, k) + else + return nil + end +end + +Augroup.__index = Augroup + +---@param name string +---@return Augroup +function Augroup._new(name) + local self = { + _ctx = EventContext._new(name, nil), + _group = name, + } + return setmetatable(self, Augroup) +end + +---@param self Augroup +---@return integer id +function Augroup:create() + return a.nvim_create_augroup(self._group, { clear = false }) +end + +---@param self Augroup +---@param opts? table +---@return integer? id +function Augroup:clear(opts) + if not opts then + return a.nvim_create_augroup(self._group, { clear = true }) + else + self._ctx:clear(opts) + end +end + +---@param self Augroup +function Augroup:del() + return a.nvim_del_augroup_by_name(self._group) +end + +---@param self Augroup +---@param opts? table +---@return table[]? +function Augroup:get(opts) + if not opts then + local exists, cmds = pcall(a.nvim_get_autocmds, { group = self._group }) + return exists and cmds or nil + else + return self._ctx:get(opts) + end +end + +---@param self Augroup +---@param spec fun(au:EventContext):any +---@return integer id +---@return any +function Augroup:__call(spec) + local id = self:create() + local res = spec(self._ctx) + return id, res +end + +--- Use `vim.autocmd` to manage autocommands. Index it by event names to create and execute them. +--- +--- To create an autocommand, index `vim.autocmd` with an event name to return a callable table. +--- Then call it with a handler (a Lua function, a Vimscript function name, or Ex command) and +--- and optional table of options to pass to |nvim_create_autocmd()|. +--- +---
lua
+---   -- prefix Ex commands with ":" to use as an event handler
+---   vim.autocmd.UIEnter(":echo 'Hello!'")
+---   -- a Lua callback as an event handler
+---   vim.autocmd.UIEnter(function()
+---     vim.cmd.echo 'Hello!'
+---   end)
+---   -- passing in additional options
+---   vim.autocmd.UIEnter(":echo 'Hello!'", {
+---     desc = "greeting",
+---     once = true,
+---   })
+---   -- specify multiple events
+---   vim.autocmd[{ "UIEnter", "TabEnter", "TermEnter" }](":echo 'Hello!'")
+--- 
+--- +--- You may also specify a pattern by indexing the event. +--- +---
lua
+---   vim.autocmd.FileType[{ "qf", "help", "man", }](function()
+---     vim.opt_local.number = false
+---     vim.opt_local.relativenumber = false
+---   end)
+--- 
+--- +vim.autocmd = EventContext._new(nil, nil) + +vim.autocmd.buf = EventContext._new(nil, 0) + +--- Create, delete, and clear autocommand groups with `vim.augroup`. +--- +vim.augroup = setmetatable({}, { + __index = function(_, k) return Augroup._new(k) end, +}) diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua index 0c4ee8636d..bee7932864 100644 --- a/runtime/lua/vim/_init_packages.lua +++ b/runtime/lua/vim/_init_packages.lua @@ -59,6 +59,9 @@ setmetatable(vim, { elseif key == 'inspect_pos' or key == 'show_pos' then require('vim._inspector') return t[key] + elseif key == 'autocmd' or key == 'augroup' then + require('vim._auto') + return t[key] elseif vim.startswith(key, 'uri_') then local val = require('vim.uri')[key] if val ~= nil then diff --git a/test/functional/lua/auto_spec.lua b/test/functional/lua/auto_spec.lua new file mode 100644 index 0000000000..2fe8e304f1 --- /dev/null +++ b/test/functional/lua/auto_spec.lua @@ -0,0 +1,230 @@ +local helpers = require('test.functional.helpers')(after_each) +local exec_lua = helpers.exec_lua +local meths = helpers.meths +local clear = helpers.clear +local eq = helpers.eq + +before_each(clear) + +describe('vim.autocmd', function() + describe('vim.autocmd:get()', function() + pending('behaves like nvim_get_autocmds') + end) + + describe('vim.autocmd:clear()', function() + pending('behaves like nvim_clear_autocmds') + end) + + describe('vim.autocmd.buf', function() + pending('manages buflocal autocommands') + + it('can create an autocommand for the current buffer', function() + exec_lua [[ vim.autocmd.buf.InsertEnter(':echo "Coding!"') ]] + eq(1, #meths.get_autocmds({ buffer = 0 })) + end) + + it('can get all autocommands attached to the current buffer', function() + meths.create_autocmd('InsertEnter', { + buffer = 0, + command = 'echo "Coding!"' + }) + meths.create_autocmd('InsertLeave', { + buffer = 0, + command = 'echo "Done!"' + }) + local aus = exec_lua [[ return vim.autocmd.buf:get() ]] + eq(2, #aus) + end) + + it('can clear all autocommands attached to the current buffer', function() + meths.create_autocmd('InsertEnter', { + buffer = 0, + command = 'echo "Coding!"' + }) + meths.create_autocmd('InsertLeave', { + buffer = 0, + command = 'echo "Done!"' + }) + eq(2, #meths.get_autocmds({ buffer = 0 })) + exec_lua [[ vim.autocmd.buf:clear() ]] + eq(0, #meths.get_autocmds({ buffer = 0 })) + end) + + pending('can be indexed with a bufnr') + end) + + it('can create an autocommand', function() + local id = exec_lua([[ + return vim.autocmd.UIEnter(':echo "Hello!"') + ]]) + assert.number(id) + local cmds = meths.get_autocmds({ event = 'UIEnter' }) + eq(1, #cmds) + eq(id, cmds[1].id) + eq('echo "Hello!"', cmds[1].command) + end) + + it('can create an autocommand with options', function() + local id = exec_lua([[ + return vim.autocmd.UIEnter(':echo "Hello!"', { + desc = 'greeting', + once = true, + }) + ]]) + assert.number(id) + local cmds = meths.get_autocmds({ event = 'UIEnter' }) + eq(id, cmds[1].id) + eq('greeting', cmds[1].desc) + end) + + it('can create an autocommand for multiple events', function() + local id = exec_lua([[ + return vim.autocmd[{ 'UIEnter', 'VimEnter', 'WinEnter' }](':echo "Hello!"') + ]]) + assert.number(id) + eq(id, meths.get_autocmds({ event = 'UIEnter' })[1].id) + eq(id, meths.get_autocmds({ event = 'VimEnter' })[1].id) + eq(id, meths.get_autocmds({ event = 'WinEnter' })[1].id) + end) + + it('can create an autocommand for an event and pattern', function() + local id = exec_lua([[ + return vim.autocmd.User.CustomEvent(':echo "Hello!"') + ]]) + assert.number(id) + eq(id, meths.get_autocmds({ event = 'User', pattern = 'CustomEvent' })[1].id) + end) + + it('can create an autocommand and specify multiple patterns', function() + local id = exec_lua([[ + return vim.autocmd.Filetype[{ 'lua', 'vim', 'sh' }](':echo "Hello!"') + ]]) + assert.number(id) + eq(3, #meths.get_autocmds({ event = 'Filetype' })) + end) + + it('can get autocommands for an event', function() + meths.create_autocmd('UIEnter', { + command = 'echo "Hello!"', + }) + local cmds = exec_lua([[ + return vim.autocmd.UIEnter:get() + ]]) + eq(1, #cmds) + end) + + it('can get autocommands for an event and pattern', function() + meths.create_autocmd('User', { + pattern = 'foo', + command = 'echo "Hello!"', + }) + meths.create_autocmd('User', { + pattern = 'bar', + command = 'echo "Hello!"', + }) + local cmds = exec_lua([[ + return vim.autocmd.User.foo:get() + ]]) + eq(1, #cmds) + end) + + it('can clear autocommands for an event', function() + meths.create_autocmd('UIEnter', { + command = 'echo "Hello!"', + }) + exec_lua([[ + vim.autocmd.UIEnter:clear() + ]]) + eq(0, #meths.get_autocmds({ event = 'UIEnter' })) + end) + + it('can execute autocommands', function() + meths.set_var("some_condition", false) + + exec_lua [[ + vim.api.nvim_create_autocmd("User", { + pattern = "Test", + desc = "A test autocommand", + callback = function() + return vim.g.some_condition + end, + }) + ]] + + exec_lua [[ vim.autocmd.User.Test:exec() ]] + + local aus = meths.get_autocmds({ event = 'User', pattern = 'Test' }) + local first = aus[1] + eq(first.id, 1) + + meths.set_var("some_condition", true) + exec_lua [[ vim.autocmd.User.Test:exec() ]] + eq({}, meths.get_autocmds({event = "User", pattern = "Test"})) + end) + +end) + +describe('vim.augroup', function() + it('can delete an existing group', function() + local id = meths.create_augroup('nvim_test_augroup', { clear = true }) + meths.create_autocmd('User', { + group = id, + pattern = "Test", + desc = "A test autocommand", + command = 'echo "Test!"', + }) + local aus = meths.get_autocmds({ group = 'nvim_test_augroup' }) + eq(1, #aus) + exec_lua [[ vim.augroup.nvim_test_augroup:del() ]] + local success = exec_lua [[ + return pcall(vim.api.nvim_get_autocmds, { group = 'nvim_test_augroup' }) + ]] + eq(false, success) + end) + + describe('Augroup:create()', function() + it('can create a group and return its id', function() + local id = exec_lua [[ return vim.augroup.nvim_test_augroup:create() ]] + meths.create_autocmd('User', { + group = id, + pattern = "Test", + desc = "A test autocommand", + command = 'echo "Test!"', + }) + local aus = meths.get_autocmds({ group = id }) + eq(1, #aus) + end) + + pending('can return the id of an existing group') + pending('does not clear an existing group') + end) + + describe('Augroup:clear()', function() + pending('clears autocommands in the group') + pending('can be called with a dictionary of autocommand options') + pending('can create the group when called without arguments') + end) + + describe('Augroup:get()', function() + pending('can return a list of autocommands in the group') + pending('can be called with a dictionary of autocommand options') + pending('returns nil when called without arguments on a nonexistant group') + end) + + describe('Augroup:__call()', function() + it('can create a group and define its autocommands', function() + exec_lua [[ + vim.augroup.nvim_test_augroup(function(au) + au.UIEnter(":echo 'Hello!'") + au.User.Test(":echo 'Test!'") + au.InsertEnter['*'](":echo 'Test!'") + end) + ]] + local aus = meths.get_autocmds({ group = 'nvim_test_augroup' }) + eq(3, #aus) + end) + + pending('can add autocommands to an existing group') + pending('returns the group id and any values returned from the function') + end) +end)