diff --git a/bin/core-post-tool.sh b/bin/core-post-tool.sh index 2bf434e..6c407ce 100755 --- a/bin/core-post-tool.sh +++ b/bin/core-post-tool.sh @@ -22,6 +22,19 @@ TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || true)" source "$SCRIPT_DIR/nvim-socket.sh" "$CWD" 2>/dev/null source "$SCRIPT_DIR/nvim-send.sh" +# Set up logging — query debug config from nvim +log_post() { :; } +if [[ -n "${NVIM_SOCKET:-}" ]]; then + _POST_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or ''})\")" 2>/dev/null || echo '{}') + _POST_DEBUG=$(echo "$_POST_CTX" | jq -r '.debug // false') + _POST_LOG_FILE=$(echo "$_POST_CTX" | jq -r '.log_file // ""') + if [[ "$_POST_DEBUG" == "true" && -n "$_POST_LOG_FILE" ]]; then + log_post() { printf '[%s] [INFO] core-post-tool.sh: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$_POST_LOG_FILE"; } + fi +fi + +log_post "tool=$TOOL_NAME" + # For Bash tool (rm detection), only clear deletion markers — don't touch edit markers or diff tab if [[ "$TOOL_NAME" == "Bash" ]]; then nvim_send "require('code-preview.changes').clear_by_status('deleted')" || true @@ -36,6 +49,7 @@ FILE_PATH_ESC="$(escape_lua "${FILE_PATH:-}")" # Tell Lua to handle this file's close — tolerates out-of-order post-hooks # (OpenCode may fire them in a different order than pre-hooks). if [[ -n "$FILE_PATH" ]]; then + log_post "closing diff for file=$FILE_PATH" nvim_send "require('code-preview.diff').close_for_file('$FILE_PATH_ESC')" || true # neo_tree.refresh() is handled inside close_for_file() via vim.schedule() fi diff --git a/bin/core-pre-tool.sh b/bin/core-pre-tool.sh index 58c47bd..c4531e4 100755 --- a/bin/core-pre-tool.sh +++ b/bin/core-pre-tool.sh @@ -31,6 +31,19 @@ if [[ -z "${NVIM_SOCKET:-}" ]]; then HAS_NVIM=false fi +# Set up logging early so all code paths can use it +log_pre() { :; } +if [[ "$HAS_NVIM" == "true" ]]; then + _PRE_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or ''})\")" 2>/dev/null || echo '{}') + _PRE_DEBUG=$(echo "$_PRE_CTX" | jq -r '.debug // false') + _PRE_LOG_FILE=$(echo "$_PRE_CTX" | jq -r '.log_file // ""') + if [[ "$_PRE_DEBUG" == "true" && -n "$_PRE_LOG_FILE" ]]; then + log_pre() { printf '[%s] [INFO] core-pre-tool.sh: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$_PRE_LOG_FILE"; } + fi +fi + +log_pre "tool=$TOOL_NAME has_nvim=$HAS_NVIM" + TMPDIR="${TMPDIR:-/tmp}" # Use unique temp files per hook invocation so rapid-fire pre-hooks # (OpenCode fires all before-hooks before any after-hooks) don't clobber @@ -164,14 +177,18 @@ if [[ "$HAS_NVIM" == "true" ]]; then FILE_VISIBLE=$(echo "$HOOK_CTX" | jq -r '.file_visible // false') DEFER_PERMISSIONS=$(echo "$HOOK_CTX" | jq -r 'if .defer_claude_permissions == true then "true" else "false" end') + log_pre "file=$FILE_PATH visible_only=$VISIBLE_ONLY file_visible=$FILE_VISIBLE" + # Decide whether to show the diff — skip nvim UI entirely when visible_only # is on and the file isn't in any visible window. SHOULD_SHOW="1" if [[ "$VISIBLE_ONLY" == "true" && "$FILE_VISIBLE" != "true" ]]; then SHOULD_SHOW="0" + log_pre "skipping diff: visible_only=true, file not visible" fi if [[ "$SHOULD_SHOW" == "1" ]]; then + log_pre "sending diff to nvim (layout via config)" nvim_send "require('code-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC', '$FILE_PATH_ESC')" || true fi fi diff --git a/lua/code-preview/diff.lua b/lua/code-preview/diff.lua index 5d2fa50..ea823a6 100644 --- a/lua/code-preview/diff.lua +++ b/lua/code-preview/diff.lua @@ -1,5 +1,7 @@ local M = {} +local log = require("code-preview.log") + -- Active diffs keyed by absolute file path. -- Each entry: { tab, bufs, augroup, inline_win } local active_diffs = {} @@ -32,6 +34,7 @@ local function mark_change_and_reveal(abs_file_path) end local status = vim.loop.fs_stat(abs_file_path) and "modified" or "created" + log.debug(log.fmt("mark_change_and_reveal: %s → %s", abs_file_path, status)) pcall(function() require("code-preview.changes").set(abs_file_path, status) end) pcall(function() require("code-preview.neo_tree").refresh() end) @@ -371,16 +374,21 @@ end function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path) local file_key = abs_file_path or real_file_path + local cfg = require("code-preview").config + log.info(log.fmt("show_diff: file=%s layout=%s active=%d", + file_key or "nil", + (cfg.diff and cfg.diff.layout) or "tab", + active_count())) + -- If a diff for this SAME file is already open, close it first (re-edit) if file_key and active_diffs[file_key] then + log.debug(log.fmt("show_diff: re-edit detected, closing existing diff for %s", file_key)) M.close_for_file(file_key) end -- Set the neo-tree indicator + reveal mark_change_and_reveal(abs_file_path) - local cfg = require("code-preview").config - -- Inline layout if cfg.diff.layout == "inline" then local result = show_inline_diff(original_path, proposed_path, real_file_path, cfg) @@ -478,9 +486,12 @@ end function M.close_for_file(file_path) local entry = active_diffs[file_path] if not entry then + log.debug(log.fmt("close_for_file: no active diff for %s, skipping", file_path)) return end + log.info(log.fmt("close_for_file: closing diff for %s (remaining=%d)", file_path, active_count() - 1)) + -- Clear neo-tree indicator (refresh is deferred until after the tab is closed -- to avoid neo-tree walking a stale tabpage id) pcall(function() require("code-preview.changes").clear(file_path) end) @@ -545,6 +556,7 @@ end -- Close ALL diffs and clear neo-tree indicators (for manual close via dq) function M.close_diff_and_clear() + log.info(log.fmt("close_diff_and_clear: closing all diffs (count=%d)", active_count())) -- Collect keys first to avoid modifying table during iteration local files = {} for file_path, _ in pairs(active_diffs) do diff --git a/lua/code-preview/init.lua b/lua/code-preview/init.lua index 3edf21e..55c1352 100644 --- a/lua/code-preview/init.lua +++ b/lua/code-preview/init.lua @@ -4,6 +4,7 @@ local M = {} M.config = {} local default_config = { + debug = false, -- enable debug logging to stdpath("log")/code-preview.log diff = { layout = "tab", -- "tab", "vsplit", or "inline" labels = { current = "CURRENT", proposed = "PROPOSED" }, @@ -80,6 +81,9 @@ end function M.setup(user_config) M.config = deep_merge(default_config, user_config or {}) + -- Initialise logging + require("code-preview.log").init({ debug = M.config.debug }) + -- ── New commands ────────────────────────────────────────────── vim.api.nvim_create_user_command("CodePreviewInstallClaudeCodeHooks", function() @@ -162,12 +166,16 @@ function M.hook_context(file_path) end end + local log = require("code-preview.log") + return vim.json.encode({ neo_tree_reveal = neo_tree_reveal, reveal_root = reveal_root, visible_only = visible_only, file_visible = file_visible, defer_claude_permissions = defer_claude_permissions, + debug = log.is_enabled(), + log_file = log.get_log_path() or "", }) end diff --git a/lua/code-preview/log.lua b/lua/code-preview/log.lua new file mode 100644 index 0000000..439f38c --- /dev/null +++ b/lua/code-preview/log.lua @@ -0,0 +1,70 @@ +--- code-preview.nvim — Logging module +--- +--- Opt-in debug logging following Neovim plugin conventions. +--- - WARN/ERROR: shown to user via vim.notify() +--- - DEBUG/INFO: written to log file only (when enabled) +--- - Log file: vim.fn.stdpath("log") .. "/code-preview.log" + +local M = {} + +local log_file_path = nil +local enabled = false + +--- Initialise logging. Called once from setup(). +--- @param opts { debug: boolean } +function M.init(opts) + enabled = opts and opts.debug or false + if enabled then + log_file_path = vim.fn.stdpath("log") .. "/code-preview.log" + end +end + +--- Write a line to the log file. No-op when debug is disabled. +--- @param level string "DEBUG"|"INFO"|"WARN"|"ERROR" +--- @param msg string +local function write(level, msg) + if not enabled or not log_file_path then + return + end + local f = io.open(log_file_path, "a") + if not f then + return + end + f:write(string.format("[%s] [%s] %s\n", os.date("%Y-%m-%d %H:%M:%S"), level, msg)) + f:close() +end + +function M.debug(msg) write("DEBUG", msg) end +function M.info(msg) write("INFO", msg) end + +function M.warn(msg) + write("WARN", msg) + vim.notify("[code-preview] " .. msg, vim.log.levels.WARN) +end + +function M.error(msg) + write("ERROR", msg) + vim.notify("[code-preview] " .. msg, vim.log.levels.ERROR) +end + +--- Format helper for structured messages. +--- @param template string format string +--- @param ... any format arguments +--- @return string +function M.fmt(template, ...) + return string.format(template, ...) +end + +--- Check whether debug logging is enabled. +--- @return boolean +function M.is_enabled() + return enabled +end + +--- Return the log file path (for shell scripts via hook_context). +--- @return string|nil +function M.get_log_path() + return log_file_path +end + +return M diff --git a/lua/code-preview/neo_tree.lua b/lua/code-preview/neo_tree.lua index 9374509..774dc5e 100644 --- a/lua/code-preview/neo_tree.lua +++ b/lua/code-preview/neo_tree.lua @@ -1,6 +1,7 @@ local M = {} local changes = require("code-preview.changes") +local log = require("code-preview.log") -- Guard: all neo-tree interaction goes through pcall local has_neo_tree = false @@ -260,6 +261,7 @@ local function inject_virtual_nodes(state, pending) pcall(function() state.tree:add_node(file_node, parent_path) virtual_nodes[filepath] = true + log.debug(log.fmt("neo_tree: injected virtual node for %s", filepath)) end) changed = true @@ -276,10 +278,12 @@ function M.setup(cfg) local ok, neo_tree_events = pcall(require, "neo-tree.events") if not ok then + log.debug("neo_tree.setup: neo-tree not found, skipping integration") return end has_neo_tree = true setup_done = true + log.info("neo_tree.setup: neo-tree integration enabled") local symbols = cfg.neo_tree.symbols local highlights = cfg.neo_tree.highlights @@ -340,6 +344,7 @@ function M.refresh() if not has_neo_tree then return end + log.debug("neo_tree.refresh: triggering filesystem refresh") pcall(function() require("neo-tree.sources.manager").refresh("filesystem") end) @@ -349,6 +354,7 @@ function M.reveal(filepath, dir) if not has_neo_tree then return end + log.debug(log.fmt("neo_tree.reveal: %s (dir=%s)", filepath, dir or "nil")) pcall(function() local cfg = require("code-preview").config local position = cfg.neo_tree.position or "right"