diff --git a/lua/persisted/config.lua b/lua/persisted/config.lua index ac1c8d2..2287942 100644 --- a/lua/persisted/config.lua +++ b/lua/persisted/config.lua @@ -1,6 +1,7 @@ local M = {} local defaults = { + log_level = "ERROR", -- One of "TRACE", "DEBUG", "ERROR" save_dir = vim.fn.expand(vim.fn.stdpath("data") .. "/sessions/"), -- directory where session files are saved silent = false, -- silent nvim message when sourcing session file diff --git a/lua/persisted/init.lua b/lua/persisted/init.lua index f0bf721..6a5f4b9 100644 --- a/lua/persisted/init.lua +++ b/lua/persisted/init.lua @@ -1,5 +1,6 @@ -local utils = require("persisted.utils") local config = require("persisted.config") +local log = require("persisted.log") +local utils = require("persisted.utils") local M = {} @@ -143,14 +144,8 @@ function M.get_branch(dir) if vim.fn.filereadable(branch_session) ~= 0 then return branch else - vim.notify( - string.format("[Persisted.nvim]: Trying to load a session for branch %s", config.options.default_branch), - vim.log.levels.INFO - ) - vim.notify( - string.format("[Persisted.nvim]: Could not load a session for branch %s.", git_branch[1]), - vim.log.levels.WARN - ) + log:debug("Trying to load a session for branch: %s", config.options.default_branch) + log:error("Could not load a session for branch: %s", git_branch[1]) vim.g.persisted_branch_session = branch_session return config.options.branch_separator .. config.options.default_branch end @@ -183,6 +178,21 @@ end ---@return nil function M.setup(opts) config.setup(opts) + + log.set_root(log.new({ + handlers = { + { + type = "echo", + level = vim.log.levels.WARN, + }, + { + type = "file", + filename = "persisted.log", + level = vim.log.levels[config.options.log_level], + }, + }, + })) + local dir = session_dir() local branch = get_branchname() @@ -192,6 +202,7 @@ function M.setup(opts) and not ignore_branch(branch) and args_check() then + log:trace("Starting session") M.start() end end @@ -210,19 +221,24 @@ function M.load(opt, dir) session = opt.session local session_data = utils.make_session_data(session) branch = session_data and session_data.branch or "" + log:trace("Session branch %s", branch) + log:trace("Session data %s", session_data) if not branch then vim.notify(string.format("[Persisted.nvim]: Invalid session file %s", session), vim.log.levels.WARN) end else branch = get_branchname() session = opt.last and get_last() or get_current(dir) + log:trace("Session branch: %s", branch) end if session then if vim.fn.filereadable(session) ~= 0 then vim.g.persisting_session = not config.options.follow_cwd and session or nil + log:trace("Session load: %s", session) utils.load_session(session, config.options.silent) elseif type(config.options.on_autoload_no_session) == "function" then + log:trace("No session to load") config.options.on_autoload_no_session() end end @@ -241,6 +257,7 @@ function M.autoload() if config.options.autoload and args_check() then if allow_dir(dir) and not ignore_dir(dir) and not ignore_branch(branch) then + log:trace("Autoloading from %s", dir) M.load({}, dir) end end @@ -268,6 +285,7 @@ local function write(session) vim.api.nvim_exec_autocmds("User", { pattern = "PersistedSavePre" }) vim.cmd("mks! " .. e(session)) vim.g.persisting = true + log:trace("Session saved") vim.api.nvim_exec_autocmds("User", { pattern = "PersistedSavePost" }) end @@ -317,6 +335,7 @@ function M.delete(dir) vim.api.nvim_exec_autocmds("User", { pattern = "PersistedDeletePre", data = session_data }) vim.schedule(function() + log:trace("Deleting session %s", session) M.stop() vim.fn.system("rm " .. e(session)) end) @@ -334,13 +353,16 @@ function M.toggle(dir) dir = dir or session_dir() if vim.g.persisting == nil then + log:trace("Toggling load") return M.load({}, dir) end if vim.g.persisting then + log:trace("Toggling stop") return M.stop() end + log:trace("Toggling start") return M.start() end diff --git a/lua/persisted/log.lua b/lua/persisted/log.lua new file mode 100644 index 0000000..6ca9a25 --- /dev/null +++ b/lua/persisted/log.lua @@ -0,0 +1,280 @@ +---@type boolean +local is_windows = vim.loop.os_uname().version:match("Windows") + +---@type string +local sep = is_windows and "\\" or "/" + +---@return string +local join = function(...) + local joined = table.concat({ ... }, sep) + if is_windows then + joined = joined:gsub("\\\\+", "\\") + else + joined = joined:gsub("//+", "/") + end + return joined +end + +---@class LogHandler +---@field type string +---@field level integer +---@field formatter? fun(level: integer, msg: string, ...: any) +---@field handle? fun(level: integer, text: string) +local LogHandler = {} + +local levels_reverse = {} +for k, v in pairs(vim.log.levels) do + levels_reverse[v] = k +end + +function LogHandler.new(opts) + vim.validate({ + type = { opts.type, "s" }, + handle = { opts.handle, "f" }, + formatter = { opts.formatter, "f" }, + level = { opts.level, "n", true }, + }) + return setmetatable({ + type = opts.type, + handle = opts.handle, + formatter = opts.formatter, + level = opts.level or vim.log.levels.INFO, + }, { __index = LogHandler }) +end + +function LogHandler:log(level, msg, ...) + if self.level <= level then + local text = self.formatter(level, msg, ...) + self.handle(level, text) + end +end + +local function default_formatter(level, msg, ...) + local args = vim.F.pack_len(...) + for i = 1, args.n do + local v = args[i] + if type(v) == "table" then + args[i] = vim.inspect(v) + elseif v == nil then + args[i] = "nil" + end + end + local ok, text = pcall(string.format, msg, vim.F.unpack_len(args)) + if ok then + local str_level = levels_reverse[level] + return string.format("[%s] %s\n%s", str_level, os.date("%Y-%m-%d %H:%M:%S"), text) + else + return string.format("[ERROR] error formatting log line: '%s' args %s", msg, vim.inspect(args)) + end +end + +---@param opts table +---@return LogHandler +local function create_file_handler(opts) + vim.validate({ + filename = { opts.filename, "s" }, + }) + local ok, stdpath = pcall(vim.fn.stdpath, "log") + if not ok then + stdpath = vim.fn.stdpath("cache") + end + local filepath = join(stdpath, opts.filename) + local logfile, openerr = io.open(filepath, "a+") + if not logfile then + local err_msg = string.format("Failed to open the CodeCompanion log file: %s", openerr) + vim.notify(err_msg, vim.log.levels.ERROR) + opts.handle = function() end + else + opts.handle = function(level, text) + logfile:write(text) + logfile:write("\n") + logfile:flush() + end + end + return LogHandler.new(opts) +end + +---@param opts table +---@return LogHandler +local function create_notify_handler(opts) + opts.handle = function(level, text) + vim.notify(text, level) + end + return LogHandler.new(opts) +end + +---@param opts table +---@return LogHandler +local function create_echo_handler(opts) + opts.handle = function(level, text) + local hl = "Normal" + if level == vim.log.levels.ERROR then + hl = "DiagnosticError" + elseif level == vim.log.levels.WARN then + hl = "DiagnosticWarn" + end + vim.api.nvim_echo({ { text, hl } }, true, {}) + end + return LogHandler.new(opts) +end + +---@return LogHandler +local function create_null_handler() + return LogHandler.new({ + formatter = function() end, + handle = function() end, + }) +end + +---@param opts table +---@return LogHandler +local function create_handler(opts) + vim.validate({ + type = { opts.type, "s" }, + }) + if not opts.formatter then + opts.formatter = default_formatter + end + if opts.type == "file" then + return create_file_handler(opts) + elseif opts.type == "notify" then + return create_notify_handler(opts) + elseif opts.type == "echo" then + return create_echo_handler(opts) + else + vim.notify(string.format("Unknown log handler %s", opts.type), vim.log.levels.ERROR) + return create_null_handler() + end +end + +---@class Logger +---@field handlers LogHandler[] +local Logger = {} + +---@class LoggerArgs +---@field handlers LogHandler[] +---@field level nil|integer + +---@param opts LoggerArgs +function Logger.new(opts) + vim.validate({ + handlers = { opts.handlers, "t" }, + level = { opts.level, "n", true }, + }) + local handlers = {} + for _, defn in ipairs(opts.handlers) do + table.insert(handlers, create_handler(defn)) + end + local log = setmetatable({ + handlers = handlers, + }, { __index = Logger }) + if opts.level then + log:set_level(opts.level) + end + return log +end + +---@param level integer +function Logger:set_level(level) + for _, handler in ipairs(self.handlers) do + handler.level = level + end +end + +---@return LogHandler[] +function Logger:get_handlers() + return self.handlers +end + +---@param level integer +---@param msg string +---@param ... any[] +function Logger:log(level, msg, ...) + for _, handler in ipairs(self.handlers) do + handler:log(level, msg, ...) + end +end + +---@param msg string +---@param ... any +function Logger:trace(msg, ...) + self:log(vim.log.levels.TRACE, msg, ...) +end + +---@param msg string +---@param ... any +function Logger:debug(msg, ...) + self:log(vim.log.levels.DEBUG, msg, ...) +end + +---@param msg string +---@param ... any +function Logger:info(msg, ...) + self:log(vim.log.levels.INFO, msg, ...) +end + +---@param msg string +---@param ... any +function Logger:warn(msg, ...) + self:log(vim.log.levels.WARN, msg, ...) +end + +---@param msg string +---@param ... any +function Logger:error(msg, ...) + self:log(vim.log.levels.ERROR, msg, ...) +end + +---@generic T : any +---@param cb T +---@param message nil|string +---@return T +function Logger:wrap_cb(cb, message) + return function(err, ...) + if err then + self:error(message or "Error: %s", err) + end + return cb(err, ...) + end +end + +local root = Logger.new({ + handlers = { + { + type = "echo", + level = vim.log.levels.WARN, + }, + }, +}) + +---@class Logger +local M = {} + +M.new = Logger.new + +M.get_logfile = function() + local ok, stdpath = pcall(vim.fn.stdpath, "log") + if not ok then + stdpath = vim.fn.stdpath("cache") + end + + return join(stdpath, "persisted.log") +end + +---@param logger Logger +M.set_root = function(logger) + root = logger +end + +---@return Logger +M.get_root = function() + return root +end + +setmetatable(M, { + __index = function(_, key) + return root[key] + end, +}) + +return M