diff --git a/lua/core/commands.lua b/lua/core/commands.lua index 040561b..a95ac52 100644 --- a/lua/core/commands.lua +++ b/lua/core/commands.lua @@ -1,3 +1,23 @@ vim.api.nvim_create_user_command("W", "w", { desc = "Alias to :w" }) vim.api.nvim_create_user_command("Q", "q", { desc = "Alias to :q" }) vim.api.nvim_create_user_command("Qa", "q", { desc = "Alias to :qa" }) + +vim.api.nvim_create_user_command("PluginWatch", function() + require("plugin").watch() +end, { desc = "Watch plugin configs for changes and hot-reload" }) + +vim.api.nvim_create_user_command("PluginUnwatch", function() + require("plugin").unwatch() +end, { desc = "Stop watching plugin configs" }) + +vim.api.nvim_create_user_command("PluginReload", function(opts) + require("plugin").reload(opts.args) +end, { + nargs = 1, + complete = function(lead) + return vim.tbl_filter(function(name) + return name:find(lead, 1, true) == 1 + end, require("plugin").get_names()) + end, + desc = "Reload a plugin config by name", +}) diff --git a/lua/plugin.lua b/lua/plugin.lua index 887bd03..cee1a43 100644 --- a/lua/plugin.lua +++ b/lua/plugin.lua @@ -13,6 +13,23 @@ local log = require("log") ---@type ow.Plugin[] local plugins = {} +---@param path string +---@return boolean success +---@return string? err +local function exec(path) + local chunk, load_err = loadfile(path) + if not chunk then + return false, load_err + end + + local ok, call_err = pcall(chunk) + if not ok then + return false, call_err + end + + return true +end + ---@param plugin ow.Plugin local function load(plugin) vim.cmd.packadd(plugin.name) @@ -25,7 +42,7 @@ local function load(plugin) ) if vim.uv.fs_stat(path) then - local ok, err = pcall(dofile, path) + local ok, err = exec(path) if not ok then log.error("Failed to load %s: %s", name, err) end @@ -97,6 +114,9 @@ local function handle_pack_events(plugin, events) return true end +---@type uv.uv_fs_event_t? +local watcher = nil + local M = {} function M.get_paths() @@ -109,6 +129,115 @@ function M.get_paths() return paths end +---@return string[] +function M.get_names() + local names = {} + + for _, plugin in ipairs(plugins) do + table.insert(names, plugin.name:match("[^.]+"):lower()) + end + + return names +end + +---@param name string +function M.reload(name) + local path = string.format( + "%s/lua/plugins/%s.lua", + vim.fn.stdpath("config"), + name + ) + + if not vim.uv.fs_stat(path) then + log.error("No config file found for %s", name) + return + end + + local ok, err = exec(path) + if ok then + log.info("Reloaded %s", name) + else + log.error("Failed to reload %s: %s", name, err) + end +end + +function M.watch() + if watcher then + return + end + + local plugins_dir = string.format( + "%s/lua/plugins", + vim.fn.stdpath("config") + ) + + watcher = vim.uv.new_fs_event() + if not watcher then + log.error("Failed to create fs_event watcher") + return + end + + ---@type table + local timers = {} + + watcher:start( + plugins_dir, + {}, + vim.schedule_wrap(function(err, filename) + if err then + log.error("Watch error: %s", err) + return + end + + if not filename or not filename:match("%.lua$") then + return + end + + if timers[filename] then + timers[filename]:stop() + else + timers[filename] = vim.uv.new_timer() + end + + timers[filename]:start( + 100, + 0, + vim.schedule_wrap(function() + timers[filename]:stop() + timers[filename]:close() + timers[filename] = nil + + local path = plugins_dir .. "/" .. filename + if not vim.uv.fs_stat(path) then + return + end + + local ok, load_err = exec(path) + if ok then + log.info("Reloaded %s", filename) + else + log.error( + "Failed to reload %s: %s", + filename, + load_err + ) + end + end) + ) + end) + ) +end + +function M.unwatch() + if not watcher then + return + end + + watcher:stop() + watcher:close() + watcher = nil +end + ---@param specs (string | ow.Plugin.Spec)[] function M.setup(specs) ---@type ow.Plugin.PackEvent[]