From cb524a6c141de96745246b3cc0f47be3bcb6d042 Mon Sep 17 00:00:00 2001 From: zolinthecow Date: Wed, 9 Jul 2025 16:57:05 -0700 Subject: [PATCH] claude-pre-edit-1752105425 --- .gitignore | 3 + LICENSE.md | 4 + lua/colinzhao/lazy/claude.lua | 1 + lua/nvim-claude/diff-review.lua | 186 ++++++++++++++++++++ lua/nvim-claude/hooks.lua | 292 ++++++++++++++++++++++++++++++++ lua/nvim-claude/init.lua | 5 + lua/nvim-claude/utils.lua | 34 ++++ 7 files changed, 525 insertions(+) create mode 100644 lua/nvim-claude/diff-review.lua create mode 100644 lua/nvim-claude/hooks.lua diff --git a/.gitignore b/.gitignore index cfa411de..c9f810d4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ lazy-lock.json # Development tracking .agent-work + +# Claude Code hooks +.claude/ diff --git a/LICENSE.md b/LICENSE.md index 9cf10627..5a5de52a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -17,3 +17,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This line was added by Claude! +Testing hooks - this should trigger pre and post hooks! +Hooks are now properly installed! diff --git a/lua/colinzhao/lazy/claude.lua b/lua/colinzhao/lazy/claude.lua index c5eff464..9009c66b 100644 --- a/lua/colinzhao/lazy/claude.lua +++ b/lua/colinzhao/lazy/claude.lua @@ -9,5 +9,6 @@ return { dependencies = { 'nvim-telescope/telescope.nvim', -- For agent picker 'tpope/vim-fugitive', -- Already installed, for diffs + 'sindrets/diffview.nvim', -- For advanced diff viewing }, } \ No newline at end of file diff --git a/lua/nvim-claude/diff-review.lua b/lua/nvim-claude/diff-review.lua new file mode 100644 index 00000000..e2b82560 --- /dev/null +++ b/lua/nvim-claude/diff-review.lua @@ -0,0 +1,186 @@ +-- Diff review system for nvim-claude using diffview.nvim +local M = {} + +-- State tracking +M.current_review = nil + +function M.setup() + -- Set up keybindings + M.setup_keybindings() + + vim.notify('Diff review system loaded (using diffview.nvim)', vim.log.levels.DEBUG) +end + +-- Handle Claude edit completion +function M.handle_claude_edit(stash_ref, pre_edit_ref) + if not stash_ref then + vim.notify('No stash reference provided for diff review', vim.log.levels.ERROR) + return + end + + vim.notify('Processing Claude edit with stash: ' .. stash_ref, vim.log.levels.INFO) + + -- Get list of changed files + local changed_files = M.get_changed_files(stash_ref) + if not changed_files or #changed_files == 0 then + vim.notify('No changes detected from Claude edit', vim.log.levels.INFO) + return + end + + -- Initialize review session + M.current_review = { + stash_ref = stash_ref, + pre_edit_ref = pre_edit_ref, -- Store the pre-edit commit reference + timestamp = os.time(), + changed_files = changed_files, + } + + -- Notify user about changes + vim.notify(string.format( + 'Claude made changes to %d file(s): %s', + #changed_files, + table.concat(changed_files, ', ') + ), vim.log.levels.INFO) + + vim.notify('Use dd to open diffview, df for fugitive, dc to clear review', vim.log.levels.INFO) + + -- Automatically open diffview + M.open_diffview() +end + +-- Get list of files changed in the stash +function M.get_changed_files(stash_ref) + local utils = require('nvim-claude.utils') + local cmd = string.format('git stash show %s --name-only', stash_ref) + local result = utils.exec(cmd) + + if not result or result == '' then + return {} + end + + local files = {} + for line in result:gmatch('[^\n]+') do + if line ~= '' then + table.insert(files, line) + end + end + return files +end + +-- Set up keybindings for diff review +function M.setup_keybindings() + -- Review actions + vim.keymap.set('n', 'dd', M.open_diffview, { desc = 'Open Claude diff in diffview' }) + vim.keymap.set('n', 'df', M.open_fugitive, { desc = 'Open Claude diff in fugitive' }) + vim.keymap.set('n', 'dc', M.clear_review, { desc = 'Clear Claude review session' }) + vim.keymap.set('n', 'dl', M.list_changes, { desc = 'List Claude changed files' }) +end + +-- Open diffview for current review +function M.open_diffview() + if not M.current_review then + vim.notify('No active review session', vim.log.levels.INFO) + return + end + + -- Check if diffview is available + local ok, diffview = pcall(require, 'diffview') + if not ok then + vim.notify('diffview.nvim not available, falling back to fugitive', vim.log.levels.WARN) + M.open_fugitive() + return + end + + -- Use the pre-edit reference if available + if M.current_review.pre_edit_ref then + local cmd = 'DiffviewOpen ' .. M.current_review.pre_edit_ref + vim.notify('Opening diffview with pre-edit commit: ' .. cmd, vim.log.levels.INFO) + vim.cmd(cmd) + else + -- Fallback to comparing stash with its parent + vim.notify('No pre-edit commit found, falling back to stash comparison', vim.log.levels.WARN) + local cmd = string.format('DiffviewOpen %s^..%s', M.current_review.stash_ref, M.current_review.stash_ref) + vim.notify('Opening diffview: ' .. cmd, vim.log.levels.INFO) + vim.cmd(cmd) + end +end + +-- Open fugitive diff (fallback) +function M.open_fugitive() + if not M.current_review then + vim.notify('No active review session', vim.log.levels.INFO) + return + end + + -- Use fugitive to show diff + local cmd = 'Gdiffsplit ' .. M.current_review.stash_ref + vim.notify('Opening fugitive: ' .. cmd, vim.log.levels.INFO) + vim.cmd(cmd) +end + +-- List changed files +function M.list_changes() + if not M.current_review then + vim.notify('No active review session', vim.log.levels.INFO) + return + end + + local files = M.current_review.changed_files + if #files == 0 then + vim.notify('No changes found', vim.log.levels.INFO) + return + end + + -- Create a telescope picker if available, otherwise just notify + local ok, telescope = pcall(require, 'telescope.pickers') + if ok then + M.telescope_changed_files() + else + vim.notify('Changed files:', vim.log.levels.INFO) + for i, file in ipairs(files) do + vim.notify(string.format(' %d. %s', i, file), vim.log.levels.INFO) + end + end +end + +-- Telescope picker for changed files +function M.telescope_changed_files() + local pickers = require('telescope.pickers') + local finders = require('telescope.finders') + local conf = require('telescope.config').values + + pickers.new({}, { + prompt_title = 'Claude Changed Files', + finder = finders.new_table({ + results = M.current_review.changed_files, + }), + sorter = conf.generic_sorter({}), + attach_mappings = function(_, map) + map('i', '', function(prompt_bufnr) + local selection = require('telescope.actions.state').get_selected_entry() + require('telescope.actions').close(prompt_bufnr) + vim.cmd('edit ' .. selection[1]) + M.open_diffview() + end) + return true + end, + }):find() +end + +-- Clear review session +function M.clear_review() + if M.current_review then + M.current_review = nil + + -- Close diffview if it's open + pcall(function() + vim.cmd('DiffviewClose') + end) + + vim.notify('Claude review session cleared', vim.log.levels.INFO) + else + vim.notify('No active Claude review session', vim.log.levels.INFO) + end +end + +return M \ No newline at end of file diff --git a/lua/nvim-claude/hooks.lua b/lua/nvim-claude/hooks.lua new file mode 100644 index 00000000..954ea7b8 --- /dev/null +++ b/lua/nvim-claude/hooks.lua @@ -0,0 +1,292 @@ +-- Claude Code hooks integration for nvim-claude +local M = {} + +-- Track hook state +M.pre_edit_commit = nil + +function M.setup() + vim.notify('Hooks module loaded', vim.log.levels.DEBUG) +end + +-- Pre-tool-use hook: Create a commit snapshot of the current state +function M.pre_tool_use_hook() + local utils = require('nvim-claude.utils') + + -- Check if we're in a git repository + local git_root = utils.get_project_root() + if not git_root then + vim.notify('Not in a git repository', vim.log.levels.WARN) + return + end + + -- Create timestamp for snapshot + local timestamp = os.time() + local commit_msg = string.format('claude-pre-edit-%d', timestamp) + + -- Stage all current changes (including untracked files) + local add_cmd = string.format('cd "%s" && git add -A', git_root) + local add_result, add_err = utils.exec(add_cmd) + + if add_err then + vim.notify('Failed to stage changes: ' .. add_err, vim.log.levels.ERROR) + return + end + + -- Create a temporary commit + local commit_cmd = string.format('cd "%s" && git commit -m "%s" --allow-empty', git_root, commit_msg) + local commit_result, commit_err = utils.exec(commit_cmd) + + if commit_err and not commit_err:match('nothing to commit') then + vim.notify('Failed to create snapshot commit: ' .. commit_err, vim.log.levels.ERROR) + return + end + + -- Store the commit reference + M.pre_edit_commit = 'HEAD' + + -- Also store in temp file for diff review to access + local temp_file = '/tmp/claude-pre-edit-commit' + utils.write_file(temp_file, 'HEAD') + + vim.notify('Pre-Claude snapshot created: ' .. commit_msg, vim.log.levels.INFO) +end + +-- Post-tool-use hook: Create stash of Claude's changes and trigger diff review +function M.post_tool_use_hook() + vim.notify('Post-tool-use hook triggered', vim.log.levels.INFO) + + -- Use vim.schedule to ensure we're in the main thread + vim.schedule(function() + local utils = require('nvim-claude.utils') + + -- Refresh all buffers to show Claude's changes + vim.cmd('checktime') + + -- Check if Claude made any changes + local git_root = utils.get_project_root() + if not git_root then + vim.notify('Not in a git repository', vim.log.levels.WARN) + return + end + + local status_cmd = string.format('cd "%s" && git status --porcelain', git_root) + local status_result = utils.exec(status_cmd) + + if not status_result or status_result == '' then + vim.notify('No changes detected from Claude', vim.log.levels.INFO) + return + end + + -- Create a stash of Claude's changes (but keep them in working directory) + local timestamp = os.date('%Y-%m-%d %H:%M:%S') + local stash_msg = string.format('[claude-edit] %s', timestamp) + + -- Use git stash create to create stash without removing changes + local stash_cmd = string.format('cd "%s" && git stash create -u', git_root) + local stash_hash, stash_err = utils.exec(stash_cmd) + + if not stash_err and stash_hash and stash_hash ~= '' then + -- Store the stash with a message + stash_hash = stash_hash:gsub('%s+', '') -- trim whitespace + local store_cmd = string.format('cd "%s" && git stash store -m "%s" %s', git_root, stash_msg, stash_hash) + utils.exec(store_cmd) + + -- Get the pre-edit commit reference + local pre_edit_ref = utils.read_file('/tmp/claude-pre-edit-commit') + if pre_edit_ref then + pre_edit_ref = pre_edit_ref:gsub('%s+', '') -- trim whitespace + end + + -- Trigger diff review + local ok, diff_review = pcall(require, 'nvim-claude.diff-review') + if ok then + diff_review.handle_claude_edit('stash@{0}', pre_edit_ref) + else + vim.notify('Diff review module not available: ' .. tostring(diff_review), vim.log.levels.ERROR) + end + else + vim.notify('Failed to create stash of Claude changes', vim.log.levels.ERROR) + end + end) +end + +-- Manual hook testing +function M.test_hooks() + vim.notify('=== Testing nvim-claude hooks ===', vim.log.levels.INFO) + + -- Test pre-tool-use hook + vim.notify('1. Testing pre-tool-use hook (creating snapshot)...', vim.log.levels.INFO) + M.pre_tool_use_hook() + + -- Simulate making a change + vim.notify('2. Make some changes to test files now...', vim.log.levels.INFO) + + -- Test post-tool-use hook after a delay + vim.notify('3. Will trigger post-tool-use hook in 3 seconds...', vim.log.levels.INFO) + + vim.defer_fn(function() + M.post_tool_use_hook() + end, 3000) + + vim.notify('=== Hook testing started - make changes now! ===', vim.log.levels.INFO) +end + +-- Install Claude Code hooks +function M.install_hooks() + local utils = require('nvim-claude.utils') + + -- Get project root + local project_root = utils.get_project_root() + if not project_root then + vim.notify('Not in a git repository', vim.log.levels.ERROR) + return + end + + -- Create .claude directory + local claude_dir = project_root .. '/.claude' + if not vim.fn.isdirectory(claude_dir) then + vim.fn.mkdir(claude_dir, 'p') + end + + -- Create hooks configuration + local server_name = vim.v.servername or 'NVIM' + local pre_command = string.format('nvim --headless --server %s --remote-send ":lua require(\'nvim-claude.hooks\').pre_tool_use_hook()" 2>/dev/null || true', server_name) + local post_command = string.format('nvim --headless --server %s --remote-send ":lua require(\'nvim-claude.hooks\').post_tool_use_hook()" 2>/dev/null || true', server_name) + + local hooks_config = { + hooks = { + PreToolUse = { + { + matcher = ".*", -- Match all tools + hooks = { + { + type = "command", + command = pre_command + } + } + } + }, + PostToolUse = { + { + matcher = ".*", -- Match all tools + hooks = { + { + type = "command", + command = post_command + } + } + } + } + } + } + + -- Write hooks configuration + local settings_file = claude_dir .. '/settings.json' + local success, err = utils.write_json(settings_file, hooks_config) + + if success then + -- Add .claude to gitignore if needed + local gitignore_path = project_root .. '/.gitignore' + local gitignore_content = utils.read_file(gitignore_path) or '' + + if not gitignore_content:match('%.claude/') then + local new_content = gitignore_content .. '\n# Claude Code hooks\n.claude/\n' + utils.write_file(gitignore_path, new_content) + vim.notify('Added .claude/ to .gitignore', vim.log.levels.INFO) + end + + vim.notify('Claude Code hooks installed successfully', vim.log.levels.INFO) + vim.notify('Hooks configuration written to: ' .. settings_file, vim.log.levels.INFO) + else + vim.notify('Failed to install hooks: ' .. (err or 'unknown error'), vim.log.levels.ERROR) + end +end + +-- Uninstall Claude Code hooks +function M.uninstall_hooks() + local utils = require('nvim-claude.utils') + + -- Get project root + local project_root = utils.get_project_root() + if not project_root then + vim.notify('Not in a git repository', vim.log.levels.ERROR) + return + end + + local settings_file = project_root .. '/.claude/settings.json' + + if vim.fn.filereadable(settings_file) then + vim.fn.delete(settings_file) + vim.notify('Claude Code hooks uninstalled', vim.log.levels.INFO) + else + vim.notify('No hooks configuration found', vim.log.levels.INFO) + end +end + +-- Commands for manual hook management +function M.setup_commands() + vim.api.nvim_create_user_command('ClaudeTestHooks', function() + M.test_hooks() + end, { + desc = 'Test Claude Code hooks' + }) + + vim.api.nvim_create_user_command('ClaudeTestDiff', function() + local utils = require('nvim-claude.utils') + + -- Check if we're in a git repository + local git_root = utils.get_project_root() + if not git_root then + vim.notify('Not in a git repository', vim.log.levels.WARN) + return + end + + -- Check if there are any changes + local status_cmd = string.format('cd "%s" && git status --porcelain', git_root) + local status_result = utils.exec(status_cmd) + + if not status_result or status_result == '' then + vim.notify('No changes to test', vim.log.levels.INFO) + return + end + + -- Create test stash without restoring (to avoid conflicts) + local timestamp = os.date('%Y-%m-%d %H:%M:%S') + local stash_msg = string.format('[claude-test] %s', timestamp) + + local stash_cmd = string.format('cd "%s" && git stash push -u -m "%s"', git_root, stash_msg) + local stash_result, stash_err = utils.exec(stash_cmd) + + if stash_err then + vim.notify('Failed to create test stash: ' .. stash_err, vim.log.levels.ERROR) + return + end + + -- Trigger diff review with the stash (no pre-edit ref for manual test) + local diff_review = require('nvim-claude.diff-review') + diff_review.handle_claude_edit('stash@{0}', nil) + + -- Pop the stash to restore changes + vim.defer_fn(function() + local pop_cmd = string.format('cd "%s" && git stash pop --quiet', git_root) + utils.exec(pop_cmd) + vim.cmd('checktime') -- Refresh buffers + end, 100) + end, { + desc = 'Test Claude diff review with current changes' + }) + + vim.api.nvim_create_user_command('ClaudeInstallHooks', function() + M.install_hooks() + end, { + desc = 'Install Claude Code hooks for this project' + }) + + vim.api.nvim_create_user_command('ClaudeUninstallHooks', function() + M.uninstall_hooks() + end, { + desc = 'Uninstall Claude Code hooks for this project' + }) +end + +return M \ No newline at end of file diff --git a/lua/nvim-claude/init.lua b/lua/nvim-claude/init.lua index 6cc5766e..bae28078 100644 --- a/lua/nvim-claude/init.lua +++ b/lua/nvim-claude/init.lua @@ -115,14 +115,19 @@ function M.setup(user_config) M.utils = require('nvim-claude.utils') M.commands = require('nvim-claude.commands') M.registry = require('nvim-claude.registry') + M.hooks = require('nvim-claude.hooks') + M.diff_review = require('nvim-claude.diff-review') -- Initialize submodules with config M.tmux.setup(M.config.tmux) M.git.setup(M.config.agents) M.registry.setup(M.config.agents) + M.hooks.setup() + M.diff_review.setup() -- Set up commands M.commands.setup(M) + M.hooks.setup_commands() -- Set up keymappings if enabled if M.config.mappings then diff --git a/lua/nvim-claude/utils.lua b/lua/nvim-claude/utils.lua index ac5043de..2be16dfe 100644 --- a/lua/nvim-claude/utils.lua +++ b/lua/nvim-claude/utils.lua @@ -113,4 +113,38 @@ function M.tmux_supports_length_percent() return M.tmux_version() >= 3.4 end +-- Write JSON to file +function M.write_json(path, data) + local success, json = pcall(vim.fn.json_encode, data) + if not success then + return false, 'Failed to encode JSON: ' .. json + end + + local file = io.open(path, 'w') + if not file then + return false, 'Failed to open file for writing: ' .. path + end + + -- Pretty print JSON + local formatted = json:gsub('},{', '},\n {'):gsub('\\{', '{\n '):gsub('\\}', '\n}') + file:write(formatted) + file:close() + return true, nil +end + +-- Read JSON from file +function M.read_json(path) + local content = M.read_file(path) + if not content then + return nil, 'Failed to read file: ' .. path + end + + local success, data = pcall(vim.fn.json_decode, content) + if not success then + return nil, 'Failed to decode JSON: ' .. data + end + + return data, nil +end + return M \ No newline at end of file