From e61adde6ce8fd2e11737fe01962cc135510171a6 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Thu, 11 Apr 2024 20:46:06 +0200 Subject: [PATCH] draft: initial implementation --- .gitignore | 1 + flake.lock | 103 ++++++++++++++++++++------ flake.nix | 89 ++++++---------------- lua/lz/n/handler/cmd.lua | 88 ++++++++++++++++++++++ lua/lz/n/handler/event.lua | 147 +++++++++++++++++++++++++++++++++++++ lua/lz/n/handler/ft.lua | 27 +++++++ lua/lz/n/handler/init.lua | 55 ++++++++++++++ lua/lz/n/handler/keys.lua | 128 ++++++++++++++++++++++++++++++++ lua/lz/n/init.lua | 27 +++++++ lua/lz/n/loader.lua | 85 +++++++++++++++++++++ lua/lz/n/spec.lua | 142 +++++++++++++++++++++++++++++++++++ lua/lz/n/state.lua | 8 ++ lua/lz/n/types.lua | 77 +++++++++++++++++++ nix/ci-overlay.nix | 11 +-- spec/keys_spec.lua | 18 +++++ 15 files changed, 904 insertions(+), 102 deletions(-) create mode 100644 lua/lz/n/handler/cmd.lua create mode 100644 lua/lz/n/handler/event.lua create mode 100644 lua/lz/n/handler/ft.lua create mode 100644 lua/lz/n/handler/init.lua create mode 100644 lua/lz/n/handler/keys.lua create mode 100644 lua/lz/n/init.lua create mode 100644 lua/lz/n/loader.lua create mode 100644 lua/lz/n/spec.lua create mode 100644 lua/lz/n/state.lua create mode 100644 lua/lz/n/types.lua create mode 100644 spec/keys_spec.lua diff --git a/.gitignore b/.gitignore index 815701c..138cd4a 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ result .pre-commit-config.yaml .direnv +.luarc.json diff --git a/flake.lock b/flake.lock index 2dc3052..6841fa2 100755 --- a/flake.lock +++ b/flake.lock @@ -66,6 +66,24 @@ "type": "github" } }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1706830856, + "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -138,6 +156,25 @@ "type": "github" } }, + "gen-luarc": { + "inputs": { + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1710933866, + "narHash": "sha256-GtYTuxY6AdFxl3uwFkTkqpvOP4lQLzu2YwqnejhDs1Q=", + "owner": "mrcjkb", + "repo": "nix-gen-luarc-json", + "rev": "6e8912ea4fbfaa10797caafb1f5628fb4178b6e8", + "type": "github" + }, + "original": { + "owner": "mrcjkb", + "repo": "nix-gen-luarc-json", + "type": "github" + } + }, "gitignore": { "inputs": { "nixpkgs": [ @@ -181,28 +218,12 @@ "type": "github" } }, - "neodev-nvim": { - "flake": false, - "locked": { - "lastModified": 1704434754, - "narHash": "sha256-guSJ809CDvzbnPyYG8ELr7LEkuX833m5TK+U0hl0hbc=", - "owner": "folke", - "repo": "neodev.nvim", - "rev": "be8d4d4cab6c13c6a572269c9d6a63774baba9a0", - "type": "github" - }, - "original": { - "owner": "folke", - "repo": "neodev.nvim", - "type": "github" - } - }, "neorocks": { "inputs": { "flake-compat": "flake-compat", "flake-utils": "flake-utils", "neovim-nightly": "neovim-nightly", - "nixpkgs": "nixpkgs", + "nixpkgs": "nixpkgs_2", "pre-commit-hooks": "pre-commit-hooks" }, "locked": { @@ -245,16 +266,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1704161960, - "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "lastModified": 1708475490, + "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "63143ac2c9186be6d9da6035fa22620018c85932", + "rev": "0e74ca98a74bc7270d28838369593635a5db3260", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixpkgs-unstable", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -277,6 +298,24 @@ "type": "github" } }, + "nixpkgs-lib_2": { + "locked": { + "dir": "lib", + "lastModified": 1706550542, + "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-stable": { "locked": { "lastModified": 1685801374, @@ -326,6 +365,22 @@ } }, "nixpkgs_3": { + "locked": { + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { "locked": { "lastModified": 1689261696, "narHash": "sha256-LzfUtFs9MQRvIoQ3MfgSuipBVMXslMPH/vZ+nM40LkA=", @@ -371,7 +426,7 @@ "flake-compat": "flake-compat_3", "flake-utils": "flake-utils_4", "gitignore": "gitignore_2", - "nixpkgs": "nixpkgs_3", + "nixpkgs": "nixpkgs_4", "nixpkgs-stable": "nixpkgs-stable_2" }, "locked": { @@ -391,9 +446,9 @@ "root": { "inputs": { "flake-parts": "flake-parts", - "neodev-nvim": "neodev-nvim", + "gen-luarc": "gen-luarc", "neorocks": "neorocks", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs_3", "pre-commit-hooks": "pre-commit-hooks_2" } }, diff --git a/flake.nix b/flake.nix index 635d477..309785c 100755 --- a/flake.nix +++ b/flake.nix @@ -10,14 +10,9 @@ url = "github:cachix/pre-commit-hooks.nix"; }; - neorocks = { - url = "github:nvim-neorocks/neorocks"; - }; + neorocks.url = "github:nvim-neorocks/neorocks"; - neodev-nvim = { - url = "github:folke/neodev.nvim"; - flake = false; - }; + gen-luarc.url = "github:mrcjkb/nix-gen-luarc-json"; }; outputs = inputs @ { @@ -26,7 +21,7 @@ flake-parts, pre-commit-hooks, neorocks, - neodev-nvim, + gen-luarc, ... }: let name = "lz.n"; @@ -49,77 +44,33 @@ ... }: let ci-overlay = import ./nix/ci-overlay.nix { - inherit - self - neodev-nvim - ; + inherit self; plugin-name = name; }; pkgs = import nixpkgs { inherit system; overlays = [ - ci-overlay + gen-luarc.overlays.default neorocks.overlays.default + ci-overlay plugin-overlay ]; }; - mkTypeCheck = { - nvim-api ? [], - disabled-diagnostics ? [], - }: - pre-commit-hooks.lib.${system}.run { - src = self; - hooks = { - lua-ls.enable = true; - }; - settings = { - lua-ls = { - config = { - runtime.version = "LuaJIT"; - Lua = { - workspace = { - library = - nvim-api - ++ [ - "\${3rd}/busted/library" - "\${3rd}/luassert/library" - ]; - ignoreDir = [ - ".git" - ".github" - ".direnv" - "result" - "nix" - "doc" - ]; - }; - diagnostics = { - libraryFiles = "Disable"; - disable = disabled-diagnostics; - }; - }; - }; - }; - }; - }; - - type-check-stable = mkTypeCheck { - nvim-api = [ - "${pkgs.neovim}/share/nvim/runtime/lua" - "${pkgs.neodev-plugin}/types/stable" - ]; - disabled-diagnostics = [ - # For compatibility with nightly, some diagnostics may have to be disabled here. - ]; + luarc = pkgs.mk-luarc { + nvim = pkgs.neovim-nightly; + neodev-types = "nightly"; }; - type-check-nightly = mkTypeCheck { - nvim-api = [ - "${pkgs.neovim-nightly}/share/nvim/runtime/lua" - "${pkgs.neodev-plugin}/types/nightly" - ]; + type-check-nightly = pre-commit-hooks.lib.${system}.run { + src = self; + hooks = { + lua-ls.enable = true; + }; + settings = { + lua-ls.config = luarc; + }; }; pre-commit-check = pre-commit-hooks.lib.${system}.run { @@ -135,7 +86,10 @@ devShell = pkgs.nvim-nightly-tests.overrideAttrs (oa: { name = "lz.n devShell"; - inherit (pre-commit-check) shellHook; + shellHook = '' + ${pre-commit-check.shellHook} + ln -fs ${pkgs.luarc-to-json luarc} .luarc.json + ''; buildInputs = with pre-commit-hooks.packages.${system}; [ alejandra @@ -162,7 +116,6 @@ checks = { inherit pre-commit-check - type-check-stable type-check-nightly ; inherit diff --git a/lua/lz/n/handler/cmd.lua b/lua/lz/n/handler/cmd.lua new file mode 100644 index 0000000..ab0925c --- /dev/null +++ b/lua/lz/n/handler/cmd.lua @@ -0,0 +1,88 @@ +local loader = require('lz.n.loader') + +---@class LzCmdHandler: LzHandler + +---@type LzCmdHandler +local M = { + active = {}, + managed = {}, + type = 'cmd', +} + +---@param cmd string +local function load(cmd) + vim.api.nvim_del_user_command(cmd) + loader.load(M.active[cmd]) +end + +---@param cmd string +local function add(cmd) + vim.api.nvim_create_user_command(cmd, function(event) + ---@cast event vim.api.keyset.user_command + local command = { + cmd = cmd, + bang = event.bang or nil, + ---@diagnostic disable-next-line: undefined-field + mods = event.smods, + ---@diagnostic disable-next-line: undefined-field + args = event.fargs, + count = event.count >= 0 and event.range == 0 and event.count or nil, + } + + if event.range == 1 then + ---@diagnostic disable-next-line: undefined-field + command.range = { event.line1 } + elseif event.range == 2 then + ---@diagnostic disable-next-line: undefined-field + command.range = { event.line1, event.line2 } + end + + ---@type string + local plugins = '`' .. table.concat(vim.tbl_values(M.active[cmd]), ', ') .. '`' + + load(cmd) + + local info = vim.api.nvim_get_commands({})[cmd] or vim.api.nvim_buf_get_commands(0, {})[cmd] + if not info then + vim.schedule(function() + vim.notify('Command `' .. cmd .. '` not found after loading ' .. plugins, vim.log.levels.ERROR) + end) + return + end + + command.nargs = info.nargs + ---@diagnostic disable-next-line: undefined-field + if event.args and event.args ~= '' and info.nargs and info.nargs:find('[1?]') then + ---@diagnostic disable-next-line: undefined-field + command.args = { event.args } + end + vim.cmd(command) + end, { + bang = true, + range = true, + nargs = '*', + complete = function(_, line) + load(cmd) + return vim.fn.getcompletion(line, 'cmdline') + end, + }) +end + +---@param cmd string +function M.del(cmd) + pcall(vim.api.nvim_del_user_command, cmd) +end + +---@param plugin LzPlugin +function M.add(plugin) + if not plugin.cmd then + return + end + for _, cmd in pairs(plugin.cmd) do + M.active[cmd] = M.active[cmd] or {} + M.active[cmd][plugin.name] = plugin.name + add(cmd) + end +end + +return M diff --git a/lua/lz/n/handler/event.lua b/lua/lz/n/handler/event.lua new file mode 100644 index 0000000..d7dfd03 --- /dev/null +++ b/lua/lz/n/handler/event.lua @@ -0,0 +1,147 @@ +local loader = require('lz.n.loader') + +---@class LzEventOpts +---@field event string +---@field group? string +---@field exclude? string[] augroups to exclude +---@field data? unknown +---@field buffer? number + +---@class LzEventHandler: LzHandler +---@field events table +---@field group number + +---@type LzEventHandler +local M = { + active = {}, + managed = {}, + type = 'event', +} + +---@param spec LzEventSpec +---@return LzEvent +function M.parse(spec) + local ret + if type(spec) == 'string' then + local event, pattern = spec:match('^(%w+)%s+(.*)$') + event = event or spec + return { id = spec, event = event, pattern = pattern } + elseif vim.tbl_islist(spec) then + ret = { id = table.concat(spec, '|'), event = spec } + else + ret = spec --[[@as LzEvent]] + if not ret.id then + ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch + ret.id = type(ret.event) == 'string' and ret.event or table.concat(ret.event, '|') + if ret.pattern then + ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch + ret.id = ret.id .. ' ' .. (type(ret.pattern) == 'string' and ret.pattern or table.concat(ret.pattern, ', ')) + end + end + end + return ret +end + +-- Get all augroups for an event +---@param event string +local function get_augroups(event) + ---@type string[] + local groups = {} + for _, autocmd in ipairs(vim.api.nvim_get_autocmds { event = event }) do + if autocmd.group_name then + table.insert(groups, autocmd.group_name) + end + end + return groups +end + +local event_triggers = { + FileType = 'BufReadPost', + BufReadPost = 'BufReadPre', +} +-- Get the current state of the event and all the events that will be fired +---@param event string +---@param buf integer +---@param data unknown +---@return LzEventOpts[] +local function get_state(event, buf, data) + ---@type LzEventOpts[] + local state = {} + while event do + ---@type LzEventOpts + local event_opts = { + event = event, + exclude = event ~= 'FileType' and get_augroups(event) or nil, + buffer = buf, + data = data, + } + table.insert(state, 1, event_opts) + data = nil -- only pass the data to the first event + event = event_triggers[event] + end + return state +end + +-- Trigger an event +---@param opts LzEventOpts +local function _trigger(opts) + xpcall( + function() + vim.api.nvim_exec_autocmds(opts.event, { + buffer = opts.buffer, + group = opts.group, + modeline = false, + data = opts.data, + }) + end, + vim.schedule_wrap(function(err) + vim.notify(err, vim.log.levels.ERROR) + end) + ) +end + +-- Trigger an event. When a group is given, only the events in that group will be triggered. +-- When exclude is set, the events in those groups will be skipped. +---@param opts LzEventOpts +local function trigger(opts) + if opts.group or opts.exclude == nil then + return _trigger(opts) + end + ---@type table + local done = {} + for _, autocmd in ipairs(vim.api.nvim_get_autocmds { event = opts.event }) do + local id = autocmd.event .. ':' .. (autocmd.group or '') ---@type string + local skip = done[id] or (opts.exclude and vim.tbl_contains(opts.exclude, autocmd.group_name)) + done[id] = true + if autocmd.group and not skip then + opts.group = autocmd.group_name + _trigger(opts) + end + end +end + +---@param event LzEvent +function M.add(event) + local done = false + vim.api.nvim_create_autocmd(event.event, { + group = M.group, + once = true, + pattern = event.pattern, + callback = function(ev) + if done or not M.active[event.id] then + return + end + -- HACK: work-around for https://github.com/neovim/neovim/issues/25526 + done = true + local state = get_state(ev.event, ev.buf, ev.data) + -- load the plugins + loader.load(M.active[event.id]) + -- check if any plugin created an event handler for this event and fire the group + for _, s in ipairs(state) do + trigger(s) + end + end, + }) +end + +return M diff --git a/lua/lz/n/handler/ft.lua b/lua/lz/n/handler/ft.lua new file mode 100644 index 0000000..6edde00 --- /dev/null +++ b/lua/lz/n/handler/ft.lua @@ -0,0 +1,27 @@ +local event = require('lz.n.handler.event') + +---@class LzFtHandler: LzHandler + +---@type LzFtHandler +local M = { + active = {}, + managed = {}, + type = 'ft', +} + +---@param value string +---@return LzEvent +function M.parse(value) + return { + id = value, + event = 'FileType', + pattern = value, + } +end + +---@param lz_event LzEvent +function M.add(lz_event) + event.add(lz_event) +end + +return M diff --git a/lua/lz/n/handler/init.lua b/lua/lz/n/handler/init.lua new file mode 100644 index 0000000..e3435e7 --- /dev/null +++ b/lua/lz/n/handler/init.lua @@ -0,0 +1,55 @@ +---@class LzHandler +---@field type LzHandlerTypes +---@field active table> +---@field managed table +---@field add fun(plugin: LzPlugin) +---@field del? fun(plugin: LzPlugin) + +local M = {} + +---@enum LzHandlerTypes +M.types = { + cmd = 'cmd', + event = 'event', + ft = 'ft', + keys = 'keys', +} + +local handlers = { + cmd = require('lz.n.handler.cmd'), + event = require('lz.n.handler.event'), + ft = require('lz.n.handler.ft'), + keys = require('lz.n.handler.keys'), +} + +---@param plugin LzPlugin +local function enable(plugin) + for _, handler in pairs(handlers) do + handler.add(plugin) + end + -- TODO: Change handler add implementations to take a LzPlugin +end + +function M.disable(plugin) + for _, handler in pairs(handlers) do + if type(handler.del) == 'function' then + -- TODO: Change handler del implementations to take a LzPlugin? + handler.del(plugin) + end + end +end + +---@param plugins table +function M.init(plugins) + for _, plugin in pairs(plugins) do + xpcall( + enable, + vim.schedule_wrap(function(err) + vim.notify(('Failed to enable handlers for %s: %s'):format(plugin.name, err), vim.log.levels.ERROR) + end), + plugin + ) + end +end + +return M diff --git a/lua/lz/n/handler/keys.lua b/lua/lz/n/handler/keys.lua new file mode 100644 index 0000000..1b42843 --- /dev/null +++ b/lua/lz/n/handler/keys.lua @@ -0,0 +1,128 @@ +local loader = require('lz.n.loader') + +---@class LzKeysHandler: LzHandler + +---@type LzKeysHandler +local M = { + active = {}, + managed = {}, + type = 'keys', +} + +---@param value string|LzKeysSpec +---@param mode? string +---@return LzKeys +function M.parse(value, mode) + value = type(value) == 'string' and { value } or value --[[@as LzKeysSpec]] + local ret = vim.deepcopy(value) --[[@as LzKeys]] + ret.lhs = ret[1] or '' + ret.rhs = ret[2] + ret[1] = nil + ret[2] = nil + ret.mode = mode or 'n' + ret.id = vim.api.nvim_replace_termcodes(ret.lhs, true, true, true) + if ret.ft then + local ft = type(ret.ft) == 'string' and { ret.ft } or ret.ft --[[@as string[] ]] + ret.id = ret.id .. ' (' .. table.concat(ft, ', ') .. ')' + end + if ret.mode ~= 'n' then + ret.id = ret.id .. ' (' .. ret.mode .. ')' + end + return ret +end + +local skip = { mode = true, id = true, ft = true, rhs = true, lhs = true } + +---@param keys LzKeys +---@return LzKeysBase +local function get_opts(keys) + ---@type LzKeysBase + local opts = {} + for k, v in pairs(keys) do + if type(k) ~= 'number' and not skip[k] then + opts[k] = v + end + end + return opts +end + +-- Create a mapping if it is managed by lz.n +---@param keys LzKeys +---@param buf integer? +local function set(keys, buf) + if keys.rhs then + local opts = get_opts(keys) + opts.buffer = buf + vim.keymap.set(keys.mode, keys.lhs, keys.rhs, opts) + end +end + +-- Delete a mapping and create the real global +-- mapping when needed +---@param keys LzKeys +local function del(keys) + pcall(vim.keymap.del, keys.mode, keys.lhs, { + -- NOTE: for buffer-local mappings, we only delete the mapping for the current buffer + -- So the mapping could still exist in other buffers + buffer = keys.ft and true or nil, + }) + -- make sure to create global mappings when needed + -- buffer-local mappings are managed by lazy + if not keys.ft then + set(keys) + end +end + +---@param keys LzKeys +M.add = function(keys) + local lhs = keys.lhs + local opts = get_opts(keys) + + ---@param buf? number + local function add(buf) + vim.keymap.set(keys.mode, lhs, function() + local plugins = M.active[keys.id] + -- always delete the mapping immediately to prevent recursive mappings + del(keys) + M.active[keys.id] = nil + if plugins then + loader.load(plugins) + end + -- Create the real buffer-local mapping + if keys.ft then + set(keys, buf) + end + if keys.mode:sub(-1) == 'a' then + lhs = lhs .. '' + end + local feed = vim.api.nvim_replace_termcodes('' .. lhs, true, true, true) + -- insert instead of append the lhs + vim.api.nvim_feedkeys(feed, 'i', false) + end, { + desc = opts.desc, + nowait = opts.nowait, + -- we do not return anything, but this is still needed to make operator pending mappings work + expr = true, + buffer = buf, + }) + end + -- buffer-local mappings + if keys.ft then + vim.api.nvim_create_autocmd('FileType', { + pattern = keys.ft, + callback = function(event) + if M.active[keys.id] then + add(event.buf) + else + -- Only create the mapping if its managed by lz.n + -- otherwise the plugin is supposed to manage it + set(keys, event.buf) + end + end, + }) + else + add() + end +end + +return M diff --git a/lua/lz/n/init.lua b/lua/lz/n/init.lua new file mode 100644 index 0000000..848dbf8 --- /dev/null +++ b/lua/lz/n/init.lua @@ -0,0 +1,27 @@ +---@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) + require('lz.n.state').plugins = plugins + require('lz.n.handler').init(plugins) +end + +return M diff --git a/lua/lz/n/loader.lua b/lua/lz/n/loader.lua new file mode 100644 index 0000000..927bc68 --- /dev/null +++ b/lua/lz/n/loader.lua @@ -0,0 +1,85 @@ +---@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 + require('lz.n.handler').disable(plugin) + -- TODO: Load plugin +end + +---@param plugins table +local function run_before_all(plugins) + for _, plugin in pairs(plugins) do + if plugin.beforeAll then + xpcall( + plugin.beforeAll, + vim.schedule_wrap(function(err) + vim.notify( + "Failed to run 'beforeAll' for " .. plugin.name .. ': ' .. tostring(err or ''), + vim.log.levels.ERROR + ) + end), + plugin + ) + end + end +end + +---@param plugins table +---@return LzPlugin[] +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 + +--- Loads startup plugins, removing loaded plugins from the table +---@param plugins table +function M.load_startup_plugins(plugins) + run_before_all(plugins) + for _, plugin in pairs(get_eager_plugins(plugins)) do + M.load(plugin) + plugins[plugin.name] = nil + 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] + 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..030548a --- /dev/null +++ b/lua/lz/n/spec.lua @@ -0,0 +1,142 @@ +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 + +---@param spec LzPluginSpec +---@return LzPlugin +local function parse(spec) + ---@type LzPlugin + ---@diagnostic disable-next-line: assign-type-mismatch + local result = vim.deepcopy(spec) + local event_spec = spec.event + if event_spec then + result.event = {} + end + if type(event_spec) == 'string' then + local event = require('lz.n.handler.event').parse(event_spec) + result.event[event.id] = event + elseif type(event_spec) == 'table' then + ---@cast event_spec LzEventSpec[] + for _, _event_spec in pairs(event_spec) do + local event = require('lz.n.handler.event').parse(_event_spec) + result.ft[event.id] = event + end + end + local ft_spec = spec.ft + if ft_spec then + result.ft = {} + end + if type(ft_spec) == 'string' then + local ft = require('lz.n.handler.ft').parse(ft_spec) + result[ft.id] = ft + elseif type(ft_spec) == 'table' then + for _, _ft_spec in pairs(ft_spec) do + local ft = require('lz.n.handler.ft').parse(_ft_spec) + result.ft[ft.id] = ft + end + end + local keys_spec = spec.keys + if keys_spec then + result.keys = {} + end + if type(keys_spec) == 'string' then + local keys = require('lz.n.handler.keys').parse(keys_spec) + result.keys[keys.id] = keys + elseif type(keys_spec) == 'table' then + ---@cast keys_spec string[] | LzKeysSpec[] + for _, _keys_spec in pairs(keys_spec) do + local keys = require('lz.n.handler.keys').parse(_keys_spec) + result.keys[keys.id] = keys + end + end + local cmd_spec = spec.cmd + if cmd_spec then + result.cmd = {} + end + if type(cmd_spec) == 'string' then + result.cmd[cmd_spec] = cmd_spec + elseif type(cmd_spec) == 'table' then + for _, _cmd_spec in pairs(cmd_spec) do + result.cmd[_cmd_spec] = _cmd_spec + end + end + return 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.name then + ---@cast spec LzPluginSpec + result[spec.name] = parse(spec) + 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..ce55af8 --- /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|LzEventSpec[] +---@field cmd? string[]|string +---@field ft? string[]|string +---@field keys? string|string[]|LzKeysSpec[] +---@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) diff --git a/nix/ci-overlay.nix b/nix/ci-overlay.nix index 798dc4f..469029f 100755 --- a/nix/ci-overlay.nix +++ b/nix/ci-overlay.nix @@ -1,18 +1,12 @@ # Add flake.nix test inputs as arguments here { self, - neodev-nvim, plugin-name, }: final: prev: with final.lib; with final.stdenv; let nvim-nightly = final.neovim-nightly; - neodev-plugin = final.pkgs.vimUtils.buildVimPlugin { - name = "neodev.nvim"; - src = neodev-nvim; - }; - mkNeorocksTest = { name, nvim ? final.neovim-unwrapped, @@ -63,8 +57,5 @@ in { name = "neovim-nightly-tests"; nvim = nvim-nightly; }; - inherit - neodev-plugin - docgen - ; + inherit docgen; } diff --git a/spec/keys_spec.lua b/spec/keys_spec.lua new file mode 100644 index 0000000..d90cef7 --- /dev/null +++ b/spec/keys_spec.lua @@ -0,0 +1,18 @@ +local keys = require('lz.n.handler.keys') + +describe('keys', function() + it('parses ids correctly', function() + local tests = { + { '', '', true }, + { '', '', true }, + { 'k', 'K', false }, + } + for _, test in ipairs(tests) do + if test[3] then + assert.same(keys.parse(test[1]).id, keys.parse(test[2]).id) + else + assert.is_not.same(keys.parse(test[1]).id, keys.parse(test[2]).id) + end + end + end) +end)