From dac46b9a934c3066cb3828331e045473b4e4574d Mon Sep 17 00:00:00 2001 From: zolinthecow Date: Thu, 10 Jul 2025 23:16:29 -0700 Subject: [PATCH] multi file edit --- LICENSE.md | 111 +----- README.md | 2 + init.lua.old | 3 + lua/nvim-claude/hooks.lua | 214 +++++++--- lua/nvim-claude/inline-diff-persistence.lua | 15 + lua/nvim-claude/inline-diff.lua | 415 ++++++++++++++++---- lua/nvim-claude/mappings.lua | 27 ++ testfile | 1 - 8 files changed, 551 insertions(+), 237 deletions(-) delete mode 100644 testfile diff --git a/LICENSE.md b/LICENSE.md index 5237e294..9cf10627 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,114 +1,19 @@ -DEBUG LOGGING: Check /tmp/claude-python-hook.log for debug output! -MIT License - ACCEPT TEST 1: Try accepting this first hunk +MIT License -MULTI-EDIT TEST #1: Permission is hereby granted, free of charge, to any person obtaining a copy +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -ACCEPT TEST 2: This should remain after accepting the first +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -hi -MULTI-EDIT TEST #2: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -ACCEPT TEST 3: Third hunk should still be visible -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. [EDIT 2: Middle change] IN NO EVENT SHALL THE -TEST 3: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. [EDIT 2: Middle change] IN NO EVENT SHALL THE -TEST 2: Middle section edit - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. - -HUNK TEST #4: test line modified by Claude - -SEPARATED HUNK #3: This line should trigger inline diff automatically via hooks! - -MANUAL HOOK TEST: This line was added after you ran pre_tool_use_hook! - -AUTOMATIC HOOK TEST: This should trigger hooks automatically! - -RESTART TEST: This edit is after Neovim restart - hooks should work now! - -HOOKS FIXED: This edit should trigger the pre/post hooks and show inline diff! - -TEST AFTER REBASE: This line should trigger the inline diff automatically! - -HOOK TEST: Testing with corrected settings.json! - -RESTART COMPLETE: Testing hooks after Neovim restart! - -MANUAL HOOK TEST: Testing with manual hook execution! - -DEBUG HOOK TEST: Testing if Claude Code calls the hook script! - -POST-HOOK DEBUG: Testing with enhanced debugging! - -FRESH NEOVIM: Testing inline diff after restart! - -DEBUG LOGGING: Testing with detailed debug logging! - -NOTIFICATION TEST: Testing with enhanced notifications after restart! - -CLEAR LOG TEST: Testing after clearing debug log! - -ESCAPE FIX: Testing with fixed escape sequence in settings.json! - -SCOPE FIX: Testing with fixed baseline_ref scope! - -INLINE DIFF TEST: Testing if inline diff finally works! - -SCRIPT APPROACH: Testing with the hook script instead of hardcoded server! - -HOOK EXECUTION TEST: Checking if Claude Code executes the hook script! - -SIMPLE HOOK TEST: Testing with a very simple hook script! - -VERBOSE HOOK TEST: Testing with detailed logging to debug hook execution! - -TOUCH HOOK TEST: Testing with simplest possible hook that just touches a file! - -PROPER HOOK TEST: Testing with a hook that follows Claude Code's expected format! - -HOOKS WORKING: The hooks are executing! Let's see if inline diff appears! - -DEBUG LOGS REMOVED: Testing inline diff without debug messages blocking execution! - -WORKING INLINE DIFF: This edit should trigger the inline diff automatically! - -FINAL TEST: With improved hook script - inline diff should appear now! - -AUTOMATIC INLINE DIFF: This should appear automatically without any prompts! - -NEW EDIT: This line is different from the baseline and should trigger inline diff! - -RESTART TEST: Testing if inline diff works after Neovim restart! - -BASELINE FIX TEST: This should properly update baseline when accepted! - -DEBUG BASELINE: This edit includes debugging to see why baseline commits fail! - -AFTER RESTART: Testing baseline commit creation with debug output! - -BASELINE ISSUE: The pre-hook runs after edits, so baseline includes changes! - -MULTI-EDIT TEST #3: Testing multiple hunks and navigation! - -TEST EDIT #2: Checking consistency of diff display! - -EDIT #2: Middle section replacement - this should be the second hunk! - -MULTI-EDIT TEST #4: Testing hunk navigation with ]h and [h! - -## INLINE DIFF SYSTEM V2 -Our enhanced inline diff now uses git's histogram algorithm and proper stash-based baselines! - -## TESTING HOOKS WITHOUT NOTIFICATIONS -The hooks now run silently without vim.notify interruptions! - -EDIT #3: Bottom addition - this is the third hunk for testing! - -EDIT 3: Final test line at the very bottom of the file! - -ACCEPT TEST 4: Fourth and final hunk for testing! diff --git a/README.md b/README.md index c4a11dc6..20484366 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # kickstart.nvim +> Enhanced with nvim-claude integration for AI-powered development ## Introduction @@ -228,3 +229,4 @@ sudo pacman -S --noconfirm --needed gcc make git ripgrep fd unzip neovim ``` +# Test manual edit diff --git a/init.lua.old b/init.lua.old index e0b94549..d8e2952f 100644 --- a/init.lua.old +++ b/init.lua.old @@ -25,10 +25,13 @@ What is Kickstart? Kickstart.nvim is *not* a distribution. Kickstart.nvim is a starting point for your own configuration. + + NOW ENHANCED: This configuration includes nvim-claude integration! The goal is that you can read every line of code, top-to-bottom, understand what your configuration is doing, and modify it to suit your needs. Once you've done that, you can start exploring, configuring and tinkering to + make Neovim your own! That might mean leaving Kickstart just the way it is for now make Neovim your own! That might mean leaving Kickstart just the way it is for a while or immediately breaking it into modular pieces. It's up to you! diff --git a/lua/nvim-claude/hooks.lua b/lua/nvim-claude/hooks.lua index d12d15ee..749e4992 100644 --- a/lua/nvim-claude/hooks.lua +++ b/lua/nvim-claude/hooks.lua @@ -4,12 +4,40 @@ local M = {} -- Track hook state M.pre_edit_commit = nil M.stable_baseline_ref = nil -- The stable baseline to compare all changes against +M.claude_edited_files = {} -- Track which files Claude has edited + +-- Update stable baseline after accepting changes +function M.update_stable_baseline() + local utils = require('nvim-claude.utils') + local persistence = require('nvim-claude.inline-diff-persistence') + + -- Create a new stash with current state as the new baseline + local message = 'nvim-claude-baseline-accepted-' .. os.time() + + -- Create a stash object without removing changes from working directory + local stash_cmd = 'git stash create' + local stash_hash, err = utils.exec(stash_cmd) + + if not 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('git stash store -m "%s" %s', message, stash_hash) + utils.exec(store_cmd) + + -- Update our stable baseline reference + M.stable_baseline_ref = stash_hash + persistence.current_stash_ref = stash_hash + end +end function M.setup() -- Setup persistence layer on startup vim.defer_fn(function() - M.create_startup_baseline() + M.setup_persistence() end, 500) + + -- Set up autocmd for opening files + M.setup_file_open_autocmd() end -- Pre-tool-use hook: Create baseline stash if we don't have one @@ -54,35 +82,33 @@ function M.post_tool_use_hook() -- Get list of modified files local modified_files = {} + local inline_diff = require 'nvim-claude.inline-diff' + for line in status_result:gmatch '[^\n]+' do local file = line:match '^.M (.+)$' or line:match '^M. (.+)$' or line:match '^.. (.+)$' if file then table.insert(modified_files, file) + -- Track that Claude edited this file + M.claude_edited_files[file] = true + + -- Track this file in the diff files list immediately + local full_path = git_root .. '/' .. file + inline_diff.diff_files[full_path] = -1 -- Use -1 to indicate no buffer yet end end - -- Use the stable baseline reference for comparison + -- Always use the stable baseline reference for comparison local stash_ref = M.stable_baseline_ref or persistence.current_stash_ref - -- Check for in-memory baselines first - local inline_diff = require 'nvim-claude.inline-diff' - local has_baselines = false - for _, content in pairs(inline_diff.original_content) do - if content then - has_baselines = true - break - end - end - -- If no baseline exists at all, create one now (shouldn't happen normally) - if not stash_ref and not has_baselines then + if not stash_ref then stash_ref = persistence.create_stash('nvim-claude: baseline ' .. os.date('%Y-%m-%d %H:%M:%S')) M.stable_baseline_ref = stash_ref persistence.current_stash_ref = stash_ref end - if stash_ref or has_baselines then - -- Check if any modified files are currently open in buffers + if stash_ref then + -- Process inline diffs for currently open buffers local opened_inline = false for _, file in ipairs(modified_files) do @@ -93,38 +119,16 @@ function M.post_tool_use_hook() if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf) then local buf_name = vim.api.nvim_buf_get_name(buf) if buf_name == full_path or buf_name:match('/' .. file:gsub('([^%w])', '%%%1') .. '$') then - -- Get the original content - prefer in-memory baseline if available - local original_content = nil - - -- Check for in-memory baseline first - if inline_diff.original_content[buf] then - original_content = inline_diff.original_content[buf] - elseif stash_ref then - -- Fall back to stash baseline - local stash_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, stash_ref, file) - original_content = utils.exec(stash_cmd) - end + -- Show inline diff for this open buffer + M.show_inline_diff_for_file(buf, file, git_root, stash_ref) + opened_inline = true - if original_content then - -- Get current content - local current_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - local current_content = table.concat(current_lines, '\n') - - -- Debug: Log content lengths - -- vim.notify(string.format('DEBUG: Baseline has %d chars, current has %d chars', - -- #(original_content or ''), #current_content), vim.log.levels.WARN) - - -- Show inline diff - inline_diff.show_inline_diff(buf, original_content, current_content) - opened_inline = true - - -- Switch to that buffer if it's not the current one - if buf ~= vim.api.nvim_get_current_buf() then - vim.api.nvim_set_current_buf(buf) - end - - break -- Only show inline diff for first matching buffer + -- Switch to that buffer if it's not the current one + if buf ~= vim.api.nvim_get_current_buf() then + vim.api.nvim_set_current_buf(buf) end + + break -- Only show inline diff for first matching buffer end end end @@ -145,6 +149,33 @@ function M.post_tool_use_hook() end end +-- Helper function to show inline diff for a file +function M.show_inline_diff_for_file(buf, file, git_root, stash_ref) + local utils = require 'nvim-claude.utils' + local inline_diff = require 'nvim-claude.inline-diff' + + -- Only show inline diff if Claude edited this file + if not M.claude_edited_files[file] then + return false + end + + -- Get baseline from git stash + local stash_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, stash_ref, file) + local original_content = utils.exec(stash_cmd) + + if original_content then + -- Get current content + local current_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local current_content = table.concat(current_lines, '\n') + + -- Show inline diff + inline_diff.show_inline_diff(buf, original_content, current_content) + return true + end + + return false +end + -- Test inline diff manually function M.test_inline_diff() vim.notify('Testing inline diff manually...', vim.log.levels.INFO) @@ -214,8 +245,38 @@ function M.test_inline_diff() inline_diff.show_inline_diff(bufnr, original_content, current_content) end --- Create baseline on Neovim startup (now just sets up persistence) -function M.create_startup_baseline() +-- Set up autocmd to check for diffs when opening files +function M.setup_file_open_autocmd() + vim.api.nvim_create_autocmd({"BufRead", "BufNewFile"}, { + pattern = "*", + callback = function(args) + local bufnr = args.buf + local file_path = vim.api.nvim_buf_get_name(bufnr) + + if file_path == '' then return end + + local utils = require 'nvim-claude.utils' + local git_root = utils.get_project_root() + + if not git_root then return end + + -- Get relative path + local relative_path = file_path:gsub(git_root .. '/', '') + + -- Check if this file was edited by Claude + if M.claude_edited_files[relative_path] and M.stable_baseline_ref then + -- Show inline diff for this file + vim.defer_fn(function() + M.show_inline_diff_for_file(bufnr, relative_path, git_root, M.stable_baseline_ref) + end, 50) -- Small delay to ensure buffer is fully loaded + end + end, + group = vim.api.nvim_create_augroup('NvimClaudeFileOpen', { clear = true }) + }) +end + +-- Setup persistence and restore saved state on Neovim startup +function M.setup_persistence() local persistence = require 'nvim-claude.inline-diff-persistence' -- Setup persistence autocmds @@ -224,14 +285,12 @@ function M.create_startup_baseline() -- Try to restore any saved diffs local restored = persistence.restore_diffs() - -- If no diffs were restored and we don't have a baseline, create one now - if not restored and not M.stable_baseline_ref then - local stash_ref = persistence.create_stash('nvim-claude: startup baseline ' .. os.date('%Y-%m-%d %H:%M:%S')) - if stash_ref then - M.stable_baseline_ref = stash_ref - persistence.current_stash_ref = stash_ref - end + -- Also restore the baseline reference from persistence if it exists + if persistence.current_stash_ref then + M.stable_baseline_ref = persistence.current_stash_ref end + + -- Don't create a startup baseline - only create baselines when Claude makes edits end -- Manual hook testing @@ -474,6 +533,7 @@ function M.setup_commands() -- Clear stable baseline reference M.stable_baseline_ref = nil persistence.current_stash_ref = nil + M.claude_edited_files = {} -- Clear persistence state persistence.clear_state() @@ -482,6 +542,52 @@ function M.setup_commands() end, { desc = 'Reset Claude baseline for cumulative diffs', }) + + vim.api.nvim_create_user_command('ClaudeTrackModified', function() + -- Manually track all modified files as Claude-edited + local utils = require 'nvim-claude.utils' + local git_root = utils.get_project_root() + + if not git_root then + vim.notify('Not in a git repository', vim.log.levels.ERROR) + 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 modified files found', vim.log.levels.INFO) + return + end + + local count = 0 + for line in status_result:gmatch '[^\n]+' do + local file = line:match '^.M (.+)$' or line:match '^M. (.+)$' + if file then + M.claude_edited_files[file] = true + count = count + 1 + end + end + + vim.notify(string.format('Tracked %d modified files as Claude-edited', count), vim.log.levels.INFO) + + -- Also ensure we have a baseline + if not M.stable_baseline_ref then + local persistence = require 'nvim-claude.inline-diff-persistence' + local stash_list = utils.exec('git stash list | grep "nvim-claude: baseline" | head -1') + if stash_list and stash_list ~= '' then + local stash_ref = stash_list:match('^(stash@{%d+})') + if stash_ref then + M.stable_baseline_ref = stash_ref + persistence.current_stash_ref = stash_ref + vim.notify('Using baseline: ' .. stash_ref, vim.log.levels.INFO) + end + end + end + end, { + desc = 'Track all modified files as Claude-edited (for debugging)', + }) end -- Cleanup old temp files (no longer cleans up commits) diff --git a/lua/nvim-claude/inline-diff-persistence.lua b/lua/nvim-claude/inline-diff-persistence.lua index 33db2b68..946842ad 100644 --- a/lua/nvim-claude/inline-diff-persistence.lua +++ b/lua/nvim-claude/inline-diff-persistence.lua @@ -23,10 +23,13 @@ function M.save_state(diff_data) -- } -- } + local hooks = require('nvim-claude.hooks') + local state = { version = 1, timestamp = os.time(), stash_ref = diff_data.stash_ref, + claude_edited_files = hooks.claude_edited_files or {}, files = {} } @@ -148,6 +151,12 @@ function M.restore_diffs() -- Store the stash reference for future operations M.current_stash_ref = state.stash_ref + -- Restore Claude edited files tracking + if state.claude_edited_files then + local hooks = require('nvim-claude.hooks') + hooks.claude_edited_files = state.claude_edited_files + end + return true end @@ -230,6 +239,12 @@ function M.setup_autocmds() if has_active_diffs and M.current_stash_ref then M.save_state({ stash_ref = M.current_stash_ref }) + else + -- Save just the Claude edited files tracking even if no active diffs + local hooks = require('nvim-claude.hooks') + if hooks.claude_edited_files and next(hooks.claude_edited_files) then + M.save_state({ stash_ref = M.current_stash_ref or '' }) + end end end }) diff --git a/lua/nvim-claude/inline-diff.lua b/lua/nvim-claude/inline-diff.lua index c9b2274a..9a224d6f 100644 --- a/lua/nvim-claude/inline-diff.lua +++ b/lua/nvim-claude/inline-diff.lua @@ -73,6 +73,9 @@ function M.compute_diff(old_text, new_text) ) local diff_output = utils.exec(cmd) + -- Debug: save raw diff + utils.write_file('/tmp/nvim-claude-raw-diff.txt', diff_output) + -- Parse diff into hunks local hunks = M.parse_diff(diff_output) @@ -258,12 +261,6 @@ function M.setup_inline_keymaps(bufnr) vim.keymap.set('n', '[h', function() M.prev_hunk(bufnr) end, vim.tbl_extend('force', opts, { desc = 'Previous Claude hunk' })) - -- Navigation between files - vim.keymap.set('n', ']f', function() M.next_diff_file() end, - vim.tbl_extend('force', opts, { desc = 'Next file with Claude diff' })) - vim.keymap.set('n', '[f', function() M.prev_diff_file() end, - vim.tbl_extend('force', opts, { desc = 'Previous file with Claude diff' })) - -- Accept/Reject vim.keymap.set('n', 'ia', function() M.accept_current_hunk(bufnr) end, vim.tbl_extend('force', opts, { desc = 'Accept Claude hunk' })) @@ -317,8 +314,11 @@ function M.jump_to_hunk(bufnr, hunk_idx) jump_line = hunk.new_start end - -- Move cursor to the actual changed line - vim.api.nvim_win_set_cursor(0, {jump_line, 0}) + -- Move cursor to the actual changed line (only if we have a valid window) + local win = vim.api.nvim_get_current_win() + if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == bufnr then + vim.api.nvim_win_set_cursor(win, {jump_line, 0}) + end -- Update status vim.notify(string.format('Hunk %d/%d', hunk_idx, #diff_data.hunks), vim.log.levels.INFO) @@ -350,41 +350,218 @@ function M.prev_hunk(bufnr) M.jump_to_hunk(bufnr, prev_idx) end +-- Generate a patch for a single hunk +function M.generate_hunk_patch(hunk, file_path) + local patch_lines = { + string.format("--- a/%s", file_path), + string.format("+++ b/%s", file_path), + hunk.header + } + + -- Add the hunk lines + for _, line in ipairs(hunk.lines) do + table.insert(patch_lines, line) + end + + -- Ensure patch ends with newline + table.insert(patch_lines, "") + + return table.concat(patch_lines, '\n') +end + +-- Apply a hunk to the baseline using git patches +function M.apply_hunk_to_baseline(bufnr, hunk_idx, action) + local utils = require('nvim-claude.utils') + local hooks = require('nvim-claude.hooks') + local diff_data = M.active_diffs[bufnr] + local hunk = diff_data.hunks[hunk_idx] + + -- Get file paths + local git_root = utils.get_project_root() + local file_path = vim.api.nvim_buf_get_name(bufnr) + local relative_path = file_path:gsub(git_root .. '/', '') + + -- Get current baseline + local persistence = require('nvim-claude.inline-diff-persistence') + local stash_ref = hooks.stable_baseline_ref or persistence.current_stash_ref + + -- If still no baseline, try to get the most recent nvim-claude baseline from stash list + if not stash_ref then + local stash_list = utils.exec('git stash list | grep "nvim-claude: baseline" | head -1') + if stash_list and stash_list ~= '' then + stash_ref = stash_list:match('^(stash@{%d+})') + if stash_ref then + -- Update both references + hooks.stable_baseline_ref = stash_ref + persistence.current_stash_ref = stash_ref + vim.notify('Using baseline: ' .. stash_ref, vim.log.levels.INFO) + end + end + end + + if not stash_ref then + vim.notify('No baseline stash found', vim.log.levels.ERROR) + return false + end + + -- Create temp directory + local temp_dir = vim.fn.tempname() + vim.fn.mkdir(temp_dir, 'p') + + -- Extract just the file we need from the stash + local extract_cmd = string.format('cd "%s" && git show %s:%s > "%s/%s"', + git_root, stash_ref, relative_path, temp_dir, relative_path) + + -- Create directory structure in temp + local file_dir = vim.fn.fnamemodify(temp_dir .. '/' .. relative_path, ':h') + vim.fn.mkdir(file_dir, 'p') + + local _, extract_err = utils.exec(extract_cmd) + if extract_err then + vim.notify('Failed to extract file from baseline: ' .. extract_err, vim.log.levels.ERROR) + vim.fn.delete(temp_dir, 'rf') + return false + end + + -- For accept: apply the hunk as-is + -- For reject: apply the hunk in reverse + local patch = M.generate_hunk_patch(hunk, relative_path) + local patch_file = temp_dir .. '/hunk.patch' + utils.write_file(patch_file, patch) + + -- Debug: save patch content for inspection + local debug_file = '/tmp/nvim-claude-debug-patch.txt' + utils.write_file(debug_file, patch) + vim.notify('Patch saved to: ' .. debug_file, vim.log.levels.INFO) + + -- Apply the patch + local apply_flags = action == 'reject' and '--reverse' or '' + local apply_cmd = string.format('cd "%s" && git apply --verbose %s "%s" 2>&1', + temp_dir, apply_flags, patch_file) + + local result, apply_err = utils.exec(apply_cmd) + if apply_err or (result and result:match('error:')) then + vim.notify('Failed to apply patch: ' .. (apply_err or result), vim.log.levels.ERROR) + vim.fn.delete(temp_dir, 'rf') + return false + end + + -- Now we need to create a new stash with this modified file + -- First, checkout the baseline into a temp git repo + local work_dir = vim.fn.tempname() + vim.fn.mkdir(work_dir, 'p') + + -- Create a work tree from the stash + local worktree_cmd = string.format('cd "%s" && git worktree add --detach "%s" %s 2>&1', + git_root, work_dir, stash_ref) + local _, worktree_err = utils.exec(worktree_cmd) + + if worktree_err then + vim.notify('Failed to create worktree: ' .. worktree_err, vim.log.levels.ERROR) + vim.fn.delete(temp_dir, 'rf') + vim.fn.delete(work_dir, 'rf') + return false + end + + -- Copy the patched file to the worktree + local copy_cmd = string.format('cp "%s/%s" "%s/%s"', temp_dir, relative_path, work_dir, relative_path) + utils.exec(copy_cmd) + + -- Stage and create a new stash + local stage_cmd = string.format('cd "%s" && git add "%s"', work_dir, relative_path) + utils.exec(stage_cmd) + + -- Create a new stash + local stash_cmd = string.format('cd "%s" && git stash create', work_dir) + local new_stash, stash_err = utils.exec(stash_cmd) + + if stash_err or not new_stash or new_stash == '' then + vim.notify('Failed to create new stash', vim.log.levels.ERROR) + else + new_stash = new_stash:gsub('%s+', '') + + -- Store the new stash + local store_cmd = string.format('cd "%s" && git stash store -m "nvim-claude-baseline-hunk-%s" %s', + git_root, action, new_stash) + utils.exec(store_cmd) + + -- Update the baseline reference + hooks.stable_baseline_ref = new_stash + persistence.current_stash_ref = new_stash + vim.notify('Updated baseline to: ' .. new_stash, vim.log.levels.INFO) + end + + -- Clean up worktree + local cleanup_cmd = string.format('cd "%s" && git worktree remove --force "%s"', git_root, work_dir) + utils.exec(cleanup_cmd) + + -- Clean up temp files + vim.fn.delete(temp_dir, 'rf') + vim.fn.delete(work_dir, 'rf') + + return true +end + -- Accept current hunk function M.accept_current_hunk(bufnr) local diff_data = M.active_diffs[bufnr] if not diff_data then return end - local hunk = diff_data.hunks[diff_data.current_hunk] + local hunk_idx = diff_data.current_hunk + local hunk = diff_data.hunks[hunk_idx] if not hunk then return end - -- Mark this hunk as processed - vim.notify(string.format('Accepted hunk %d/%d', diff_data.current_hunk, #diff_data.hunks), vim.log.levels.INFO) + vim.notify(string.format('Accepting hunk %d/%d', hunk_idx, #diff_data.hunks), vim.log.levels.INFO) - -- For single hunk case - if #diff_data.hunks == 1 then - vim.notify('All changes accepted. Closing inline diff.', vim.log.levels.INFO) - M.close_inline_diff(bufnr, true) -- Keep new baseline - return - end + -- Debug: Show current baseline + local hooks = require('nvim-claude.hooks') + local persistence = require('nvim-claude.inline-diff-persistence') + vim.notify('Current baseline: ' .. (hooks.stable_baseline_ref or persistence.current_stash_ref or 'none'), vim.log.levels.INFO) - -- Multiple hunks: remove the accepted hunk and continue - table.remove(diff_data.hunks, diff_data.current_hunk) + -- Accept = keep current state, update baseline + M.apply_hunk_to_baseline(bufnr, hunk_idx, 'accept') - -- Adjust current hunk index - if diff_data.current_hunk > #diff_data.hunks then - diff_data.current_hunk = #diff_data.hunks + -- Recalculate diff against new baseline + local utils = require('nvim-claude.utils') + local hooks = require('nvim-claude.hooks') + local git_root = utils.get_project_root() + local file_path = vim.api.nvim_buf_get_name(bufnr) + local relative_path = file_path:gsub(git_root .. '/', '') + + -- Get the new baseline content + local persistence = require('nvim-claude.inline-diff-persistence') + local stash_ref = hooks.stable_baseline_ref or persistence.current_stash_ref + if not stash_ref then + vim.notify('No baseline found for recalculation', vim.log.levels.ERROR) + return end - if #diff_data.hunks == 0 then - -- No more hunks - vim.notify('All changes accepted. Closing inline diff.', vim.log.levels.INFO) - M.close_inline_diff(bufnr, true) - else - -- Refresh visualization to show remaining hunks - M.apply_diff_visualization(bufnr) - M.jump_to_hunk(bufnr, diff_data.current_hunk) - vim.notify(string.format('%d hunks remaining', #diff_data.hunks), vim.log.levels.INFO) + local baseline_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, stash_ref, relative_path) + local new_baseline = utils.exec(baseline_cmd) + + if new_baseline then + M.original_content[bufnr] = new_baseline + + -- Get current content + local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local current_content = table.concat(current_lines, '\n') + + -- Recalculate diff + local new_diff_data = M.compute_diff(new_baseline, current_content) + + if not new_diff_data or #new_diff_data.hunks == 0 then + vim.notify('All changes accepted. Closing inline diff.', vim.log.levels.INFO) + M.close_inline_diff(bufnr, true) + else + -- Update diff data + diff_data.hunks = new_diff_data.hunks + diff_data.current_hunk = 1 + + -- Refresh visualization + M.apply_diff_visualization(bufnr) + M.jump_to_hunk(bufnr, 1) + vim.notify(string.format('%d hunks remaining', #new_diff_data.hunks), vim.log.levels.INFO) + end end end @@ -396,47 +573,84 @@ function M.reject_current_hunk(bufnr) return end - local hunk = diff_data.hunks[diff_data.current_hunk] + local hunk_idx = diff_data.current_hunk + local hunk = diff_data.hunks[hunk_idx] if not hunk then - vim.notify('No hunk at index ' .. tostring(diff_data.current_hunk), vim.log.levels.ERROR) + vim.notify('No hunk at index ' .. tostring(hunk_idx), vim.log.levels.ERROR) return end - -- vim.notify(string.format('Rejecting hunk %d/%d', diff_data.current_hunk, #diff_data.hunks), vim.log.levels.INFO) + vim.notify(string.format('Rejecting hunk %d/%d', hunk_idx, #diff_data.hunks), vim.log.levels.INFO) + + -- For reject, apply the patch in reverse to the current file + -- The baseline stays unchanged + local utils = require('nvim-claude.utils') + local git_root = utils.get_project_root() + local file_path = vim.api.nvim_buf_get_name(bufnr) + local relative_path = file_path:gsub(git_root .. '/', '') + + -- Generate patch for this hunk + local patch = M.generate_hunk_patch(hunk, relative_path) + local patch_file = vim.fn.tempname() .. '.patch' + utils.write_file(patch_file, patch) + + -- Debug: Show hunk details + vim.notify(string.format('Hunk %d: old_start=%d, new_start=%d, lines=%d', + hunk_idx, hunk.old_start, hunk.new_start, #hunk.lines), vim.log.levels.INFO) + + -- Debug: save patch for inspection + local debug_file = '/tmp/nvim-claude-reject-patch.txt' + utils.write_file(debug_file, patch) + + -- Apply reverse patch to the working directory + local apply_cmd = string.format('cd "%s" && git apply --reverse --verbose "%s" 2>&1', git_root, patch_file) + local result, err = utils.exec(apply_cmd) + + if err or (result and result:match('error:')) then + vim.notify('Failed to reject hunk: ' .. (err or result), vim.log.levels.ERROR) + vim.notify('Patch saved to: ' .. debug_file, vim.log.levels.INFO) + vim.fn.delete(patch_file) + return + end - -- Revert the hunk by applying original content - M.revert_hunk_changes(bufnr, hunk) + vim.fn.delete(patch_file) - -- Save the buffer to ensure changes are on disk + -- Reload the buffer vim.api.nvim_buf_call(bufnr, function() - if vim.bo.modified then - vim.cmd('write') - end + vim.cmd('checktime') end) - -- Get current content after rejection - local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local current_content = table.concat(current_lines, '\n') + -- Recalculate diff against unchanged baseline + local hooks = require('nvim-claude.hooks') - -- Recalculate diff between current state (with rejected hunk) and original baseline - local new_diff_data = M.compute_diff(M.original_content[bufnr], current_content) + -- Get the new baseline content + local stash_ref = hooks.stable_baseline_ref + local baseline_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, stash_ref, relative_path) + local new_baseline = utils.exec(baseline_cmd) - if not new_diff_data or #new_diff_data.hunks == 0 then - -- No more changes from baseline - close the diff - vim.notify('All changes processed. Closing inline diff.', vim.log.levels.INFO) - M.close_inline_diff(bufnr, false) - else - -- Update diff data with remaining hunks - diff_data.hunks = new_diff_data.hunks - diff_data.current_hunk = 1 + if new_baseline then + M.original_content[bufnr] = new_baseline - -- The new_content should remain as Claude's original suggestion - -- so we can continue to accept remaining hunks if desired + -- Get current content + local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local current_content = table.concat(current_lines, '\n') - -- Refresh visualization and jump to first remaining hunk - M.apply_diff_visualization(bufnr) - M.jump_to_hunk(bufnr, 1) - vim.notify(string.format('%d hunks remaining', #diff_data.hunks), vim.log.levels.INFO) + -- Recalculate diff + local new_diff_data = M.compute_diff(new_baseline, current_content) + + if not new_diff_data or #new_diff_data.hunks == 0 then + vim.notify('All changes processed. Closing inline diff.', vim.log.levels.INFO) + M.close_inline_diff(bufnr, false) + else + -- Update diff data + diff_data.hunks = new_diff_data.hunks + diff_data.current_hunk = 1 + + -- Refresh visualization + M.apply_diff_visualization(bufnr) + M.jump_to_hunk(bufnr, 1) + vim.notify(string.format('%d hunks remaining', #new_diff_data.hunks), vim.log.levels.INFO) + end end end @@ -633,8 +847,6 @@ function M.close_inline_diff(bufnr, keep_baseline) -- Remove buffer-local keymaps pcall(vim.keymap.del, 'n', ']h', { buffer = bufnr }) pcall(vim.keymap.del, 'n', '[h', { buffer = bufnr }) - pcall(vim.keymap.del, 'n', ']f', { buffer = bufnr }) - pcall(vim.keymap.del, 'n', '[f', { buffer = bufnr }) pcall(vim.keymap.del, 'n', 'ia', { buffer = bufnr }) pcall(vim.keymap.del, 'n', 'ir', { buffer = bufnr }) pcall(vim.keymap.del, 'n', 'iA', { buffer = bufnr }) @@ -651,10 +863,8 @@ function M.close_inline_diff(bufnr, keep_baseline) M.diff_files[file_path] = nil end - -- Only clear baseline if not explicitly told to keep it - if not keep_baseline then - M.original_content[bufnr] = nil - end + -- Clear the in-memory baseline + M.original_content[bufnr] = nil -- Check if all diffs are closed local has_active_diffs = false @@ -674,6 +884,7 @@ function M.close_inline_diff(bufnr, keep_baseline) -- Reset the stable baseline in hooks local hooks = require('nvim-claude.hooks') hooks.stable_baseline_ref = nil + hooks.claude_edited_files = {} end vim.notify('Inline diff closed', vim.log.levels.INFO) @@ -709,10 +920,15 @@ end function M.next_diff_file() local current_file = vim.api.nvim_buf_get_name(0) local files_with_diffs = {} + local hooks = require('nvim-claude.hooks') + + -- Collect all files with diffs (both opened and unopened) + local utils = require('nvim-claude.utils') + local git_root = utils.get_project_root() - -- Collect all files with active diffs for file_path, bufnr in pairs(M.diff_files) do - if M.active_diffs[bufnr] then + local git_relative = file_path:gsub('^' .. vim.pesc(git_root) .. '/', '') + if hooks.claude_edited_files[git_relative] then table.insert(files_with_diffs, file_path) end end @@ -749,10 +965,15 @@ end function M.prev_diff_file() local current_file = vim.api.nvim_buf_get_name(0) local files_with_diffs = {} + local hooks = require('nvim-claude.hooks') + + -- Collect all files with diffs (both opened and unopened) + local utils = require('nvim-claude.utils') + local git_root = utils.get_project_root() - -- Collect all files with active diffs for file_path, bufnr in pairs(M.diff_files) do - if M.active_diffs[bufnr] then + local git_relative = file_path:gsub('^' .. vim.pesc(git_root) .. '/', '') + if hooks.claude_edited_files[git_relative] then table.insert(files_with_diffs, file_path) end end @@ -788,15 +1009,27 @@ end -- List all files with active diffs function M.list_diff_files() local files_with_diffs = {} + local hooks = require('nvim-claude.hooks') for file_path, bufnr in pairs(M.diff_files) do - if M.active_diffs[bufnr] then - local diff_data = M.active_diffs[bufnr] - table.insert(files_with_diffs, { - path = file_path, - hunks = #diff_data.hunks, - name = vim.fn.fnamemodify(file_path, ':t') - }) + -- Check if we have active diffs for this buffer, or if it's a tracked file not yet opened + if (bufnr > 0 and M.active_diffs[bufnr]) or bufnr == -1 then + local diff_data = bufnr > 0 and M.active_diffs[bufnr] or nil + local relative_path = vim.fn.fnamemodify(file_path, ':~:.') + + -- Check if this file is still tracked as Claude-edited + local utils = require('nvim-claude.utils') + local git_root = utils.get_project_root() + local git_relative = file_path:gsub('^' .. vim.pesc(git_root) .. '/', '') + if hooks.claude_edited_files[git_relative] then + table.insert(files_with_diffs, { + path = file_path, + hunks = diff_data and #diff_data.hunks or '?', + name = vim.fn.fnamemodify(file_path, ':t'), + relative_path = relative_path, + current_hunk = diff_data and diff_data.current_hunk or 1 + }) + end end end @@ -808,11 +1041,35 @@ function M.list_diff_files() -- Sort by filename table.sort(files_with_diffs, function(a, b) return a.name < b.name end) - -- Display list - vim.notify('Files with active diffs:', vim.log.levels.INFO) + -- Create items for vim.ui.select + local items = {} + local display_items = {} + for i, file_info in ipairs(files_with_diffs) do - vim.notify(string.format(' %d. %s (%d hunks)', i, file_info.name, file_info.hunks), vim.log.levels.INFO) + table.insert(items, file_info) + local hunk_info = type(file_info.hunks) == 'number' + and string.format('%d hunks, on hunk %d', file_info.hunks, file_info.current_hunk) + or 'not opened yet' + table.insert(display_items, string.format('%s (%s)', + file_info.relative_path, hunk_info)) end + + -- Use vim.ui.select for a telescope-like experience + vim.ui.select(display_items, { + prompt = 'Select file with Claude edits:', + format_item = function(item) return item end, + }, function(choice, idx) + if choice and idx then + local selected_file = items[idx] + vim.cmd('edit ' .. vim.fn.fnameescape(selected_file.path)) + + -- Jump to the current hunk in the selected file + local bufnr = M.diff_files[selected_file.path] + if bufnr and M.active_diffs[bufnr] then + M.jump_to_hunk(bufnr, M.active_diffs[bufnr].current_hunk) + end + end + end) end return M \ No newline at end of file diff --git a/lua/nvim-claude/mappings.lua b/lua/nvim-claude/mappings.lua index 0fcfc9e6..1f446b29 100644 --- a/lua/nvim-claude/mappings.lua +++ b/lua/nvim-claude/mappings.lua @@ -59,9 +59,36 @@ function M.setup(config, commands) l = { 'List Agents' }, k = { 'Kill Agent' }, x = { 'Clean Old Agents' }, + i = { 'List files with diffs' }, } }) end + + -- Global keymaps for navigating between files with Claude diffs + vim.keymap.set('n', ']f', function() + local inline_diff = require('nvim-claude.inline-diff') + inline_diff.next_diff_file() + end, { + desc = 'Next file with Claude diff', + silent = true + }) + + vim.keymap.set('n', '[f', function() + local inline_diff = require('nvim-claude.inline-diff') + inline_diff.prev_diff_file() + end, { + desc = 'Previous file with Claude diff', + silent = true + }) + + -- Global keymap for listing files with diffs + vim.keymap.set('n', prefix .. 'i', function() + local inline_diff = require('nvim-claude.inline-diff') + inline_diff.list_diff_files() + end, { + desc = 'List files with Claude diffs', + silent = true + }) end return M \ No newline at end of file diff --git a/testfile b/testfile deleted file mode 100644 index 623d98c3..00000000 --- a/testfile +++ /dev/null @@ -1 +0,0 @@ -hihi