Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,37 @@ return {

---

## Picker internals (module paths)
## Debug & Troubleshooting

### Debug Commands

| Command | Description |
|---------|-------------|
| `:RaphaelDebug` or `:RaphaelDebug status` | Show diagnostics |
| `:RaphaelDebug repair` | Fix corrupted state |
| `:RaphaelDebug backup` | Create backup |
| `:RaphaelDebug restore` | Restore from backup |
| `:RaphaelDebug export` | Export state to file |
| `:RaphaelDebug clear` | Reset all state |
| `:RaphaelDebug stats` | Show statistics |

### State File

Located at `stdpath("data")/raphael/state.json`:
- `current`, `saved`, `previous` themes
- `bookmarks`, `history`, `quick_slots`
- `undo_history` stack

Backup saved to `state.json.backup`.

### Corrupted State?

1. Run `:RaphaelDebug status` to check
2. Run `:RaphaelDebug repair` to fix
3. If still broken: `:RaphaelDebug restore` from backup
4. Last resort: `:RaphaelDebug clear` to reset

### Picker Debug

```vim
:lua require("raphael.picker.ui").toggle_debug()
Expand Down
2 changes: 1 addition & 1 deletion lua/raphael/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ M.defaults = {
random = "r",
},

default_theme = "kanagawa-paper-ink",
default_theme = "habamax",

bookmark_group = true,
recent_group = true,
Expand Down
6 changes: 3 additions & 3 deletions lua/raphael/config_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ function M.validate_config_sections(config_data)

if type(config_data.mappings) == "table" then
results.mappings = true
for key, val in pairs(config_data.mappings) do
for _, val in pairs(config_data.mappings) do
if type(val) ~= "string" then
results.mappings = false
break
Expand Down Expand Up @@ -230,7 +230,7 @@ function M.get_config_diagnostics(config_data)
missing_defaults = {},
}

for k, _ in pairs(config_data) do
for _ in pairs(config_data) do
diagnostics.total_keys = diagnostics.total_keys + 1
end

Expand All @@ -240,7 +240,7 @@ function M.get_config_diagnostics(config_data)
end
end

for key, default_val in pairs(config.defaults) do
for key in pairs(config.defaults) do
if config_data[key] == nil then
table.insert(diagnostics.missing_defaults, key)
end
Expand Down
18 changes: 2 additions & 16 deletions lua/raphael/core/autocmds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -243,21 +243,13 @@ function M.picker_cursor_autocmd(picker_buf, cbs)
local update_preview = cbs.update_preview
local ctx = cbs.ctx or {}

local initial_setup_phase = true

local debounce_utils = require("raphael.utils.debounce")
local debounced_preview = debounce_utils.debounce(function(theme)
if theme and type(preview_fn) == "function" then
local should_preview = not initial_setup_phase
if ctx.initial_render ~= nil then
should_preview = should_preview and not ctx.initial_render
end

if should_preview then
preview_fn(theme)
else
if ctx.picker_ready ~= true then
return
end
preview_fn(theme)
end
end, 100)

Expand Down Expand Up @@ -311,7 +303,6 @@ function M.picker_cursor_autocmd(picker_buf, cbs)
theme = parse(line)
end
if theme then
-- The preview is handled by the debounced_preview function which checks initial_setup_phase
debounced_preview(theme)
end
end
Expand All @@ -321,11 +312,6 @@ function M.picker_cursor_autocmd(picker_buf, cbs)
debounced_update_preview({ debounced = true })
end,
})

-- Set a timer to disable the initial setup phase after a delay
vim.defer_fn(function()
initial_setup_phase = false
end, 300) -- 300ms to ensure full setup completion
end

--- Attach a BufDelete autocmd to the picker buffer.
Expand Down
132 changes: 130 additions & 2 deletions lua/raphael/core/cache.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
--- - Every helper reads the JSON, mutates, and writes it back.
--- - The in-memory "authoritative" state lives in raphael.core; this
--- module is the disk-backed source of truth.
---
--- Features:
--- - Automatic backup before writes
--- - State validation and auto-repair on read
--- - Graceful handling of corrupted state

local constants = require("raphael.constants")

Expand All @@ -14,6 +19,7 @@ local M = {}
local uv = vim.loop

local decode_failed_once = false
local auto_repair_enabled = true

local function ensure_dir()
local dir = vim.fn.fnamemodify(constants.STATE_FILE, ":h")
Expand All @@ -22,6 +28,39 @@ local function ensure_dir()
end
end

local function get_backup_path()
return constants.STATE_FILE .. ".backup"
end

local function create_backup()
local file = io.open(constants.STATE_FILE, "r")
if not file then
return false
end
local content = file:read("*a")
file:close()

local backup_path = get_backup_path()
local backup_file = io.open(backup_path, "w")
if not backup_file then
return false
end
backup_file:write(content)
backup_file:close()
return true
end

local function restore_backup()
local backup_path = get_backup_path()
local file = io.open(backup_path, "r")
if not file then
return nil, "No backup file"
end
local content = file:read("*a")
file:close()
return content, nil
end

--- Default state structure (pure, no side effects).
---
--- @return table state
Expand Down Expand Up @@ -143,12 +182,15 @@ end
--- Async write helper.
---
--- Writes `data` to `path` asynchronously using libuv, and notifies on error.
--- Creates a backup before writing.
---
--- @param path string Absolute path to state file
--- @param data string JSON-encoded string
local function async_write(path, data)
ensure_dir()

create_backup()

uv.fs_open(path, "w", 438, function(open_err, fd)
if open_err or not fd then
vim.schedule(function()
Expand All @@ -171,6 +213,8 @@ end
--- Read state from disk (or return defaults if file doesn't exist / is invalid).
---
--- This is the canonical entry for reading the on-disk state.
--- If the file is corrupted, attempts to restore from backup.
--- Validates and auto-repairs state if needed.
---
--- @return table state
function M.read()
Expand All @@ -189,13 +233,42 @@ function M.read()
local ok, decoded = pcall(vim.json.decode, content)
if not ok then
if not decode_failed_once then
vim.notify("raphael.nvim: Failed to decode state file, using defaults", vim.log.levels.WARN)
vim.notify("raphael.nvim: State file corrupted, attempting backup restore...", vim.log.levels.WARN)
decode_failed_once = true
end

local backup_content, backup_err = restore_backup()
if backup_content then
local backup_ok, backup_decoded = pcall(vim.json.decode, backup_content)
if backup_ok then
vim.notify("raphael.nvim: Restored from backup successfully", vim.log.levels.INFO)
return normalize_state(backup_decoded)
end
end

vim.notify(
"raphael.nvim: Backup restore failed (" .. tostring(backup_err) .. "), using defaults",
vim.log.levels.WARN
)
return default_state()
end

return normalize_state(decoded)
local state = normalize_state(decoded)

if auto_repair_enabled then
local ok_debug, debug_mod = pcall(require, "raphael.debug")
if ok_debug and debug_mod then
local issues = debug_mod.validate_state(state)
if #issues > 0 then
state = debug_mod.repair_state(state)
vim.schedule(function()
M.write(state)
end)
end
end
end

return state
end

--- Write full state to disk (async).
Expand Down Expand Up @@ -663,4 +736,59 @@ function M.redo_pop()
return undo.stack[undo.index]
end

--- Get path to backup file.
---
--- @return string
function M.get_backup_path()
return get_backup_path()
end

--- Manually create a backup of current state.
---
--- @return boolean success
function M.create_backup()
return create_backup()
end

--- Restore state from backup file.
---
--- @return boolean success
--- @return string|nil error
function M.restore_from_backup()
local content, err = restore_backup()
if not content then
return false, err
end

local ok, decoded = pcall(vim.json.decode, content)
if not ok then
return false, "Backup file is corrupted"
end

local state = normalize_state(decoded)
M.write(state)
return true, nil
end

--- Check if state file exists.
---
--- @return boolean
function M.state_exists()
return vim.fn.filereadable(constants.STATE_FILE) == 1
end

--- Check if backup file exists.
---
--- @return boolean
function M.backup_exists()
return vim.fn.filereadable(get_backup_path()) == 1
end

--- Get state file path.
---
--- @return string
function M.get_state_path()
return constants.STATE_FILE
end

return M
63 changes: 62 additions & 1 deletion lua/raphael/core/cmds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ function M.setup(core)
end, { desc = "Apply a random theme" })

vim.api.nvim_create_user_command("RaphaelBookmarkToggle", function()
if not core.picker or not core.picker.picker_win or not vim.api.nvim_win_is_valid(core.picker.picker_win) then
if not picker.is_open() then
vim.notify("Picker not open – opening it first…", vim.log.levels.INFO)
core.open_picker({ only_configured = true })
vim.defer_fn(function()
Expand Down Expand Up @@ -601,6 +601,67 @@ function M.setup(core)
end,
desc = "Apply a Raphael configuration preset (:RaphaelConfigPreset [preset_name])",
})

vim.api.nvim_create_user_command("RaphaelDebug", function(opts)
local ok, debug_mod = pcall(require, "raphael.debug")
if not ok then
vim.notify("raphael: debug module not available", vim.log.levels.ERROR)
return
end

local subcmd = vim.trim(opts.args or "")
if subcmd == "" or subcmd == "status" then
debug_mod.show_diagnostics()
elseif subcmd == "repair" then
debug_mod.repair_and_save()
elseif subcmd == "backup" then
local success, err = debug_mod.backup_state()
if success then
vim.notify("raphael: backup created at " .. debug_mod.get_backup_path(), vim.log.levels.INFO)
else
vim.notify("raphael: backup failed: " .. tostring(err), vim.log.levels.ERROR)
end
elseif subcmd == "restore" then
local cache = require("raphael.core.cache")
local success, err = cache.restore_from_backup()
if success then
vim.notify("raphael: restored from backup", vim.log.levels.INFO)
else
vim.notify("raphael: restore failed: " .. tostring(err), vim.log.levels.ERROR)
end
elseif subcmd == "export" then
local success, path = debug_mod.export_state()
if not success then
vim.notify("raphael: export failed: " .. tostring(path), vim.log.levels.ERROR)
end
elseif subcmd == "clear" then
local cache = require("raphael.core.cache")
cache.clear()
vim.notify("raphael: state cleared", vim.log.levels.INFO)
elseif subcmd == "stats" then
local stats = debug_mod.get_stats()
vim.notify(vim.inspect(stats), vim.log.levels.INFO)
else
vim.notify("raphael: unknown debug command: " .. subcmd, vim.log.levels.WARN)
vim.notify("Available: status, repair, backup, restore, export, clear, stats", vim.log.levels.INFO)
end
end, {
nargs = "?",
complete = function(ArgLead)
local cmds = { "status", "repair", "backup", "restore", "export", "clear", "stats" }
if not ArgLead or ArgLead == "" then
return cmds
end
local res = {}
for _, c in ipairs(cmds) do
if c:find(ArgLead:lower(), 1, true) then
table.insert(res, c)
end
end
return res
end,
desc = "Raphael debug commands: status, repair, backup, restore, export, clear, stats",
})
end

return M
Loading
Loading