diff --git a/README.md b/README.md index 24365e7..ffdd437 100644 --- a/README.md +++ b/README.md @@ -394,7 +394,37 @@ return { --- -## Picker internals (module paths) +## Debug & Troubleshooting + +### Debug Commands + +| Command | Description | +|---------|-------------| +| `:RaphaelDebug` or `:RaphaelDebug status` | Show diagnostics | +| `:RaphaelDebug repair` | Fix corrupted state | +| `:RaphaelDebug backup` | Create backup | +| `:RaphaelDebug restore` | Restore from backup | +| `:RaphaelDebug export` | Export state to file | +| `:RaphaelDebug clear` | Reset all state | +| `:RaphaelDebug stats` | Show statistics | + +### State File + +Located at `stdpath("data")/raphael/state.json`: +- `current`, `saved`, `previous` themes +- `bookmarks`, `history`, `quick_slots` +- `undo_history` stack + +Backup saved to `state.json.backup`. + +### Corrupted State? + +1. Run `:RaphaelDebug status` to check +2. Run `:RaphaelDebug repair` to fix +3. If still broken: `:RaphaelDebug restore` from backup +4. Last resort: `:RaphaelDebug clear` to reset + +### Picker Debug ```vim :lua require("raphael.picker.ui").toggle_debug() diff --git a/lua/raphael/config.lua b/lua/raphael/config.lua index 6c9d3fb..2a051d5 100644 --- a/lua/raphael/config.lua +++ b/lua/raphael/config.lua @@ -53,7 +53,7 @@ M.defaults = { random = "r", }, - default_theme = "kanagawa-paper-ink", + default_theme = "habamax", bookmark_group = true, recent_group = true, diff --git a/lua/raphael/config_manager.lua b/lua/raphael/config_manager.lua index 10f1b4a..aa62b73 100644 --- a/lua/raphael/config_manager.lua +++ b/lua/raphael/config_manager.lua @@ -147,7 +147,7 @@ function M.validate_config_sections(config_data) if type(config_data.mappings) == "table" then results.mappings = true - for key, val in pairs(config_data.mappings) do + for _, val in pairs(config_data.mappings) do if type(val) ~= "string" then results.mappings = false break @@ -230,7 +230,7 @@ function M.get_config_diagnostics(config_data) missing_defaults = {}, } - for k, _ in pairs(config_data) do + for _ in pairs(config_data) do diagnostics.total_keys = diagnostics.total_keys + 1 end @@ -240,7 +240,7 @@ function M.get_config_diagnostics(config_data) end end - for key, default_val in pairs(config.defaults) do + for key in pairs(config.defaults) do if config_data[key] == nil then table.insert(diagnostics.missing_defaults, key) end diff --git a/lua/raphael/core/autocmds.lua b/lua/raphael/core/autocmds.lua index 65625fe..1be2ea2 100644 --- a/lua/raphael/core/autocmds.lua +++ b/lua/raphael/core/autocmds.lua @@ -243,21 +243,13 @@ function M.picker_cursor_autocmd(picker_buf, cbs) local update_preview = cbs.update_preview local ctx = cbs.ctx or {} - local initial_setup_phase = true - local debounce_utils = require("raphael.utils.debounce") local debounced_preview = debounce_utils.debounce(function(theme) if theme and type(preview_fn) == "function" then - local should_preview = not initial_setup_phase - if ctx.initial_render ~= nil then - should_preview = should_preview and not ctx.initial_render - end - - if should_preview then - preview_fn(theme) - else + if ctx.picker_ready ~= true then return end + preview_fn(theme) end end, 100) @@ -311,7 +303,6 @@ function M.picker_cursor_autocmd(picker_buf, cbs) theme = parse(line) end if theme then - -- The preview is handled by the debounced_preview function which checks initial_setup_phase debounced_preview(theme) end end @@ -321,11 +312,6 @@ function M.picker_cursor_autocmd(picker_buf, cbs) debounced_update_preview({ debounced = true }) end, }) - - -- Set a timer to disable the initial setup phase after a delay - vim.defer_fn(function() - initial_setup_phase = false - end, 300) -- 300ms to ensure full setup completion end --- Attach a BufDelete autocmd to the picker buffer. diff --git a/lua/raphael/core/cache.lua b/lua/raphael/core/cache.lua index 5918a56..5206152 100644 --- a/lua/raphael/core/cache.lua +++ b/lua/raphael/core/cache.lua @@ -6,6 +6,11 @@ --- - Every helper reads the JSON, mutates, and writes it back. --- - The in-memory "authoritative" state lives in raphael.core; this --- module is the disk-backed source of truth. +--- +--- Features: +--- - Automatic backup before writes +--- - State validation and auto-repair on read +--- - Graceful handling of corrupted state local constants = require("raphael.constants") @@ -14,6 +19,7 @@ local M = {} local uv = vim.loop local decode_failed_once = false +local auto_repair_enabled = true local function ensure_dir() local dir = vim.fn.fnamemodify(constants.STATE_FILE, ":h") @@ -22,6 +28,39 @@ local function ensure_dir() end end +local function get_backup_path() + return constants.STATE_FILE .. ".backup" +end + +local function create_backup() + local file = io.open(constants.STATE_FILE, "r") + if not file then + return false + end + local content = file:read("*a") + file:close() + + local backup_path = get_backup_path() + local backup_file = io.open(backup_path, "w") + if not backup_file then + return false + end + backup_file:write(content) + backup_file:close() + return true +end + +local function restore_backup() + local backup_path = get_backup_path() + local file = io.open(backup_path, "r") + if not file then + return nil, "No backup file" + end + local content = file:read("*a") + file:close() + return content, nil +end + --- Default state structure (pure, no side effects). --- --- @return table state @@ -143,12 +182,15 @@ end --- Async write helper. --- --- Writes `data` to `path` asynchronously using libuv, and notifies on error. +--- Creates a backup before writing. --- --- @param path string Absolute path to state file --- @param data string JSON-encoded string local function async_write(path, data) ensure_dir() + create_backup() + uv.fs_open(path, "w", 438, function(open_err, fd) if open_err or not fd then vim.schedule(function() @@ -171,6 +213,8 @@ end --- Read state from disk (or return defaults if file doesn't exist / is invalid). --- --- This is the canonical entry for reading the on-disk state. +--- If the file is corrupted, attempts to restore from backup. +--- Validates and auto-repairs state if needed. --- --- @return table state function M.read() @@ -189,13 +233,42 @@ function M.read() local ok, decoded = pcall(vim.json.decode, content) if not ok then if not decode_failed_once then - vim.notify("raphael.nvim: Failed to decode state file, using defaults", vim.log.levels.WARN) + vim.notify("raphael.nvim: State file corrupted, attempting backup restore...", vim.log.levels.WARN) decode_failed_once = true end + + local backup_content, backup_err = restore_backup() + if backup_content then + local backup_ok, backup_decoded = pcall(vim.json.decode, backup_content) + if backup_ok then + vim.notify("raphael.nvim: Restored from backup successfully", vim.log.levels.INFO) + return normalize_state(backup_decoded) + end + end + + vim.notify( + "raphael.nvim: Backup restore failed (" .. tostring(backup_err) .. "), using defaults", + vim.log.levels.WARN + ) return default_state() end - return normalize_state(decoded) + local state = normalize_state(decoded) + + if auto_repair_enabled then + local ok_debug, debug_mod = pcall(require, "raphael.debug") + if ok_debug and debug_mod then + local issues = debug_mod.validate_state(state) + if #issues > 0 then + state = debug_mod.repair_state(state) + vim.schedule(function() + M.write(state) + end) + end + end + end + + return state end --- Write full state to disk (async). @@ -663,4 +736,59 @@ function M.redo_pop() return undo.stack[undo.index] end +--- Get path to backup file. +--- +--- @return string +function M.get_backup_path() + return get_backup_path() +end + +--- Manually create a backup of current state. +--- +--- @return boolean success +function M.create_backup() + return create_backup() +end + +--- Restore state from backup file. +--- +--- @return boolean success +--- @return string|nil error +function M.restore_from_backup() + local content, err = restore_backup() + if not content then + return false, err + end + + local ok, decoded = pcall(vim.json.decode, content) + if not ok then + return false, "Backup file is corrupted" + end + + local state = normalize_state(decoded) + M.write(state) + return true, nil +end + +--- Check if state file exists. +--- +--- @return boolean +function M.state_exists() + return vim.fn.filereadable(constants.STATE_FILE) == 1 +end + +--- Check if backup file exists. +--- +--- @return boolean +function M.backup_exists() + return vim.fn.filereadable(get_backup_path()) == 1 +end + +--- Get state file path. +--- +--- @return string +function M.get_state_path() + return constants.STATE_FILE +end + return M diff --git a/lua/raphael/core/cmds.lua b/lua/raphael/core/cmds.lua index becf830..54cf79d 100644 --- a/lua/raphael/core/cmds.lua +++ b/lua/raphael/core/cmds.lua @@ -158,7 +158,7 @@ function M.setup(core) end, { desc = "Apply a random theme" }) vim.api.nvim_create_user_command("RaphaelBookmarkToggle", function() - if not core.picker or not core.picker.picker_win or not vim.api.nvim_win_is_valid(core.picker.picker_win) then + if not picker.is_open() then vim.notify("Picker not open – opening it first…", vim.log.levels.INFO) core.open_picker({ only_configured = true }) vim.defer_fn(function() @@ -601,6 +601,67 @@ function M.setup(core) end, desc = "Apply a Raphael configuration preset (:RaphaelConfigPreset [preset_name])", }) + + vim.api.nvim_create_user_command("RaphaelDebug", function(opts) + local ok, debug_mod = pcall(require, "raphael.debug") + if not ok then + vim.notify("raphael: debug module not available", vim.log.levels.ERROR) + return + end + + local subcmd = vim.trim(opts.args or "") + if subcmd == "" or subcmd == "status" then + debug_mod.show_diagnostics() + elseif subcmd == "repair" then + debug_mod.repair_and_save() + elseif subcmd == "backup" then + local success, err = debug_mod.backup_state() + if success then + vim.notify("raphael: backup created at " .. debug_mod.get_backup_path(), vim.log.levels.INFO) + else + vim.notify("raphael: backup failed: " .. tostring(err), vim.log.levels.ERROR) + end + elseif subcmd == "restore" then + local cache = require("raphael.core.cache") + local success, err = cache.restore_from_backup() + if success then + vim.notify("raphael: restored from backup", vim.log.levels.INFO) + else + vim.notify("raphael: restore failed: " .. tostring(err), vim.log.levels.ERROR) + end + elseif subcmd == "export" then + local success, path = debug_mod.export_state() + if not success then + vim.notify("raphael: export failed: " .. tostring(path), vim.log.levels.ERROR) + end + elseif subcmd == "clear" then + local cache = require("raphael.core.cache") + cache.clear() + vim.notify("raphael: state cleared", vim.log.levels.INFO) + elseif subcmd == "stats" then + local stats = debug_mod.get_stats() + vim.notify(vim.inspect(stats), vim.log.levels.INFO) + else + vim.notify("raphael: unknown debug command: " .. subcmd, vim.log.levels.WARN) + vim.notify("Available: status, repair, backup, restore, export, clear, stats", vim.log.levels.INFO) + end + end, { + nargs = "?", + complete = function(ArgLead) + local cmds = { "status", "repair", "backup", "restore", "export", "clear", "stats" } + if not ArgLead or ArgLead == "" then + return cmds + end + local res = {} + for _, c in ipairs(cmds) do + if c:find(ArgLead:lower(), 1, true) then + table.insert(res, c) + end + end + return res + end, + desc = "Raphael debug commands: status, repair, backup, restore, export, clear, stats", + }) end return M diff --git a/lua/raphael/core/init.lua b/lua/raphael/core/init.lua index b96a643..4dcd91e 100644 --- a/lua/raphael/core/init.lua +++ b/lua/raphael/core/init.lua @@ -395,30 +395,8 @@ end function M.open_picker(opts) opts = opts or {} - local preload_themes = vim.schedule_wrap(function() - local themes_to_preload = {} - if opts.exclude_configured then - local all_installed = vim.tbl_keys(themes.installed) - local all_configured = themes.get_all_themes() - for _, theme in ipairs(all_installed) do - if not vim.tbl_contains(all_configured, theme) then - table.insert(themes_to_preload, theme) - end - end - else - themes_to_preload = themes.get_all_themes() - end - - local palette_cache_mod = get_palette_cache() - if palette_cache_mod then - palette_cache_mod.preload_palettes(themes_to_preload) - end - end) - local result = picker.open(M, opts or {}) - preload_themes() - return result end diff --git a/lua/raphael/debug.lua b/lua/raphael/debug.lua new file mode 100644 index 0000000..989b17a --- /dev/null +++ b/lua/raphael/debug.lua @@ -0,0 +1,486 @@ +-- lua/raphael/debug.lua +-- Debug and diagnostics module for raphael.nvim +-- +-- Provides: +-- - State inspection and validation +-- - History repair +-- - Backup/restore +-- - Diagnostic commands + +local M = {} + +local constants = require("raphael.constants") +local themes = require("raphael.themes") + +local LOG_LEVELS = { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3 } +local current_log_level = LOG_LEVELS.WARN +local log_buffer = {} +local MAX_LOG_ENTRIES = 100 + +local function log(level, msg, data) + if level < current_log_level then + return + end + + local entry = { + time = os.date("%H:%M:%S"), + level = level, + msg = msg, + data = data, + } + + table.insert(log_buffer, entry) + while #log_buffer > MAX_LOG_ENTRIES do + table.remove(log_buffer, 1) + end +end + +function M.set_log_level(level) + if type(level) == "string" then + level = LOG_LEVELS[level:upper()] or LOG_LEVELS.WARN + end + current_log_level = level +end + +function M.debug(msg, data) + log(LOG_LEVELS.DEBUG, msg, data) +end + +function M.info(msg, data) + log(LOG_LEVELS.INFO, msg, data) +end + +function M.warn(msg, data) + log(LOG_LEVELS.WARN, msg, data) +end + +function M.error(msg, data) + log(LOG_LEVELS.ERROR, msg, data) +end + +function M.get_logs() + return log_buffer +end + +function M.clear_logs() + log_buffer = {} +end + +function M.get_state_file_path() + return constants.STATE_FILE +end + +function M.get_backup_path() + return constants.STATE_FILE .. ".backup" +end + +function M.read_raw_state() + local file = io.open(constants.STATE_FILE, "r") + if not file then + return nil, "File not found" + end + local content = file:read("*a") + file:close() + return content, nil +end + +function M.validate_state(state) + local issues = {} + local warnings = {} + + if type(state) ~= "table" then + return { fatal = "State is not a table" }, {} + end + + if state.current and not themes.is_available(state.current) then + table.insert(warnings, "current theme '" .. tostring(state.current) .. "' not available") + end + + if state.saved and not themes.is_available(state.saved) then + table.insert(warnings, "saved theme '" .. tostring(state.saved) .. "' not available") + end + + if state.previous and not themes.is_available(state.previous) then + table.insert(warnings, "previous theme '" .. tostring(state.previous) .. "' not available") + end + + if type(state.history) ~= "table" then + table.insert(issues, "history is not a table, resetting") + else + local invalid = {} + for i, theme in ipairs(state.history) do + if type(theme) ~= "string" or theme == "" then + table.insert(invalid, i) + elseif not themes.is_available(theme) then + table.insert(warnings, "history[" .. i .. "] theme '" .. theme .. "' not available") + end + end + if #invalid > 0 then + table.insert(issues, "history has " .. #invalid .. " invalid entries") + end + end + + if type(state.bookmarks) ~= "table" then + table.insert(issues, "bookmarks is not a table, resetting") + else + for scope, list in pairs(state.bookmarks) do + if type(list) ~= "table" then + table.insert(issues, "bookmarks[" .. scope .. "] is not a list") + else + for i, theme in ipairs(list) do + if type(theme) ~= "string" or theme == "" then + table.insert(issues, "bookmarks[" .. scope .. "][" .. i .. "] is invalid") + end + end + end + end + end + + if type(state.undo_history) ~= "table" then + table.insert(issues, "undo_history is not a table, resetting") + else + if type(state.undo_history.stack) ~= "table" then + table.insert(issues, "undo_history.stack is not a table") + end + if type(state.undo_history.index) ~= "number" then + table.insert(issues, "undo_history.index is not a number") + end + if state.undo_history.index < 0 then + table.insert(issues, "undo_history.index is negative") + end + if state.undo_history.stack and state.undo_history.index > #state.undo_history.stack then + table.insert(warnings, "undo_history.index exceeds stack size") + end + end + + if type(state.quick_slots) ~= "table" then + table.insert(issues, "quick_slots is not a table, resetting") + end + + if type(state.usage) ~= "table" then + table.insert(issues, "usage is not a table, resetting") + end + + if state.current_profile ~= nil and type(state.current_profile) ~= "string" then + table.insert(issues, "current_profile is not a string or nil") + end + + if state.sort_mode ~= nil and type(state.sort_mode) ~= "string" then + table.insert(issues, "sort_mode is not a string") + end + + return issues, warnings +end + +function M.repair_state(state) + local repaired = vim.deepcopy(state) + local repairs = {} + + local function fix(key, default) + if repaired[key] == nil or type(repaired[key]) ~= type(default) then + repaired[key] = default + table.insert(repairs, "Fixed: " .. key) + end + end + + fix("current", nil) + fix("saved", nil) + fix("previous", nil) + fix("auto_apply", false) + + if type(repaired.history) ~= "table" then + repaired.history = {} + table.insert(repairs, "Reset: history") + else + local clean = {} + for _, theme in ipairs(repaired.history) do + if type(theme) == "string" and theme ~= "" then + table.insert(clean, theme) + end + end + if #clean ~= #repaired.history then + repaired.history = clean + table.insert(repairs, "Cleaned: removed invalid history entries") + end + end + + if type(repaired.bookmarks) ~= "table" then + repaired.bookmarks = { __global = {} } + table.insert(repairs, "Reset: bookmarks") + else + for scope, list in pairs(repaired.bookmarks) do + if vim.islist(list) then + local clean = {} + for _, theme in ipairs(list) do + if type(theme) == "string" and theme ~= "" then + table.insert(clean, theme) + end + end + if #clean ~= #list then + repaired.bookmarks[scope] = clean + table.insert(repairs, "Cleaned: bookmarks[" .. scope .. "]") + end + end + end + if not repaired.bookmarks.__global then + repaired.bookmarks.__global = {} + table.insert(repairs, "Added: bookmarks.__global") + end + end + + if + type(repaired.undo_history) ~= "table" + or type(repaired.undo_history.stack) ~= "table" + or type(repaired.undo_history.index) ~= "number" + then + repaired.undo_history = { + stack = {}, + index = 0, + max_size = constants.HISTORY_MAX_SIZE, + } + table.insert(repairs, "Reset: undo_history") + else + repaired.undo_history.index = math.max(0, math.min(repaired.undo_history.index, #repaired.undo_history.stack)) + if repaired.undo_history.index ~= state.undo_history.index then + table.insert(repairs, "Fixed: undo_history.index clamped") + end + end + + if type(repaired.quick_slots) ~= "table" then + repaired.quick_slots = { __global = {} } + table.insert(repairs, "Reset: quick_slots") + else + if not repaired.quick_slots.__global then + repaired.quick_slots.__global = {} + table.insert(repairs, "Added: quick_slots.__global") + end + end + + fix("usage", {}) + fix("collapsed", {}) + + if repaired.current_profile ~= nil and type(repaired.current_profile) ~= "string" then + repaired.current_profile = nil + table.insert(repairs, "Fixed: current_profile set to nil") + end + + if repaired.sort_mode ~= nil and type(repaired.sort_mode) ~= "string" then + repaired.sort_mode = "alpha" + table.insert(repairs, "Fixed: sort_mode set to alpha") + end + + return repaired, repairs +end + +function M.backup_state() + local content, err = M.read_raw_state() + if not content then + return false, err + end + + local backup_path = M.get_backup_path() + local file, ferr = io.open(backup_path, "w") + if not file then + return false, ferr + end + + file:write(content) + file:close() + return true, nil +end + +function M.restore_backup() + local backup_path = M.get_backup_path() + local file = io.open(backup_path, "r") + if not file then + return false, "No backup file found" + end + + local content = file:read("*a") + file:close() + + local ok, _ = pcall(vim.json.decode, content) + if not ok then + return false, "Backup file is corrupted" + end + + local main_file = io.open(constants.STATE_FILE, "w") + if not main_file then + return false, "Cannot write state file" + end + + main_file:write(content) + main_file:close() + return true, nil +end + +function M.health_check() + local results = { + status = "ok", + state_file = "unknown", + backup = "unknown", + validation = {}, + warnings = {}, + repairs_needed = {}, + } + + local dir = vim.fn.fnamemodify(constants.STATE_FILE, ":h") + if vim.fn.isdirectory(dir) == 0 then + results.state_file = "directory missing" + results.status = "error" + return results + end + + local file = io.open(constants.STATE_FILE, "r") + if not file then + results.state_file = "not found (will be created)" + results.status = "ok" + return results + end + file:close() + results.state_file = "exists" + + local content, err = M.read_raw_state() + if not content then + results.state_file = "cannot read: " .. tostring(err) + results.status = "error" + return results + end + + local ok, _ = pcall(vim.json.decode, content) + if not ok then + results.state_file = "invalid JSON" + results.status = "error" + results.validation = { fatal = "JSON decode failed" } + return results + end + + local cache = require("raphael.core.cache") + local state = cache.read() + local issues, warnings = M.validate_state(state) + + results.validation = issues + results.warnings = warnings + + if #issues > 0 then + results.status = "needs_repair" + results.repairs_needed = issues + elseif #warnings > 0 then + results.status = "warnings" + end + + local backup_file = io.open(M.get_backup_path(), "r") + if backup_file then + backup_file:close() + results.backup = "exists" + else + results.backup = "none" + end + + return results +end + +function M.show_diagnostics() + local health = M.health_check() + + local lines = { + "╔════════════════════════════════════════╗", + "║ Raphael Diagnostics ║", + "╚════════════════════════════════════════╝", + "", + "State file: " .. constants.STATE_FILE, + "Status: " .. health.status, + "Backup: " .. health.backup, + "", + } + + if health.validation and #health.validation > 0 then + table.insert(lines, "Issues:") + for _, issue in ipairs(health.validation) do + table.insert(lines, " - " .. issue) + end + table.insert(lines, "") + end + + if health.warnings and #health.warnings > 0 then + table.insert(lines, "Warnings:") + for _, warning in ipairs(health.warnings) do + table.insert(lines, " - " .. warning) + end + table.insert(lines, "") + end + + local cache = require("raphael.core.cache") + local state = cache.read() + + table.insert(lines, "Current state:") + table.insert(lines, " current: " .. tostring(state.current)) + table.insert(lines, " saved: " .. tostring(state.saved)) + table.insert(lines, " previous: " .. tostring(state.previous)) + table.insert(lines, " history: " .. #state.history .. " entries") + table.insert(lines, " bookmarks: " .. vim.tbl_count(state.bookmarks) .. " scopes") + table.insert( + lines, + " undo_stack: " .. #state.undo_history.stack .. " entries (index: " .. state.undo_history.index .. ")" + ) + + vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO) + return health +end + +function M.repair_and_save() + local cache = require("raphael.core.cache") + + M.backup_state() + + local state = cache.read() + local repaired, repairs = M.repair_state(state) + + if #repairs > 0 then + vim.notify("Raphael: Repaired " .. #repairs .. " issues:\n" .. table.concat(repairs, "\n"), vim.log.levels.INFO) + else + vim.notify("Raphael: State is healthy, no repairs needed", vim.log.levels.INFO) + end + + cache.write(repaired) + return repaired, repairs +end + +function M.export_state(path) + local content, err = M.read_raw_state() + if not content then + return false, err + end + + path = path or vim.fn.stdpath("config") .. "/raphael/state_export.json" + local file, ferr = io.open(path, "w") + if not file then + return false, ferr + end + + file:write(content) + file:close() + vim.notify("State exported to: " .. path, vim.log.levels.INFO) + return true, path +end + +function M.get_stats() + local cache = require("raphael.core.cache") + local state = cache.read() + + return { + state_file = constants.STATE_FILE, + backup_file = M.get_backup_path(), + has_backup = vim.fn.filereadable(M.get_backup_path()) == 1, + current = state.current, + saved = state.saved, + history_count = #(state.history or {}), + bookmarks_count = vim.tbl_count(state.bookmarks or {}), + undo_stack_size = #(state.undo_history and state.undo_history.stack or {}), + undo_index = state.undo_history and state.undo_history.index or 0, + quick_slots_count = vim.tbl_count(state.quick_slots or {}), + log_entries = #log_buffer, + } +end + +return M diff --git a/lua/raphael/init.lua b/lua/raphael/init.lua index 7498bc0..92e37d4 100644 --- a/lua/raphael/init.lua +++ b/lua/raphael/init.lua @@ -135,6 +135,31 @@ function M.statusline() return "󰉼 " .. theme end +--- Create a lualine component for displaying the current theme. +--- +--- Automatically adjusts colors based on light/dark theme detection. +--- +---@param opts table|nil +--- - icon: string (default: "󰉼") +--- - show_profile: boolean (default: true) +--- - separator: string (default: " ") +--- - dynamic_color: boolean (default: true) +--- - brackets: table (default: {"[", "]"}) +---@return function +--- +---@usage +--- require('lualine').setup({ +--- sections = { +--- lualine_c = { +--- { require('raphael').lualine_component() } +--- } +--- } +--- }) +function M.lualine_component(opts) + local lualine = require("raphael.lualine") + return lualine.component(opts) +end + --- Export current configuration to a file. --- --- @param file_path string|nil Path to export configuration to (optional) diff --git a/lua/raphael/lualine.lua b/lua/raphael/lualine.lua new file mode 100644 index 0000000..f07db36 --- /dev/null +++ b/lua/raphael/lualine.lua @@ -0,0 +1,86 @@ +-- lua/raphael/lualine.lua +-- Lualine component for raphael.nvim +-- +-- Usage: +-- require('lualine').setup({ +-- sections = { +-- lualine_c = { +-- { require('raphael').lualine_component() } +-- } +-- } +-- }) +-- +-- Options: +-- icon: string (default: "󰉼") +-- show_profile: boolean (default: true) +-- separator: string (default: " ") +-- dynamic_color: boolean (default: true) - auto light/dark color +-- brackets: table (default: {"[", "]"}) - profile brackets + +local M = {} + +local core = nil + +local function get_core() + if not core then + core = require("raphael.core") + end + return core +end + +local function detect_theme_type() + local ok, bg = pcall(vim.api.nvim_get_option, "bg") + if ok and bg then + return bg + end + return "unknown" +end + +local function get_dynamic_color() + local theme_type = detect_theme_type() + + if theme_type == "light" then + return { fg = "#1a1a1a", bg = "#d0d0d0", gui = "bold" } + elseif theme_type == "dark" then + return { fg = "#e0e0e0", bg = "#3a3a3a", gui = "bold" } + end + return "lualine_c_normal" +end + +function M.component(opts) + opts = opts or {} + local icon = opts.icon or "󰉼" + local show_profile = opts.show_profile + if show_profile == nil then + show_profile = true + end + local separator = opts.separator or " " + local dynamic_color = opts.dynamic_color + if dynamic_color == nil then + dynamic_color = true + end + local brackets = opts.brackets or { "[", "]" } + + return function() + local c = get_core() + if not c then + return "" + end + + local theme = c.get_current_theme() or "none" + local profile = c.get_current_profile() + + local text = icon .. separator .. theme + if show_profile and profile then + text = text .. " " .. brackets[1] .. profile .. brackets[2] + end + + if dynamic_color then + return { text, color = get_dynamic_color() } + end + + return text + end +end + +return M diff --git a/lua/raphael/picker/keymaps.lua b/lua/raphael/picker/keymaps.lua index 7bdccee..db88552 100644 --- a/lua/raphael/picker/keymaps.lua +++ b/lua/raphael/picker/keymaps.lua @@ -950,29 +950,32 @@ function M.attach(ctx, fns) return i >= skip_start and i <= skip_end end - for i = cur + 1, #lines do + local i = cur + 1 + while i <= #lines do if is_in_skip(i) then i = skip_end + 1 - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and ctx.bookmarks[theme] then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + else + local theme = render.parse_line_theme(core, lines[i]) + if theme and ctx.bookmarks[theme] then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end + i = i + 1 end end - for i = 1, cur - 1 do - if is_in_skip(i) then - goto continue - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and ctx.bookmarks[theme] then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + i = 1 + while i < cur do + if not is_in_skip(i) then + local theme = render.parse_line_theme(core, lines[i]) + if theme and ctx.bookmarks[theme] then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end end - ::continue:: + i = i + 1 end end, { buffer = buf, desc = "Next bookmark (skip group)" }) @@ -1005,29 +1008,32 @@ function M.attach(ctx, fns) return i >= skip_start and i <= skip_end end - for i = cur - 1, 1, -1 do + local i = cur - 1 + while i >= 1 do if is_in_skip(i) then i = skip_start - 1 - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and ctx.bookmarks[theme] then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + else + local theme = render.parse_line_theme(core, lines[i]) + if theme and ctx.bookmarks[theme] then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end + i = i - 1 end end - for i = #lines, cur + 1, -1 do - if is_in_skip(i) then - goto continue - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and ctx.bookmarks[theme] then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + i = #lines + while i > cur do + if not is_in_skip(i) then + local theme = render.parse_line_theme(core, lines[i]) + if theme and ctx.bookmarks[theme] then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end end - ::continue:: + i = i - 1 end end, { buffer = buf, desc = "Prev bookmark (skip group)" }) @@ -1060,29 +1066,32 @@ function M.attach(ctx, fns) return i >= skip_start and i <= skip_end end - for i = cur + 1, #lines do + local i = cur + 1 + while i <= #lines do if is_in_skip(i) then i = skip_end + 1 - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and vim.tbl_contains(state.history, theme) then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + else + local theme = render.parse_line_theme(core, lines[i]) + if theme and vim.tbl_contains(state.history, theme) then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end + i = i + 1 end end - for i = 1, cur - 1 do - if is_in_skip(i) then - goto continue - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and vim.tbl_contains(state.history, theme) then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + i = 1 + while i < cur do + if not is_in_skip(i) then + local theme = render.parse_line_theme(core, lines[i]) + if theme and vim.tbl_contains(state.history, theme) then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end end - ::continue:: + i = i + 1 end end, { buffer = buf, desc = "Next recent (skip group)" }) @@ -1115,29 +1124,32 @@ function M.attach(ctx, fns) return i >= skip_start and i <= skip_end end - for i = cur - 1, 1, -1 do + local i = cur - 1 + while i >= 1 do if is_in_skip(i) then i = skip_start - 1 - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and vim.tbl_contains(state.history, theme) then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + else + local theme = render.parse_line_theme(core, lines[i]) + if theme and vim.tbl_contains(state.history, theme) then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end + i = i - 1 end end - for i = #lines, cur + 1, -1 do - if is_in_skip(i) then - goto continue - end - local theme = render.parse_line_theme(core, lines[i]) - if theme and vim.tbl_contains(state.history, theme) then - vim.api.nvim_win_set_cursor(win, { i, 0 }) - M.highlight_current_line(ctx) - return + i = #lines + while i > cur do + if not is_in_skip(i) then + local theme = render.parse_line_theme(core, lines[i]) + if theme and vim.tbl_contains(state.history, theme) then + vim.api.nvim_win_set_cursor(win, { i, 0 }) + M.highlight_current_line(ctx) + return + end end - ::continue:: + i = i - 1 end end, { buffer = buf, desc = "Prev recent (skip group)" }) diff --git a/lua/raphael/picker/preview.lua b/lua/raphael/picker/preview.lua index d628e4f..32ffd48 100644 --- a/lua/raphael/picker/preview.lua +++ b/lua/raphael/picker/preview.lua @@ -228,9 +228,7 @@ end ---@param ctx table ---@param theme string function M.preview_theme(ctx, theme) - vim.notify("DEBUG: preview_theme called with theme: " .. tostring(theme), vim.log.levels.INFO) if not theme or not themes.is_available(theme) then - vim.notify("DEBUG: preview_theme - theme is nil or not available", vim.log.levels.INFO) return end @@ -239,13 +237,11 @@ function M.preview_theme(ctx, theme) compare_active_side = "candidate" end - vim.notify("DEBUG: preview_theme - about to load theme: " .. theme, vim.log.levels.INFO) local ok, err = pcall(load_theme_raw, theme, false) if not ok then vim.notify("raphael: failed to preview theme: " .. tostring(err), vim.log.levels.ERROR) return end - vim.notify("DEBUG: preview_theme - theme loaded successfully: " .. theme, vim.log.levels.INFO) active_preview_theme = theme palette_hl_cache = {} @@ -253,7 +249,6 @@ function M.preview_theme(ctx, theme) if not ok2 then vim.notify("raphael: failed to update palette: " .. tostring(err2), vim.log.levels.ERROR) end - vim.notify("DEBUG: preview_theme - completed for theme: " .. theme, vim.log.levels.INFO) end --- Expose raw load_theme for UI revert logic / compare. diff --git a/lua/raphael/picker/render.lua b/lua/raphael/picker/render.lua index 09656c9..7ca1c0f 100644 --- a/lua/raphael/picker/render.lua +++ b/lua/raphael/picker/render.lua @@ -23,6 +23,9 @@ local GROUP_ALIAS_REVERSE = {} ---@param line string ---@return string|nil group_name function M.parse_line_header(line) + if not line or line == "" then + return nil + end local captured = line:match("^%s*[^%s]+%s+(.+)%s*%(%d+%)%s*$") if not captured then return nil diff --git a/lua/raphael/picker/ui.lua b/lua/raphael/picker/ui.lua index bcf7659..fd60be0 100644 --- a/lua/raphael/picker/ui.lua +++ b/lua/raphael/picker/ui.lua @@ -66,7 +66,8 @@ local ctx = { debug = false, }, - initial_render = true, -- Flag to indicate initial render phase + initial_render = true, + picker_ready = false, instances = picker_instances, } @@ -155,6 +156,8 @@ local function close_picker(revert) ctx.bookmarks = {} ctx.flags.disable_sorting = false ctx.flags.reverse_sorting = false + ctx.initial_render = true + ctx.picker_ready = false if ctx.picker_type and ctx.instances then ctx.instances[ctx.picker_type] = false @@ -340,6 +343,13 @@ function M.get_cache_stats() return { palette_cache_size = 0, active_timers = 0 } end +--- Check if the picker is currently open. +--- +---@return boolean +function M.is_open() + return ctx.win ~= nil and vim.api.nvim_win_is_valid(ctx.win) +end + --- Get the theme under cursor in the picker, or nil if not available. --- ---@return string|nil @@ -393,15 +403,6 @@ function M.open(core, opts) keymaps_mod.highlight_current_line(ctx) end - ---@diagnostic disable-next-line: undefined-field - if ctx.state.current then - local preview = lazy_loader.get_preview() - if preview then - ---@diagnostic disable-next-line: undefined-field - preview.update_palette(ctx, ctx.state.current) - end - end - local keymaps = lazy_loader.get_keymaps() if keymaps then keymaps.attach(ctx, { @@ -414,51 +415,12 @@ function M.open(core, opts) setup_autocmds_for_picker() - -- Reset the initial render flag after a delay to allow normal previews vim.defer_fn(function() - if ctx then + if ctx and ctx.buf and vim.api.nvim_buf_is_valid(ctx.buf) then ctx.initial_render = false + ctx.picker_ready = true end - end, 350) - - ---@diagnostic disable-next-line: undefined-field - if ctx.state.current then - local preview = lazy_loader.get_preview() - if preview then - -- Immediate update - ---@diagnostic disable-next-line: undefined-field - preview.update_palette(ctx, ctx.state.current) - - vim.defer_fn(function() - if ctx.buf and vim.api.nvim_buf_is_valid(ctx.buf) then - ---@diagnostic disable-next-line: undefined-field - preview.update_palette(ctx, ctx.state.current) - end - end, 100) - - vim.defer_fn(function() - if ctx.buf and vim.api.nvim_buf_is_valid(ctx.buf) then - ---@diagnostic disable-next-line: undefined-field - preview.update_palette(ctx, ctx.state.current) - end - end, 200) - end - end - - vim.defer_fn(function() - if ctx.state and ctx.state.current then - local success, err = pcall(function() - local theme = ctx.state.current - local preview_module = lazy_loader.get_preview() - if preview_module and theme then - preview_module.load_theme(theme, true) - end - end) - if not success then - vim.notify("raphael: failed to re-apply current theme: " .. tostring(err), vim.log.levels.WARN) - end - end - end, 400) + end, 100) log("DEBUG", "Picker opened successfully") end diff --git a/tests/config_manager_test.lua b/tests/config_manager_test.lua index cfd6c2b..dc449d1 100644 --- a/tests/config_manager_test.lua +++ b/tests/config_manager_test.lua @@ -23,19 +23,19 @@ describe("config_manager integration tests", function() enable_keymaps = true, enable_picker = true, } - + -- Validate the config first local validated_config = config.validate(test_config) - + -- Export the config local core_mock = { base_config = validated_config, state = { current_profile = nil }, get_profile_config = function(profile_name) return validated_config - end + end, } - + local exported = config_manager.export_config(core_mock) assert.are.same(validated_config, exported) end) @@ -46,25 +46,25 @@ describe("config_manager integration tests", function() leader = "tf", bookmark_group = false, } - + local temp_file = os.tmpname() .. ".json" - + -- Save config to file local save_success = config_manager.save_config_to_file(test_config, temp_file) assert.is_true(save_success) - + -- Verify file exists and can be read local file = io.open(temp_file, "r") assert.truthy(file, "File should exist after save") file:close() - + -- Import config from file local imported_config = config_manager.import_config_from_file(temp_file) assert.truthy(imported_config, "Config should be imported successfully") assert.are.equal("test-theme-file-io", imported_config.default_theme) assert.are.equal("tf", imported_config.leader) assert.is_false(imported_config.bookmark_group) - + -- Clean up os.remove(temp_file) end) @@ -83,7 +83,7 @@ describe("config_manager integration tests", function() enable_keymaps = true, enable_picker = true, } - + local is_valid, error_msg = config_manager.validate_config(valid_config) assert.is_true(is_valid) assert.is_nil(error_msg) @@ -95,7 +95,7 @@ describe("config_manager integration tests", function() leader = 456, -- should be string bookmark_group = "not_boolean", -- should be boolean } - + local is_valid, error_msg = config_manager.validate_config(invalid_config) assert.is_false(is_valid) assert.truthy(error_msg) @@ -116,9 +116,9 @@ describe("config_manager integration tests", function() enable_keymaps = true, enable_picker = true, } - + local results = config_manager.validate_config_sections(config_with_sections) - + -- Check that all expected sections are validated assert.is_boolean(results.default_theme) assert.is_boolean(results.leader) @@ -132,7 +132,7 @@ describe("config_manager integration tests", function() assert.is_boolean(results.enable_commands) assert.is_boolean(results.enable_keymaps) assert.is_boolean(results.enable_picker) - + -- All should be true for our valid config assert.is_true(results.default_theme) assert.is_true(results.leader) @@ -152,18 +152,21 @@ describe("config_manager integration tests", function() describe("preset functionality", function() it("should return available presets", function() local presets = config_manager.get_presets() - + assert.truthy(presets.minimal, "Should have minimal preset") assert.truthy(presets.full_featured, "Should have full_featured preset") assert.truthy(presets.presentation, "Should have presentation preset") - + -- Check that minimal preset has expected properties assert.is_false(presets.minimal.bookmark_group, "Minimal preset should have bookmark_group = false") assert.is_true(presets.minimal.enable_picker, "Minimal preset should have enable_picker = true") - + -- Check that presentation preset has expected properties assert.is_false(presets.presentation.bookmark_group, "Presentation preset should have bookmark_group = false") - assert.is_false(presets.presentation.sample_preview.enabled, "Presentation preset should have sample_preview.enabled = false") + assert.is_false( + presets.presentation.sample_preview.enabled, + "Presentation preset should have sample_preview.enabled = false" + ) end) it("should apply a preset correctly", function() @@ -174,12 +177,12 @@ describe("config_manager integration tests", function() config = { default_theme = "original-theme" }, get_profile_config = function(profile_name) return mock_core.base_config - end + end, } - + -- Apply the minimal preset local success = config_manager.apply_preset("minimal", mock_core) - + assert.is_true(success, "Preset application should succeed") assert.is_false(mock_core.base_config.bookmark_group, "bookmark_group should be false after minimal preset") assert.is_true(mock_core.base_config.enable_picker, "enable_picker should be true after minimal preset") @@ -192,11 +195,11 @@ describe("config_manager integration tests", function() config = { default_theme = "original-theme" }, get_profile_config = function(profile_name) return mock_core.base_config - end + end, } - + local success = config_manager.apply_preset("non_existent_preset", mock_core) - + assert.is_false(success, "Should return false for invalid preset") end) end) @@ -208,13 +211,13 @@ describe("config_manager integration tests", function() unknown_option = "should_not_exist", another_unknown = "also_should_not_exist", } - + local diagnostics = config_manager.get_config_diagnostics(test_config) - + assert.are.equal(3, diagnostics.total_keys, "Should count all keys") assert.are.equal(2, #diagnostics.unknown_keys, "Should find 2 unknown keys") assert.truthy(vim.tbl_contains(diagnostics.unknown_keys, "unknown_option"), "Should include unknown_option") assert.truthy(vim.tbl_contains(diagnostics.unknown_keys, "another_unknown"), "Should include another_unknown") end) end) -end) \ No newline at end of file +end) diff --git a/tests/core_test.lua b/tests/core_test.lua index aeaa22a..ba13b15 100644 --- a/tests/core_test.lua +++ b/tests/core_test.lua @@ -312,8 +312,7 @@ describe("raphael.nvim core functionality", function() -- Restore original config core.base_config = original_config local profile_name = core.state.current_profile - core.config = core.get_profile_config and - core.get_profile_config(profile_name) or original_config + core.config = core.get_profile_config and core.get_profile_config(profile_name) or original_config end) it("should handle invalid preset", function() diff --git a/tests/mock_cache.lua b/tests/mock_cache.lua index 9932c7d..3b73a48 100644 --- a/tests/mock_cache.lua +++ b/tests/mock_cache.lua @@ -380,4 +380,3 @@ function M.redo_pop() end return M - diff --git a/tests/neovim_test_runner.lua b/tests/neovim_test_runner.lua index e8c6b97..44cc110 100644 --- a/tests/neovim_test_runner.lua +++ b/tests/neovim_test_runner.lua @@ -326,4 +326,3 @@ else end -- Test completed using mock cache, no cleanup needed for real cache - diff --git a/tests/safe_test_runner.lua b/tests/safe_test_runner.lua index 6065391..52e376d 100644 --- a/tests/safe_test_runner.lua +++ b/tests/safe_test_runner.lua @@ -23,20 +23,20 @@ end -- Simple assertion library local assert = { - truthy = function(value, msg) - if not value then - error(msg or "Expected value to be truthy, got " .. tostring(value)) - end + truthy = function(value, msg) + if not value then + error(msg or "Expected value to be truthy, got " .. tostring(value)) + end end, - falsy = function(value, msg) - if value then - error(msg or "Expected value to be falsy, got " .. tostring(value)) - end + falsy = function(value, msg) + if value then + error(msg or "Expected value to be falsy, got " .. tostring(value)) + end end, - equals = function(expected, actual, msg) - if expected ~= actual then - error(msg or string.format("Expected %s, got %s", tostring(expected), tostring(actual))) - end + equals = function(expected, actual, msg) + if expected ~= actual then + error(msg or string.format("Expected %s, got %s", tostring(expected), tostring(actual))) + end end, same = function(expected, actual, msg) if vim.deep_equal(expected, actual) ~= true then @@ -83,30 +83,30 @@ print_header("Testing Configuration Management Features") do print("\nConfiguration export/import tests:") - + -- Test export with a mock object local export = config_manager.export_config({ base_config = { default_theme = "test-theme", leader = "te" }, - state = { current_profile = nil } + state = { current_profile = nil }, }) assert.truthy(type(export) == "table") assert.equals("test-theme", export.default_theme) print(" ✓ Config export works") total_tests = total_tests + 1 total_passed = total_passed + 1 - + -- Test validation local is_valid, error_msg = config_manager.validate_config({ default_theme = "test-theme", leader = "t", - bookmark_group = true + bookmark_group = true, }) assert.truthy(is_valid) assert.truthy(error_msg == nil) print(" ✓ Config validation works") total_tests = total_tests + 1 total_passed = total_passed + 1 - + -- Test that validation handles fixable configs properly local is_valid_fixable, error_msg_fixable = config_manager.validate_config({ default_theme = 123, -- should be string, but will be fixed @@ -121,7 +121,7 @@ end do print("\nConfiguration section validation tests:") - + local results = config_manager.validate_config_sections({ default_theme = "test-theme", leader = "tt", @@ -136,7 +136,7 @@ do enable_keymaps = true, enable_picker = true, }) - + assert.truthy(type(results) == "table") assert.truthy(results.default_theme == true) assert.truthy(results.leader == true) @@ -148,13 +148,13 @@ end do print("\nConfiguration diagnostics tests:") - + local diagnostics = config_manager.get_config_diagnostics({ default_theme = "test-theme", unknown_key = "should_not_exist", another_unknown = "also_should_not_exist", }) - + assert.truthy(type(diagnostics) == "table") assert.truthy(diagnostics.total_keys == 3) assert.truthy(#diagnostics.unknown_keys == 2) @@ -166,22 +166,22 @@ end do print("\nConfiguration file I/O tests:") - + local test_config = { default_theme = "test-theme-save", leader = "ts", bookmark_group = false, } - + local temp_file = os.tmpname() .. ".json" - + -- Test save local save_success = config_manager.save_config_to_file(test_config, temp_file) assert.truthy(save_success) print(" ✓ Config save works") total_tests = total_tests + 1 total_passed = total_passed + 1 - + -- Test load local imported_config = config_manager.import_config_from_file(temp_file) assert.truthy(type(imported_config) == "table") @@ -191,14 +191,14 @@ do print(" ✓ Config load works") total_tests = total_tests + 1 total_passed = total_passed + 1 - + -- Clean up os.remove(temp_file) end do print("\nConfiguration presets tests:") - + local presets = config_manager.get_presets() assert.truthy(type(presets) == "table") assert.truthy(presets.minimal ~= nil) @@ -207,7 +207,7 @@ do print(" ✓ Presets available") total_tests = total_tests + 1 total_passed = total_passed + 1 - + -- Test applying a preset with a mock local mock_core = { base_config = { default_theme = "original-theme" }, @@ -219,7 +219,7 @@ do function mock_core.get_profile_config(profile_name) return mock_core.base_config end - + local success = config_manager.apply_preset("minimal", mock_core) assert.truthy(success) assert.falsy(mock_core.base_config.bookmark_group) @@ -231,7 +231,7 @@ end -- Test basic functionality without affecting cache do print("\nBasic functionality tests:") - + -- Test theme discovery (this doesn't affect cache) themes.refresh() local installed = themes.installed @@ -239,7 +239,7 @@ do print(" ✓ Theme discovery works") total_tests = total_tests + 1 total_passed = total_passed + 1 - + -- Test configuration validation (this doesn't affect cache) local validated = config.validate(nil) assert.truthy(type(validated) == "table", "Validated config should be a table") @@ -247,7 +247,7 @@ do print(" ✓ Default config validation works") total_tests = total_tests + 1 total_passed = total_passed + 1 - + local user_config = { default_theme = "test-theme", leader = "tt", @@ -267,4 +267,4 @@ if total_passed == total_tests then print(" (No real cache was modified during these tests)") else print("⚠️ Some tests failed. Please review the output above.") -end \ No newline at end of file +end diff --git a/tests/simple_test_runner.lua b/tests/simple_test_runner.lua index 5de360f..7248d0e 100644 --- a/tests/simple_test_runner.lua +++ b/tests/simple_test_runner.lua @@ -25,25 +25,23 @@ end local function run_testsuite(suite_name, test_func) print_header("Running " .. suite_name) - + local total_tests = 0 local passed_tests = 0 - + -- Capture test results by running the test function - local success, err = pcall(test_func, - function(name, func) - total_tests = total_tests + 1 - if run_test(name, func) then - passed_tests = passed_tests + 1 - end + local success, err = pcall(test_func, function(name, func) + total_tests = total_tests + 1 + if run_test(name, func) then + passed_tests = passed_tests + 1 end - ) - + end) + if not success then print("Error running testsuite: " .. err) return 0, 0 end - + print_header(string.format("Results: %d/%d tests passed", passed_tests, total_tests)) return passed_tests, total_tests end @@ -51,16 +49,16 @@ end -- Define a simple describe/it implementation for our tests local function describe(description, test_block) print("\n" .. description .. ":") - + local tests = {} - + local function it(name, test_func) - table.insert(tests, {name = name, func = test_func}) + table.insert(tests, { name = name, func = test_func }) end - + -- Run the test block to register tests test_block(it) - + -- Execute registered tests for _, test in ipairs(tests) do run_test(test.name, test.func) @@ -71,24 +69,24 @@ end local function load_test_file(filepath) local env = { describe = describe, - it = function(name, func) + it = function(name, func) run_test(name, func) end, assert = { - truthy = function(value, msg) - if not value then - error(msg or "Expected value to be truthy, got " .. tostring(value)) - end + truthy = function(value, msg) + if not value then + error(msg or "Expected value to be truthy, got " .. tostring(value)) + end end, - falsy = function(value, msg) - if value then - error(msg or "Expected value to be falsy, got " .. tostring(value)) - end + falsy = function(value, msg) + if value then + error(msg or "Expected value to be falsy, got " .. tostring(value)) + end end, - equals = function(expected, actual, msg) - if expected ~= actual then - error(msg or string.format("Expected %s, got %s", tostring(expected), tostring(actual))) - end + equals = function(expected, actual, msg) + if expected ~= actual then + error(msg or string.format("Expected %s, got %s", tostring(expected), tostring(actual))) + end end, same = function(expected, actual, msg) if vim then @@ -141,12 +139,12 @@ local function load_test_file(filepath) vim = vim, math = math, } - + local chunk, err = loadfile(filepath, "t", env) if not chunk then error("Failed to load test file: " .. err) end - + local success, result = pcall(chunk) if not success then error("Error running test file: " .. result) @@ -159,7 +157,7 @@ print("====================================") local test_files = { "tests/core_test.lua", - "tests/config_manager_test.lua" + "tests/config_manager_test.lua", } local total_passed = 0 @@ -167,7 +165,7 @@ local total_tests = 0 for _, test_file in ipairs(test_files) do print_header("Loading test file: " .. test_file) - + local success, err = pcall(load_test_file, test_file) if not success then print("Failed to load " .. test_file .. ": " .. err) @@ -176,4 +174,4 @@ end print("\n" .. string.rep("=", 60)) print("All tests completed!") -print(string.rep("=", 60)) \ No newline at end of file +print(string.rep("=", 60)) diff --git a/tests/test_constants.lua b/tests/test_constants.lua index 9600356..2220eb9 100644 --- a/tests/test_constants.lua +++ b/tests/test_constants.lua @@ -53,4 +53,3 @@ M.HL = { M.FOOTER_HINTS = " apply • b bookmark • / search • s sort • q close" return M -