diff --git a/.luarc.json b/.luarc.json index 1e1765c..7d9c583 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,5 +1,4 @@ { - "diagnostics.globals": [ - "vim" - ] -} \ No newline at end of file + "diagnostics.globals": ["vim"] +} + diff --git a/README.md b/README.md index aaca69f..24365e7 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,13 @@ Among La Italia's finest painters, Raphael stood out for his harmony in color - **Configurable Icons**: All icons used in the picker (bookmarks, group arrows, history markers, etc.) are configurable via `opts.icons` (see below). +- **Configuration Management**: Export, import, validate, and manage your configurations with presets: + - Export your current configuration to a file with `:RaphaelConfigExport` + - Import configurations from files with `:RaphaelConfigImport` + - Validate your configuration with `:RaphaelConfigValidate` + - Use predefined presets like "minimal", "full-featured", or "presentation" with `:RaphaelConfigPreset` + - List available config files with `:RaphaelConfigList` + --- ## Installation @@ -368,6 +375,23 @@ return { - `:RaphaelProfile work` / `night` / `presentation` → switch to that profile. - `:RaphaelProfile base` → clear profile (use base config only). +- `:RaphaelConfigExport [file_path]` + Export current configuration to a JSON file (defaults to `~/.config/nvim/raphael/configs/exported_config.json`). + +- `:RaphaelConfigImport file_path` + Import and apply configuration from a JSON file. + +- `:RaphaelConfigValidate` + Validate current configuration and show diagnostics. + +- `:RaphaelConfigList` + List available configuration files in the raphael configs directory. + +- `:RaphaelConfigPreset [preset_name]` + Apply a configuration preset: + - `:RaphaelConfigPreset` → list all available presets. + - `:RaphaelConfigPreset minimal` / `full-featured` / `presentation` → apply that preset. + --- ## Picker internals (module paths) diff --git a/lua/raphael/config_manager.lua b/lua/raphael/config_manager.lua new file mode 100644 index 0000000..10f1b4a --- /dev/null +++ b/lua/raphael/config_manager.lua @@ -0,0 +1,380 @@ +-- lua/raphael/config_manager.lua +-- Configuration export/import and management utilities for raphael.nvim + +local M = {} + +local config = require("raphael.config") + +--- Export current configuration to a table that can be serialized +--- +---@param core_module table The core module containing the current config +---@return table|nil export The exported configuration +function M.export_config(core_module) + if not core_module or not core_module.base_config then + vim.notify("raphael: core module not available for config export", vim.log.levels.ERROR) + return nil + end + + local export = vim.deepcopy(core_module.base_config) + + export.on_apply = nil + + if core_module.state and core_module.state.current_profile then + export.current_profile = core_module.state.current_profile + end + + return export +end + +--- Import configuration from a file path +--- +---@param file_path string Path to the configuration file +---@return table|nil config The imported configuration, or nil on failure +function M.import_config_from_file(file_path) + local full_path = vim.fn.expand(file_path) + + local file = io.open(full_path, "r") + if not file then + vim.notify("raphael: config file not found: " .. full_path, vim.log.levels.ERROR) + return nil + end + + local content = file:read("*a") + file:close() + + if not content or content:match("^%s*$") then + vim.notify("raphael: config file is empty: " .. full_path, vim.log.levels.ERROR) + return nil + end + + local ok, decoded = pcall(vim.json.decode, content) + if not ok or type(decoded) ~= "table" then + vim.notify("raphael: failed to decode config file: " .. full_path, vim.log.levels.ERROR) + return nil + end + + return decoded +end + +--- Import configuration from a table +--- +---@param config_data table The configuration data to import +---@return table|nil validated_config The validated configuration, or nil on failure +function M.import_config_from_table(config_data) + if type(config_data) ~= "table" then + vim.notify("raphael: config data must be a table", vim.log.levels.ERROR) + return nil + end + + local validated = config.validate(config_data) + return validated +end + +--- Save configuration to a file +--- +---@param config_data table The configuration to save +---@param file_path string Path to save the configuration file +---@return boolean success Whether the save was successful +function M.save_config_to_file(config_data, file_path) + local full_path = vim.fn.expand(file_path) + + local dir = vim.fn.fnamemodify(full_path, ":h") + if vim.fn.isdirectory(dir) == 0 then + vim.fn.mkdir(dir, "p") + end + + local ok, encoded = pcall(vim.json.encode, config_data) + if not ok then + vim.notify("raphael: failed to encode config for saving", vim.log.levels.ERROR) + return false + end + + local file = io.open(full_path, "w") + if not file then + vim.notify("raphael: failed to open file for writing: " .. full_path, vim.log.levels.ERROR) + return false + end + + file:write(encoded) + file:close() + + vim.notify("raphael: configuration saved to " .. full_path, vim.log.levels.INFO) + return true +end + +--- Get a list of available config files in the raphael config directory +--- +---@return string[] List of config file paths +function M.list_config_files() + local config_dir = vim.fn.stdpath("config") .. "/raphael/configs" + local files = vim.fn.globpath(config_dir, "*.json", false, true) + return files +end + +--- Validate a configuration table +--- +---@param config_data table The configuration to validate +---@return boolean is_valid Whether the configuration is valid +---@return string|nil error_msg Error message if validation failed +function M.validate_config(config_data) + if type(config_data) ~= "table" then + return false, "Configuration must be a table" + end + + local ok, result = pcall(config.validate, config_data) + if not ok then + return false, "Configuration validation failed: " .. tostring(result) + end + + return true, nil +end + +--- Validate specific configuration sections +--- +---@param config_data table The configuration to validate +---@return table validation_results Results for each section +function M.validate_config_sections(config_data) + if type(config_data) ~= "table" then + return { error = "Configuration must be a table" } + end + + local results = {} + + results.leader = type(config_data.leader) == "string" and config_data.leader ~= "" + results.default_theme = type(config_data.default_theme) == "string" and config_data.default_theme ~= "" + results.bookmark_group = type(config_data.bookmark_group) == "boolean" + results.recent_group = type(config_data.recent_group) == "boolean" + + if type(config_data.mappings) == "table" then + results.mappings = true + for key, val in pairs(config_data.mappings) do + if type(val) ~= "string" then + results.mappings = false + break + end + end + else + results.mappings = false + end + + if config_data.theme_map ~= nil then + results.theme_map = type(config_data.theme_map) == "table" or config_data.theme_map == nil + else + results.theme_map = true + end + + if type(config_data.filetype_themes) == "table" then + results.filetype_themes = true + for ft, theme in pairs(config_data.filetype_themes) do + if type(ft) ~= "string" or type(theme) ~= "string" or theme == "" then + results.filetype_themes = false + break + end + end + else + results.filetype_themes = false + end + + if type(config_data.project_themes) == "table" then + results.project_themes = true + for path, theme in pairs(config_data.project_themes) do + if type(path) ~= "string" or type(theme) ~= "string" or theme == "" then + results.project_themes = false + break + end + end + else + results.project_themes = false + end + + if config_data.profiles ~= nil then + if type(config_data.profiles) == "table" then + results.profiles = true + for name, prof in pairs(config_data.profiles) do + if type(name) ~= "string" or type(prof) ~= "table" then + results.profiles = false + break + end + end + else + results.profiles = false + end + else + results.profiles = true + end + + local feature_toggles = { "enable_autocmds", "enable_commands", "enable_keymaps", "enable_picker" } + for _, toggle in ipairs(feature_toggles) do + if config_data[toggle] ~= nil then + results[toggle] = type(config_data[toggle]) == "boolean" + else + results[toggle] = true + end + end + + return results +end + +--- Get configuration diagnostics +--- +---@param config_data table The configuration to analyze +---@return table diagnostics Information about the configuration +function M.get_config_diagnostics(config_data) + if type(config_data) ~= "table" then + return { error = "Configuration must be a table" } + end + + local diagnostics = { + total_keys = 0, + unknown_keys = {}, + missing_defaults = {}, + } + + for k, _ in pairs(config_data) do + diagnostics.total_keys = diagnostics.total_keys + 1 + end + + for key in pairs(config_data) do + if not config.defaults[key] then + table.insert(diagnostics.unknown_keys, key) + end + end + + for key, default_val in pairs(config.defaults) do + if config_data[key] == nil then + table.insert(diagnostics.missing_defaults, key) + end + end + + return diagnostics +end + +--- Get available configuration presets +--- +---@return table presets A table of available presets +function M.get_presets() + local presets = { + minimal = { + leader = "t", + mappings = { + picker = "p", + next = ">", + previous = "<", + auto = "a", + }, + default_theme = "default", + bookmark_group = false, + recent_group = false, + enable_picker = true, + enable_commands = true, + enable_keymaps = true, + enable_autocmds = false, + icons = config.defaults.icons, + }, + full_featured = { + leader = "t", + mappings = config.defaults.mappings, + default_theme = "kanagawa-paper-ink", + bookmark_group = true, + recent_group = true, + theme_map = nil, + filetype_themes = {}, + project_themes = {}, + filetype_overrides_project = false, + project_overrides_filetype = true, + profiles = {}, + current_profile = nil, + profile_scoped_state = false, + sort_mode = "alpha", + custom_sorts = {}, + theme_aliases = {}, + group_aliases = {}, + history_max_size = 13, + sample_preview = { + enabled = true, + relative_size = 0.5, + languages = nil, + }, + group_indent = 2, + icons = config.defaults.icons, + on_apply = config.defaults.on_apply, + enable_autocmds = true, + enable_commands = true, + enable_keymaps = true, + enable_picker = true, + }, + presentation = { + leader = "t", + mappings = { + picker = "p", + next = ">", + previous = "<", + others = "/", + auto = "a", + refresh = "R", + status = "s", + }, + default_theme = "default", + bookmark_group = false, + recent_group = false, + theme_map = nil, + filetype_themes = {}, + project_themes = {}, + filetype_overrides_project = false, + project_overrides_filetype = true, + profiles = {}, + current_profile = nil, + profile_scoped_state = false, + sort_mode = "alpha", + custom_sorts = {}, + theme_aliases = {}, + group_aliases = {}, + history_max_size = 5, + sample_preview = { + enabled = false, + relative_size = 0.5, + languages = nil, + }, + group_indent = 2, + icons = config.defaults.icons, + on_apply = config.defaults.on_apply, + enable_autocmds = false, + enable_commands = true, + enable_keymaps = true, + enable_picker = true, + }, + } + + return presets +end + +--- Apply a preset configuration +--- +---@param preset_name string Name of the preset to apply +---@param core_module table The core module to update +---@return boolean success Whether the preset was applied successfully +function M.apply_preset(preset_name, core_module) + local presets = M.get_presets() + local preset = presets[preset_name] + + if not preset then + vim.notify("raphael: unknown preset '" .. preset_name .. "'", vim.log.levels.ERROR) + return false + end + + local validated_config = config.validate(preset) + if not validated_config then + vim.notify("raphael: failed to validate preset '" .. preset_name .. "'", vim.log.levels.ERROR) + return false + end + + core_module.base_config = validated_config + local profile_name = core_module.state and core_module.state.current_profile or nil + core_module.config = core_module.get_profile_config and core_module.get_profile_config(profile_name) + or validated_config + + vim.notify("raphael: applied preset '" .. preset_name .. "'", vim.log.levels.INFO) + return true +end + +return M diff --git a/lua/raphael/core/autocmds.lua b/lua/raphael/core/autocmds.lua index 5140def..65625fe 100644 --- a/lua/raphael/core/autocmds.lua +++ b/lua/raphael/core/autocmds.lua @@ -241,11 +241,23 @@ function M.picker_cursor_autocmd(picker_buf, cbs) local preview_fn = cbs.preview local highlight = cbs.highlight 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 - preview_fn(theme) + 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 + return + end end end, 100) @@ -263,12 +275,45 @@ function M.picker_cursor_autocmd(picker_buf, cbs) return end - local theme - if type(parse) == "function" then - theme = parse(line) + local is_header = false + local render_ok, render_module = pcall(require, "raphael.picker.render") + if render_ok and render_module then + if type(render_module.parse_line_header) == "function" then + local header = render_module.parse_line_header(line) + if header then + is_header = true + end + end + end + + if not is_header and line:match("%(%d+%)%s*$") then + is_header = true end - if theme then - debounced_preview(theme) + + if not is_header then + local lower_line = line:lower() + if + lower_line:match("bookmarks") + or lower_line:match("recent") + or lower_line:match("results") + or lower_line:match("all") + then + -- Check if it also has the number pattern to be sure + if line:match("%(%d+%)") then + is_header = true + end + end + end + + if not is_header then + local theme + if type(parse) == "function" then + 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 if type(highlight) == "function" then highlight() @@ -276,6 +321,11 @@ 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/cmds.lua b/lua/raphael/core/cmds.lua index 5e8b91c..becf830 100644 --- a/lua/raphael/core/cmds.lua +++ b/lua/raphael/core/cmds.lua @@ -436,6 +436,171 @@ function M.setup(core) end, desc = "Show diff of profile vs base (:RaphaelProfileInfo [name])", }) + + local config_manager = require("raphael.config_manager") + + vim.api.nvim_create_user_command("RaphaelConfigExport", function(opts) + local export_path = opts.args ~= "" and opts.args + or vim.fn.stdpath("config") .. "/raphael/configs/exported_config.json" + local config_to_export = config_manager.export_config(core) + + if not config_to_export then + vim.notify("raphael: failed to export configuration", vim.log.levels.ERROR) + return + end + + if config_manager.save_config_to_file(config_to_export, export_path) then + vim.notify("raphael: configuration exported to " .. export_path, vim.log.levels.INFO) + end + end, { + nargs = "?", + desc = "Export current Raphael configuration to a file", + }) + + vim.api.nvim_create_user_command("RaphaelConfigImport", function(opts) + if opts.args == "" then + vim.notify("raphael: please specify a config file path to import", vim.log.levels.WARN) + return + end + + local imported_config = config_manager.import_config_from_file(opts.args) + if not imported_config then + vim.notify("raphael: failed to import configuration from " .. opts.args, vim.log.levels.ERROR) + return + end + + local is_valid, error_msg = config_manager.validate_config(imported_config) + if not is_valid then + vim.notify("raphael: imported config is invalid: " .. error_msg, vim.log.levels.ERROR) + return + end + + core.base_config = imported_config + local profile_name = core.state.current_profile + core.config = core.get_profile_config(profile_name) or imported_config + + vim.notify("raphael: configuration imported and applied from " .. opts.args, vim.log.levels.INFO) + end, { + nargs = 1, + desc = "Import Raphael configuration from a file", + }) + + vim.api.nvim_create_user_command("RaphaelConfigValidate", function() + local diagnostics = config_manager.get_config_diagnostics(core.base_config) + local validation_results = config_manager.validate_config_sections(core.base_config) + + local lines = { "Raphael Configuration Validation:", "" } + + table.insert(lines, string.format("Total keys: %d", diagnostics.total_keys)) + table.insert(lines, string.format("Unknown keys: %d", #diagnostics.unknown_keys)) + if #diagnostics.unknown_keys > 0 then + for _, key in ipairs(diagnostics.unknown_keys) do + table.insert(lines, string.format(" - %s", key)) + end + end + table.insert(lines, string.format("Missing defaults: %d", #diagnostics.missing_defaults)) + if #diagnostics.missing_defaults > 0 then + for _, key in ipairs(diagnostics.missing_defaults) do + table.insert(lines, string.format(" - %s", key)) + end + end + + table.insert(lines, "") + table.insert(lines, "Section validation:") + for section, is_valid in pairs(validation_results) do + if type(is_valid) == "boolean" then + local status = is_valid and "✓" or "✗" + table.insert(lines, string.format(" %s %s", status, section)) + end + end + + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) + vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf }) + vim.api.nvim_buf_set_name(buf, "RaphaelConfigValidate") + + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_set_option_value("filetype", "text", { buf = buf }) + vim.api.nvim_win_set_buf(0, buf) + end, { + desc = "Validate current Raphael configuration", + }) + + vim.api.nvim_create_user_command("RaphaelConfigList", function() + local config_files = config_manager.list_config_files() + + if #config_files == 0 then + vim.notify("raphael: no config files found in ~/.config/nvim/raphael/configs/", vim.log.levels.INFO) + return + end + + local lines = { "Available Raphael configuration files:", "" } + for _, file in ipairs(config_files) do + table.insert(lines, file) + end + + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) + vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf }) + vim.api.nvim_buf_set_name(buf, "RaphaelConfigList") + + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_set_option_value("filetype", "text", { buf = buf }) + vim.api.nvim_win_set_buf(0, buf) + end, { + desc = "List available Raphael configuration files", + }) + + vim.api.nvim_create_user_command("RaphaelConfigPreset", function(opts) + local preset_name = opts.args + if preset_name == "" or not preset_name then + local presets = config_manager.get_presets() + local preset_names = {} + for name, _ in pairs(presets) do + table.insert(preset_names, name) + end + + local lines = { "Available Raphael configuration presets:", "" } + for _, name in ipairs(preset_names) do + table.insert(lines, "- " .. name) + end + + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) + vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf }) + vim.api.nvim_buf_set_name(buf, "RaphaelConfigPreset") + + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_set_option_value("filetype", "text", { buf = buf }) + vim.api.nvim_win_set_buf(0, buf) + return + end + + config_manager.apply_preset(preset_name, core) + end, { + nargs = "?", + complete = function(ArgLead) + local presets = config_manager.get_presets() + local names = {} + for name, _ in pairs(presets) do + table.insert(names, name) + end + + if not ArgLead or ArgLead == "" then + return names + end + + local res = {} + local needle = ArgLead:lower() + for _, n in ipairs(names) do + if n:lower():find(needle, 1, true) then + table.insert(res, n) + end + end + return res + end, + desc = "Apply a Raphael configuration preset (:RaphaelConfigPreset [preset_name])", + }) end return M diff --git a/lua/raphael/init.lua b/lua/raphael/init.lua index 13bfb84..7498bc0 100644 --- a/lua/raphael/init.lua +++ b/lua/raphael/init.lua @@ -135,6 +135,66 @@ function M.statusline() return "󰉼 " .. theme end +--- Export current configuration to a file. +--- +--- @param file_path string|nil Path to export configuration to (optional) +--- @return boolean success Whether the export was successful +function M.export_config(file_path) + local config_manager = require("raphael.config_manager") + local config_to_export = config_manager.export_config(core) + + if not config_to_export then + return false + end + + return config_manager.save_config_to_file( + config_to_export, + file_path or vim.fn.stdpath("config") .. "/raphael/configs/exported_config.json" + ) +end + +--- Import configuration from a file and apply it. +--- +--- @param file_path string Path to import configuration from +--- @return boolean success Whether the import was successful +function M.import_config(file_path) + local config_manager = require("raphael.config_manager") + local imported_config = config_manager.import_config_from_file(file_path) + + if not imported_config then + return false + end + + local is_valid, error_msg = config_manager.validate_config(imported_config) + if not is_valid then + vim.notify("raphael: imported config is invalid: " .. error_msg, vim.log.levels.ERROR) + return false + end + + core.base_config = imported_config + local profile_name = core.state.current_profile + core.config = core.get_profile_config(profile_name) or imported_config + + return true +end + +--- Apply a configuration preset. +--- +--- @param preset_name string Name of the preset to apply +--- @return boolean success Whether the preset was applied successfully +function M.apply_preset(preset_name) + local config_manager = require("raphael.config_manager") + return config_manager.apply_preset(preset_name, core) +end + +--- Validate current configuration. +--- +--- @return table results Validation results +function M.validate_config() + local config_manager = require("raphael.config_manager") + return config_manager.validate_config_sections(M.config or {}) +end + -- ──────────────────────────────────────────────────────────────────────── -- Setup -- ──────────────────────────────────────────────────────────────────────── diff --git a/lua/raphael/picker/lazy_loader.lua b/lua/raphael/picker/lazy_loader.lua new file mode 100644 index 0000000..6a194c2 --- /dev/null +++ b/lua/raphael/picker/lazy_loader.lua @@ -0,0 +1,67 @@ +-- lua/raphael/picker/lazy_loader.lua +-- Lazy loading system for picker components to improve startup performance + +local M = {} + +-- Cache for loaded modules +local loaded_modules = {} + +-- Load a picker module lazily +function M.load_module(module_name) + if loaded_modules[module_name] then + return loaded_modules[module_name] + end + + local success, module = pcall(require, module_name) + if success then + loaded_modules[module_name] = module + return module + else + vim.notify("raphael: failed to load module " .. module_name .. ": " .. tostring(module), vim.log.levels.WARN) + return nil + end +end + +-- Lazy load the render module +function M.get_render() + return M.load_module("raphael.picker.render") +end + +-- Lazy load the search module +function M.get_search() + return M.load_module("raphael.picker.search") +end + +-- Lazy load the preview module +function M.get_preview() + return M.load_module("raphael.picker.preview") +end + +-- Lazy load the keymaps module +function M.get_keymaps() + return M.load_module("raphael.picker.keymaps") +end + +-- Lazy load the bookmarks module +function M.get_bookmarks() + return M.load_module("raphael.picker.bookmarks") +end + +-- Clear the module cache (for debugging or reloading) +function M.clear_cache() + loaded_modules = {} +end + +-- Get statistics about loaded modules +function M.get_stats() + local count = 0 + for _ in pairs(loaded_modules) do + count = count + 1 + end + return { + loaded_modules_count = count, + loaded_modules = vim.tbl_keys(loaded_modules), + } +end + +return M diff --git a/lua/raphael/picker/preview.lua b/lua/raphael/picker/preview.lua index 32ffd48..d628e4f 100644 --- a/lua/raphael/picker/preview.lua +++ b/lua/raphael/picker/preview.lua @@ -228,7 +228,9 @@ 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 @@ -237,11 +239,13 @@ 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 = {} @@ -249,6 +253,7 @@ 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/ui.lua b/lua/raphael/picker/ui.lua index 5c76f38..bcf7659 100644 --- a/lua/raphael/picker/ui.lua +++ b/lua/raphael/picker/ui.lua @@ -8,11 +8,7 @@ local M = {} local themes = require("raphael.themes") local autocmds = require("raphael.core.autocmds") -local render = require("raphael.picker.render") -local search = require("raphael.picker.search") -local preview = require("raphael.picker.preview") -local keymaps = require("raphael.picker.keymaps") -local bookmarks_mod = require("raphael.picker.bookmarks") +local lazy_loader = require("raphael.picker.lazy_loader") -- Picker instances (to avoid multiple windows per type) -- Used to prevent opening multiple pickers of the same "type". @@ -39,6 +35,7 @@ local picker_instances = { --- search_buf, search_win : search prompt buffer & window --- flags : { disable_sorting:boolean, reverse_sorting:boolean, debug:boolean } --- instances : reference to picker_instances +--- initial_render : boolean - flag to indicate initial render phase ---@type table local ctx = { core = nil, @@ -69,6 +66,8 @@ local ctx = { debug = false, }, + initial_render = true, -- Flag to indicate initial render phase + instances = picker_instances, } @@ -102,7 +101,9 @@ end --- - ctx.state.saved local function save_previous_theme() local state = ctx.state + ---@diagnostic disable-next-line: need-check-nil, inject-field, undefined-field state.previous = state.current or vim.g.colors_name or state.saved + ---@diagnostic disable-next-line: need-check-nil log("DEBUG", "Previous theme saved", state.previous) end @@ -128,14 +129,22 @@ end local function close_picker(revert) log("DEBUG", "Closing picker", { revert = revert }) + ---@diagnostic disable-next-line: undefined-field if revert and ctx.state and ctx.state.previous and themes.is_available(ctx.state.previous) then - local ok, err = pcall(preview.load_theme, ctx.state.previous, true) - if not ok then - log("ERROR", "Failed to revert theme", err) + local preview = lazy_loader.get_preview() + if preview then + ---@diagnostic disable-next-line: undefined-field + local ok, err = pcall(preview.load_theme, ctx.state.previous, true) + if not ok then + log("ERROR", "Failed to revert theme", err) + end end end - preview.close_all() + local preview = lazy_loader.get_preview() + if preview then + preview.close_all() + end close_picker_windows() ctx.search_query = "" @@ -156,6 +165,7 @@ end --- Persist ctx.collapsed into core.state.collapsed and call core.save_state() if present. local function update_state_collapsed() + ---@diagnostic disable-next-line: inject-field ctx.state.collapsed = vim.deepcopy(ctx.collapsed) if ctx.core and ctx.core.save_state then pcall(ctx.core.save_state) @@ -164,7 +174,10 @@ end --- Render the picker using the current ctx. local function render_picker() - render.render(ctx) + local render = lazy_loader.get_render() + if render then + render.render(ctx) + end end --- Setup autocmds specifically for the picker buffer. @@ -174,36 +187,59 @@ end --- - BufDelete: log + preview.close_all() local function setup_autocmds_for_picker() autocmds.picker_cursor_autocmd(ctx.buf, { + ctx = ctx, -- Pass the context to the autocmds parse = function(line) - return render.parse_line_theme(ctx.core, line) + local render = lazy_loader.get_render() + if render then + return render.parse_line_theme(ctx.core, line) + end + return nil end, preview = function(theme) - preview.preview_theme(ctx, theme) + local preview = lazy_loader.get_preview() + if preview then + preview.preview_theme(ctx, theme) + end end, highlight = function() - keymaps.highlight_current_line(ctx) + local keymaps_mod = lazy_loader.get_keymaps() + if keymaps_mod then + keymaps_mod.highlight_current_line(ctx) + end end, update_preview = function() - preview.update_code_preview(ctx) + local preview = lazy_loader.get_preview() + if preview then + preview.update_code_preview(ctx) + end end, }) autocmds.picker_bufdelete_autocmd(ctx.buf, { log = log, cleanup = function() - preview.close_all() + local preview = lazy_loader.get_preview() + if preview then + preview.close_all() + end end, }) end --- Open the search prompt window attached to the picker. local function setup_search() - search.open(ctx, { - render = render_picker, - highlight = function() - keymaps.highlight_current_line(ctx) - end, - }) + local search = lazy_loader.get_search() + if search then + search.open(ctx, { + render = render_picker, + highlight = function() + local keymaps = lazy_loader.get_keymaps() + if keymaps then + keymaps.highlight_current_line(ctx) + end + end, + }) + end end --- Build the picker window title based on picker type and sort flags. @@ -213,6 +249,7 @@ local function build_title() local state = ctx.state local core = ctx.core + ---@diagnostic disable-next-line: need-check-nil, undefined-field local sort = ctx.flags.disable_sorting and "off" or (state.sort_mode or core.config.sort_mode or "alpha") local suffix = sort .. (ctx.flags.reverse_sorting and " reverse " or "") ctx.base_title = ctx.opts.exclude_configured and "Raphael - Other Themes" or "Raphael - Configured Themes" @@ -269,7 +306,12 @@ local function init_context(core, opts) ctx.collapsed["__bookmarks"] = ctx.collapsed["__bookmarks"] or false ctx.collapsed["__recent"] = ctx.collapsed["__recent"] or false - ctx.bookmarks = bookmarks_mod.build_set(ctx.state, core) + local bookmarks_mod = lazy_loader.get_bookmarks() + if bookmarks_mod then + ctx.bookmarks = bookmarks_mod.build_set(ctx.state, core) + else + ctx.bookmarks = {} + end ctx.header_lines = {} ctx.last_cursor = {} @@ -291,7 +333,11 @@ end --- ---@return table function M.get_cache_stats() - return preview.get_cache_stats() + local preview = lazy_loader.get_preview() + if preview then + return preview.get_cache_stats() + end + return { palette_cache_size = 0, active_timers = 0 } end --- Get the theme under cursor in the picker, or nil if not available. @@ -302,14 +348,21 @@ function M.get_current_theme() return nil end local line = vim.api.nvim_get_current_line() - return render.parse_line_theme(ctx.core, line) + local render = lazy_loader.get_render() + if render then + return render.parse_line_theme(ctx.core, line) + end + return nil end --- Update palette preview for a given theme. --- ---@param theme string function M.update_palette(theme) - preview.update_palette(ctx, theme) + local preview = lazy_loader.get_preview() + if preview then + preview.update_palette(ctx, theme) + end end --- Open the picker UI. @@ -335,21 +388,78 @@ function M.open(core, opts) save_previous_theme() render_picker() - keymaps.highlight_current_line(ctx) + local keymaps_mod = lazy_loader.get_keymaps() + if keymaps_mod then + keymaps_mod.highlight_current_line(ctx) + end + ---@diagnostic disable-next-line: undefined-field if ctx.state.current then - preview.update_palette(ctx, ctx.state.current) + local preview = lazy_loader.get_preview() + if preview then + ---@diagnostic disable-next-line: undefined-field + preview.update_palette(ctx, ctx.state.current) + end end - keymaps.attach(ctx, { - close_picker = close_picker, - render = render_picker, - update_state_collapsed = update_state_collapsed, - open_search = setup_search, - }) + local keymaps = lazy_loader.get_keymaps() + if keymaps then + keymaps.attach(ctx, { + close_picker = close_picker, + render = render_picker, + update_state_collapsed = update_state_collapsed, + open_search = setup_search, + }) + end setup_autocmds_for_picker() + -- Reset the initial render flag after a delay to allow normal previews + vim.defer_fn(function() + if ctx then + ctx.initial_render = false + 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) + log("DEBUG", "Picker opened successfully") end diff --git a/tests/cache_test.lua b/tests/cache_test.lua new file mode 100644 index 0000000..c216bc8 --- /dev/null +++ b/tests/cache_test.lua @@ -0,0 +1,234 @@ +-- tests/cache_test.lua +-- Unit tests for raphael.nvim cache functionality + +local cache = require("raphael.core.cache") + +describe("raphael.nvim cache functionality", function() + -- Test state to use for testing + local test_theme = "test-cache-theme" + local test_scope = "__global" + + before_each(function() + -- Ensure clean state for each test + cache.clear() + end) + + describe("state management", function() + it("should read default state", function() + local state = cache.read() + assert.truthy(type(state) == "table") + assert.truthy(state.bookmarks ~= nil) + assert.truthy(state.history ~= nil) + assert.truthy(state.usage ~= nil) + assert.truthy(state.undo_history ~= nil) + end) + + it("should write and read state", function() + local test_state = { + current = test_theme, + saved = test_theme, + bookmarks = { [test_scope] = { test_theme } }, + history = { test_theme }, + usage = { [test_theme] = 5 }, + undo_history = { + stack = { test_theme }, + index = 1, + max_size = 10, + }, + } + + cache.write(test_state) + local read_state = cache.read() + + assert.equals(test_theme, read_state.current) + assert.equals(test_theme, read_state.saved) + assert.truthy(vim.tbl_contains(read_state.history, test_theme)) + assert.equals(5, read_state.usage[test_theme]) + assert.equals(1, read_state.undo_history.index) + end) + end) + + describe("bookmarks", function() + it("should handle bookmark toggling", function() + -- Initially not bookmarked + assert.falsy(cache.is_bookmarked(test_theme, test_scope)) + + -- Toggle on + local is_bookmarked = cache.toggle_bookmark(test_theme, test_scope) + assert.truthy(is_bookmarked) + assert.truthy(cache.is_bookmarked(test_theme, test_scope)) + + -- Toggle off + local is_bookmarked2, _ = cache.toggle_bookmark(test_theme, test_scope) + assert.falsy(is_bookmarked2) + assert.falsy(cache.is_bookmarked(test_theme, test_scope)) + end) + + it("should get bookmarks table", function() + local bookmarks = cache.get_bookmarks_table() + assert.truthy(type(bookmarks) == "table") + assert.truthy(type(bookmarks[test_scope]) == "table") + end) + + it("should get bookmarks for scope", function() + cache.toggle_bookmark(test_theme, test_scope) + + local bookmarks = cache.get_bookmarks(test_scope) + assert.truthy(vim.tbl_contains(bookmarks, test_theme)) + end) + end) + + describe("history", function() + it("should add to history", function() + cache.add_to_history(test_theme) + local history = cache.get_history() + assert.truthy(vim.tbl_contains(history, test_theme)) + end) + + it("should maintain history order", function() + local theme1 = "theme1" + local theme2 = "theme2" + + cache.add_to_history(theme1) + cache.add_to_history(theme2) + + local history = cache.get_history() + assert.equals(theme2, history[1]) -- Most recent first + assert.equals(theme1, history[2]) + end) + + it("should deduplicate history", function() + cache.add_to_history(test_theme) + cache.add_to_history(test_theme) + + local history = cache.get_history() + local count = 0 + for _, theme in ipairs(history) do + if theme == test_theme then + count = count + 1 + end + end + assert.equals(1, count) -- Should only appear once + end) + end) + + describe("usage tracking", function() + it("should increment usage", function() + cache.increment_usage(test_theme) + local usage = cache.get_usage(test_theme) + assert.equals(1, usage) + + cache.increment_usage(test_theme) + local usage2 = cache.get_usage(test_theme) + assert.equals(2, usage2) + end) + + it("should get all usage", function() + cache.increment_usage(test_theme) + local all_usage = cache.get_all_usage() + assert.truthy(type(all_usage) == "table") + assert.equals(1, all_usage[test_theme]) + end) + end) + + describe("undo history", function() + it("should push to undo stack", function() + cache.undo_push(test_theme) + local state = cache.read() + assert.truthy(vim.tbl_contains(state.undo_history.stack, test_theme)) + assert.equals(1, state.undo_history.index) + end) + + it("should pop from undo stack", function() + cache.undo_push(test_theme) + local theme = cache.undo_pop() + assert.equals(test_theme, theme) + end) + + it("should pop from redo stack", function() + cache.undo_push(test_theme) + cache.undo_pop() -- Go back + local theme = cache.redo_pop() + assert.equals(test_theme, theme) + end) + end) + + describe("auto apply", function() + it("should get and set auto apply", function() + assert.falsy(cache.get_auto_apply()) + + cache.set_auto_apply(true) + assert.truthy(cache.get_auto_apply()) + + cache.set_auto_apply(false) + assert.falsy(cache.get_auto_apply()) + end) + end) + + describe("quick slots", function() + it("should set and get quick slots", function() + local slot = "1" + cache.set_quick_slot(slot, test_theme) + + local retrieved = cache.get_quick_slot(slot) + assert.equals(test_theme, retrieved) + end) + + it("should get quick slots table", function() + local slots = cache.get_quick_slots_table() + assert.truthy(type(slots) == "table") + assert.truthy(type(slots[test_scope]) == "table") + end) + + it("should clear quick slots", function() + local slot = "2" + cache.set_quick_slot(slot, test_theme) + cache.clear_quick_slot(slot) + + local retrieved = cache.get_quick_slot(slot) + assert.is_nil(retrieved) + end) + end) + + describe("collapsed state", function() + it("should get and set collapsed state", function() + local group = "test-group" + assert.falsy(cache.collapsed(group)) + + cache.collapsed(group, true) + assert.truthy(cache.collapsed(group)) + + cache.collapsed(group, false) + assert.falsy(cache.collapsed(group)) + end) + end) + + describe("sort mode", function() + it("should get and set sort mode", function() + assert.equals("alpha", cache.get_sort_mode()) + + cache.set_sort_mode("recent") + assert.equals("recent", cache.get_sort_mode()) + end) + end) + + describe("clear function", function() + it("should clear all state", function() + cache.set_quick_slot("1", test_theme) + cache.toggle_bookmark(test_theme) + cache.add_to_history(test_theme) + + cache.clear() + + local state = cache.read() + assert.is_nil(state.current) + assert.is_nil(state.saved) + assert.is_nil(state.previous) + assert.falsy(state.auto_apply) + assert.truthy(next(state.bookmarks[test_scope]) == nil) + assert.truthy(#state.history == 0) + assert.truthy(next(state.usage) == nil) + assert.truthy(#state.undo_history.stack == 0) + end) + end) +end) diff --git a/tests/config_manager_test.lua b/tests/config_manager_test.lua new file mode 100644 index 0000000..cfd6c2b --- /dev/null +++ b/tests/config_manager_test.lua @@ -0,0 +1,220 @@ +-- tests/config_manager_test.lua +-- Integration tests for the configuration management features + +local config_manager = require("raphael.config_manager") +local config = require("raphael.config") + +describe("config_manager integration tests", function() + describe("export and import functionality", function() + it("should properly export and import configuration", function() + -- Create a test config + local test_config = { + default_theme = "test-theme-export-import", + leader = "tx", + bookmark_group = true, + recent_group = false, + mappings = { + picker = "p", + next = ">", + previous = "<", + }, + enable_autocmds = false, + enable_commands = true, + 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 + } + + local exported = config_manager.export_config(core_mock) + assert.are.same(validated_config, exported) + end) + + it("should save and load config from file", function() + local test_config = { + default_theme = "test-theme-file-io", + 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) + end) + + describe("validation functionality", function() + it("should validate correct configuration", function() + local valid_config = { + default_theme = "test-theme", + leader = "t", + bookmark_group = true, + recent_group = false, + mappings = { picker = "p" }, + enable_autocmds = true, + enable_commands = true, + 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) + end) + + it("should detect invalid configuration", function() + local invalid_config = { + default_theme = 123, -- should be string + 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) + end) + + it("should validate configuration sections properly", function() + local config_with_sections = { + default_theme = "test-theme", + leader = "t", + bookmark_group = true, + recent_group = false, + mappings = { picker = "p", next = ">", previous = "<" }, + filetype_themes = { lua = "test-theme" }, + project_themes = { ["/test/path"] = "test-theme" }, + profiles = { test_profile = { default_theme = "other-theme" } }, + enable_autocmds = true, + enable_commands = true, + 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) + assert.is_boolean(results.bookmark_group) + assert.is_boolean(results.recent_group) + assert.is_boolean(results.mappings) + assert.is_boolean(results.filetype_themes) + assert.is_boolean(results.project_themes) + assert.is_boolean(results.profiles) + assert.is_boolean(results.enable_autocmds) + 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) + assert.is_true(results.bookmark_group) + assert.is_true(results.recent_group) + assert.is_true(results.mappings) + assert.is_true(results.filetype_themes) + assert.is_true(results.project_themes) + assert.is_true(results.profiles) + assert.is_true(results.enable_autocmds) + assert.is_true(results.enable_commands) + assert.is_true(results.enable_keymaps) + assert.is_true(results.enable_picker) + end) + end) + + 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") + end) + + it("should apply a preset correctly", function() + -- Create a mock core module + local mock_core = { + base_config = { default_theme = "original-theme" }, + state = { current_profile = nil }, + config = { default_theme = "original-theme" }, + get_profile_config = function(profile_name) + return mock_core.base_config + 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") + end) + + it("should handle invalid preset name", function() + local mock_core = { + base_config = { default_theme = "original-theme" }, + state = { current_profile = nil }, + config = { default_theme = "original-theme" }, + get_profile_config = function(profile_name) + return mock_core.base_config + end + } + + local success = config_manager.apply_preset("non_existent_preset", mock_core) + + assert.is_false(success, "Should return false for invalid preset") + end) + end) + + describe("diagnostics functionality", function() + it("should provide configuration diagnostics", function() + local test_config = { + default_theme = "test-theme", + 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 diff --git a/tests/core_test.lua b/tests/core_test.lua new file mode 100644 index 0000000..aeaa22a --- /dev/null +++ b/tests/core_test.lua @@ -0,0 +1,324 @@ +-- tests/core_test.lua +-- Unit tests for raphael.nvim core functionality + +local core = require("raphael.core") +local themes = require("raphael.themes") +local cache = require("raphael.core.cache") + +describe("raphael.nvim core functionality", function() + -- Setup function to run before each test + local original_state = nil + setup(function() + -- Save original state + original_state = cache.read() + end) + + -- Teardown function to run after each test + teardown(function() + -- Restore original state + if original_state then + cache.write(original_state) + end + end) + + describe("theme discovery", function() + it("should discover installed themes", function() + themes.refresh() + local installed = themes.installed + assert.truthy(type(installed) == "table") + assert.truthy(next(installed) ~= nil) -- Should have at least one theme + end) + + it("should check if a theme is available", function() + themes.refresh() + local all_themes = themes.get_all_themes() + if #all_themes > 0 then + local first_theme = all_themes[1] + assert.truthy(themes.is_available(first_theme)) + end + end) + + it("should get all configured themes", function() + themes.theme_map = { test_theme = { "default" } } + local all_themes = themes.get_all_themes() + assert.truthy(vim.tbl_contains(all_themes, "default")) + end) + end) + + describe("configuration validation", function() + it("should validate default configuration", function() + local config = require("raphael.config") + local validated = config.validate(nil) + assert.truthy(type(validated) == "table") + assert.truthy(validated.default_theme ~= nil) + assert.truthy(validated.leader ~= nil) + end) + + it("should handle user overrides", function() + local config = require("raphael.config") + local user_config = { + default_theme = "test-theme", + leader = "tt", + } + local validated = config.validate(user_config) + assert.equals("test-theme", validated.default_theme) + assert.equals("tt", validated.leader) + end) + end) + + describe("core state management", function() + it("should initialize with default state", function() + local state = core.state + assert.truthy(type(state) == "table") + assert.truthy(state.bookmarks ~= nil) + assert.truthy(state.history ~= nil) + assert.truthy(state.usage ~= nil) + end) + + it("should have working setup function", function() + -- Test that setup doesn't error with minimal config + local success, err = pcall(core.setup, { default_theme = "default" }) + assert.truthy(success, "Setup should not error: " .. tostring(err)) + end) + end) + + describe("cache functionality", function() + it("should read and write state", function() + local test_state = { + current = "test-theme", + bookmarks = { __global = { "test-theme" } }, + history = { "test-theme" }, + usage = { ["test-theme"] = 1 }, + } + + cache.write(test_state) + local read_state = cache.read() + + assert.equals("test-theme", read_state.current) + assert.truthy(vim.tbl_contains(read_state.history, "test-theme")) + assert.equals(1, read_state.usage["test-theme"]) + end) + + it("should handle bookmark toggling", function() + local theme = "test-bookmark-theme" + local scope = "__global" + + -- Initially should not be bookmarked + local is_bookmarked = cache.is_bookmarked(theme, scope) + assert.falsy(is_bookmarked) + + -- Toggle on + local new_state = cache.toggle_bookmark(theme, scope) + assert.truthy(new_state) + + -- Should now be bookmarked + is_bookmarked = cache.is_bookmarked(theme, scope) + assert.truthy(is_bookmarked) + + -- Toggle off + local new_state2, _ = cache.toggle_bookmark(theme, scope) + assert.falsy(new_state2) + end) + + it("should handle history", function() + local theme = "test-history-theme" + + cache.add_to_history(theme) + local history = cache.get_history() + + assert.truthy(vim.tbl_contains(history, theme)) + end) + + it("should handle usage counts", function() + local theme = "test-usage-theme" + + cache.increment_usage(theme) + local count = cache.get_usage(theme) + + assert.equals(1, count) + + cache.increment_usage(theme) + local count2 = cache.get_usage(theme) + + assert.equals(2, count2) + end) + end) + + describe("theme application", function() + it("should have apply function", function() + assert.truthy(type(core.apply) == "function") + end) + + it("should have toggle_auto function", function() + assert.truthy(type(core.toggle_auto) == "function") + end) + + it("should have toggle_bookmark function", function() + assert.truthy(type(core.toggle_bookmark) == "function") + end) + end) + + describe("picker functionality", function() + it("should have open_picker function", function() + assert.truthy(type(core.open_picker) == "function") + end) + + it("should have get_current_theme function", function() + assert.truthy(type(core.get_current_theme) == "function") + end) + end) + + describe("configuration management", function() + local config_manager = require("raphael.config_manager") + + it("should export configuration correctly", function() + local export = config_manager.export_config(core) + assert.truthy(type(export) == "table") + assert.truthy(export.default_theme ~= nil) + assert.truthy(export.leader ~= nil) + end) + + it("should validate configuration correctly", function() + local test_config = { + default_theme = "test-theme", + leader = "tt", + bookmark_group = true, + recent_group = true, + mappings = { picker = "p", next = ">", previous = "<" }, + enable_autocmds = true, + enable_commands = true, + enable_keymaps = true, + enable_picker = true, + } + + local is_valid, error_msg = config_manager.validate_config(test_config) + assert.truthy(is_valid) + assert.truthy(error_msg == nil) + end) + + it("should detect invalid configuration", function() + local invalid_config = { + default_theme = 123, -- should be string + leader = 456, -- should be string + bookmark_group = "not_boolean", -- should be boolean + } + + local is_valid, error_msg = config_manager.validate_config(invalid_config) + assert.falsy(is_valid) + assert.truthy(type(error_msg) == "string") + end) + + it("should validate configuration sections correctly", function() + local test_config = { + default_theme = "test-theme", + leader = "tt", + bookmark_group = true, + recent_group = true, + mappings = { picker = "p", next = ">" }, + filetype_themes = { lua = "test-theme" }, + project_themes = { ["/test/path"] = "test-theme" }, + profiles = { test = { default_theme = "test-theme" } }, + enable_autocmds = true, + enable_commands = true, + enable_keymaps = true, + enable_picker = true, + } + + local results = config_manager.validate_config_sections(test_config) + assert.truthy(type(results) == "table") + assert.truthy(results.default_theme == true) + assert.truthy(results.leader == true) + assert.truthy(results.bookmark_group == true) + assert.truthy(results.mappings == true) + assert.truthy(results.filetype_themes == true) + assert.truthy(results.project_themes == true) + assert.truthy(results.profiles == true) + end) + + it("should get configuration diagnostics", function() + local test_config = { + default_theme = "test-theme", + unknown_key = "should_not_exist", + another_unknown_key = "also_should_not_exist", + } + + local diagnostics = config_manager.get_config_diagnostics(test_config) + assert.truthy(type(diagnostics) == "table") + assert.truthy(diagnostics.total_keys == 3) + assert.truthy(#diagnostics.unknown_keys == 2) + assert.truthy(vim.tbl_contains(diagnostics.unknown_keys, "unknown_key")) + assert.truthy(vim.tbl_contains(diagnostics.unknown_keys, "another_unknown_key")) + end) + + it("should save and load config to/from file", function() + local test_config = { + default_theme = "test-theme-save", + leader = "ts", + 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.truthy(save_success) + + -- Import config from file + local imported_config = config_manager.import_config_from_file(temp_file) + assert.truthy(type(imported_config) == "table") + assert.equals("test-theme-save", imported_config.default_theme) + assert.equals("ts", imported_config.leader) + assert.falsy(imported_config.bookmark_group) + + -- Clean up + os.remove(temp_file) + end) + + it("should handle invalid config file import", function() + local non_existent_file = "/non/existent/path/config.json" + local imported_config = config_manager.import_config_from_file(non_existent_file) + assert.falsy(imported_config) + + local empty_file = os.tmpname() + local f = io.open(empty_file, "w") + f:write("") + f:close() + + local imported_empty = config_manager.import_config_from_file(empty_file) + assert.falsy(imported_empty) + + -- Clean up + os.remove(empty_file) + end) + + it("should get available presets", function() + local presets = config_manager.get_presets() + assert.truthy(type(presets) == "table") + assert.truthy(presets.minimal ~= nil) + assert.truthy(presets.full_featured ~= nil) + assert.truthy(presets.presentation ~= nil) + end) + + it("should apply a preset configuration", function() + -- Save original config + local original_config = vim.deepcopy(core.base_config) + + local success = config_manager.apply_preset("minimal", core) + assert.truthy(success) + + -- Check that the preset values were applied (bookmark_group should be false for minimal preset) + assert.falsy(core.base_config.bookmark_group) + + -- 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 + end) + + it("should handle invalid preset", function() + local success = config_manager.apply_preset("non_existent_preset", core) + assert.falsy(success) + end) + end) +end) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua new file mode 100644 index 0000000..d2ec99b --- /dev/null +++ b/tests/minimal_init.lua @@ -0,0 +1,11 @@ +-- tests/minimal_init.lua +-- Minimal init file for testing raphael.nvim + +-- Add the current directory to runtime path so we can require the modules +vim.cmd("set rtp+=" .. vim.fn.getcwd()) + +-- Setup plenary if available +local plenary_avail = pcall(require, "plenary") +if plenary_avail then + require("plenary.test_harness"):setup() +end diff --git a/tests/mock_cache.lua b/tests/mock_cache.lua new file mode 100644 index 0000000..9932c7d --- /dev/null +++ b/tests/mock_cache.lua @@ -0,0 +1,383 @@ +-- tests/mock_cache.lua +-- A mock cache module for testing that doesn't affect the real cache + +local M = {} + +local test_state = { + current = nil, + saved = nil, + previous = nil, + auto_apply = false, + bookmarks = { __global = {} }, + history = {}, + usage = {}, + collapsed = {}, + sort_mode = "alpha", + undo_history = { + stack = {}, + index = 0, + max_size = 100, + }, + quick_slots = { __global = {} }, + current_profile = nil, +} + +--- Read state from test memory (or return defaults) +function M.read() + return test_state +end + +--- Write full state to test memory +function M.write(state) + for k, v in pairs(state) do + test_state[k] = v + end + + test_state.bookmarks = test_state.bookmarks or { __global = {} } + test_state.history = test_state.history or {} + test_state.usage = test_state.usage or {} + test_state.collapsed = test_state.collapsed or {} + test_state.undo_history = test_state.undo_history or { + stack = {}, + index = 0, + max_size = 100, + } + test_state.quick_slots = test_state.quick_slots or { __global = {} } + return true +end + +--- For debugging only: return current state +function M.get_state() + return test_state +end + +--- Clear everything and reset to defaults +function M.clear() + test_state = { + current = nil, + saved = nil, + previous = nil, + auto_apply = false, + bookmarks = { __global = {} }, + history = {}, + usage = {}, + collapsed = {}, + sort_mode = "alpha", + undo_history = { + stack = {}, + index = 0, + max_size = 100, + }, + quick_slots = { __global = {} }, + current_profile = nil, + } +end + +--- Get current theme from test state +function M.get_current() + return test_state.current +end + +--- Get saved theme from test state +function M.get_saved() + return test_state.saved +end + +--- Set current theme in test state +function M.set_current(theme, save) + test_state.previous = test_state.current + test_state.current = theme + + if save then + test_state.saved = theme + end +end + +--- Get bookmarks table +function M.get_bookmarks_table() + return test_state.bookmarks or { __global = {} } +end + +--- Get bookmarks list for given scope +function M.get_bookmarks(scope) + scope = scope or "__global" + local all = M.get_bookmarks_table() + return all[scope] or {} +end + +--- Toggle bookmark for a theme in a scope +function M.toggle_bookmark(theme, scope) + scope = scope or "__global" + test_state.bookmarks = test_state.bookmarks or { __global = {} } + + if type(test_state.bookmarks[scope]) ~= "table" then + test_state.bookmarks[scope] = {} + end + + local list = test_state.bookmarks[scope] + local idx = nil + for i, name in ipairs(list) do + if name == theme then + idx = i + break + end + end + + if idx then + table.remove(list, idx) + return false, test_state.bookmarks + else + if #list >= 50 then + return false, nil + end + table.insert(list, theme) + + return true, test_state.bookmarks + end +end + +--- Check if theme is bookmarked +function M.is_bookmarked(theme, scope) + scope = scope or "__global" + local list = M.get_bookmarks(scope) + for _, name in ipairs(list) do + if name == theme then + return true + end + end + return false +end + +--- Add theme to history in test state +function M.add_to_history(theme) + test_state.history = test_state.history or {} + + for i, name in ipairs(test_state.history) do + if name == theme then + table.remove(test_state.history, i) + break + end + end + + table.insert(test_state.history, 1, theme) + + while #test_state.history > 12 do + table.remove(test_state.history) + end +end + +--- Get history from test state +function M.get_history() + return test_state.history or {} +end + +--- Increment usage count for theme in test state +function M.increment_usage(theme) + test_state.usage = test_state.usage or {} + test_state.usage[theme] = (test_state.usage[theme] or 0) + 1 +end + +--- Get usage count for theme from test state +function M.get_usage(theme) + return (test_state.usage or {})[theme] or 0 +end + +--- Get full usage map from test state +function M.get_all_usage() + return test_state.usage or {} +end + +--- Get or set collapsed state +function M.collapsed(group_key, collapsed) + test_state.collapsed = test_state.collapsed or {} + + if collapsed ~= nil then + test_state.collapsed[group_key] = collapsed + end + + return test_state.collapsed[group_key] or false +end + +--- Get current sort mode +function M.get_sort_mode() + local mode = test_state.sort_mode or "alpha" + if mode == "alphabetical" then + mode = "alpha" + end + return mode +end + +--- Set current sort mode +function M.set_sort_mode(mode) + test_state.sort_mode = mode +end + +--- Get auto-apply flag +function M.get_auto_apply() + return test_state.auto_apply or false +end + +--- Set auto-apply flag +function M.set_auto_apply(enabled) + test_state.auto_apply = enabled and true or false +end + +--- Get quick slots table +function M.get_quick_slots_table() + return test_state.quick_slots or { __global = {} } +end + +--- Get quick slots map for a scope +function M.get_quick_slots(scope) + scope = scope or "__global" + local all = M.get_quick_slots_table() + if type(all[scope]) ~= "table" then + return {} + end + return all[scope] +end + +--- Set a quick slot +function M.set_quick_slot(slot, theme, scope) + local normalize_slot = function(s) + if type(s) == "number" then + s = tostring(s) + end + if type(s) ~= "string" then + return nil + end + if not s:match("^[0-9]$") then + return nil + end + return s + end + + slot = normalize_slot(slot) + scope = scope or "__global" + if not slot then + return + end + if not theme or theme == "" then + return + end + + test_state.quick_slots = test_state.quick_slots or { __global = {} } + if type(test_state.quick_slots[scope]) ~= "table" then + test_state.quick_slots[scope] = {} + end + test_state.quick_slots[scope][slot] = theme + return theme +end + +--- Clear a quick slot +function M.clear_quick_slot(slot, scope) + local normalize_slot = function(s) + if type(s) == "number" then + s = tostring(s) + end + if type(s) ~= "string" then + return nil + end + if not s:match("^[0-9]$") then + return nil + end + return s + end + + slot = normalize_slot(slot) + scope = scope or "__global" + if not slot then + return + end + test_state.quick_slots = test_state.quick_slots or { __global = {} } + if type(test_state.quick_slots[scope]) ~= "table" then + return + end + test_state.quick_slots[scope][slot] = nil +end + +--- Get a single quick slot theme +function M.get_quick_slot(slot, scope) + local normalize_slot = function(s) + if type(s) == "number" then + s = tostring(s) + end + if type(s) ~= "string" then + return nil + end + if not s:match("^[0-9]$") then + return nil + end + return s + end + + slot = normalize_slot(slot) + scope = scope or "__global" + if not slot then + return nil + end + local slots = M.get_quick_slots(scope) + return slots[slot] +end + +--- Push theme onto undo stack in test state +function M.undo_push(theme) + local undo = test_state.undo_history or { + stack = {}, + index = 0, + max_size = 100, + } + + while #undo.stack > undo.index do + table.remove(undo.stack) + end + + for i = #undo.stack, 1, -1 do + if undo.stack[i] == theme then + table.remove(undo.stack, i) + if i <= undo.index then + undo.index = undo.index - 1 + end + end + end + + table.insert(undo.stack, theme) + undo.index = #undo.stack + + local max_size = undo.max_size or 100 + while #undo.stack > max_size do + table.remove(undo.stack, 1) + undo.index = undo.index - 1 + end + + test_state.undo_history = undo +end + +--- Undo to previous theme in test state +function M.undo_pop() + local undo = test_state.undo_history + if not undo or undo.index <= 1 then + return nil + end + + undo.index = undo.index - 1 + test_state.undo_history = undo + + return undo.stack[undo.index] +end + +--- Redo to next theme in test state +function M.redo_pop() + local undo = test_state.undo_history + if not undo or undo.index >= #undo.stack then + return nil + end + + undo.index = undo.index + 1 + test_state.undo_history = undo + + return undo.stack[undo.index] +end + +return M + diff --git a/tests/neovim_test_runner.lua b/tests/neovim_test_runner.lua new file mode 100644 index 0000000..e8c6b97 --- /dev/null +++ b/tests/neovim_test_runner.lua @@ -0,0 +1,329 @@ +-- neovim_test_runner.lua +-- A test runner that runs inside Neovim to properly load raphael modules + +local function print_header(text) + print("\n" .. string.rep("=", 60)) + print(text) + print(string.rep("=", 60)) +end + +local function print_result(test_name, passed, error_msg) + local status = passed and "✓ PASS" or "✗ FAIL" + print(string.format(" %s: %s", status, test_name)) + if error_msg then + print(" Error: " .. error_msg) + end +end + +local function run_test(test_name, test_func) + local success, result = pcall(test_func) + print_result(test_name, success and result == nil or result == true, not success and result or nil) + return success and (result == nil or result == true) +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 }) + end + + test_block(it) + + local passed_count = 0 + for _, test in ipairs(tests) do + if run_test(test.name, test.func) then + passed_count = passed_count + 1 + end + end + + print(string.format(" %d/%d tests passed in '%s'", passed_count, #tests, description)) + return passed_count, #tests +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 + 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 + end, + same = function(expected, actual, msg) + if vim.deep_equal(expected, actual) ~= true then + error(msg or string.format("Expected %s, got %s", vim.inspect(expected), vim.inspect(actual))) + end + end, + is_true = function(value, msg) + if value ~= true then + error(msg or string.format("Expected true, got %s", tostring(value))) + end + end, + is_false = function(value, msg) + if value ~= false then + error(msg or string.format("Expected false, got %s", tostring(value))) + end + end, + tbl_contains = function(tbl, value, msg) + local found = false + for _, v in ipairs(tbl) do + if v == value then + found = true + break + end + end + if not found then + error(msg or string.format("Table does not contain value %s", tostring(value))) + end + end, +} + +print("Neovim Test Runner for raphael.nvim") +print("====================================") + +-- Use mock cache to avoid affecting real cache +local mock_cache = require("tests.mock_cache") + +-- Load the modules we need to test (but use mock cache for testing) +local core = require("raphael.core") +local config_manager = require("raphael.config_manager") +local config = require("raphael.config") +local themes = require("raphael.themes") + +-- Use mock cache instead of real cache for tests +local cache = mock_cache + +-- Run tests for core functionality +local total_passed = 0 +local total_tests = 0 + +print_header("Testing Core Raphael Functionality") + +-- Test theme discovery +do + print("\nTheme discovery tests:") + themes.refresh() + local installed = themes.installed + assert.truthy(type(installed) == "table", "Installed themes should be a table") + print(" ✓ Installed themes is a table") + total_tests = total_tests + 1 + total_passed = total_passed + 1 + + local all_themes = themes.get_all_themes() + assert.truthy(#all_themes >= 0, "Should have at least 0 themes") + print(" ✓ Can get all themes") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +end + +-- Test configuration validation +do + print("\nConfiguration validation tests:") + local validated = config.validate(nil) + assert.truthy(type(validated) == "table", "Validated config should be a table") + assert.truthy(validated.default_theme ~= nil, "Should have default theme") + print(" ✓ Default config validation works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 + + local user_config = { + default_theme = "test-theme", + leader = "tt", + } + local validated_user = config.validate(user_config) + assert.equals("test-theme", validated_user.default_theme) + assert.equals("tt", validated_user.leader) + print(" ✓ User config validation works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +end + +-- Test cache functionality +do + print("\nCache functionality tests:") + local original_state = cache.read() + + local test_state = { + current = "test-theme", + bookmarks = { __global = { "test-theme" } }, + history = { "test-theme" }, + usage = { ["test-theme"] = 1 }, + } + + cache.write(test_state) + local read_state = cache.read() + + assert.equals("test-theme", read_state.current) + assert.truthy(vim.tbl_contains(read_state.history, "test-theme")) + assert.equals(1, read_state.usage["test-theme"]) + print(" ✓ Cache read/write works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 + + cache.write(original_state) +end + +-- Test configuration management functionality +print_header("Testing Configuration Management Features") + +do + print("\nConfiguration export/import tests:") + + local export = config_manager.export_config({ + base_config = { default_theme = "test-theme", leader = "te" }, + 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 + + local is_valid, error_msg = config_manager.validate_config({ + default_theme = "test-theme", + leader = "t", + 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 + + local is_valid, error_msg = config_manager.validate_config({ + default_theme = 123, + leader = 456, + }) + assert.truthy(is_valid, "Should return true since validation fixes issues") + assert.truthy(error_msg == nil, "Should not return error message for fixable config") + print(" ✓ Config validation fixes issues instead of rejecting") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +end + +do + print("\nConfiguration section validation tests:") + + local results = config_manager.validate_config_sections({ + default_theme = "test-theme", + leader = "tt", + bookmark_group = true, + recent_group = true, + mappings = { picker = "p", next = ">" }, + filetype_themes = { lua = "test-theme" }, + project_themes = { ["/test/path"] = "test-theme" }, + profiles = { test = { default_theme = "test-theme" } }, + enable_autocmds = true, + enable_commands = true, + enable_keymaps = true, + enable_picker = true, + }) + + assert.truthy(type(results) == "table") + assert.truthy(results.default_theme == true) + assert.truthy(results.leader == true) + assert.truthy(results.bookmark_group == true) + print(" ✓ Section validation works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +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) + assert.truthy(vim.tbl_contains(diagnostics.unknown_keys, "unknown_key")) + print(" ✓ Config diagnostics work") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +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" + + 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 + + local imported_config = config_manager.import_config_from_file(temp_file) + assert.truthy(type(imported_config) == "table") + assert.equals("test-theme-save", imported_config.default_theme) + assert.equals("ts", imported_config.leader) + assert.falsy(imported_config.bookmark_group) + print(" ✓ Config load works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 + + 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) + assert.truthy(presets.full_featured ~= nil) + assert.truthy(presets.presentation ~= nil) + print(" ✓ Presets available") + total_tests = total_tests + 1 + total_passed = total_passed + 1 + + local mock_core = { + base_config = { default_theme = "original-theme" }, + state = { current_profile = nil }, + config = { default_theme = "original-theme" }, + } + + 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) + print(" ✓ Preset application works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +end + +print_header(string.format("Final Results: %d/%d tests passed", total_passed, total_tests)) + +if total_passed == total_tests then + print("🎉 All tests passed! Configuration management features are working correctly.") +else + print("⚠️ Some tests failed. Please review the output above.") +end + +-- Test completed using mock cache, no cleanup needed for real cache + diff --git a/tests/palette_cache_test.lua b/tests/palette_cache_test.lua new file mode 100644 index 0000000..bcc1a7f --- /dev/null +++ b/tests/palette_cache_test.lua @@ -0,0 +1,106 @@ +-- tests/palette_cache_test.lua +-- Unit tests for raphael.nvim palette cache functionality + +local palette_cache = require("raphael.core.palette_cache") + +describe("raphael.nvim palette cache functionality", function() + local test_theme = "default" -- Use default theme which should always exist + + describe("palette generation", function() + it("should generate palette data for a theme", function() + local palette_data = palette_cache.generate_palette_data(test_theme) + assert.truthy(palette_data ~= nil) + assert.truthy(type(palette_data) == "table") + + -- Should contain expected highlight groups + local expected_groups = { "Normal", "Comment", "String", "Keyword", "Function", "Type", "Constant", "Special" } + for _, group in ipairs(expected_groups) do + assert.truthy(palette_data[group] ~= nil, "Missing highlight group: " .. group) + assert.truthy(type(palette_data[group]) == "table") + end + end) + + it("should return nil for invalid theme", function() + local palette_data = palette_cache.generate_palette_data("nonexistent-theme-12345") + assert.is_nil(palette_data) + end) + end) + + describe("caching system", function() + it("should cache and retrieve palette data", function() + -- Clear cache first + palette_cache.clear_cache() + + local palette_data = palette_cache.generate_palette_data(test_theme) + assert.truthy(palette_data ~= nil) + + -- Cache the data + palette_cache.cache_palette(test_theme, palette_data) + + -- Retrieve from cache + local cached_data = palette_cache.get_cached_palette(test_theme) + assert.truthy(cached_data ~= nil) + assert.truthy(type(cached_data) == "table") + end) + + it("should return nil for non-existent cache entry", function() + local cached_data = palette_cache.get_cached_palette("nonexistent-theme") + assert.is_nil(cached_data) + end) + + it("should handle cache expiration", function() + -- This test checks that the expiration logic works + local stats_before = palette_cache.get_stats() + assert.truthy(type(stats_before) == "table") + assert.truthy(type(stats_before.valid_entries) == "number") + assert.truthy(type(stats_before.expired_entries) == "number") + end) + + it("should clear expired entries", function() + -- Set up a scenario where we can test expiration + palette_cache.clear_expired() + local stats = palette_cache.get_stats() + assert.truthy(type(stats) == "table") + end) + + it("should get stats", function() + local stats = palette_cache.get_stats() + assert.truthy(type(stats) == "table") + assert.truthy(type(stats.total_entries) == "number") + assert.truthy(type(stats.valid_entries) == "number") + assert.truthy(type(stats.expired_entries) == "number") + assert.truthy(type(stats.max_size) == "number") + assert.truthy(type(stats.timeout_seconds) == "number") + end) + end) + + describe("get_palette_with_cache", function() + it("should get palette with caching", function() + local palette_data = palette_cache.get_palette_with_cache(test_theme) + assert.truthy(palette_data ~= nil) + assert.truthy(type(palette_data) == "table") + + -- Should now be cached, so getting it again should work + local cached_data = palette_cache.get_palette_with_cache(test_theme) + assert.truthy(cached_data ~= nil) + end) + + it("should handle nil theme gracefully", function() + local palette_data = palette_cache.get_palette_with_cache(nil) + assert.is_nil(palette_data) + end) + end) + + describe("preload functionality", function() + it("should handle preload with empty list", function() + palette_cache.preload_palettes({}) + -- Should not error + end) + + it("should handle preload with valid themes", function() + -- Test with a valid theme + palette_cache.preload_palettes({ test_theme }) + -- Should not error + end) + end) +end) diff --git a/tests/picker_test.lua b/tests/picker_test.lua new file mode 100644 index 0000000..c6d1991 --- /dev/null +++ b/tests/picker_test.lua @@ -0,0 +1,68 @@ +-- tests/picker_test.lua +-- Unit tests for raphael.nvim picker functionality + +local picker = require("raphael.picker.ui") +local lazy_loader = require("raphael.picker.lazy_loader") + +describe("raphael.nvim picker functionality", function() + describe("lazy loader", function() + it("should load render module", function() + local render = lazy_loader.get_render() + assert.truthy(render ~= nil) + assert.truthy(type(render.render) == "function") + end) + + it("should load search module", function() + local search = lazy_loader.get_search() + assert.truthy(search ~= nil) + assert.truthy(type(search.open) == "function") + end) + + it("should load preview module", function() + local preview = lazy_loader.get_preview() + assert.truthy(preview ~= nil) + assert.truthy(type(preview.update_palette) == "function") + end) + + it("should load keymaps module", function() + local keymaps = lazy_loader.get_keymaps() + assert.truthy(keymaps ~= nil) + assert.truthy(type(keymaps.attach) == "function") + end) + + it("should load bookmarks module", function() + local bookmarks = lazy_loader.get_bookmarks() + assert.truthy(bookmarks ~= nil) + assert.truthy(type(bookmarks.build_set) == "function") + end) + + it("should provide stats", function() + local stats = lazy_loader.get_stats() + assert.truthy(type(stats) == "table") + assert.truthy(type(stats.loaded_modules_count) == "number") + assert.truthy(type(stats.loaded_modules) == "table") + end) + end) + + describe("picker UI", function() + it("should have get_cache_stats function", function() + assert.truthy(type(picker.get_cache_stats) == "function") + end) + + it("should have get_current_theme function", function() + assert.truthy(type(picker.get_current_theme) == "function") + end) + + it("should have update_palette function", function() + assert.truthy(type(picker.update_palette) == "function") + end) + + it("should have toggle_debug function", function() + assert.truthy(type(picker.toggle_debug) == "function") + end) + + it("should have open function", function() + assert.truthy(type(picker.open) == "function") + end) + end) +end) diff --git a/tests/raphael_spec.lua b/tests/raphael_spec.lua new file mode 100644 index 0000000..ae778c8 --- /dev/null +++ b/tests/raphael_spec.lua @@ -0,0 +1,217 @@ +-- tests/raphael_spec.lua +-- Main test suite for raphael.nvim using plenary.nvim test runner + +-- Only require plenary if we're in a test environment +local success, async = pcall(require, "plenary.async") +local async_fn + +if success then + async_fn = async.tests +else + -- Provide mock functions for basic validation + async_fn = { + describe = function(desc, fn) + print("Testing: " .. desc) + fn() + end, + it = function(desc, fn) + local test_success, err = pcall(fn) + if test_success then + print(" ✓ " .. desc) + else + print(" ✗ " .. desc .. " - " .. tostring(err)) + end + end, + setup = function(fn) + fn() + end, + } +end + +-- Test core functionality +async_fn.describe("raphael core functionality", function() + local themes = require("raphael.themes") + local cache = require("raphael.core.cache") + + async_fn.it("should have working theme discovery", function() + themes.refresh() + local installed = themes.installed + assert(type(installed) == "table") + end) + + async_fn.it("should validate configuration properly", function() + local config = require("raphael.config") + local validated = config.validate(nil) + assert(type(validated) == "table") + assert(validated.default_theme ~= nil) + end) + + async_fn.it("should have working cache system", function() + local test_state = { + current = "test-theme", + bookmarks = { __global = { "test-theme" } }, + } + cache.write(test_state) + local read_state = cache.read() + assert(read_state.current == "test-theme") + end) + + async_fn.it("should handle bookmark toggling", function() + local theme = "test-bookmark-theme" + local scope = "__global" + + -- Initially should not be bookmarked + local is_bookmarked = cache.is_bookmarked(theme, scope) + assert(not is_bookmarked) + + -- Toggle on + local new_state = cache.toggle_bookmark(theme, scope) + assert(new_state) + + -- Should now be bookmarked + is_bookmarked = cache.is_bookmarked(theme, scope) + assert(is_bookmarked) + end) +end) + +-- Test picker functionality +async_fn.describe("raphael picker functionality", function() + local lazy_loader = require("raphael.picker.lazy_loader") + + async_fn.it("should load picker modules lazily", function() + local render = lazy_loader.get_render() + assert(render ~= nil) + assert(type(render.render) == "function") + + local search = lazy_loader.get_search() + assert(search ~= nil) + assert(type(search.open) == "function") + + local preview = lazy_loader.get_preview() + assert(preview ~= nil) + assert(type(preview.update_palette) == "function") + + local keymaps = lazy_loader.get_keymaps() + assert(keymaps ~= nil) + assert(type(keymaps.attach) == "function") + + local bookmarks = lazy_loader.get_bookmarks() + assert(bookmarks ~= nil) + assert(type(bookmarks.build_set) == "function") + end) + + async_fn.it("should provide lazy loader stats", function() + local stats = lazy_loader.get_stats() + assert(type(stats) == "table") + assert(type(stats.loaded_modules_count) == "number") + end) +end) + +-- Test cache functionality +async_fn.describe("raphael cache functionality", function() + local cache = require("raphael.core.cache") + local test_theme = "test-cache-theme" + + async_fn.setup(function() + -- Clean state before tests + cache.clear() + end) + + async_fn.it("should read default state", function() + local state = cache.read() + assert(type(state) == "table") + assert(state.bookmarks ~= nil) + assert(state.history ~= nil) + end) + + async_fn.it("should handle history operations", function() + cache.add_to_history(test_theme) + local history = cache.get_history() + assert(vim.tbl_contains(history, test_theme)) + end) + + async_fn.it("should handle usage tracking", function() + cache.increment_usage(test_theme) + local usage = cache.get_usage(test_theme) + assert(usage == 1) + end) + + async_fn.it("should handle undo operations", function() + cache.undo_push(test_theme) + local theme = cache.undo_pop() + assert(theme == test_theme) + end) +end) + +-- Test palette cache functionality +async_fn.describe("raphael palette cache functionality", function() + local palette_cache = require("raphael.core.palette_cache") + local test_theme = "default" -- Use default theme which should exist + + async_fn.it("should generate palette data", function() + local palette_data = palette_cache.generate_palette_data(test_theme) + assert(palette_data ~= nil) + assert(type(palette_data) == "table") + end) + + async_fn.it("should cache and retrieve palette data", function() + local palette_data = palette_cache.generate_palette_data(test_theme) + assert(palette_data ~= nil) + + -- Cache the data + palette_cache.cache_palette(test_theme, palette_data) + + -- Retrieve from cache + local cached_data = palette_cache.get_cached_palette(test_theme) + assert(cached_data ~= nil) + end) + + async_fn.it("should get palette with caching", function() + local palette_data = palette_cache.get_palette_with_cache(test_theme) + assert(palette_data ~= nil) + assert(type(palette_data) == "table") + end) + + async_fn.it("should provide cache stats", function() + local stats = palette_cache.get_stats() + assert(type(stats) == "table") + assert(type(stats.total_entries) == "number") + end) +end) + +-- Test utils functionality +async_fn.describe("raphael utils functionality", function() + local utils = require("raphael.utils") + + async_fn.it("should handle fuzzy scoring", function() + local score1 = utils.fuzzy_score("testing", "test") + assert(score1 >= 500) -- Should be high for prefix match + + local score2 = utils.fuzzy_score("anything", "") + assert(score2 == 1) -- Empty query should score 1 + end) + + async_fn.it("should handle deep copy", function() + local original = { a = { b = { c = 1 } } } + local copy = utils.deep_copy(original) + assert(original.a.b.c == copy.a.b.c) + assert(original ~= copy) -- Different objects + end) + + async_fn.it("should handle table contains", function() + local tbl = { "a", "b", "c" } + assert(utils.tbl_contains(tbl, "b")) + assert(not utils.tbl_contains(tbl, "d")) + end) + + async_fn.it("should handle random theme selection", function() + local empty_result = utils.random_theme({}) + assert(empty_result == nil) + + local single_result = utils.random_theme({ "only-theme" }) + assert(single_result == "only-theme") + + local multi_result = utils.random_theme({ "theme1", "theme2" }) + assert(multi_result ~= nil) + end) +end) diff --git a/tests/run_config_tests.sh b/tests/run_config_tests.sh new file mode 100755 index 0000000..6d3f016 --- /dev/null +++ b/tests/run_config_tests.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# run_config_tests.sh +# Script to run the configuration management tests (safe version) + +echo "Running configuration management tests for raphael.nvim..." + +# Run the safe test runner that doesn't modify the real cache +nvim --headless -c "luafile tests/safe_test_runner.lua" -c "qa" + +if [ $? -eq 0 ]; then + echo "All configuration management tests passed!" + echo "Note: These tests don't modify the real cache, so your configuration is safe." +else + echo "Some configuration management tests failed!" + exit 1 +fi \ No newline at end of file diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..5afbaad --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Run tests for raphael.nvim using plenary.nvim + +echo "Running raphael.nvim tests..." + +# Check if plenary is available +# Run from the project root directory to ensure proper path resolution +cd "$(dirname "$0")/.." # Go up to project root +nvim --headless -u tests/minimal_init.lua -c "lua require('plenary.test_harness').test_directory('tests', {minimal_init = 'tests/minimal_init.lua'})" -c "qa" + +if [ $? -eq 0 ]; then + echo "All tests passed!" +else + echo "Some tests failed!" + exit 1 +fi \ No newline at end of file diff --git a/tests/safe_test_runner.lua b/tests/safe_test_runner.lua new file mode 100644 index 0000000..6065391 --- /dev/null +++ b/tests/safe_test_runner.lua @@ -0,0 +1,270 @@ +-- safe_test_runner.lua +-- A safe test runner that doesn't modify the real cache + +local function print_header(text) + print("\n" .. string.rep("=", 60)) + print(text) + print(string.rep("=", 60)) +end + +local function print_result(test_name, passed, error_msg) + local status = passed and "✓ PASS" or "✗ FAIL" + print(string.format(" %s: %s", status, test_name)) + if error_msg then + print(" Error: " .. error_msg) + end +end + +local function run_test(test_name, test_func) + local success, result = pcall(test_func) + print_result(test_name, success and result == nil or result == true, not success and result or nil) + return success and (result == nil or result == true) +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 + 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 + end, + same = function(expected, actual, msg) + if vim.deep_equal(expected, actual) ~= true then + error(msg or string.format("Expected %s, got %s", vim.inspect(expected), vim.inspect(actual))) + end + end, + is_true = function(value, msg) + if value ~= true then + error(msg or string.format("Expected true, got %s", tostring(value))) + end + end, + is_false = function(value, msg) + if value ~= false then + error(msg or string.format("Expected false, got %s", tostring(value))) + end + end, + tbl_contains = function(tbl, value, msg) + local found = false + for _, v in ipairs(tbl) do + if v == value then + found = true + break + end + end + if not found then + error(msg or string.format("Table does not contain value %s", tostring(value))) + end + end, +} + +print("Safe Test Runner for raphael.nvim Configuration Features") +print("========================================================") + +-- Load the modules we need to test (without affecting cache) +local config_manager = require("raphael.config_manager") +local config = require("raphael.config") +local themes = require("raphael.themes") + +-- Run tests for configuration management functionality only +local total_passed = 0 +local total_tests = 0 + +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 } + }) + 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 + }) + 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 + leader = 456, -- should be string, but will be fixed + }) + assert.truthy(is_valid_fixable, "Should return true since validation fixes issues") + assert.truthy(error_msg_fixable == nil, "Should not return error message for fixable config") + print(" ✓ Config validation fixes issues instead of rejecting") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +end + +do + print("\nConfiguration section validation tests:") + + local results = config_manager.validate_config_sections({ + default_theme = "test-theme", + leader = "tt", + bookmark_group = true, + recent_group = true, + mappings = { picker = "p", next = ">" }, + filetype_themes = { lua = "test-theme" }, + project_themes = { ["/test/path"] = "test-theme" }, + profiles = { test = { default_theme = "test-theme" } }, + enable_autocmds = true, + enable_commands = true, + enable_keymaps = true, + enable_picker = true, + }) + + assert.truthy(type(results) == "table") + assert.truthy(results.default_theme == true) + assert.truthy(results.leader == true) + assert.truthy(results.bookmark_group == true) + print(" ✓ Section validation works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +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) + assert.truthy(vim.tbl_contains(diagnostics.unknown_keys, "unknown_key")) + print(" ✓ Config diagnostics work") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +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") + assert.equals("test-theme-save", imported_config.default_theme) + assert.equals("ts", imported_config.leader) + assert.falsy(imported_config.bookmark_group) + 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) + assert.truthy(presets.full_featured ~= nil) + assert.truthy(presets.presentation ~= nil) + 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" }, + state = { current_profile = nil }, + config = { default_theme = "original-theme" }, + } + + -- Define get_profile_config function that has access to the mock_core + 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) + print(" ✓ Preset application works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +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 + assert.truthy(type(installed) == "table", "Installed themes should be a table") + 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") + assert.truthy(validated.default_theme ~= nil, "Should have default theme") + print(" ✓ Default config validation works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 + + local user_config = { + default_theme = "test-theme", + leader = "tt", + } + local validated_user = config.validate(user_config) + assert.equals("test-theme", validated_user.default_theme) + assert.equals("tt", validated_user.leader) + print(" ✓ User config validation works") + total_tests = total_tests + 1 + total_passed = total_passed + 1 +end + +print_header(string.format("Final Results: %d/%d tests passed", total_passed, total_tests)) + +if total_passed == total_tests then + print("🎉 All tests passed! Configuration management features are working correctly.") + 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 diff --git a/tests/simple_test_runner.lua b/tests/simple_test_runner.lua new file mode 100644 index 0000000..5de360f --- /dev/null +++ b/tests/simple_test_runner.lua @@ -0,0 +1,179 @@ +#!/usr/bin/env lua + +-- simple_test_runner.lua +-- A simple test runner for raphael.nvim tests that doesn't require plenary.nvim + +local function print_header(text) + print("\n" .. string.rep("=", 60)) + print(text) + print(string.rep("=", 60)) +end + +local function print_result(test_name, passed, error_msg) + local status = passed and "✓ PASS" or "✗ FAIL" + print(string.format(" %s: %s", status, test_name)) + if error_msg then + print(" Error: " .. error_msg) + end +end + +local function run_test(test_name, test_func) + local success, result = pcall(test_func) + print_result(test_name, success and result == nil or result == true, not success and result or nil) + return success and (result == nil or result == true) +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 + 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 + +-- 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}) + 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) + end +end + +-- Load and run the test files +local function load_test_file(filepath) + local env = { + describe = describe, + 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 + 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 + end, + same = function(expected, actual, msg) + if vim then + if vim.deep_equal(expected, actual) ~= true then + error(msg or string.format("Expected %s, got %s", vim.inspect(expected), vim.inspect(actual))) + end + else + -- Fallback for basic comparison + if expected ~= actual then + error(msg or string.format("Expected %s, got %s", tostring(expected), tostring(actual))) + end + end + end, + is_true = function(value, msg) + if value ~= true then + error(msg or string.format("Expected true, got %s", tostring(value))) + end + end, + is_false = function(value, msg) + if value ~= false then + error(msg or string.format("Expected false, got %s", tostring(value))) + end + end, + tbl_contains = function(tbl, value, msg) + local found = false + for _, v in ipairs(tbl) do + if v == value then + found = true + break + end + end + if not found then + error(msg or string.format("Table does not contain value %s", tostring(value))) + end + end, + }, + type = type, + pcall = pcall, + os = os, + io = io, + string = string, + table = table, + pairs = pairs, + ipairs = ipairs, + next = next, + tostring = tostring, + tonumber = tonumber, + print = print, + require = require, + 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) + end +end + +-- Main execution +print("Simple Test Runner for raphael.nvim") +print("====================================") + +local test_files = { + "tests/core_test.lua", + "tests/config_manager_test.lua" +} + +local total_passed = 0 +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) + end +end + +print("\n" .. string.rep("=", 60)) +print("All tests completed!") +print(string.rep("=", 60)) \ No newline at end of file diff --git a/tests/test_constants.lua b/tests/test_constants.lua new file mode 100644 index 0000000..9600356 --- /dev/null +++ b/tests/test_constants.lua @@ -0,0 +1,56 @@ +-- test_constants.lua +-- Test-specific constants that use temporary files to avoid affecting real cache + +local M = {} + +local temp_dir = vim.fn.tempname() + +temp_dir = temp_dir:gsub("%.tmp.*$", "") +M.STATE_FILE = temp_dir .. "/raphael_test_state.json" + +local dir = vim.fn.fnamemodify(M.STATE_FILE, ":h") +if vim.fn.isdirectory(dir) == 0 then + vim.fn.mkdir(dir, "p") +end + +M.HISTORY_MAX_SIZE = 100 +M.RECENT_THEMES_MAX = 12 +M.MAX_BOOKMARKS = 50 + +M.ICON = { + HEADER = "Colorschemes", + RECENT_HEADER = "Recent", + BOOKMARKS_HEADER = "Bookmarks", + + BOOKMARK = "  ", + CURRENT_ON = "  ", + CURRENT_OFF = "  ", + WARN = " 󰝧 ", + + GROUP_EXPANDED = "  ", + GROUP_COLLAPSED = "  ", + + BLOCK = "  ", + SEARCH = "  ", + STATS = "  ", + + UNDO_ICON = "󰓕", + REDO_ICON = "󰓗", + HISTORY = "󰋚 ", +} + +M.NS = { + PICKER_CURSOR = vim.api.nvim_create_namespace("raphael_picker_cursor"), + PALETTE = vim.api.nvim_create_namespace("raphael_palette"), + SEARCH_MATCH = vim.api.nvim_create_namespace("raphael_search_match"), +} + +M.HL = { + PICKER_CURSOR = "Visual", + SEARCH_MATCH = "Search", +} + +M.FOOTER_HINTS = " apply • b bookmark • / search • s sort • q close" + +return M + diff --git a/test_performance.lua b/tests/test_performance.lua similarity index 100% rename from test_performance.lua rename to tests/test_performance.lua diff --git a/tests/test_runner.lua b/tests/test_runner.lua new file mode 100644 index 0000000..6dbc97e --- /dev/null +++ b/tests/test_runner.lua @@ -0,0 +1,86 @@ +-- tests/test_runner.lua +-- Simple test runner for raphael.nvim tests + +local M = {} + +-- Load and run the test file to validate syntax +local function run_test_file(test_file) + local success, err = pcall(function() + dofile(test_file) + end) + + if success then + print("✓ Test file syntax is valid: " .. test_file) + return true + else + print("✗ Test file has syntax errors: " .. test_file) + print(" Error: " .. tostring(err)) + return false + end +end + +-- Validate all test files +function M.validate_tests() + print("Validating raphael.nvim test files...\n") + + local test_files = { + "tests/raphael_spec.lua", + } + + local all_valid = true + for _, file in ipairs(test_files) do + if not run_test_file(file) then + all_valid = false + end + end + + print("\n" .. (all_valid and "All test files are syntactically valid!" or "Some test files have errors!")) + return all_valid +end + +-- Run basic functionality tests +function M.run_basic_tests() + print("\nRunning basic functionality tests...\n") + + -- Test that we can require all core modules + local modules_to_test = { + "raphael", + "raphael.core", + "raphael.themes", + "raphael.core.cache", + "raphael.core.palette_cache", + "raphael.picker.lazy_loader", + "raphael.utils", + "raphael.utils.debounce", + } + + for _, module in ipairs(modules_to_test) do + local success, err = pcall(require, module) + if success then + print("✓ Module loaded: " .. module) + else + print("✗ Module failed to load: " .. module .. " - " .. tostring(err)) + end + end + + -- Test basic functionality + print("\nTesting basic functionality...") + + local themes = require("raphael.themes") + themes.refresh() + print("✓ Theme discovery works") + + local cache = require("raphael.core.cache") + cache.read() + print("✓ Cache system works") + + require("raphael.core.palette_cache") + print("✓ Palette cache system works") + + require("raphael.picker.lazy_loader") + print("✓ Lazy loader system works") + + print("\nAll basic functionality tests passed!") +end + +return M diff --git a/tests/utils_test.lua b/tests/utils_test.lua new file mode 100644 index 0000000..72e3d12 --- /dev/null +++ b/tests/utils_test.lua @@ -0,0 +1,299 @@ +-- tests/utils_test.lua +-- +-- Unit tests for raphael.nvim utility functions + +local utils = require("raphael.utils") +local debounce_utils = require("raphael.utils.debounce") + +describe("raphael.nvim utils functionality", function() + describe("get_all_themes", function() + it("should return a list of themes", function() + local themes = utils.get_all_themes() + assert.truthy(type(themes) == "table") + -- Themes list can be empty if no themes are installed, so we just check type + end) + end) + + describe("get_configured_themes", function() + it("should handle empty theme_map", function() + local themes = utils.get_configured_themes({}) + assert.truthy(type(themes) == "table") + assert.equals(0, #themes) + end) + + it("should extract themes from theme_map", function() + local theme_map = { + group1 = { "theme1", "theme2" }, + single = "theme3", + } + local themes = utils.get_configured_themes(theme_map) + assert.truthy(type(themes) == "table") + assert.truthy(vim.tbl_contains(themes, "theme1")) + assert.truthy(vim.tbl_contains(themes, "theme2")) + assert.truthy(vim.tbl_contains(themes, "theme3")) + end) + end) + + describe("flatten_theme_map", function() + it("should flatten a simple theme_map", function() + local theme_map = { + group1 = { "theme1", "theme2" }, + single = "theme3", + } + local flattened = utils.flatten_theme_map(theme_map) + assert.truthy(type(flattened) == "table") + + -- Should have 4 items: 1 header + 2 themes + 1 single theme + assert.truthy(#flattened >= 3) -- At least 3 items + + -- Find the header + local header_found = false + for _, item in ipairs(flattened) do + if item.name == "group1" and item.is_header then + header_found = true + break + end + end + assert.truthy(header_found) + end) + end) + + describe("theme_exists", function() + it("should handle empty theme name", function() + local exists = utils.theme_exists("") + assert.falsy(exists) + end) + + it("should handle nil theme name", function() + local exists = utils.theme_exists(nil) + assert.falsy(exists) + end) + end) + + describe("safe_colorscheme", function() + it("should handle empty theme name", function() + local success, error_msg = utils.safe_colorscheme("") + assert.falsy(success) + assert.truthy(error_msg ~= nil) + end) + + it("should handle nil theme name", function() + local success, error_msg = utils.safe_colorscheme(nil) + assert.falsy(success) + assert.truthy(error_msg ~= nil) + end) + end) + + describe("fuzzy_score", function() + it("should score empty query as 1", function() + local score = utils.fuzzy_score("anything", "") + assert.equals(1, score) + end) + + it("should score exact match high", function() + local score = utils.fuzzy_score("test", "test") + assert.truthy(score >= 1000) + end) + + it("should score prefix match high", function() + local score = utils.fuzzy_score("testing", "test") + assert.truthy(score >= 500) + end) + + it("should score substring match", function() + local score = utils.fuzzy_score("testing", "est") + assert.truthy(score >= 250) + end) + + it("should score no match as 0", function() + local score = utils.fuzzy_score("test", "xyz") + assert.equals(0, score) + end) + end) + + describe("fuzzy_filter", function() + it("should handle empty query", function() + local items = { { name = "test1" }, { name = "test2" } } + local filtered = utils.fuzzy_filter(items, "") + assert.equals(#items, #filtered) + end) + + it("should filter and sort items", function() + local items = { + { name = "zeta" }, + { name = "alpha" }, + { name = "beta" }, + } + local filtered = utils.fuzzy_filter(items, "al") + assert.truthy(type(filtered) == "table") + -- Should contain items that match the query + if #filtered > 0 then + assert.truthy(filtered[1].name:lower():find("al", 1, true) ~= nil) + end + end) + end) + + describe("deep_copy", function() + it("should copy simple table", function() + local original = { a = 1, b = 2 } + local copy = utils.deep_copy(original) + assert.equals(original.a, copy.a) + assert.equals(original.b, copy.b) + assert.not_equals(original, copy) -- Different objects + end) + + it("should copy nested table", function() + local original = { a = { b = { c = 1 } } } + local copy = utils.deep_copy(original) + assert.equals(original.a.b.c, copy.a.b.c) + assert.not_equals(original.a, copy.a) -- Different nested objects + end) + + it("should copy non-table values directly", function() + local original = "string" + local copy = utils.deep_copy(original) + assert.equals(original, copy) + end) + end) + + describe("tbl_contains", function() + it("should find existing value", function() + local tbl = { "a", "b", "c" } + assert.truthy(utils.tbl_contains(tbl, "b")) + end) + + it("should not find non-existing value", function() + local tbl = { "a", "b", "c" } + assert.falsy(utils.tbl_contains(tbl, "d")) + end) + end) + + describe("random_theme", function() + it("should return nil for empty list", function() + local theme = utils.random_theme({}) + assert.is_nil(theme) + end) + + it("should return theme from single-item list", function() + local theme = utils.random_theme({ "only-theme" }) + assert.equals("only-theme", theme) + end) + + it("should return theme from multi-item list", function() + local themes = { "theme1", "theme2", "theme3" } + local theme = utils.random_theme(themes) + assert.truthy(theme ~= nil) + assert.truthy(vim.tbl_contains(themes, theme)) + end) + end) + + describe("clamp", function() + it("should clamp value below min", function() + local result = utils.clamp(0, 5, 10) + assert.equals(5, result) + end) + + it("should clamp value above max", function() + local result = utils.clamp(15, 5, 10) + assert.equals(10, result) + end) + + it("should not clamp value in range", function() + local result = utils.clamp(7, 5, 10) + assert.equals(7, result) + end) + end) + + describe("get_picker_dimensions", function() + it("should calculate dimensions", function() + local dims = utils.get_picker_dimensions(0.5, 0.5) + assert.truthy(type(dims) == "table") + assert.truthy(type(dims.width) == "number") + assert.truthy(type(dims.height) == "number") + assert.truthy(type(dims.row) == "number") + assert.truthy(type(dims.col) == "number") + end) + end) + + describe("truncate", function() + it("should not truncate short string", function() + local result = utils.truncate("short", 10) + assert.equals("short", result) + end) + + it("should truncate long string", function() + local result = utils.truncate("very long string", 5) + assert.equals("very…", result) + end) + end) + + describe("pad_right", function() + it("should pad short string", function() + local result = utils.pad_right("hi", 5) + assert.equals("hi ", result) + end) + + it("should not pad long string", function() + local result = utils.pad_right("hello", 3) + assert.equals("hello", result) + end) + end) + + describe("notify", function() + it("should handle basic notification", function() + -- Just test that it doesn't error + local success = pcall(utils.notify, "test message", vim.log.levels.INFO) + assert.truthy(success) + end) + end) +end) + +describe("raphael.nvim debounce utilities", function() + describe("debounce function", function() + it("should create a debounced function", function() + local call_count = 0 + local test_fn = function() + call_count = call_count + 1 + end + + local debounced_fn = debounce_utils.debounce(test_fn, 10) -- 10ms delay + + -- Call multiple times quickly + for _ = 1, 5 do + debounced_fn() + end + + -- Wait for debounce to complete + vim.wait(50, function() + return call_count >= 1 + end, 100) + + -- Should have been called at least once + assert.truthy(call_count >= 1) + end) + end) + + describe("throttle function", function() + it("should create a throttled function", function() + local call_count = 0 + local test_fn = function() + call_count = call_count + 1 + end + + local throttled_fn = debounce_utils.throttle(test_fn, 10) -- 10ms delay + + -- Call multiple times quickly + for _ = 1, 5 do + throttled_fn() + end + + -- Wait for throttle to complete + vim.wait(50, function() + return call_count >= 1 + end, 100) + + -- Should have been called at least once + assert.truthy(call_count >= 1) + end) + end) +end)