diff --git a/lua/lz/n/init.lua b/lua/lz/n/init.lua new file mode 100644 index 0000000..e59aa95 --- /dev/null +++ b/lua/lz/n/init.lua @@ -0,0 +1,28 @@ +---@mod lz.n + +local M = {} + +-- TODO: Is this necessary? +if not vim.loader or vim.fn.has('nvim-0.9.1') ~= 1 then + error('lz.n requires Neovim >= 0.9.1') +end + +---@param spec string | LzSpec +function M.load(spec) + if vim.g.lzn_did_load then + return vim.notify('lz.n has already loaded your plugins.', vim.log.levels.WARN, { title = 'lz.n' }) + end + vim.g.lzn_did_load = true + + if type(spec) == 'string' then + spec = { import = spec } + end + ---@cast spec LzSpec + local plugins = require('lz.n.spec').parse(spec) + require('lz.n.loader').load_startup_plugins(plugins) + -- DONE: Plugin.load() + -- TODO: Handler.init() + -- TODO: Handler.setup() +end + +return M diff --git a/lua/lz/n/loader.lua b/lua/lz/n/loader.lua new file mode 100644 index 0000000..efd70bb --- /dev/null +++ b/lua/lz/n/loader.lua @@ -0,0 +1,86 @@ +---@mod lz.n.loader + +local state = require('lz.n.state') + +local M = {} + +local DEFAULT_PRIORITY = 50 + +---@package +---@param plugin LzPlugin +function M._load(plugin) + if plugin.enable == false or (type(plugin.enable) == 'function' and not plugin.enable()) then + return + end + -- TODO: Load plugin +end + +---@param plugins table +local function run_before_all(plugins) + for _, plugin in pairs(plugins) do + if plugin.beforeAll then + local ok, err = pcall(plugin.beforeAll, plugin) + if not ok then + vim.schedule(function() + vim.notify( + "Failed to run 'beforeAll' for " .. plugin.name .. ': ' .. tostring(err or ''), + vim.log.levels.ERROR + ) + end) + end + end + end +end + +---@param plugins table +local function get_eager_plugins(plugins) + local result = {} + for _, plugin in pairs(plugins) do + if plugin.lazy == false then + table.insert(result, plugin) + end + end + table.sort(result, function(a, b) + ---@cast a LzPlugin + ---@cast b LzPlugin + return (a.priority or DEFAULT_PRIORITY) > (b.priority or DEFAULT_PRIORITY) + end) + return result +end + +---@param plugins table +function M.load_startup_plugins(plugins) + run_before_all(plugins) + for _, plugin in pairs(get_eager_plugins(plugins)) do + if not plugin._.loaded then + M.load(plugin) + end + end +end + +---@param plugins string | LzPlugin | string[] | LzPlugin[] +function M.load(plugins) + plugins = (type(plugins) == 'string' or plugins.name) and { plugins } or plugins + ---@cast plugins (string|LzPlugin)[] + for _, plugin in pairs(plugins) do + local loadable = true + if type(plugin) == 'string' then + if state.plugins[plugin] then + plugin = state.plugins[plugin] + -- TODO:? + -- elseif state.spec.disabled[plugin] then + -- plugin = nil + -- loadable = false + else + vim.notify('Plugin ' .. plugin .. ' not found', vim.log.levels.ERROR, { title = 'lz.n' }) + loadable = false + end + ---@cast plugin LzPlugin + end + if loadable then + M._load(plugin) + end + end +end + +return M diff --git a/lua/lz/n/spec.lua b/lua/lz/n/spec.lua new file mode 100644 index 0000000..542934f --- /dev/null +++ b/lua/lz/n/spec.lua @@ -0,0 +1,78 @@ +local M = {} + +---@param spec LzSpecImport +---@param result table +local function import_spec(spec, result) + if spec.import == 'lz.n' then + vim.schedule(function() + vim.notify("Plugins modules cannot be called 'lz.n'", vim.log.levels.ERROR) + end) + return + end + if type(spec.import) ~= 'string' then + vim.schedule(function() + vim.notify( + "Invalid import spec. The 'import' field should be a module name: " .. vim.inspect(spec), + vim.log.levels.ERROR + ) + end) + return + end + if spec.cond == false or (type(spec.cond) == 'function' and not spec.cond()) then + return + end + if spec.enabled == false or (type(spec.enabled) == 'function' and not spec.enabled()) then + return + end + local modname = 'plugin.' .. spec.import + local ok, mod = pcall(require, modname) + if not ok then + vim.schedule(function() + local err = type(mod) == 'string' and ': ' .. mod or '' + vim.notify("Failed to load module '" .. modname .. err, vim.log.levels.ERROR) + end) + return + end + if type(mod) ~= table then + vim.schedule(function() + vim.notify("Invalid plugin spec module '" .. modname .. "' of type '" .. type(mod) .. "'", vim.log.levels.ERROR) + end) + return + end + M._normalize(mod, result) +end + +---@private +---@param spec LzSpec +---@param result table +function M._normalize(spec, result) + if #spec > 1 or vim.tbl_islist(spec) then + for _, sp in ipairs(spec) do + M._normalize(sp, result) + end + elseif spec.import then + ---@cast spec LzSpecImport + import_spec(spec, result) + end +end + +---@param result table +local function remove_disabled_plugins(result) + for _, plugin in ipairs(result) do + local disabled = plugin.enabled == false or (type(plugin.enabled) == 'function' and not plugin.enabled()) + if disabled then + result[plugin.name] = nil + end + end +end + +---@param spec LzSpec +---@return table +function M.parse(spec) + local result = {} + M._normalize(spec, result) + remove_disabled_plugins(result) + return result +end + +return M diff --git a/lua/lz/n/state.lua b/lua/lz/n/state.lua new file mode 100644 index 0000000..17b729d --- /dev/null +++ b/lua/lz/n/state.lua @@ -0,0 +1,8 @@ +---@mod lz.n.state + +local M = {} + +---@type table +M.plugins = {} + +return M diff --git a/lua/lz/n/types.lua b/lua/lz/n/types.lua new file mode 100644 index 0000000..587bda3 --- /dev/null +++ b/lua/lz/n/types.lua @@ -0,0 +1,77 @@ +---@meta +error('Cannot import a meta module') + +---@class VimGTable vim.g config table +---@field name? string Name of the vim.g config table, e.g. "rustaceanvim" for "vim.g.rustaceanvim". Defaults to the plugin name. +---@field type 'vim.g' + +---@class ConfigFunction Lua function +---@field module? string Module name containing the function. Defaults to the plugin name. +---@field name? string Name of the config function. Defaults to 'setup', the most common in the Neovim plugin community. +---@field type 'func' + +---@alias LzPluginOptsSpec VimGTable | ConfigFunction How a plugin accepts its options + +---@class LzPluginBase +---@field name string Display name and name used for plugin config files, e.g. "neorg" +---@field optsSpec? LzPluginOptsSpec +---@field enabled? boolean|(fun():boolean) +---@field enable? boolean|(fun():boolean) Whether to enable this plugin. Useful to disable plugins under certain conditions. +---@field lazy? boolean +---@field priority? number Only useful for lazy=false plugins to force loading certain plugins first. Default priority is 50 + +---@alias LzEvent {id:string, event:string[]|string, pattern?:string[]|string} +---@alias LzEventSpec string|{event?:string|string[], pattern?:string|string[]}|string[] + +---@alias PluginOpts table|fun(self:LzPlugin, opts:table):table? + +---@class LzPluginHooks +---@field beforeAll? fun(self:LzPlugin) Will be run before loading any plugins +---@field deactivate? fun(self:LzPlugin) Unload/Stop a plugin +---@field after? fun(self:LzPlugin, opts:table)|true Will be executed when loading the plugin +---@field opts? PluginOpts + +---@class LzPluginHandlers +---@field event? table +---@field ft? table +---@field keys? table +---@field cmd? table + +---@class LzPluginSpecHandlers +---@field event? string[]|string|LzEventSpec[]|fun(self:LzPlugin, event:string[]):string[] +---@field cmd? string[]|string|fun(self:LzPlugin, cmd:string[]):string[] +---@field ft? string[]|string|fun(self:LzPlugin, ft:string[]):string[] +---@field keys? string|string[]|LzKeysSpec[]|fun(self:LzPlugin, keys:string[]):(string|LzKeys)[] +---@field module? false + +---@class LzKeysBase +---@field desc? string +---@field noremap? boolean +---@field remap? boolean +---@field expr? boolean +---@field nowait? boolean +---@field ft? string|string[] + +---@class LzKeysSpec: LzKeysBase +---@field [1] string lhs +---@field [2]? string|fun()|false rhs +---@field mode? string|string[] + +---@class LzKeys: LzKeysBase +---@field lhs string lhs +---@field rhs? string|fun() rhs +---@field mode? string +---@field id string +---@field name string + +---@package +---@class LzPlugin: LzPluginBase,LzPluginHandlers,LzPluginHooks + +---@class LzPluginSpec: LzPluginBase,LzPluginSpecHandlers,LzPluginHooks + +---@alias LzSpec LzPluginSpec|LzSpecImport|LzSpec[] + +---@class LzSpecImport +---@field import string spec module to import +---@field enabled? boolean|(fun():boolean) +---@field cond? boolean|(fun():boolean)