From f58f3e8b1c19849741a5f704eae39ef44c563192 Mon Sep 17 00:00:00 2001 From: connorgmeean Date: Thu, 13 Oct 2022 19:03:54 +1100 Subject: [PATCH] refact(core): Split command / autocommand utility functions into their own services. --- lua/doom/core/modules.lua | 32 ++---- lua/doom/modules/core/reloader/init.lua | 9 +- lua/doom/services/autocommands.lua | 144 ++++++++++++++++++++++++ lua/doom/services/commands.lua | 101 +++++++++++++++++ lua/doom/utils/init.lua | 126 ++++++++------------- 5 files changed, 308 insertions(+), 104 deletions(-) create mode 100644 lua/doom/services/autocommands.lua create mode 100644 lua/doom/services/commands.lua diff --git a/lua/doom/core/modules.lua b/lua/doom/core/modules.lua index 1b72243..babe1cd 100644 --- a/lua/doom/core/modules.lua +++ b/lua/doom/core/modules.lua @@ -109,6 +109,8 @@ modules.start = function() end local keymaps_service = require("doom.services.keymaps") +local commands_service = require("doom.services.commands") +local autocmds_service = require("doom.services.autocommands") --- Applies commands, autocommands, packages from enabled modules (`modules.lua`). modules.load_modules = function() @@ -155,19 +157,9 @@ modules.load_modules = function() -- Set/unset frozen packer dependencies if type(spec.commit) == "table" then - local last_commit = nil - for version, commit in pairs(spec.commit) do - if version == "latest" then - version = utils.nvim_latest_supported - end - - if vim.fn.has(version) == 1 then - last_commit = commit - else - break - end - end - spec.commit = last_commit + -- Commit can be a table of values, where the keys indicate + -- which neovim version is required. + spec.commit = utils.pick_compatible_field(spec.commit) end if not doom.freeze_dependencies then @@ -183,12 +175,14 @@ modules.load_modules = function() if module.autocmds then local autocmds = type(module.autocmds) == "function" and module.autocmds() or module.autocmds - utils.make_augroup(module_name, autocmds) + for _, autocmd_spec in ipairs(autocmds) do + autocmds_service.set(autocmd_spec[1], autocmd_spec[2], autocmd_spec[3], autocmd_spec) + end end if module.cmds then for _, cmd_spec in ipairs(module.cmds) do - utils.make_cmd(cmd_spec[1], cmd_spec[2], cmd_spec) + commands_service.set(cmd_spec[1], cmd_spec[2], cmd_spec[3] or cmd_spec.opts) end end @@ -213,15 +207,13 @@ modules.handle_user_config = function() -- Handle extra user cmds for _, cmd_spec in pairs(doom.cmds) do - utils.make_cmd(cmd_spec[1], cmd_spec[2], cmd_spec) + commands_service.set(cmd_spec[1], cmd_spec[2], cmd_spec[3] or cmd_spec.opts) end -- Handle extra user autocmds - local autocmds = {} - for _, cmd_spec in pairs(doom.autocmds) do - table.insert(autocmds, cmd_spec) + for _, autocmd_spec in pairs(doom.autocmds) do + autocmds_service.set(autocmd_spec[1], autocmd_spec[2], autocmd_spec[3], autocmd_spec) end - utils.make_augroup("user", autocmds) -- Handle extra user keybinds for _, keybinds in ipairs(doom.binds) do diff --git a/lua/doom/modules/core/reloader/init.lua b/lua/doom/modules/core/reloader/init.lua index 66ee4a3..de1f8c7 100644 --- a/lua/doom/modules/core/reloader/init.lua +++ b/lua/doom/modules/core/reloader/init.lua @@ -92,10 +92,11 @@ reloader._reload_doom = function() end, doom.packages) -- Reset State - if _doom and _doom.cmd_funcs then - _doom.cmd_funcs = {} - end - doom.packages = {} + local commands_service = require("doom.services.commands") + commands_service.del_all() + print('deleting autocommands') + local autocmds_service = require("doom.services.autocommands") + autocmds_service.del_all() -- Unload doom.modules/doom.core lua files for k, _ in pairs(package.loaded) do diff --git a/lua/doom/services/autocommands.lua b/lua/doom/services/autocommands.lua new file mode 100644 index 0000000..d0cde03 --- /dev/null +++ b/lua/doom/services/autocommands.lua @@ -0,0 +1,144 @@ +--- AutoCommands Service, +--- Provides functions to wrap neovims APIs to set and remove autocmds +--- Acts as a compatibility layer between different API versions. +--- Manages references to all commands to be cleared for :DoomReload + +-- TYPES + +--- @class AutoCommandArgs +--- @field args string Args parsed to command (if any) +--- @field fargs string[] Args split by unescaped whitespace (if any) +--- @field line1 number Starting line of the command range +--- @field line2 number Final line of the command range +--- @field count number Any count supplied (if any) + +--- @class SetAutoCommandOptions +--- @field nested boolean|nil +--- @field once boolean|nil + +--- IMPLEMENTATIONS +--- Wraps the nvim functionality to handle different neovim versions. +local utils = require("doom.utils") + +-- Data to be stored globally so it can be accessed from the nvim-0.5 implementation +local data = _G._doom_autocmds_service_data + or { + -- Stores data relating to the auto command so they can be deleted on neovim < 0.8 + autocmd_signatures = {}, + -- Stores the lua function handlers for nvim version < 0.8 + autocmd_actions = {}, + -- Stores created autocommand ids from vim.api.nvim_create_autocmd (or custom shim in the v0.5 version) + autocmd_ids = {}, + } +_G._doom_autocmds_service_data = data + +-- Store all autocommands inside of an augroup for doom-nvim +if vim.fn.has("nvim-0.8") then + vim.api.nvim_create_augroup("DoomAutoCommands", { clear = true }) +else + vim.cmd([[ + augroup DoomAutoCommands + autocmd! + augroup END + ]]) +end + +local set_autocmd_implementations = { + ["nvim-0.5"] = function(event, pattern, action, opts) + local cmd_string = "autocmd! " + cmd_string = cmd_string .. ("%s %s "):format(event, pattern) + + local uid = utils.unique_index() + data.autocmd_ids[uid] = true + data.autocmd_signatures = cmd_string + + if opts.nested then + cmd_string = cmd_string .. "++nested " + end + if opts.once then + cmd_string = cmd_string .. "++once " + end + + if type(action) == "string" then + cmd_string = cmd_string .. action .. " " + else + data.autocmd_actions[uid] = action + + cmd_string = cmd_string .. (":lua _doom_autocmds_service_data.autocmd_actions[%d]()"):format(uid) + end + vim.cmd(cmd_string) + return uid + end, + ["latest"] = function(event, pattern, action, opts) + local merged_opts = vim.tbl_extend('keep', opts, { + callback = action, + pattern = pattern, + group = "DoomAutoCommands" + }) + local id = vim.api.nvim_create_autocmd(event, merged_opts) + data.autocmd_ids[id] = true + data.autocmd_signatures[id] = ("%s %s"):format(event, pattern) + return id + end, +} +local set_autocmd_fn = utils.pick_compatible_field(set_autocmd_implementations) + +local del_autocmd_implementations = { + ["nvim-0.5"] = function(id) + local delete_signature = data.autocmd_signatures[id] + if delete_signature then + vim.cmd(delete_signature) + end + end, + ["latest"] = function(id) + vim.api.nvim_del_autocmd(id) + end, +} +local del_autocmd_fn = utils.pick_compatible_field(del_autocmd_implementations) + +local del_all_autocmd_implementations = { + ["nvim-0.5"] = function() + vim.cmd([[ + augroup DoomAutoCommands + autocmd! + augroup END + ]]) + end, + ["latest"] = function() + vim.api.nvim_create_augroup("DoomAutoCommands", { clear = true }) + end, +} +local del_all_autocmd_fn = utils.pick_compatible_field(del_all_autocmd_implementations) + +-- API +local autocmds_service = {} + +--- Set a neovim autocmd +---@param event string Name of autocmd +---@param pattern string Pattern to match autocommand with +---@param action string|function(AutoCommandArgs) +---@param opts SetAutoCommandOptions|nil +---@return number ID of autocommand, used to delete it later on +autocmds_service.set = function(event, pattern, action, opts) + local resolved_opts = opts or {} + local stripped_opts = { + nested = resolved_opts.nested or false, + once = resolved_opts.once or false, + } + return set_autocmd_fn(event, pattern, action, stripped_opts) +end + +--- Deletes an autocommand from a given id +---@param id number ID of autocommand to delete +autocmds_service.del = function(id) + del_autocmd_fn(id) + data.autocmd_ids[id] = nil + data.autocmd_signatures[id] = nil + data.autocmd_actions[id] = nil +end + +autocmds_service.del_all = function() + del_all_autocmd_fn() +end + +return autocmds_service diff --git a/lua/doom/services/commands.lua b/lua/doom/services/commands.lua new file mode 100644 index 0000000..9f3a0ac --- /dev/null +++ b/lua/doom/services/commands.lua @@ -0,0 +1,101 @@ +--- Commands Service, +--- Provides functions to wrap neovims APIs to set and remove commands +--- Acts as a compatibility layer between different API versions. +--- Manages references to all commands to be cleared for :DoomReload + +-- TYPES + +--- @class CommandArgs +--- @field args string Args parsed to command (if any) +--- @field fargs string[] Args split by unescaped whitespace (if any) +--- @field line1 number Starting line of the command range +--- @field line2 number Final line of the command range +--- @field count number Any count supplied (if any) + +--- @class SetCommandOptions +--- @field nargs number|'*'|nil Number of expected arguments for the command. '*' for variable. + +--- IMPLEMENTATIONS +--- Wraps the nvim functionality to handle different neovim versions. +local utils = require("doom.utils") + +-- Data to be stored globally so it can be accessed from the nvim-0.5 implementation +local data = _G._doom_commands_service_data or { + command_actions = {}, +} +_G._doom_commands_service_data = data + +local set_command_implementations = { + ["nvim-0.5"] = function(name, command, opts) + -- Build the command constructor + local cmd_string = "command! " + if opts and opts.nargs ~= nil then + cmd_string = cmd_string .. ("-nargs=%s "):format(opts.nargs) + end + if opts and opts.completion ~= nil then + cmd_string = cmd_string .. ("-complete=%s "):format(table.concat(opts.complete, ",")) + end + cmd_string = cmd_string .. " " .. name .. " " + + if type(command) == "string" then + cmd_string = cmd_string .. command .. " " + else + local uid = utils.unique_index() + data.command_actions[uid] = command + + cmd_string = cmd_string .. ("lua _doom_commands_service_data.command_actions[%d]"):format(uid) + if opts.nargs ~= nil then + cmd_string = cmd_string .. "()" + else + cmd_string = cmd_string .. "()" + end + end + vim.cmd(cmd_string) + end, + ["nvim-0.8"] = function(name, command, opts) + vim.api.nvim_create_user_command(name, command, opts) + end, +} +local set_command_fn = utils.pick_compatible_field(set_command_implementations) + +local del_command_implementations = { + ["nvim-0.5"] = function(name) + vim.cmd(("delcommand %s"):format(name)) + end, + ["nvim-0.8"] = function(name) + vim.api.nvim_del_user_command(name) + end, +} +local del_command_fn = utils.pick_compatible_field(del_command_implementations) + +-- API +local commands_service = {} + +--- List of all commands set so they can be deleted by `commands.del_all()` +--- @type table +commands_service.stored_names = {} + +--- Set a neovim command +---@param name string Name of command +---@param command string|function(CommandArgs) +---@param opts SetCommandOptions|nil +commands_service.set = function(name, command, opts) + commands_service.stored_names[name] = true + set_command_fn(name, command, opts or {}) +end + +commands_service.del = function(name) + commands_service.stored_names[name] = nil + del_command_fn(name) +end + +commands_service.del_all = function() + for name, _ in pairs(commands_service.stored_names) do + if name then + del_command_fn(name) + end + end + commands_service.stored_names = {} +end + +return commands_service diff --git a/lua/doom/utils/init.lua b/lua/doom/utils/init.lua index 7413a9b..184c685 100644 --- a/lua/doom/utils/init.lua +++ b/lua/doom/utils/init.lua @@ -11,7 +11,7 @@ utils.version = { } --- Currently supported version of neovim for this build of doom-nvim -utils.nvim_latest_supported = 'nvim-0.8' +utils.nvim_latest_supported = "nvim-0.8" utils.doom_version = string.format("%d.%d.%d", utils.version.major, utils.version.minor, utils.version.patch) @@ -21,6 +21,7 @@ utils.find_config = function(filename) local function get_filepath(dir) return table.concat({ dir, filename }, system.sep) end + local path = get_filepath(system.doom_configs_root) if fs.file_exists(path) then return path @@ -74,85 +75,6 @@ utils.safe_require = function(path) end end ---- Stores a function in a global table, returns a string to execute the function --- @param fn function --- @return string -utils.commandify_function = function(fn, has_arguments) - if not _G._doom then - _G._doom = {} - end - if not _doom.cmd_funcs then - _doom.cmd_funcs = {} - end - -- Nobody is going to need more than a million of these, right? - local unique_number = utils.unique_index() - _doom.cmd_funcs[unique_number] = fn - - if has_arguments then - return ("lua _doom.cmd_funcs[%d]()"):format(unique_number) - else - return ("lua _doom.cmd_funcs[%d]()"):format(unique_number) - end -end - --- @type MakeCmdOptions --- @field nargs string|number|nil --- @field complete string[]|nil - ---- Creates a new command that can be executed from the neovim command line --- @param cmd_name string The name of the command, i.e. `:DoomReload` --- @param action string|function The action to execute when the cmd is entered. --- @param opts MakeCmdOptions -utils.make_cmd = function(cmd_name, action, opts) - local cmd_string = "command! " - if opts and opts.nargs ~= nil then - cmd_string = cmd_string .. ("-nargs=%s "):format(opts.nargs) - end - if opts and opts.completion ~= nil then - cmd_string = cmd_string .. ("-complete=%s "):format(table.concat(opts.complete, ",")) - end - cmd_string = cmd_string .. " " .. cmd_name .. " " - cmd_string = type(action) == "function" and cmd_string .. utils.commandify_function(action, opts and opts.nargs ~= nil) or cmd_string .. action - vim.cmd(cmd_string) -end - -utils.make_autocmd = function(event, pattern, action, group, nested, once) - local cmd = "autocmd " - - if group then - cmd = cmd .. group .. " " - end - - cmd = cmd .. event .. " " - cmd = cmd .. pattern .. " " - - if nested then - cmd = cmd .. "++nested " - end - if once then - cmd = cmd .. "++once " - end - - cmd = type(action) == "function" and cmd .. utils.commandify_function(action) or cmd .. action - - vim.cmd(cmd) -end - -utils.make_augroup = function(group_name, cmds, existing_group) - if not existing_group then - vim.cmd("augroup " .. group_name) - vim.cmd("autocmd!") - end - - for _, cmd in ipairs(cmds) do - utils.make_autocmd(cmd[1], cmd[2], cmd[3], existing_group and group_name, cmd.nested, cmd.once) - end - - if not existing_group then - vim.cmd("augroup END") - end -end - utils.get_sysname = function() return vim.loop.os_uname().sysname end @@ -286,4 +208,48 @@ utils.iter_string_at = function(str, sep) return string.gmatch(str, "([^" .. sep .. "]+)") end +--- Picks a field from a table by checking the keys if it's compatible +---@generic T +---@param compatibility_table table +---@return T +---@example +---```lua +---local val = utils.pick_compatible_field({ +--- ['nvim-0.5'] = 'this will be picked', +--- ['nvim-9.9'] = 'version too high, wont be picked' +---}) +---print(val) -- > 'this will be picked' +---``` +utils.pick_compatible_field = function(compatibility_table) + -- Sort the keys in order of neovim version + local sorted = vim.tbl_keys(compatibility_table) + table.sort(sorted, function(a, b) + return a < b + end) + -- Need "latest" to be last as it is a catch all for the default behaviour + if sorted[1] == "latest" then + table.remove(sorted, 1) + table.insert(sorted, "latest") + end + + -- Find the last key that is compatible with this neovim version + local last_field = nil + for _, version in ipairs(sorted) do + local field = compatibility_table[version] + local ver = version == "latest" and utils.nvim_latest_supported or version + + if vim.fn.has(ver) == 1 then + last_field = field + else + break + end + end + + -- Must always return a value. + if last_field == nil then + error("Error getting compatible field.") + end + return last_field +end + return utils