inline diffs are pretty good now
This commit is contained in:
parent
91c24b34a6
commit
720778cd2c
29
LICENSE.md
29
LICENSE.md
|
@ -1,19 +1,22 @@
|
||||||
MIT License - SEPARATED HUNK #1
|
DEBUG LOGGING: Check /tmp/claude-python-hook.log for debug output!
|
||||||
|
MIT License - ACCEPT TEST 1: Try accepting this first hunk
|
||||||
|
|
||||||
MULTI-EDIT TEST #1: Permission is hereby granted, free of charge, to any person obtaining a copy
|
MULTI-EDIT TEST #1: Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
HUNK TEST #2: copies of the Software, and to permit persons to whom the Software is
|
ACCEPT TEST 2: This should remain after accepting the first
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The above copyright notice and this permission notice shall be included in all
|
||||||
copies or substantial portions of the Software.
|
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
|
MULTI-EDIT TEST #2: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
SEPARATED HUNK #2: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
ACCEPT TEST 3: Third hunk should still be visible
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. [EDIT 2: Middle change] IN NO EVENT SHALL THE
|
||||||
HUNK TEST #3: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
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
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
@ -94,10 +97,18 @@ MULTI-EDIT TEST #3: Testing multiple hunks and navigation!
|
||||||
|
|
||||||
TEST EDIT #2: Checking consistency of diff display!
|
TEST EDIT #2: Checking consistency of diff display!
|
||||||
|
|
||||||
SEPARATED HUNK #4: Final test with better hunk separation!
|
EDIT #2: Middle section replacement - this should be the second hunk!
|
||||||
|
|
||||||
MULTI-EDIT TEST #4: Testing hunk navigation with ]h and [h!
|
MULTI-EDIT TEST #4: Testing hunk navigation with ]h and [h!
|
||||||
|
|
||||||
## PERSISTENCE TEST
|
## INLINE DIFF SYSTEM V2
|
||||||
This tests our new stash-based persistence system that doesn't create git commits!
|
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!
|
||||||
|
|
|
@ -3,6 +3,7 @@ local M = {}
|
||||||
|
|
||||||
-- Track hook state
|
-- Track hook state
|
||||||
M.pre_edit_commit = nil
|
M.pre_edit_commit = nil
|
||||||
|
M.stable_baseline_ref = nil -- The stable baseline to compare all changes against
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
-- Setup persistence layer on startup
|
-- Setup persistence layer on startup
|
||||||
|
@ -11,11 +12,22 @@ function M.setup()
|
||||||
end, 500)
|
end, 500)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pre-tool-use hook: Now just validates existing baseline
|
-- Pre-tool-use hook: Create baseline stash if we don't have one
|
||||||
function M.pre_tool_use_hook()
|
function M.pre_tool_use_hook()
|
||||||
-- Pre-hook no longer creates baselines
|
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||||
-- Baselines are created on startup or through accept/reject
|
|
||||||
return
|
-- Only create a baseline if we don't have one yet
|
||||||
|
if not M.stable_baseline_ref then
|
||||||
|
-- Create baseline stash synchronously
|
||||||
|
local stash_ref = persistence.create_stash('nvim-claude: 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
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return success to allow the tool to proceed
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Post-tool-use hook: Create stash of Claude's changes and trigger diff review
|
-- Post-tool-use hook: Create stash of Claude's changes and trigger diff review
|
||||||
|
@ -49,17 +61,28 @@ function M.post_tool_use_hook()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get the stash reference from pre-hook
|
-- Use the stable baseline reference for comparison
|
||||||
local stash_ref = persistence.current_stash_ref
|
local stash_ref = M.stable_baseline_ref or persistence.current_stash_ref
|
||||||
if not stash_ref then
|
|
||||||
-- If no pre-hook stash, create one now
|
-- Check for in-memory baselines first
|
||||||
stash_ref = persistence.create_stash('nvim-claude: changes detected ' .. os.date('%Y-%m-%d %H:%M:%S'))
|
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
|
||||||
|
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
|
persistence.current_stash_ref = stash_ref
|
||||||
end
|
end
|
||||||
|
|
||||||
if stash_ref then
|
if stash_ref or has_baselines then
|
||||||
-- Check if any modified files are currently open in buffers
|
-- Check if any modified files are currently open in buffers
|
||||||
local inline_diff = require 'nvim-claude.inline-diff'
|
|
||||||
local opened_inline = false
|
local opened_inline = false
|
||||||
|
|
||||||
for _, file in ipairs(modified_files) do
|
for _, file in ipairs(modified_files) do
|
||||||
|
@ -70,15 +93,27 @@ function M.post_tool_use_hook()
|
||||||
if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf) then
|
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)
|
local buf_name = vim.api.nvim_buf_get_name(buf)
|
||||||
if buf_name == full_path or buf_name:match('/' .. file:gsub('([^%w])', '%%%1') .. '$') then
|
if buf_name == full_path or buf_name:match('/' .. file:gsub('([^%w])', '%%%1') .. '$') then
|
||||||
-- Get the original content from stash
|
-- Get the original content - prefer in-memory baseline if available
|
||||||
local stash_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, stash_ref, file)
|
local original_content = nil
|
||||||
local original_content, orig_err = utils.exec(stash_cmd)
|
|
||||||
|
|
||||||
if not orig_err and original_content then
|
-- 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
|
||||||
|
|
||||||
|
if original_content then
|
||||||
-- Get current content
|
-- Get current content
|
||||||
local current_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
local current_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
local current_content = table.concat(current_lines, '\n')
|
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
|
-- Show inline diff
|
||||||
inline_diff.show_inline_diff(buf, original_content, current_content)
|
inline_diff.show_inline_diff(buf, original_content, current_content)
|
||||||
opened_inline = true
|
opened_inline = true
|
||||||
|
@ -187,7 +222,16 @@ function M.create_startup_baseline()
|
||||||
persistence.setup_autocmds()
|
persistence.setup_autocmds()
|
||||||
|
|
||||||
-- Try to restore any saved diffs
|
-- Try to restore any saved diffs
|
||||||
persistence.restore_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
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Manual hook testing
|
-- Manual hook testing
|
||||||
|
@ -338,6 +382,12 @@ function M.setup_commands()
|
||||||
desc = 'Test Claude keymap functionality',
|
desc = 'Test Claude keymap functionality',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command('ClaudeDebugInlineDiff', function()
|
||||||
|
require('nvim-claude.inline-diff-debug').debug_inline_diff()
|
||||||
|
end, {
|
||||||
|
desc = 'Debug Claude inline diff state',
|
||||||
|
})
|
||||||
|
|
||||||
vim.api.nvim_create_user_command('ClaudeUpdateBaseline', function()
|
vim.api.nvim_create_user_command('ClaudeUpdateBaseline', function()
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
@ -412,6 +462,26 @@ function M.setup_commands()
|
||||||
end, {
|
end, {
|
||||||
desc = 'Uninstall Claude Code hooks for this project',
|
desc = 'Uninstall Claude Code hooks for this project',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command('ClaudeResetBaseline', function()
|
||||||
|
-- Clear all baselines and force new baseline on next edit
|
||||||
|
local inline_diff = require 'nvim-claude.inline-diff'
|
||||||
|
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||||
|
|
||||||
|
-- Clear in-memory baselines
|
||||||
|
inline_diff.original_content = {}
|
||||||
|
|
||||||
|
-- Clear stable baseline reference
|
||||||
|
M.stable_baseline_ref = nil
|
||||||
|
persistence.current_stash_ref = nil
|
||||||
|
|
||||||
|
-- Clear persistence state
|
||||||
|
persistence.clear_state()
|
||||||
|
|
||||||
|
vim.notify('Baseline reset. Next edit will create a new baseline.', vim.log.levels.INFO)
|
||||||
|
end, {
|
||||||
|
desc = 'Reset Claude baseline for cumulative diffs',
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Cleanup old temp files (no longer cleans up commits)
|
-- Cleanup old temp files (no longer cleans up commits)
|
||||||
|
|
|
@ -121,6 +121,7 @@ function M.setup(user_config)
|
||||||
M.registry = require('nvim-claude.registry')
|
M.registry = require('nvim-claude.registry')
|
||||||
M.hooks = require('nvim-claude.hooks')
|
M.hooks = require('nvim-claude.hooks')
|
||||||
M.diff_review = require('nvim-claude.diff-review')
|
M.diff_review = require('nvim-claude.diff-review')
|
||||||
|
M.settings_updater = require('nvim-claude.settings-updater')
|
||||||
|
|
||||||
-- Initialize submodules with config
|
-- Initialize submodules with config
|
||||||
M.tmux.setup(M.config.tmux)
|
M.tmux.setup(M.config.tmux)
|
||||||
|
@ -128,6 +129,7 @@ function M.setup(user_config)
|
||||||
M.registry.setup(M.config.agents)
|
M.registry.setup(M.config.agents)
|
||||||
M.hooks.setup()
|
M.hooks.setup()
|
||||||
M.diff_review.setup()
|
M.diff_review.setup()
|
||||||
|
M.settings_updater.setup()
|
||||||
|
|
||||||
-- Set up commands
|
-- Set up commands
|
||||||
M.commands.setup(M)
|
M.commands.setup(M)
|
||||||
|
@ -137,7 +139,6 @@ function M.setup(user_config)
|
||||||
vim.defer_fn(function()
|
vim.defer_fn(function()
|
||||||
if M.utils.get_project_root() then
|
if M.utils.get_project_root() then
|
||||||
M.hooks.install_hooks()
|
M.hooks.install_hooks()
|
||||||
vim.notify('Claude Code hooks auto-installed', vim.log.levels.INFO)
|
|
||||||
end
|
end
|
||||||
end, 100)
|
end, 100)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- Debug function to check inline diff state
|
||||||
|
function M.debug_inline_diff()
|
||||||
|
local inline_diff = require('nvim-claude.inline-diff')
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
|
||||||
|
vim.notify('=== Inline Diff Debug Info ===', vim.log.levels.INFO)
|
||||||
|
|
||||||
|
-- Check if inline diff is active for current buffer
|
||||||
|
local diff_data = inline_diff.active_diffs[bufnr]
|
||||||
|
if diff_data then
|
||||||
|
vim.notify(string.format('✓ Inline diff ACTIVE for buffer %d', bufnr), vim.log.levels.INFO)
|
||||||
|
vim.notify(string.format(' - Hunks: %d', #diff_data.hunks), vim.log.levels.INFO)
|
||||||
|
vim.notify(string.format(' - Current hunk: %d', diff_data.current_hunk or 0), vim.log.levels.INFO)
|
||||||
|
vim.notify(string.format(' - Original content length: %d', #(inline_diff.original_content[bufnr] or '')), vim.log.levels.INFO)
|
||||||
|
vim.notify(string.format(' - New content length: %d', #(diff_data.new_content or '')), vim.log.levels.INFO)
|
||||||
|
else
|
||||||
|
vim.notify(string.format('✗ No inline diff for buffer %d', bufnr), vim.log.levels.WARN)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check all active diffs
|
||||||
|
local count = 0
|
||||||
|
for buf, _ in pairs(inline_diff.active_diffs) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
vim.notify(string.format('Total active inline diffs: %d', count), vim.log.levels.INFO)
|
||||||
|
|
||||||
|
-- Check keymaps
|
||||||
|
local keymaps = vim.api.nvim_buf_get_keymap(bufnr, 'n')
|
||||||
|
local found_ir = false
|
||||||
|
local leader = vim.g.mapleader or '\\'
|
||||||
|
local ir_pattern = leader .. 'ir'
|
||||||
|
|
||||||
|
vim.notify(string.format('Looking for keymap: %s', ir_pattern), vim.log.levels.INFO)
|
||||||
|
|
||||||
|
for _, map in ipairs(keymaps) do
|
||||||
|
if map.lhs == ir_pattern or map.lhs == '<leader>ir' then
|
||||||
|
found_ir = true
|
||||||
|
vim.notify(string.format('✓ Found keymap: %s -> %s', map.lhs, map.desc or 'no desc'), vim.log.levels.INFO)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not found_ir then
|
||||||
|
vim.notify('✗ <leader>ir keymap not found', vim.log.levels.WARN)
|
||||||
|
-- List all keymaps that start with leader
|
||||||
|
vim.notify('Buffer keymaps starting with leader:', vim.log.levels.INFO)
|
||||||
|
for _, map in ipairs(keymaps) do
|
||||||
|
if map.lhs:match('^' .. vim.pesc(leader)) or map.lhs:match('^<leader>') then
|
||||||
|
vim.notify(string.format(' %s -> %s', map.lhs, map.desc or 'no desc'), vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
|
@ -142,7 +142,7 @@ function M.restore_diffs()
|
||||||
end
|
end
|
||||||
|
|
||||||
if restored_count > 0 then
|
if restored_count > 0 then
|
||||||
vim.notify(string.format('Restored inline diffs for %d file(s)', restored_count), vim.log.levels.INFO)
|
-- Silent restore - no notification
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Store the stash reference for future operations
|
-- Store the stash reference for future operations
|
||||||
|
@ -180,33 +180,24 @@ function M.check_pending_restore(bufnr)
|
||||||
-- Remove from pending
|
-- Remove from pending
|
||||||
M.pending_restores[file_path] = nil
|
M.pending_restores[file_path] = nil
|
||||||
|
|
||||||
vim.notify('Restored inline diff for ' .. vim.fn.fnamemodify(file_path, ':~:.'), vim.log.levels.INFO)
|
-- Silent restore - no notification
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create a stash of current changes (instead of baseline commit)
|
-- Create a stash of current changes (instead of baseline commit)
|
||||||
function M.create_stash(message)
|
function M.create_stash(message)
|
||||||
|
local utils = require('nvim-claude.utils')
|
||||||
message = message or 'nvim-claude: pre-edit state'
|
message = message or 'nvim-claude: pre-edit state'
|
||||||
|
|
||||||
-- Check if there are changes to stash
|
-- Create a stash object without removing changes from working directory
|
||||||
local status = utils.exec('git status --porcelain')
|
local stash_cmd = 'git stash create'
|
||||||
if not status or status == '' then
|
local stash_hash, err = utils.exec(stash_cmd)
|
||||||
-- No changes, but we still need to track current state
|
|
||||||
-- Create an empty stash by making a tiny change
|
if not err and stash_hash and stash_hash ~= '' then
|
||||||
local temp_file = '.nvim-claude-temp'
|
-- Store the stash with a message
|
||||||
utils.write_file(temp_file, 'temp')
|
stash_hash = stash_hash:gsub('%s+', '') -- trim whitespace
|
||||||
utils.exec('git add ' .. temp_file)
|
local store_cmd = string.format('git stash store -m "%s" %s', message, stash_hash)
|
||||||
utils.exec(string.format('git stash push -m "%s" -- %s', message, temp_file))
|
utils.exec(store_cmd)
|
||||||
os.remove(temp_file)
|
|
||||||
else
|
|
||||||
-- Stash all current changes
|
|
||||||
local cmd = string.format('git stash push -m "%s" --include-untracked', message)
|
|
||||||
local result, err = utils.exec(cmd)
|
|
||||||
if err and not err:match('Saved working directory') then
|
|
||||||
vim.notify('Failed to create stash: ' .. err, vim.log.levels.ERROR)
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Get the stash reference
|
-- Get the stash reference
|
||||||
local stash_list = utils.exec('git stash list -n 1')
|
local stash_list = utils.exec('git stash list -n 1')
|
||||||
|
@ -214,6 +205,7 @@ function M.create_stash(message)
|
||||||
local stash_ref = stash_list:match('^(stash@{%d+})')
|
local stash_ref = stash_list:match('^(stash@{%d+})')
|
||||||
return stash_ref
|
return stash_ref
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,6 +40,9 @@ function M.show_inline_diff(bufnr, old_content, new_content)
|
||||||
applied_hunks = {}
|
applied_hunks = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Debug: Log target content length
|
||||||
|
-- vim.notify(string.format('DEBUG: Stored target content with %d chars', #new_content), vim.log.levels.WARN)
|
||||||
|
|
||||||
-- Apply visual indicators
|
-- Apply visual indicators
|
||||||
M.apply_diff_visualization(bufnr)
|
M.apply_diff_visualization(bufnr)
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ function M.show_inline_diff(bufnr, old_content, new_content)
|
||||||
-- Jump to first hunk
|
-- Jump to first hunk
|
||||||
M.jump_to_hunk(bufnr, 1)
|
M.jump_to_hunk(bufnr, 1)
|
||||||
|
|
||||||
vim.notify('Inline diff active. Use [h/]h to navigate, <leader>ia/<leader>ir to accept/reject hunks', vim.log.levels.INFO)
|
-- Silent activation - no notification
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Compute diff between two texts
|
-- Compute diff between two texts
|
||||||
|
@ -63,8 +66,11 @@ function M.compute_diff(old_text, new_text)
|
||||||
utils.write_file(old_file, old_text)
|
utils.write_file(old_file, old_text)
|
||||||
utils.write_file(new_file, new_text)
|
utils.write_file(new_file, new_text)
|
||||||
|
|
||||||
-- Generate unified diff with minimal context to avoid grouping nearby changes
|
-- Use git diff with histogram algorithm for better code diffs
|
||||||
local cmd = string.format('diff -U1 "%s" "%s" || true', old_file, new_file)
|
local cmd = string.format(
|
||||||
|
'git diff --no-index --no-prefix --unified=1 --diff-algorithm=histogram "%s" "%s" 2>/dev/null || true',
|
||||||
|
old_file, new_file
|
||||||
|
)
|
||||||
local diff_output = utils.exec(cmd)
|
local diff_output = utils.exec(cmd)
|
||||||
|
|
||||||
-- Parse diff into hunks
|
-- Parse diff into hunks
|
||||||
|
@ -101,6 +107,9 @@ function M.parse_diff(diff_text)
|
||||||
elseif in_hunk and (line:match('^[%+%-]') or line:match('^%s')) then
|
elseif in_hunk and (line:match('^[%+%-]') or line:match('^%s')) then
|
||||||
-- Diff line
|
-- Diff line
|
||||||
table.insert(current_hunk.lines, line)
|
table.insert(current_hunk.lines, line)
|
||||||
|
elseif line:match('^diff %-%-git') or line:match('^index ') or line:match('^%+%+%+ ') or line:match('^%-%-%-') then
|
||||||
|
-- Skip git diff headers
|
||||||
|
in_hunk = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -349,46 +358,51 @@ function M.accept_current_hunk(bufnr)
|
||||||
local hunk = diff_data.hunks[diff_data.current_hunk]
|
local hunk = diff_data.hunks[diff_data.current_hunk]
|
||||||
if not hunk then return end
|
if not hunk then return end
|
||||||
|
|
||||||
-- Mark as applied (the changes are already in the buffer)
|
-- Mark this hunk as processed
|
||||||
diff_data.applied_hunks[diff_data.current_hunk] = true
|
vim.notify(string.format('Accepted hunk %d/%d', diff_data.current_hunk, #diff_data.hunks), vim.log.levels.INFO)
|
||||||
|
|
||||||
-- Update in-memory baseline to current state
|
-- For single hunk case
|
||||||
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
if #diff_data.hunks == 1 then
|
||||||
local current_content = table.concat(current_lines, '\n')
|
vim.notify('All changes accepted. Closing inline diff.', vim.log.levels.INFO)
|
||||||
M.original_content[bufnr] = current_content
|
M.close_inline_diff(bufnr, true) -- Keep new baseline
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Remove this hunk from the diff data since it's accepted
|
-- Multiple hunks: remove the accepted hunk and continue
|
||||||
table.remove(diff_data.hunks, diff_data.current_hunk)
|
table.remove(diff_data.hunks, diff_data.current_hunk)
|
||||||
|
|
||||||
-- Adjust current hunk index
|
-- Adjust current hunk index
|
||||||
if diff_data.current_hunk > #diff_data.hunks then
|
if diff_data.current_hunk > #diff_data.hunks then
|
||||||
diff_data.current_hunk = math.max(1, #diff_data.hunks)
|
diff_data.current_hunk = #diff_data.hunks
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.notify(string.format('Accepted hunk - %d hunks remaining', #diff_data.hunks), vim.log.levels.INFO)
|
|
||||||
|
|
||||||
-- Save state for persistence
|
|
||||||
local persistence = require('nvim-claude.inline-diff-persistence')
|
|
||||||
persistence.save_state({ stash_ref = persistence.current_stash_ref })
|
|
||||||
|
|
||||||
if #diff_data.hunks == 0 then
|
if #diff_data.hunks == 0 then
|
||||||
-- No more hunks to review
|
-- No more hunks
|
||||||
vim.notify('All hunks processed! Closing inline diff.', vim.log.levels.INFO)
|
vim.notify('All changes accepted. Closing inline diff.', vim.log.levels.INFO)
|
||||||
M.close_inline_diff(bufnr, true) -- Keep baseline for future diffs
|
M.close_inline_diff(bufnr, true)
|
||||||
else
|
else
|
||||||
-- Refresh visualization and move to current hunk
|
-- Refresh visualization to show remaining hunks
|
||||||
M.apply_diff_visualization(bufnr)
|
M.apply_diff_visualization(bufnr)
|
||||||
M.jump_to_hunk(bufnr, diff_data.current_hunk)
|
M.jump_to_hunk(bufnr, diff_data.current_hunk)
|
||||||
|
vim.notify(string.format('%d hunks remaining', #diff_data.hunks), vim.log.levels.INFO)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Reject current hunk
|
-- Reject current hunk
|
||||||
function M.reject_current_hunk(bufnr)
|
function M.reject_current_hunk(bufnr)
|
||||||
local diff_data = M.active_diffs[bufnr]
|
local diff_data = M.active_diffs[bufnr]
|
||||||
if not diff_data then return end
|
if not diff_data then
|
||||||
|
vim.notify('No diff data for buffer', vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local hunk = diff_data.hunks[diff_data.current_hunk]
|
local hunk = diff_data.hunks[diff_data.current_hunk]
|
||||||
if not hunk then return end
|
if not hunk then
|
||||||
|
vim.notify('No hunk at index ' .. tostring(diff_data.current_hunk), 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)
|
||||||
|
|
||||||
-- Revert the hunk by applying original content
|
-- Revert the hunk by applying original content
|
||||||
M.revert_hunk_changes(bufnr, hunk)
|
M.revert_hunk_changes(bufnr, hunk)
|
||||||
|
@ -400,33 +414,29 @@ function M.reject_current_hunk(bufnr)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Update in-memory baseline to current state (with rejected changes)
|
-- Get current content after rejection
|
||||||
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
local current_content = table.concat(current_lines, '\n')
|
local current_content = table.concat(current_lines, '\n')
|
||||||
M.original_content[bufnr] = current_content
|
|
||||||
|
|
||||||
-- Remove this hunk from the diff data since it's rejected
|
-- Recalculate diff between current state (with rejected hunk) and original baseline
|
||||||
table.remove(diff_data.hunks, diff_data.current_hunk)
|
local new_diff_data = M.compute_diff(M.original_content[bufnr], current_content)
|
||||||
|
|
||||||
-- Adjust current hunk index
|
if not new_diff_data or #new_diff_data.hunks == 0 then
|
||||||
if diff_data.current_hunk > #diff_data.hunks then
|
-- No more changes from baseline - close the diff
|
||||||
diff_data.current_hunk = math.max(1, #diff_data.hunks)
|
vim.notify('All changes processed. Closing inline diff.', vim.log.levels.INFO)
|
||||||
end
|
M.close_inline_diff(bufnr, false)
|
||||||
|
|
||||||
vim.notify(string.format('Rejected hunk - %d hunks remaining', #diff_data.hunks), vim.log.levels.INFO)
|
|
||||||
|
|
||||||
-- Save state for persistence
|
|
||||||
local persistence = require('nvim-claude.inline-diff-persistence')
|
|
||||||
persistence.save_state({ stash_ref = persistence.current_stash_ref })
|
|
||||||
|
|
||||||
if #diff_data.hunks == 0 then
|
|
||||||
-- No more hunks to review
|
|
||||||
vim.notify('All hunks processed! Closing inline diff.', vim.log.levels.INFO)
|
|
||||||
M.close_inline_diff(bufnr, true) -- Keep baseline after rejection too
|
|
||||||
else
|
else
|
||||||
-- Refresh visualization and move to current hunk
|
-- Update diff data with remaining hunks
|
||||||
|
diff_data.hunks = new_diff_data.hunks
|
||||||
|
diff_data.current_hunk = 1
|
||||||
|
|
||||||
|
-- The new_content should remain as Claude's original suggestion
|
||||||
|
-- so we can continue to accept remaining hunks if desired
|
||||||
|
|
||||||
|
-- Refresh visualization and jump to first remaining hunk
|
||||||
M.apply_diff_visualization(bufnr)
|
M.apply_diff_visualization(bufnr)
|
||||||
M.jump_to_hunk(bufnr, diff_data.current_hunk)
|
M.jump_to_hunk(bufnr, 1)
|
||||||
|
vim.notify(string.format('%d hunks remaining', #diff_data.hunks), vim.log.levels.INFO)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -434,43 +444,117 @@ end
|
||||||
function M.revert_hunk_changes(bufnr, hunk)
|
function M.revert_hunk_changes(bufnr, hunk)
|
||||||
-- Get current buffer lines
|
-- Get current buffer lines
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
local original_content = M.original_content[bufnr]
|
|
||||||
|
|
||||||
if not original_content then
|
-- Extract the expected content from the hunk
|
||||||
vim.notify('No original content available for rejection', vim.log.levels.ERROR)
|
local expected_lines = {}
|
||||||
|
local original_lines = {}
|
||||||
|
|
||||||
|
for _, diff_line in ipairs(hunk.lines) do
|
||||||
|
if diff_line:match('^%+') then
|
||||||
|
-- Lines that were added (these should be in current buffer)
|
||||||
|
table.insert(expected_lines, diff_line:sub(2))
|
||||||
|
elseif diff_line:match('^%-') then
|
||||||
|
-- Lines that were removed (these should be restored)
|
||||||
|
table.insert(original_lines, diff_line:sub(2))
|
||||||
|
elseif diff_line:match('^%s') then
|
||||||
|
-- Context lines (should be in both)
|
||||||
|
table.insert(expected_lines, diff_line:sub(2))
|
||||||
|
table.insert(original_lines, diff_line:sub(2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find where this hunk actually is in the current buffer
|
||||||
|
-- We'll look for the best match by checking context lines too
|
||||||
|
local hunk_start = nil
|
||||||
|
local hunk_end = nil
|
||||||
|
local best_score = -1
|
||||||
|
local best_start = nil
|
||||||
|
|
||||||
|
-- Include some context before and after for better matching
|
||||||
|
local context_before = {}
|
||||||
|
local context_after = {}
|
||||||
|
|
||||||
|
-- Extract context from the diff
|
||||||
|
local in_changes = false
|
||||||
|
for i, diff_line in ipairs(hunk.lines) do
|
||||||
|
if diff_line:match('^[%+%-]') then
|
||||||
|
in_changes = true
|
||||||
|
elseif diff_line:match('^%s') and not in_changes then
|
||||||
|
-- Context before changes
|
||||||
|
table.insert(context_before, diff_line:sub(2))
|
||||||
|
elseif diff_line:match('^%s') and in_changes then
|
||||||
|
-- Context after changes
|
||||||
|
table.insert(context_after, diff_line:sub(2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Search for the hunk by matching content with context
|
||||||
|
for i = 1, #lines - #expected_lines + 1 do
|
||||||
|
local score = 0
|
||||||
|
local matches = true
|
||||||
|
|
||||||
|
-- Check the main content
|
||||||
|
for j = 1, #expected_lines do
|
||||||
|
if lines[i + j - 1] == expected_lines[j] then
|
||||||
|
score = score + 1
|
||||||
|
else
|
||||||
|
matches = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if matches then
|
||||||
|
-- Bonus points for matching context before
|
||||||
|
local before_start = i - #context_before
|
||||||
|
if before_start > 0 then
|
||||||
|
for j = 1, #context_before do
|
||||||
|
if lines[before_start + j - 1] == context_before[j] then
|
||||||
|
score = score + 2 -- Context is worth more
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Bonus points for matching context after
|
||||||
|
local after_start = i + #expected_lines
|
||||||
|
if after_start + #context_after - 1 <= #lines then
|
||||||
|
for j = 1, #context_after do
|
||||||
|
if lines[after_start + j - 1] == context_after[j] then
|
||||||
|
score = score + 2 -- Context is worth more
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Keep the best match
|
||||||
|
if score > best_score then
|
||||||
|
best_score = score
|
||||||
|
best_start = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if best_start then
|
||||||
|
hunk_start = best_start
|
||||||
|
hunk_end = best_start + #expected_lines - 1
|
||||||
|
else
|
||||||
|
vim.notify('Could not find hunk in current buffer - content may have changed', vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Split original content into lines
|
-- Build new buffer content
|
||||||
local original_lines = vim.split(original_content, '\n')
|
|
||||||
|
|
||||||
-- Build new lines by reverting this hunk
|
|
||||||
local new_lines = {}
|
local new_lines = {}
|
||||||
local buffer_line = 1
|
|
||||||
local applied = false
|
|
||||||
|
|
||||||
while buffer_line <= #lines do
|
-- Copy lines before the hunk
|
||||||
if buffer_line >= hunk.new_start and buffer_line < hunk.new_start + hunk.new_count and not applied then
|
for i = 1, hunk_start - 1 do
|
||||||
-- Revert this section by using original lines
|
table.insert(new_lines, lines[i])
|
||||||
local orig_start = hunk.old_start
|
|
||||||
local orig_end = hunk.old_start + hunk.old_count - 1
|
|
||||||
|
|
||||||
for orig_line = orig_start, orig_end do
|
|
||||||
if orig_line <= #original_lines then
|
|
||||||
table.insert(new_lines, original_lines[orig_line])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Skip the modified lines in current buffer
|
-- Insert the original lines
|
||||||
buffer_line = hunk.new_start + hunk.new_count
|
for _, line in ipairs(original_lines) do
|
||||||
applied = true
|
table.insert(new_lines, line)
|
||||||
else
|
|
||||||
-- Copy unchanged line
|
|
||||||
if buffer_line <= #lines then
|
|
||||||
table.insert(new_lines, lines[buffer_line])
|
|
||||||
end
|
|
||||||
buffer_line = buffer_line + 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Copy lines after the hunk
|
||||||
|
for i = hunk_end + 1, #lines do
|
||||||
|
table.insert(new_lines, lines[i])
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Update buffer
|
-- Update buffer
|
||||||
|
@ -581,11 +665,15 @@ function M.close_inline_diff(bufnr, keep_baseline)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If no more active diffs, clear persistence state
|
-- If no more active diffs, clear persistence state and reset baseline
|
||||||
if not has_active_diffs then
|
if not has_active_diffs then
|
||||||
local persistence = require('nvim-claude.inline-diff-persistence')
|
local persistence = require('nvim-claude.inline-diff-persistence')
|
||||||
persistence.clear_state()
|
persistence.clear_state()
|
||||||
persistence.current_stash_ref = nil
|
persistence.current_stash_ref = nil
|
||||||
|
|
||||||
|
-- Reset the stable baseline in hooks
|
||||||
|
local hooks = require('nvim-claude.hooks')
|
||||||
|
hooks.stable_baseline_ref = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.notify('Inline diff closed', vim.log.levels.INFO)
|
vim.notify('Inline diff closed', vim.log.levels.INFO)
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
local M = {}
|
||||||
|
local utils = require('nvim-claude.utils')
|
||||||
|
|
||||||
|
-- Update Claude settings with current Neovim server address
|
||||||
|
function M.update_claude_settings()
|
||||||
|
local project_root = utils.get_project_root()
|
||||||
|
if not project_root then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local settings_path = project_root .. '/.claude/settings.json'
|
||||||
|
local settings_dir = project_root .. '/.claude'
|
||||||
|
|
||||||
|
-- Get current Neovim server address
|
||||||
|
local server_addr = vim.v.servername
|
||||||
|
if not server_addr or server_addr == '' then
|
||||||
|
-- If no servername, we can't communicate
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure .claude directory exists
|
||||||
|
if vim.fn.isdirectory(settings_dir) == 0 then
|
||||||
|
vim.fn.mkdir(settings_dir, 'p')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read existing settings or create new
|
||||||
|
local settings = {}
|
||||||
|
if vim.fn.filereadable(settings_path) == 1 then
|
||||||
|
local ok, content = pcall(vim.fn.readfile, settings_path)
|
||||||
|
if ok and #content > 0 then
|
||||||
|
local decode_ok, decoded = pcall(vim.json.decode, table.concat(content, '\n'))
|
||||||
|
if decode_ok then
|
||||||
|
settings = decoded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure hooks structure exists
|
||||||
|
if not settings.hooks then
|
||||||
|
settings.hooks = {}
|
||||||
|
end
|
||||||
|
if not settings.hooks.PreToolUse then
|
||||||
|
settings.hooks.PreToolUse = {}
|
||||||
|
end
|
||||||
|
if not settings.hooks.PostToolUse then
|
||||||
|
settings.hooks.PostToolUse = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update hook commands with current server address
|
||||||
|
local pre_hook_cmd = string.format(
|
||||||
|
'nvr --servername "%s" --remote-expr \'luaeval("require(\\"nvim-claude.hooks\\").pre_tool_use_hook()")\'',
|
||||||
|
server_addr
|
||||||
|
)
|
||||||
|
|
||||||
|
local post_hook_cmd = string.format(
|
||||||
|
'nvr --servername "%s" --remote-send "<C-\\\\><C-N>:lua require(\'nvim-claude.hooks\').post_tool_use_hook()<CR>"',
|
||||||
|
server_addr
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Update PreToolUse hooks
|
||||||
|
local pre_hook_found = false
|
||||||
|
for _, hook_group in ipairs(settings.hooks.PreToolUse) do
|
||||||
|
if hook_group.matcher == "Edit|Write|MultiEdit" then
|
||||||
|
hook_group.hooks = {{type = "command", command = pre_hook_cmd}}
|
||||||
|
pre_hook_found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not pre_hook_found then
|
||||||
|
table.insert(settings.hooks.PreToolUse, {
|
||||||
|
matcher = "Edit|Write|MultiEdit",
|
||||||
|
hooks = {{type = "command", command = pre_hook_cmd}}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update PostToolUse hooks
|
||||||
|
local post_hook_found = false
|
||||||
|
for _, hook_group in ipairs(settings.hooks.PostToolUse) do
|
||||||
|
if hook_group.matcher == "Edit|Write|MultiEdit" then
|
||||||
|
hook_group.hooks = {{type = "command", command = post_hook_cmd}}
|
||||||
|
post_hook_found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not post_hook_found then
|
||||||
|
table.insert(settings.hooks.PostToolUse, {
|
||||||
|
matcher = "Edit|Write|MultiEdit",
|
||||||
|
hooks = {{type = "command", command = post_hook_cmd}}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write updated settings
|
||||||
|
local encoded = vim.json.encode(settings)
|
||||||
|
vim.fn.writefile({encoded}, settings_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Setup autocmds to update settings
|
||||||
|
function M.setup()
|
||||||
|
vim.api.nvim_create_autocmd({"VimEnter", "DirChanged"}, {
|
||||||
|
group = vim.api.nvim_create_augroup("NvimClaudeSettingsUpdater", { clear = true }),
|
||||||
|
callback = function()
|
||||||
|
-- Defer to ensure servername is available
|
||||||
|
vim.defer_fn(function()
|
||||||
|
M.update_claude_settings()
|
||||||
|
end, 100)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
Loading…
Reference in New Issue